Completed
Push — master ( 066dd2...eed2c7 )
by Chris
01:25
created

ef="#download-json"]ꞌ).click   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 9.4285
1
/** global: d3 */
2
/**
3
 * Bootstrapping functions, event handling, etc... for application.
4
 */
5
6
var jsondash = function() {
7
    var my = {
8
        chart_wall: null,
9
    };
10
    var MIN_CHART_SIZE   = 200;
11
    var API_ROUTE_URL    = $('[name="dataSource"]');
12
    var API_PREVIEW      = $('#api-output');
13
    var API_PREVIEW_BTN  = $('#api-output-preview');
14
    var API_PREVIEW_CONT = $('.api-preview-container');
15
    var WIDGET_FORM      = $('#module-form');
16
    var VIEW_BUILDER     = $('#view-builder');
17
    var ADD_MODULE       = $('#add-module');
18
    var MAIN_CONTAINER   = $('#container');
19
    var EDIT_MODAL       = $('#chart-options');
20
    var DELETE_BTN       = $('#delete-widget');
21
    var DELETE_DASHBOARD = $('.delete-dashboard');
22
    var SAVE_WIDGET_BTN  = $('#save-module');
23
    var EDIT_CONTAINER   = $('#edit-view-container');
24
    var MAIN_FORM        = $('#save-view-form');
25
    var JSON_DATA        = $('#raw-config');
26
    var ADD_ROW_CONTS    = $('.add-new-row-container');
27
    var EDIT_TOGGLE_BTN  = $('[href=".edit-mode-component"]');
28
    var UPDATE_FORM_BTN  = $('#update-module');
29
    var CHART_TEMPLATE   = $('#chart-template');
30
    var ROW_TEMPLATE     = $('#row-template').find('.grid-row');
31
32
    function Widgets() {
33
        var self = this;
34
        self.widgets = {};
35
        self.container = MAIN_CONTAINER.selector;
36
        self.all = function() {
37
            return self.widgets;
38
        };
39
        self.add = function(config) {
40
            self.widgets[config.guid] = new Widget(self.container, config);
41
            return self.widgets[config.guid];
42
        };
43
        self.addFromForm = function() {
44
            return self.add(self.newModel());
45
        };
46
        self._delete = function(guid) {
47
            delete self.widgets[guid];
48
        };
49
        self.get = function(guid) {
50
            return self.widgets[guid];
51
        };
52
        self.getByEl = function(el) {
53
            return self.get(el.data().guid);
54
        };
55
        self.getAllOfProp = function(propname) {
56
            var props = [];
57
            $.each(self.all(), function(i, widg){
58
                props.push(widg.config[propname]);
59
            });
60
            return props;
61
        };
62
        self.newModel = function() {
63
            var config = getParsedFormConfig();
64
            var guid   = jsondash.util.guid();
65
            config['guid'] = guid;
66
            config['family'] = WIDGET_FORM.find('[name="type"]').find('option:selected').data().family;
67
            if(!config.refresh || !refreshableType(config.type)) {config['refresh'] = false;}
68
            if(!config.override) {config['override'] = false;}
69
            return config;
70
        };
71
        self.populate = function(data) {
72
            for(var name in data.modules){
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
73
                // Closure to maintain each chart data value in loop
74
                (function(config){
75
                    var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 72. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
76
                    // Add div wrappers for js grid layout library,
77
                    // and add title, icons, and buttons
78
                    // This is the widget "model"/object used throughout.
79
                    self.add(config);
80
                })(data.modules[name]);
81
            }
82
        };
83
    }
84
85
    function Widget(container, config) {
86
        // model for a chart widget
87
        var self = this;
88
        self.config = config;
89
        self.guid = self.config.guid;
90
        self.container = container;
91
        self._refreshInterval = null;
92
        self._makeWidget = function(config) {
93
            if(document.querySelector('[data-guid="' + config.guid + '"]')){
94
                return d3.select('[data-guid="' + config.guid + '"]');
95
            }
96
            return d3.select(self.container).select('div')
97
                .append('div')
98
                .classed({item: true, widget: true})
99
                .attr('data-guid', config.guid)
100
                .attr('data-refresh', config.refresh)
101
                .attr('data-refresh-interval', config.refreshInterval)
102
                .style('width', config.width + 'px')
103
                .style('height', config.height + 'px')
104
                .html(d3.select(CHART_TEMPLATE.selector).html())
105
                .select('.widget-title .widget-title-text').text(config.name);
106
        };
107
        // d3 el
108
        self.el = self._makeWidget(config);
109
        // Jquery el
110
        self.$el = $(self.el[0]);
111
        self.init = function() {
112
            // Add event handlers for widget UI
113
            self.$el.find('.widget-refresh').on('click.charts', refreshWidget);
114
            self.$el.find('.widget-delete').on('click.charts.delete', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
115
                self.delete();
116
            });
117
            // Allow swapping of edit/update events
118
            // for the edit button and form modal
119
            self.$el.find('.widget-edit').on('click.charts', function(){
120
                SAVE_WIDGET_BTN
121
                .attr('id', UPDATE_FORM_BTN.selector.replace('#', ''))
122
                .text('Update widget')
123
                .off('click.charts.save')
124
                .on('click.charts', onUpdateWidget);
125
            });
126
            if(self.config.refresh && self.config.refreshInterval) {
127
                self._refreshInterval = setInterval(function(){
128
                    loadWidgetData(my.widgets.get(self.config.guid));
129
                }, parseInt(self.config.refreshInterval, 10));
130
            }
131
            if(my.layout === 'grid') {
132
                updateRowControls();
133
            }
134
        };
135
        self.getInput = function() {
136
            // Get the form input for this widget.
137
            return $('input[id="' + self.guid + '"]');
138
        };
139
        self.delete = function(bypass_confirm) {
140
            if(!bypass_confirm){
141
                if(!confirm('Are you sure?')) {
142
                    return;
143
                }
144
            }
145
            var row = self.$el.closest('.grid-row');
146
            clearInterval(self._refreshInterval);
147
            // Delete the input
148
            self.getInput().remove();
149
            // Delete the widget
150
            self.el.remove();
151
            // Remove reference to the collection by guid
152
            my.widgets._delete(self.guid);
153
            EDIT_MODAL.modal('hide');
154
            // Redraw wall to replace visual 'hole'
155
            if(my.layout === 'grid') {
156
                // Fill empty holes in this charts' row
157
                fillEmptyCols(row);
158
                updateRowControls();
159
            }
160
            // Trigger update form into view since data is dirty
161
            EDIT_CONTAINER.collapse('show');
162
            // Refit grid - this should be last.
163
            fitGrid();
164
        };
165
        self.addGridClasses = function(sel, classes) {
166
            d3.map(classes, function(colcount){
167
                var classlist = {};
168
                classlist['col-md-' + colcount] = true;
169
                classlist['col-lg-' + colcount] = true;
170
                sel.classed(classlist);
171
            });
172
        };
173
        self.removeGridClasses = function(sel) {
174
            var bootstrap_classes = d3.range(1, 13);
175
            d3.map(bootstrap_classes, function(i){
176
                var classes = {};
177
                classes['col-md-' + i] = false;
178
                classes['col-lg-' + i] = false;
179
                sel.classed(classes);
180
            });
181
        };
182
        self.update = function(conf, dont_refresh) {
183
                /**
184
             * Single source to update all aspects of a widget - in DOM, in model, etc...
185
             */
186
            var widget = self.el;
187
            // Update model data
188
            self.config = $.extend(self.config, conf);
189
            // Trigger update form into view since data is dirty
190
            // Update visual size to existing widget.
191
            loader(widget);
192
            widget.style({
193
                height: self.config.height + 'px',
194
                width: my.layout === 'grid' ? '100%' : self.config.width + 'px'
195
            });
196
            if(my.layout === 'grid') {
197
                // Extract col number from config: format is "col-N"
198
                var colcount = self.config.width.split('-')[1];
199
                var parent = d3.select(widget.node().parentNode);
200
                // Reset all other grid classes and then add new one.
201
                self.removeGridClasses(parent);
202
                self.addGridClasses(parent, [colcount]);
203
                // Update row buttons based on current state
204
                updateRowControls();
205
            }
206
            widget.select('.widget-title .widget-title-text').text(self.config.name);
207
            // Update the form input for this widget.
208
            self._updateForm();
209
210
            if(!dont_refresh) {
211
                loadWidgetData(self, self.config);
0 ignored issues
show
Bug introduced by
The call to loadWidgetData seems to have too many arguments starting with self.config.
Loading history...
212
                EDIT_CONTAINER.collapse();
213
                // Refit the grid
214
                fitGrid();
215
            } else {
216
                unload(widget);
217
            }
218
        };
219
        self._updateForm = function() {
220
            self.getInput().val(JSON.stringify(self.config));
221
        };
222
223
        // Run init script on creation
224
        self.init();
225
    }
226
227
    /**
228
     * [fillEmptyCols Fill in gaps in a row when an item has been deleted (fixed grid only)]
229
     */
230
    function fillEmptyCols(row) {
231
        row.each(function(_, row){
232
            var items = $(row).find('.item.widget');
0 ignored issues
show
Unused Code introduced by
The variable items seems to be never used. Consider removing it.
Loading history...
233
            var cols = $(row).find('> div');
234
            cols.filter(function(i, col){
235
                return $(col).find('.item.widget').length === 0;
236
            }).remove();
237
        });
238
    }
239
240
    function togglePreviewOutput(is_on) {
241
        if(is_on) {
242
            API_PREVIEW_CONT.show();
243
            return;
244
        }
245
        API_PREVIEW_CONT.hide();
246
    }
247
248
    function previewAPIRoute(e) {
249
        e.preventDefault();
250
        // Shows the response of the API field as a json payload, inline.
251
        $.ajax({
252
            type: 'get',
253
            url: API_ROUTE_URL.val().trim(),
254
            success: function(data) {
255
                API_PREVIEW.html(prettyCode(data));
256
            },
257
            error: function(data, status, error) {
258
                API_PREVIEW.html(error);
259
            }
260
        });
261
    }
262
263
    function refreshableType(type) {
264
        if(type === 'youtube') {return false;}
265
        return true;
266
    }
267
268
    function validateWidgetForm() {
269
        var is_valid = true;
270
        var url_field = WIDGET_FORM.find('[name="dataSource"]');
271
        WIDGET_FORM.find('[required]').each(function(i, el){
272
            if($(el).val() === '') {
273
                $(el).parent().addClass('has-error').removeClass('has-success');
274
                is_valid = false;
275
                return false;
276
            } else {
277
                $(el).parent().addClass('has-success').removeClass('has-error');
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
278
            }
279
        });
280
        // Validate youtube videos
281
        if(WIDGET_FORM.find('[name="type"]').val() === 'youtube') {
282
            if(!url_field.val().startsWith('<iframe')) {
283
                url_field.parent().addClass('has-error');
284
                is_valid = false;
0 ignored issues
show
Unused Code introduced by
The assignment to variable is_valid seems to be never used. Consider removing it.
Loading history...
285
                return false;
286
            }
287
        }
288
        return is_valid;
289
    }
290
291
    function saveWidget(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
292
        if(!(validateWidgetForm())) {
293
            return false;
294
        }
295
        var new_config = my.widgets.newModel();
296
        // Remove empty rows and then update the order so it's consecutive.
297
        $('.grid-row').not('.grid-row-template').each(function(i, row){
298
            // Delete empty rows - except any empty rows that have been created
299
            // for the purpose of this new chart.
300
            if($(row).find('.item.widget').length === 0 && new_config.row !== i + 1) {
301
                $(row).remove();
302
            }
303
        });
304
        // Update the row orders after deleting empty ones
305
        updateRowOrder();
306
        var newfield = $('<input class="form-control" type="text">');
307
        // Add a unique guid for referencing later.
308
        newfield.attr('name', 'module_' + new_config.id);
309
        newfield.val(JSON.stringify(new_config));
310
        $('.modules').append(newfield);
311
        // Save immediately.
312
        MAIN_FORM.submit();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
313
    }
314
315
    function isModalButton(e) {
316
        return e.relatedTarget.id === ADD_MODULE.selector.replace('#', '');
317
    }
318
319
    function isRowButton(e) {
320
        return $(e.relatedTarget).hasClass('grid-row-label');
321
    }
322
323
    function clearForm() {
324
        WIDGET_FORM.find('label')
325
        .removeClass('has-error')
326
        .removeClass('has-success')
327
        .find('input, select')
328
        .each(function(_, input){
329
            $(input).val('');
330
        });
331
    }
332
333
    function deleteRow(row) {
334
        var rownum = row.find('.grid-row-label').data().row;
0 ignored issues
show
Unused Code introduced by
The variable rownum seems to be never used. Consider removing it.
Loading history...
335
        row.find('.item.widget').each(function(i, widget){
0 ignored issues
show
Unused Code introduced by
The parameter widget is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter i is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
336
            var guid = $(this).data().guid;
337
            var widget = my.widgets.get(guid).delete(true);
0 ignored issues
show
Unused Code introduced by
The variable widget seems to be never used. Consider removing it.
Loading history...
338
        });
339
        // Remove AFTER removing the charts contained within
340
        row.remove();
341
        updateRowOrder();
342
    }
343
344
    function populateEditForm(e) {
345
        // If the modal caller was the add modal button, skip populating the field.
346
        API_PREVIEW.text('...');
347
        clearForm();
348
        if(isModalButton(e) || isRowButton(e)) {
349
            DELETE_BTN.hide();
350
            if(isRowButton(e)) {
351
                var row = $(e.relatedTarget).data().row;
352
                populateRowField(row);
353
                // Trigger the order field update based on the current row
354
                WIDGET_FORM.find('[name="row"]').change();
355
            } else {
356
                populateRowField();
357
            }
358
            return;
359
        }
360
        DELETE_BTN.show();
361
        // Updates the fields in the edit form to the active widgets values.
362
        var item = $(e.relatedTarget).closest('.item.widget');
363
        var guid = item.data().guid;
364
        var widget = my.widgets.get(guid);
365
        var conf = widget.config;
366
        populateRowField(conf.row);
367
        // Update the modal fields with this widgets' value.
368
        $.each(conf, function(field, val){
369
            if(field === 'override' || field === 'refresh') {
370
                WIDGET_FORM.find('[name="' + field + '"]').prop('checked', val);
371
            } else {
372
                WIDGET_FORM.find('[name="' + field + '"]').val(val);
373
            }
374
        });
375
        // Update with current guid for referencing the module.
376
        WIDGET_FORM.attr('data-guid', guid);
377
        populateOrderField(widget);
378
        // Update form for specific row if row button was caller
379
        // Trigger event for select dropdown to ensure any UI is consistent.
380
        // This is done AFTER the fields have been pre-populated.
381
        WIDGET_FORM.find('[name="type"]').change();
382
    }
383
384
    function populateRowField(row) {
385
        var rows_field = $('[name="row"]');
386
        var num_rows = $('.grid-row').not('.grid-row-template').length;
387
        // Don't try and populate if not in freeform mode.
388
        if(my.layout === 'freeform') {return;}
389
        if(num_rows === 0){
390
            addNewRow();
391
        }
392
        rows_field.find('option').remove();
393
        // Add new option fields - d3 range is exclusive so we add one
394
        d3.map(d3.range(1, num_rows + 1), function(i){
395
            var option = $('<option></option>');
396
            option.val(i).text('row ' + i);
397
            rows_field.append(option);
398
        });
399
        // Update current value
400
        if(row) {rows_field.val(row)};
401
    }
402
403
    /**
404
     * [populateOrderField Destroy and re-create order dropdown input based on number of items in a row, or in a dashboard.]
405
     * @param  {[object]} config [The widget config (optional)]
0 ignored issues
show
Documentation introduced by
The parameter config does not exist. Did you maybe forget to remove this comment?
Loading history...
406
     */
407
    function populateOrderField(widget) {
408
        // Add the number of items to order field.
409
        var order_field = WIDGET_FORM.find('[name="order"]');
410
        var max_options = 0;
411
        if(my.layout === 'grid') {
412
            if(!widget) {
413
                var row = WIDGET_FORM.find('[name="row"]').val();
414
                // Get the max options based on the currently selected value in the row dropdown
415
                // We also add one since this is "adding" a new item so the order should include
416
                // one more than is currently there.
417
                max_options = $('.grid-row').eq(row - 1).find('.item.widget').length + 1;
418
            } else {
419
                // Get parent row and find number of widget children for this rows' order max
420
                max_options = $(widget.el[0]).closest('.grid-row').find('.item.widget').length;
421
            }
422
        } else {
423
            var widgets = $('.item.widget');
424
            max_options = widgets.length > 0 ? widgets.length: 2;
425
        }
426
        order_field.find('option').remove();
427
        // Add empty option.
428
        order_field.append('<option value=""></option>');
429
        d3.map(d3.range(1, max_options + 1), function(i){
430
            var option = $('<option></option>');
431
            option.val(i).text(i);
432
            order_field.append(option);
433
        });
434
        order_field.val(widget && widget.config ? widget.config.order : '');
435
    }
436
437
    /**
438
     * [getParsedFormConfig Get a config usable for each json widget based on the forms active values.]
439
     * @return {[object]} [The serialized config]
440
     */
441
    function getParsedFormConfig() {
442
        var conf = {};
443
        WIDGET_FORM.find('.form-control').each(function(_, input){
444
            var name = $(input).attr('name');
445
            var val = $(input).val();
446
            if(name === 'override' ||
447
                name === 'refresh') {
448
                // Convert checkbox to json friendly format.
449
                conf[name] = $(input).is(':checked');
450
            } else if(name === 'refreshInterval' ||
451
                      name === 'row' ||
452
                      name === 'height' ||
453
                      name === 'order') {
454
                conf[name] = parseInt(val, 10);
455
                if(isNaN(conf[name])) {
456
                    conf[name] = null;
457
                }
458
            } else {
459
                conf[name] = val;
460
            }
461
            // This is not amenable to integer parsing
462
            if(name === 'width' && my.layout === 'grid') {
463
                conf['width'] = val;
464
            }
465
        });
466
        return conf;
467
    }
468
469
    function onUpdateWidget(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
470
        var guid = WIDGET_FORM.attr('data-guid');
471
        var widget = my.widgets.get(guid);
472
        var conf = getParsedFormConfig();
473
        widget.update(conf);
474
    }
475
476
    function refreshWidget(e) {
477
        e.preventDefault();
478
        loadWidgetData(my.widgets.getByEl($(this).closest('.widget')));
479
        fitGrid();
480
    }
481
482
    /**
483
     * [isPreviewableType Determine if a chart type can be previewed in the 'preview api' section of the modal]
484
     * @param  {[type]}  string [The chart type]
0 ignored issues
show
Documentation introduced by
The parameter string does not exist. Did you maybe forget to remove this comment?
Loading history...
485
     * @return {Boolean}      [Whether or not it's previewable]
486
     */
487
    function isPreviewableType(type) {
488
        if(type === 'iframe') {return false;}
489
        if(type === 'youtube') {return false;}
490
        if(type === 'custom') {return false;}
491
        return true;
492
    }
493
494
    /**
495
     * [chartsTypeChanged Event handler for onChange event for chart type field]
496
     */
497
    function chartsTypeChanged(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
498
        var active_conf = getParsedFormConfig();
499
        var previewable = isPreviewableType(active_conf.type);
500
        togglePreviewOutput(previewable);
501
    }
502
503
    function chartsRowChanged(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
504
        // Update the order field based on the current rows item length.
505
        populateOrderField();
506
    }
507
508
    function loader(container) {
509
        container.select('.loader-overlay').classed({hidden: false});
510
        container.select('.widget-loader').classed({hidden: false});
511
    }
512
513
    function unload(container) {
514
        container.select('.loader-overlay').classed({hidden: true});
515
        container.select('.widget-loader').classed({hidden: true});
516
    }
517
518
    /**
519
     * [addDomEvents Add all dom event handlers here]
520
     */
521
    function addDomEvents() {
522
        WIDGET_FORM.find('[name="row"]').on('change.charts.row', chartsRowChanged);
523
        // Chart type change
524
        WIDGET_FORM.find('[name="type"]').on('change.charts.type', chartsTypeChanged);
525
        // TODO: debounce/throttle
526
        API_PREVIEW_BTN.on('click.charts', previewAPIRoute);
527
        // Save module popup form
528
        SAVE_WIDGET_BTN.on('click.charts.save', saveWidget);
529
        // Edit existing modules
530
        EDIT_MODAL.on('show.bs.modal', populateEditForm);
531
        UPDATE_FORM_BTN.on('click.charts.save', onUpdateWidget);
532
533
        // Allow swapping of edit/update events
534
        // for the add module button and form modal
535
        ADD_MODULE.on('click.charts', function(){
536
            UPDATE_FORM_BTN
537
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
538
            .text('Save widget')
539
            .off('click.charts.save')
540
            .on('click.charts.save', saveWidget);
541
        });
542
543
        // Allow swapping of edit/update events
544
        // for the add module per row button and form modal
545
        VIEW_BUILDER.on('click.charts', '.grid-row-label', function(){
546
            UPDATE_FORM_BTN
547
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
548
            .text('Save widget')
549
            .off('click.charts.save')
550
            .on('click.charts.save', saveWidget);
551
        });
552
553
        // Add delete button for existing widgets.
554
        DELETE_BTN.on('click.charts', function(e){
555
            e.preventDefault();
556
            var guid = WIDGET_FORM.attr('data-guid');
557
            var widget = my.widgets.get(guid).delete(false);
0 ignored issues
show
Unused Code introduced by
The variable widget seems to be never used. Consider removing it.
Loading history...
558
        });
559
        // Add delete confirm for dashboards.
560
        DELETE_DASHBOARD.on('submit.charts', function(e){
561
            if(!confirm('Are you sure?')) e.preventDefault();
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
562
        });
563
564
        // Format json config display
565
        $('#json-output').on('show.bs.modal', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
566
            var code = $(this).find('code').text();
567
            $(this).find('code').text(prettyCode(code));
568
        });
569
570
        // Add event for downloading json config raw.
571
        // Will provide decent support but still not major: http://caniuse.com/#search=download
572
        $('[href="#download-json"]').on('click', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
573
            var datestr = new Date().toString().replace(/ /gi, '-');
574
            var data = encodeURIComponent(JSON.stringify(JSON_DATA.val(), null, 4));
575
            data = "data:text/json;charset=utf-8," + data;
576
            $(this).attr('href', data);
577
            $(this).attr('download', 'charts-config-raw-' + datestr + '.json');
578
        });
579
580
        // For fixed grid, add events for making new rows.
581
        ADD_ROW_CONTS.find('.btn').on('click', addNewRow);
582
583
        EDIT_TOGGLE_BTN.on('click', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
584
            $('body').toggleClass('jsondash-editing');
585
            updateRowControls();
586
        });
587
588
        $('.delete-row').on('click', function(e){
589
            e.preventDefault();
590
            var row = $(this).closest('.grid-row');
591
            if(row.find('.item.widget').length > 0) {
592
                if(!confirm('Are you sure?')) {
593
                    return;
594
                }
595
            }
596
            deleteRow(row);
597
        });
598
    }
599
600
    function initFixedDragDrop(options) {
601
        var grid_drag_opts = {
602
            connectToSortable: '.grid-row'
603
        };
604
        $('.grid-row').droppable({
605
            drop: function(event, ui) {
606
                // update the widgets location
607
                var idx    = $(this).index();
608
                var el     = $(ui.draggable);
609
                var widget = my.widgets.getByEl(el);
610
                widget.update({row: idx}, true);
611
                // Actually move the dom element, and reset
612
                // the dragging css so it snaps into the row container
613
                el.parent().appendTo($(this));
614
                el.css({
615
                    position: 'relative',
616
                    top: 0,
617
                    left: 0
618
                });
619
            }
620
        });
621
        $('.item.widget').draggable($.extend(grid_drag_opts, options));
622
    }
623
624
    function fitGrid(grid_packer_opts, init) {
625
        var packer_options = $.isPlainObject(grid_packer_opts) ? grid_packer_opts : {};
626
        var grid_packer_options = $.extend({}, packer_options, {});
627
        var drag_options = {
628
            scroll: true,
629
            handle: '.dragger',
630
            start: function() {
631
                $('.grid-row').addClass('drag-target');
632
            },
633
            stop: function(){
634
                $('.grid-row').removeClass('drag-target');
635
                EDIT_CONTAINER.collapse('show');
636
                if(my.layout === 'grid') {
637
                    // Update row order.
638
                    updateChartsRowOrder();
639
                } else {
640
                    my.chart_wall.packery(grid_packer_options);
641
                    updateChartsOrder();
642
                }
643
            }
644
        };
645
        if(my.layout === 'grid' && $('.grid-row').length > 1) {
646
            initFixedDragDrop(drag_options);
647
            return;
648
        }
649
        if(init) {
650
            my.chart_wall = $('#container').packery(grid_packer_options);
651
            items = my.chart_wall.find('.item').draggable(drag_options);
0 ignored issues
show
Bug introduced by
The variable items seems to be never declared. Assigning variables without defining them first makes them global. If this was intended, consider making it explicit like using window.items.
Loading history...
652
            my.chart_wall.packery('bindUIDraggableEvents', items);
653
        } else {
654
            my.chart_wall.packery(grid_packer_options);
655
        }
656
    }
657
658
    function updateChartsOrder() {
659
        // Update the order and order value of each chart
660
        var items = my.chart_wall.packery('getItemElements');
661
        // Update module order
662
        $.each(items, function(i, el){
0 ignored issues
show
Unused Code introduced by
The parameter el is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
663
            var widget = my.widgets.getByEl($(this));
664
            widget.update({order: i}, true);
665
        });
666
    }
667
668
    function handleInputs(widget, config) {
669
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
670
        // Load event handlers for these newly created forms.
671
        $(inputs_selector).find('form').on('submit', function(e){
672
            e.stopImmediatePropagation();
673
            e.preventDefault();
674
            // Just create a new url for this, but use existing config.
675
            // The global object config will not be altered.
676
            // The first {} here is important, as it enforces a deep copy,
677
            // not a mutation of the original object.
678
            var url = config.dataSource;
679
            // Ensure we don't lose params already save on this endpoint url.
680
            var existing_params = url.split('?')[1];
681
            var params = jsondash.util.getValidParamString($(this).serializeArray());
682
            var _config = $.extend({}, config, {
0 ignored issues
show
Unused Code introduced by
The variable _config seems to be never used. Consider removing it.
Loading history...
683
                dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params
684
            });
685
            // Otherwise reload like normal.
686
            loadWidgetData(my.widgets.get(config.guid));
687
            // Hide the form again
688
            $(inputs_selector).removeClass('in');
689
        });
690
    }
691
692
    /**
693
     * [loadWidgetData Load a widgets data source/re-render]
694
     * @param  {[dom selection]} widget [The dom selection]
0 ignored issues
show
Documentation Bug introduced by
The parameter widget does not exist. Did you maybe mean widg instead?
Loading history...
695
     * @param  {[object]} config [The chart config]
0 ignored issues
show
Documentation introduced by
The parameter config does not exist. Did you maybe forget to remove this comment?
Loading history...
696
     */
697
    function loadWidgetData(widg) {
698
        var widget = widg.el;
699
        var config = widg.config;
700
701
        widget.classed({error: false});
702
        widget.select('.error-overlay')
703
            .classed({hidden: true})
704
            .select('.alert')
705
            .text('');
706
707
        loader(widget);
708
        try {
709
            // Handle any custom inputs the user specified for this module.
710
            // They map to standard form inputs and correspond to query
711
            // arguments for this dataSource.
712
            if(config.inputs) {handleInputs(widg, config);}
713
714
            if(config.type === 'datatable') {
715
                jsondash.handlers.handleDataTable(widget, config);
716
            }
717
            else if(jsondash.util.isSparkline(config.type)) {
718
                jsondash.handlers.handleSparkline(widget, config);
719
            }
720
            else if(config.type === 'iframe') {
721
                jsondash.handlers.handleIframe(widget, config);
722
            }
723
            else if(config.type === 'timeline') {
724
                jsondash.handlers.handleTimeline(widget, config);
725
            }
726
            else if(config.type === 'venn') {
727
                jsondash.handlers.handleVenn(widget, config);
728
            }
729
            else if(config.type === 'number') {
730
                jsondash.handlers.handleSingleNum(widget, config);
731
            }
732
            else if(config.type === 'youtube') {
733
                jsondash.handlers.handleYoutube(widget, config);
734
            }
735
            else if(config.type === 'graph'){
736
                jsondash.handlers.handleGraph(widget, config);
737
            }
738
            else if(config.type === 'custom') {
739
                jsondash.handlers.handleCustom(widget, config);
740
            }
741
            else if(config.type === 'wordcloud') {
742
                jsondash.handlers.handleWordCloud(widget, config);
743
            }
744
            else if(config.type === 'vega-lite') {
745
                jsondash.handlers.handleVegaLite(widget, config);
746
            }
747
            else if(config.type === 'plotly-any') {
748
                jsondash.handlers.handlePlotly(widget, config);
749
            }
750
            else if(jsondash.util.isD3Subtype(config)) {
751
                jsondash.handlers.handleD3(widget, config);
752
            } else {
753
                jsondash.handlers.handleC3(widget, config);
754
            }
755
        } catch(e) {
756
            if(console && console.error) console.error(e);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
757
            widget.classed({error: true});
758
            widget.select('.error-overlay')
759
                .classed({hidden: false})
760
                .select('.alert')
761
                .text('Loading error: "' + e + '"');
762
            unload(widget);
763
        }
764
        addResizeEvent(widg);
765
    }
766
767
    function addResizeEvent(widg) {
768
        // Add resize event
769
        var resize_opts = {
770
            helper: 'resizable-helper',
771
            minWidth: MIN_CHART_SIZE,
772
            minHeight: MIN_CHART_SIZE,
773
            maxWidth: VIEW_BUILDER.width(),
774
            handles: my.layout === 'grid' ? 's' : 'e, s, se',
775
            stop: function(event, ui) {
776
                var newconf = {height: ui.size.height};
777
                if(my.layout !== 'grid') {
778
                    newconf['width'] = ui.size.width;
779
                }
780
                // Update the configs dimensions.
781
                widg.update(newconf);
782
                fitGrid();
783
                // Open save panel
784
                EDIT_CONTAINER.collapse('show');
785
            }
786
        };
787
        // Add snap to grid (vertical only) in fixed grid mode.
788
        // This makes aligning charts easier because the snap points
789
        // are more likely to be consistent.
790
        if(my.layout === 'grid') {resize_opts['grid'] = 20;}
791
        $(widg.el[0]).resizable(resize_opts);
792
    }
793
794
    function prettyCode(code) {
795
        if(typeof code === "object") return JSON.stringify(code, null, 4);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
796
        return JSON.stringify(JSON.parse(code), null, 4);
797
    }
798
799
    function prettifyJSONPreview() {
800
        // Reformat the code inside of the raw json field,
801
        // to pretty print for the user.
802
        JSON_DATA.text(prettyCode(JSON_DATA.text()));
803
    }
804
805
    function addNewRow(e) {
806
        // Add a new row with a toggleable label that indicates
807
        // which row it is for user editing.
808
        var placement = 'top';
809
        if(e) {
810
            e.preventDefault();
811
            placement = $(this).closest('.row').data().rowPlacement;
812
        }
813
        var el = ROW_TEMPLATE.clone(true);
814
        el.removeClass('grid-row-template');
815
        if(placement === 'top') {
816
            VIEW_BUILDER.find('.add-new-row-container:first').after(el);
817
        } else {
818
            VIEW_BUILDER.find('.add-new-row-container:last').before(el);
819
        }
820
        // Update the row ordering text
821
        updateRowOrder();
822
        // Add new events for dragging/dropping
823
        fitGrid();
824
    }
825
826
    function updateChartsRowOrder() {
827
        // Update the row order for each chart.
828
        // This is necessary for cases like adding a new row,
829
        // where the order is updated (before or after) the current row.
830
        // NOTE: This function assumes the row order has been recalculated in advance!
831
        $('.grid-row').each(function(i, row){
832
            $(row).find('.item.widget').each(function(j, item){
833
                var widget = my.widgets.getByEl($(item));
834
                widget.update({row: i + 1, order: j + 1}, true);
835
            });
836
        });
837
    }
838
839
    function updateRowOrder() {
840
        $('.grid-row').not('.grid-row-template').each(function(i, row){
841
            var idx = $(row).index();
842
            $(row).find('.grid-row-label').attr('data-row', idx);
843
            $(row).find('.rownum').text(idx);
844
        });
845
        updateChartsRowOrder();
846
    }
847
848
    function loadDashboard(data) {
849
        // Load the grid before rendering the ajax, since the DOM
850
        // is rendered server side.
851
        fitGrid({
852
            columnWidth: 5,
853
            itemSelector: '.item',
854
            transitionDuration: 0,
855
            fitWidth: true
856
        }, true);
857
        $('.item.widget').removeClass('hidden');
858
859
        // Populate widgets with the config data.
860
        my.widgets.populate(data);
861
862
        // Load all widgets, adding actual ajax data.
863
        for(var guid in my.widgets.all()){
864
            loadWidgetData(my.widgets.get(guid));
865
        }
866
867
        // Setup responsive handlers
868
        var jres = jRespond([{
869
            label: 'handheld',
870
            enter: 0,
871
            exit: 767
872
        }]);
873
        jres.addFunc({
874
            breakpoint: 'handheld',
875
            enter: function() {
876
                $('.widget').css({
877
                    'max-width': '100%',
878
                    'width': '100%',
879
                    'position': 'static'
880
                });
881
            }
882
        });
883
        prettifyJSONPreview();
884
        populateRowField();
885
        fitGrid();
886
        if(isEmptyDashboard()) {EDIT_TOGGLE_BTN.click();}
887
    }
888
889
    /**
890
     * [updateRowControls Check each row's buttons and disable the "add" button if that row
891
     * is at the maximum colcount (12)]
892
     */
893
    function updateRowControls() {
894
        $('.grid-row').not('.grid-row-template').each(function(i, row){
895
            var count = getRowColCount($(row));
896
            if(count >= 12) {
897
                $(row).find('.grid-row-label').addClass('disabled');
898
            } else {
899
                $(row).find('.grid-row-label').removeClass('disabled');
900
            }
901
        });
902
    }
903
904
    /**
905
     * [getRowColCount Return the column count of a row.]
906
     * @param  {[dom selection]} row [The row selection]
907
     */
908
    function getRowColCount(row) {
909
        var count = 0;
910
        row.find('.item.widget').each(function(j, item){
911
            var classes = $(item).parent().attr('class').split(/\s+/);
912
            for(var i in classes) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
913
                if(classes[i].startsWith('col-md-')) {
914
                    count += parseInt(classes[i].replace('col-md-', ''), 10);
0 ignored issues
show
Bug introduced by
The variable count is changed as part of the for-each loop for example by parseInt(classes.i.replace("col-md-", ""), 10) on line 914. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
915
                }
916
            }
917
        });
918
        return count;
919
    }
920
921
    function isEmptyDashboard() {
922
        return $('.item.widget').length === 0;
923
    }
924
925
    my.config = {
926
        WIDGET_MARGIN_X: 20,
927
        WIDGET_MARGIN_Y: 60
928
    };
929
    my.loadDashboard = loadDashboard;
930
    my.handlers = {};
931
    my.util = {};
932
    my.loader = loader;
933
    my.unload = unload;
934
    my.addDomEvents = addDomEvents;
935
    my.getActiveConfig = getParsedFormConfig;
936
    my.layout = VIEW_BUILDER.length > 0 ? VIEW_BUILDER.data().layout : null;
937
    my.widgets = new Widgets();
938
    return my;
939
}();
940