Completed
Push — master ( 2d87c5...932c46 )
by Chris
01:23
created

app.js ➔ Widgets   A

Complexity

Conditions 1
Paths 2

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 1
c 5
b 0
f 0
dl 0
loc 52
rs 9.4929
nc 2
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            }
204
            widget.select('.widget-title .widget-title-text').text(self.config.name);
205
            // Update the form input for this widget.
206
            self._updateForm();
207
208
            if(!dont_refresh) {
209
                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...
210
                EDIT_CONTAINER.collapse();
211
                // Refit the grid
212
                fitGrid();
213
            } else {
214
                unload(widget);
215
            }
216
        };
217
        self._updateForm = function() {
218
            self.getInput().val(JSON.stringify(self.config));
219
        };
220
221
        // Run init script on creation
222
        self.init();
223
    }
224
225
    /**
226
     * [fillEmptyCols Fill in gaps in a row when an item has been deleted (fixed grid only)]
227
     */
228
    function fillEmptyCols(row) {
229
        row.each(function(_, row){
230
            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...
231
            var cols = $(row).find('> div');
232
            cols.filter(function(i, col){
233
                return $(col).find('.item.widget').length === 0;
234
            }).remove();
235
        });
236
    }
237
238
    function togglePreviewOutput(is_on) {
239
        if(is_on) {
240
            API_PREVIEW_CONT.show();
241
            return;
242
        }
243
        API_PREVIEW_CONT.hide();
244
    }
245
246
    function previewAPIRoute(e) {
247
        e.preventDefault();
248
        // Shows the response of the API field as a json payload, inline.
249
        $.ajax({
250
            type: 'get',
251
            url: API_ROUTE_URL.val().trim(),
252
            success: function(data) {
253
                API_PREVIEW.html(prettyCode(data));
254
            },
255
            error: function(data, status, error) {
256
                API_PREVIEW.html(error);
257
            }
258
        });
259
    }
260
261
    function refreshableType(type) {
262
        if(type === 'youtube') {return false;}
263
        return true;
264
    }
265
266
    function validateWidgetForm() {
267
        var is_valid = true;
268
        var url_field = WIDGET_FORM.find('[name="dataSource"]');
269
        WIDGET_FORM.find('[required]').each(function(i, el){
270
            if($(el).val() === '') {
271
                $(el).parent().addClass('has-error').removeClass('has-success');
272
                is_valid = false;
273
                return false;
274
            } else {
275
                $(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...
276
            }
277
        });
278
        // Validate youtube videos
279
        if(WIDGET_FORM.find('[name="type"]').val() === 'youtube') {
280
            if(!url_field.val().startsWith('<iframe')) {
281
                url_field.parent().addClass('has-error');
282
                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...
283
                return false;
284
            }
285
        }
286
        return is_valid;
287
    }
288
289
    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...
290
        if(!(validateWidgetForm())) {
291
            return false;
292
        }
293
        var new_config = my.widgets.newModel();
294
        // Remove empty rows and then update the order so it's consecutive.
295
        $('.grid-row').not('.grid-row-template').each(function(i, row){
296
            // Delete empty rows - except any empty rows that have been created
297
            // for the purpose of this new chart.
298
            if($(row).find('.item.widget').length === 0 && new_config.row !== i + 1) {
299
                $(row).remove();
300
            }
301
        });
302
        // Update the row orders after deleting empty ones
303
        updateRowOrder();
304
        var newfield = $('<input class="form-control" type="text">');
305
        // Add a unique guid for referencing later.
306
        newfield.attr('name', 'module_' + new_config.id);
307
        newfield.val(JSON.stringify(new_config));
308
        $('.modules').append(newfield);
309
        // Save immediately.
310
        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...
311
    }
312
313
    function isModalButton(e) {
314
        return e.relatedTarget.id === ADD_MODULE.selector.replace('#', '');
315
    }
316
317
    function isRowButton(e) {
318
        return $(e.relatedTarget).hasClass('grid-row-label');
319
    }
320
321
    function clearForm() {
322
        WIDGET_FORM.find('label')
323
        .removeClass('has-error')
324
        .removeClass('has-success')
325
        .find('input, select')
326
        .each(function(_, input){
327
            $(input).val('');
328
        });
329
    }
330
331
    function deleteRow(row) {
332
        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...
333
        row.find('.item.widget').each(function(i, widget){
0 ignored issues
show
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...
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...
334
            var guid = $(this).data().guid;
335
            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...
336
        });
337
        // Remove AFTER removing the charts contained within
338
        row.remove();
339
        updateRowOrder();
340
    }
341
342
    function populateEditForm(e) {
343
        // If the modal caller was the add modal button, skip populating the field.
344
        API_PREVIEW.text('...');
345
        clearForm();
346
        if(isModalButton(e) || isRowButton(e)) {
347
            DELETE_BTN.hide();
348
            if(isRowButton(e)) {
349
                var row = $(e.relatedTarget).data().row;
350
                populateRowField(row);
351
                // Trigger the order field update based on the current row
352
                WIDGET_FORM.find('[name="row"]').change();
353
            } else {
354
                populateRowField();
355
            }
356
            return;
357
        }
358
        DELETE_BTN.show();
359
        // Updates the fields in the edit form to the active widgets values.
360
        var item = $(e.relatedTarget).closest('.item.widget');
361
        var guid = item.data().guid;
362
        var widget = my.widgets.get(guid);
363
        var conf = widget.config;
364
        populateRowField(conf.row);
365
        // Update the modal fields with this widgets' value.
366
        $.each(conf, function(field, val){
367
            if(field === 'override' || field === 'refresh') {
368
                WIDGET_FORM.find('[name="' + field + '"]').prop('checked', val);
369
            } else {
370
                WIDGET_FORM.find('[name="' + field + '"]').val(val);
371
            }
372
        });
373
        // Update with current guid for referencing the module.
374
        WIDGET_FORM.attr('data-guid', guid);
375
        populateOrderField(widget);
376
        // Update form for specific row if row button was caller
377
        // Trigger event for select dropdown to ensure any UI is consistent.
378
        // This is done AFTER the fields have been pre-populated.
379
        WIDGET_FORM.find('[name="type"]').change();
380
    }
381
382
    function populateRowField(row) {
383
        var rows_field = $('[name="row"]');
384
        var num_rows = $('.grid-row').not('.grid-row-template').length;
385
        // Don't try and populate if not in freeform mode.
386
        if(my.layout === 'freeform') {return;}
387
        if(num_rows === 0){
388
            addNewRow();
389
        }
390
        rows_field.find('option').remove();
391
        // Add new option fields - d3 range is exclusive so we add one
392
        d3.map(d3.range(1, num_rows + 1), function(i){
393
            var option = $('<option></option>');
394
            option.val(i).text('row ' + i);
395
            rows_field.append(option);
396
        });
397
        // Update current value
398
        if(row) {rows_field.val(row)};
399
    }
400
401
    /**
402
     * [populateOrderField Destroy and re-create order dropdown input based on number of items in a row, or in a dashboard.]
403
     * @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...
404
     */
405
    function populateOrderField(widget) {
406
        // Add the number of items to order field.
407
        var order_field = WIDGET_FORM.find('[name="order"]');
408
        var max_options = 0;
409
        if(my.layout === 'grid') {
410
            if(!widget) {
411
                var row = WIDGET_FORM.find('[name="row"]').val();
412
                // Get the max options based on the currently selected value in the row dropdown
413
                // We also add one since this is "adding" a new item so the order should include
414
                // one more than is currently there.
415
                max_options = $('.grid-row').eq(row - 1).find('.item.widget').length + 1;
416
            } else {
417
                // Get parent row and find number of widget children for this rows' order max
418
                max_options = $(widget.el[0]).closest('.grid-row').find('.item.widget').length;
419
            }
420
        } else {
421
            var widgets = $('.item.widget');
422
            max_options = widgets.length > 0 ? widgets.length: 2;
423
        }
424
        order_field.find('option').remove();
425
        // Add empty option.
426
        order_field.append('<option value=""></option>');
427
        d3.map(d3.range(1, max_options + 1), function(i){
428
            var option = $('<option></option>');
429
            option.val(i).text(i);
430
            order_field.append(option);
431
        });
432
        order_field.val(widget && widget.config ? widget.config.order : '');
433
    }
434
435
    /**
436
     * [getParsedFormConfig Get a config usable for each json widget based on the forms active values.]
437
     * @return {[object]} [The serialized config]
438
     */
439
    function getParsedFormConfig() {
440
        var conf = {};
441
        WIDGET_FORM.find('.form-control').each(function(_, input){
442
            var name = $(input).attr('name');
443
            var val = $(input).val();
444
            if(name === 'override' ||
445
                name === 'refresh') {
446
                // Convert checkbox to json friendly format.
447
                conf[name] = $(input).is(':checked');
448
            } else if(name === 'refreshInterval' ||
449
                      name === 'row' ||
450
                      name === 'height' ||
451
                      name === 'order') {
452
                conf[name] = parseInt(val, 10);
453
                if(isNaN(conf[name])) {
454
                    conf[name] = null;
455
                }
456
            } else {
457
                conf[name] = val;
458
            }
459
            // This is not amenable to integer parsing
460
            if(name === 'width' && my.layout === 'grid') {
461
                conf['width'] = val;
462
            }
463
        });
464
        return conf;
465
    }
466
467
    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...
468
        var guid = WIDGET_FORM.attr('data-guid');
469
        var widget = my.widgets.get(guid);
470
        var conf = getParsedFormConfig();
471
        widget.update(conf);
472
    }
473
474
    function refreshWidget(e) {
475
        e.preventDefault();
476
        loadWidgetData(my.widgets.getByEl($(this).closest('.widget')));
477
        fitGrid();
478
    }
479
480
    /**
481
     * [isPreviewableType Determine if a chart type can be previewed in the 'preview api' section of the modal]
482
     * @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...
483
     * @return {Boolean}      [Whether or not it's previewable]
484
     */
485
    function isPreviewableType(type) {
486
        if(type === 'iframe') {return false;}
487
        if(type === 'youtube') {return false;}
488
        if(type === 'custom') {return false;}
489
        return true;
490
    }
491
492
    /**
493
     * [chartsTypeChanged Event handler for onChange event for chart type field]
494
     */
495
    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...
496
        var active_conf = getParsedFormConfig();
497
        var previewable = isPreviewableType(active_conf.type);
498
        togglePreviewOutput(previewable);
499
    }
500
501
    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...
502
        // Update the order field based on the current rows item length.
503
        populateOrderField();
504
    }
505
506
    function loader(container) {
507
        container.select('.loader-overlay').classed({hidden: false});
508
        container.select('.widget-loader').classed({hidden: false});
509
    }
510
511
    function unload(container) {
512
        container.select('.loader-overlay').classed({hidden: true});
513
        container.select('.widget-loader').classed({hidden: true});
514
    }
515
516
    /**
517
     * [addDomEvents Add all dom event handlers here]
518
     */
519
    function addDomEvents() {
520
        WIDGET_FORM.find('[name="row"]').on('change.charts.row', chartsRowChanged);
521
        // Chart type change
522
        WIDGET_FORM.find('[name="type"]').on('change.charts.type', chartsTypeChanged);
523
        // TODO: debounce/throttle
524
        API_PREVIEW_BTN.on('click.charts', previewAPIRoute);
525
        // Save module popup form
526
        SAVE_WIDGET_BTN.on('click.charts.save', saveWidget);
527
        // Edit existing modules
528
        EDIT_MODAL.on('show.bs.modal', populateEditForm);
529
        UPDATE_FORM_BTN.on('click.charts.save', onUpdateWidget);
530
531
        // Allow swapping of edit/update events
532
        // for the add module button and form modal
533
        ADD_MODULE.on('click.charts', function(){
534
            UPDATE_FORM_BTN
535
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
536
            .text('Save widget')
537
            .off('click.charts.save')
538
            .on('click.charts.save', saveWidget);
539
        });
540
541
        // Allow swapping of edit/update events
542
        // for the add module per row button and form modal
543
        VIEW_BUILDER.on('click.charts', '.grid-row-label', function(){
544
            UPDATE_FORM_BTN
545
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
546
            .text('Save widget')
547
            .off('click.charts.save')
548
            .on('click.charts.save', saveWidget);
549
        });
550
551
        // Add delete button for existing widgets.
552
        DELETE_BTN.on('click.charts', function(e){
553
            e.preventDefault();
554
            var guid = WIDGET_FORM.attr('data-guid');
555
            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...
556
        });
557
        // Add delete confirm for dashboards.
558
        DELETE_DASHBOARD.on('submit.charts', function(e){
559
            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...
560
        });
561
    }
562
563
    function initFixedDragDrop(options) {
564
        var grid_drag_opts = {
565
            connectToSortable: '.grid-row'
566
        };
567
        $('.grid-row').droppable({
568
            drop: function(event, ui) {
569
                // update the widgets location
570
                var idx    = $(this).index();
571
                var el     = $(ui.draggable);
572
                var widget = my.widgets.getByEl(el);
573
                widget.update({row: idx}, true);
574
                // Actually move the dom element, and reset
575
                // the dragging css so it snaps into the row container
576
                el.parent().appendTo($(this));
577
                el.css({
578
                    position: 'relative',
579
                    top: 0,
580
                    left: 0
581
                });
582
            }
583
        });
584
        $('.item.widget').draggable($.extend(grid_drag_opts, options));
585
    }
586
587
    function fitGrid(grid_packer_opts, init) {
588
        var packer_options = $.isPlainObject(grid_packer_opts) ? grid_packer_opts : {};
589
        var grid_packer_options = $.extend({}, packer_options, {});
590
        var drag_options = {
591
            scroll: true,
592
            handle: '.dragger',
593
            start: function() {
594
                $('.grid-row').addClass('drag-target');
595
            },
596
            stop: function(){
597
                $('.grid-row').removeClass('drag-target');
598
                EDIT_CONTAINER.collapse('show');
599
                if(my.layout === 'grid') {
600
                    // Update row order.
601
                    updateChartsRowOrder();
602
                } else {
603
                    my.chart_wall.packery(grid_packer_options);
604
                    updateChartsOrder();
605
                }
606
            }
607
        };
608
        if(my.layout === 'grid' && $('.grid-row').length > 1) {
609
            initFixedDragDrop(drag_options);
610
            return;
611
        }
612
        if(init) {
613
            my.chart_wall = $('#container').packery(grid_packer_options);
614
            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...
615
            my.chart_wall.packery('bindUIDraggableEvents', items);
616
        } else {
617
            my.chart_wall.packery(grid_packer_options);
618
        }
619
    }
620
621
    function updateChartsOrder() {
622
        // Update the order and order value of each chart
623
        var items = my.chart_wall.packery('getItemElements');
624
        // Update module order
625
        $.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...
626
            var widget = my.widgets.getByEl($(this));
627
            widget.update({order: i}, true);
628
        });
629
    }
630
631
    function handleInputs(widget, config) {
632
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
633
        // Load event handlers for these newly created forms.
634
        $(inputs_selector).find('form').on('submit', function(e){
635
            e.stopImmediatePropagation();
636
            e.preventDefault();
637
            // Just create a new url for this, but use existing config.
638
            // The global object config will not be altered.
639
            // The first {} here is important, as it enforces a deep copy,
640
            // not a mutation of the original object.
641
            var url = config.dataSource;
642
            // Ensure we don't lose params already save on this endpoint url.
643
            var existing_params = url.split('?')[1];
644
            var params = jsondash.util.getValidParamString($(this).serializeArray());
645
            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...
646
                dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params
647
            });
648
            // Otherwise reload like normal.
649
            loadWidgetData(my.widgets.get(config.guid));
650
            // Hide the form again
651
            $(inputs_selector).removeClass('in');
652
        });
653
    }
654
655
    /**
656
     * [loadWidgetData Load a widgets data source/re-render]
657
     * @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...
658
     * @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...
659
     */
660
    function loadWidgetData(widg) {
661
        var widget = widg.el;
662
        var config = widg.config;
663
664
        widget.classed({error: false});
665
        widget.select('.error-overlay')
666
            .classed({hidden: true})
667
            .select('.alert')
668
            .text('');
669
670
        loader(widget);
671
        try {
672
            // Handle any custom inputs the user specified for this module.
673
            // They map to standard form inputs and correspond to query
674
            // arguments for this dataSource.
675
            if(config.inputs) {handleInputs(widg, config);}
676
677
            if(config.type === 'datatable') {
678
                jsondash.handlers.handleDataTable(widget, config);
679
            }
680
            else if(jsondash.util.isSparkline(config.type)) {
681
                jsondash.handlers.handleSparkline(widget, config);
682
            }
683
            else if(config.type === 'iframe') {
684
                jsondash.handlers.handleIframe(widget, config);
685
            }
686
            else if(config.type === 'timeline') {
687
                jsondash.handlers.handleTimeline(widget, config);
688
            }
689
            else if(config.type === 'venn') {
690
                jsondash.handlers.handleVenn(widget, config);
691
            }
692
            else if(config.type === 'number') {
693
                jsondash.handlers.handleSingleNum(widget, config);
694
            }
695
            else if(config.type === 'youtube') {
696
                jsondash.handlers.handleYoutube(widget, config);
697
            }
698
            else if(config.type === 'graph'){
699
                jsondash.handlers.handleGraph(widget, config);
700
            }
701
            else if(config.type === 'custom') {
702
                jsondash.handlers.handleCustom(widget, config);
703
            }
704
            else if(config.type === 'wordcloud') {
705
                jsondash.handlers.handleWordCloud(widget, config);
706
            }
707
            else if(config.type === 'plotly-any') {
708
                jsondash.handlers.handlePlotly(widget, config);
709
            }
710
            else if(jsondash.util.isD3Subtype(config)) {
711
                jsondash.handlers.handleD3(widget, config);
712
            } else {
713
                jsondash.handlers.handleC3(widget, config);
714
            }
715
        } catch(e) {
716
            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...
717
            widget.classed({error: true});
718
            widget.select('.error-overlay')
719
                .classed({hidden: false})
720
                .select('.alert')
721
                .text('Loading error: "' + e + '"');
722
            unload(widget);
723
        }
724
        addResizeEvent(widg);
725
    }
726
727
    function addResizeEvent(widg) {
728
        // Add resize event
729
        var resize_opts = {
730
            helper: 'resizable-helper',
731
            minWidth: MIN_CHART_SIZE,
732
            minHeight: MIN_CHART_SIZE,
733
            maxWidth: VIEW_BUILDER.width(),
734
            handles: my.layout === 'grid' ? 's' : 'e, s, se',
735
            stop: function(event, ui) {
736
                var newconf = {height: ui.size.height};
737
                if(my.layout !== 'grid') {
738
                    newconf['width'] = ui.size.width;
739
                }
740
                // Update the configs dimensions.
741
                widg.update(newconf);
742
                loadWidgetData(widg);
743
                fitGrid();
744
                // Open save panel
745
                EDIT_CONTAINER.collapse('show');
746
            }
747
        };
748
        // Add snap to grid (vertical only) in fixed grid mode.
749
        // This makes aligning charts easier because the snap points
750
        // are more likely to be consistent.
751
        if(my.layout === 'grid') {resize_opts['grid'] = 20;}
752
        $(widg.el[0]).resizable(resize_opts);
753
    }
754
755
    function prettyCode(code) {
756
        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...
757
        return JSON.stringify(JSON.parse(code), null, 4);
758
    }
759
760
    function prettifyJSONPreview() {
761
        // Reformat the code inside of the raw json field,
762
        // to pretty print for the user.
763
        JSON_DATA.text(prettyCode(JSON_DATA.text()));
764
    }
765
766
    function addNewRow(e) {
767
        // Add a new row with a toggleable label that indicates
768
        // which row it is for user editing.
769
        var placement = 'top';
770
        if(e) {
771
            e.preventDefault();
772
            placement = $(this).closest('.row').data().rowPlacement;
773
        }
774
        var el = ROW_TEMPLATE.clone(true);
775
        el.removeClass('grid-row-template');
776
        if(placement === 'top') {
777
            VIEW_BUILDER.find('.add-new-row-container:first').after(el);
778
        } else {
779
            VIEW_BUILDER.find('.add-new-row-container:last').before(el);
780
        }
781
        // Update the row ordering text
782
        updateRowOrder();
783
        // Add new events for dragging/dropping
784
        fitGrid();
785
    }
786
787
    function updateChartsRowOrder() {
788
        // Update the row order for each chart.
789
        // This is necessary for cases like adding a new row,
790
        // where the order is updated (before or after) the current row.
791
        // NOTE: This function assumes the row order has been recalculated in advance!
792
        $('.grid-row').each(function(i, row){
793
            $(row).find('.item.widget').each(function(j, item){
794
                var widget = my.widgets.getByEl($(item));
795
                widget.update({row: i + 1, order: j + 1}, true);
796
            });
797
        });
798
    }
799
800
    function updateRowOrder() {
801
        $('.grid-row').not('.grid-row-template').each(function(i, row){
802
            var idx = $(row).index();
803
            $(row).find('.grid-row-label').attr('data-row', idx);
804
            $(row).find('.rownum').text(idx);
805
        });
806
        updateChartsRowOrder();
807
    }
808
809
    function loadDashboard(data) {
810
        // Load the grid before rendering the ajax, since the DOM
811
        // is rendered server side.
812
        fitGrid({
813
            columnWidth: 5,
814
            itemSelector: '.item',
815
            transitionDuration: 0,
816
            fitWidth: true
817
        }, true);
818
        $('.item.widget').removeClass('hidden');
819
820
        // Populate widgets with the config data.
821
        my.widgets.populate(data);
822
823
        // Add actual ajax data.
824
        for(var guid in my.widgets.all()){
825
            loadWidgetData(my.widgets.get(guid));
826
        }
827
828
        // Format json config display
829
        $('#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...
830
            var code = $(this).find('code').text();
831
            $(this).find('code').text(prettyCode(code));
832
        });
833
834
        // Add event for downloading json config raw.
835
        // Will provide decent support but still not major: http://caniuse.com/#search=download
836
        $('[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...
837
            var datestr = new Date().toString().replace(/ /gi, '-');
838
            var data = encodeURIComponent(JSON.stringify(JSON_DATA.val(), null, 4));
839
            data = "data:text/json;charset=utf-8," + data;
840
            $(this).attr('href', data);
841
            $(this).attr('download', 'charts-config-raw-' + datestr + '.json');
842
        });
843
844
        // For fixed grid, add events for making new rows.
845
        ADD_ROW_CONTS.find('.btn').on('click', addNewRow);
846
847
        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...
848
            $('body').toggleClass('jsondash-editing');
849
            updateRowControls();
850
        });
851
852
        $('.delete-row').on('click', function(e){
853
            e.preventDefault();
854
            var row = $(this).closest('.grid-row');
855
            if(row.find('.item.widget').length > 0) {
856
                if(!confirm('Are you sure?')) {
857
                    return;
858
                }
859
            }
860
            deleteRow(row);
861
        });
862
        // Setup responsive handlers
863
        var jres = jRespond([{
864
            label: 'handheld',
865
            enter: 0,
866
            exit: 767
867
        }]);
868
        jres.addFunc({
869
            breakpoint: 'handheld',
870
            enter: function() {
871
                $('.widget').css({
872
                    'max-width': '100%',
873
                    'width': '100%',
874
                    'position': 'static'
875
                });
876
            }
877
        });
878
        prettifyJSONPreview();
879
        populateRowField();
880
        fitGrid();
881
        if(isEmptyDashboard()) {EDIT_TOGGLE_BTN.click();}
882
    }
883
884
    /**
885
     * [updateRowControls Check each row's buttons and disable the "add" button if that row
886
     * is at the maximum colcount (12)]
887
     */
888
    function updateRowControls() {
889
        $('.grid-row').not('.grid-row-template').each(function(i, row){
890
            var count = getRowColCount($(row));
891
            if(count >= 12) {
892
                $(row).find('.grid-row-label').addClass('disabled');
893
            } else {
894
                $(row).find('.grid-row-label').removeClass('disabled');
895
            }
896
        });
897
    }
898
899
    /**
900
     * [getRowColCount Return the column count of a row.]
901
     * @param  {[dom selection]} row [The row selection]
902
     */
903
    function getRowColCount(row) {
904
        var count = 0;
905
        row.find('.item.widget').each(function(j, item){
906
            var classes = $(item).parent().attr('class').split(/\s+/);
907
            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...
908
                if(classes[i].startsWith('col-md-')) {
909
                    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 909. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
910
                }
911
            }
912
        });
913
        return count;
914
    }
915
916
    function isEmptyDashboard() {
917
        return $('.item.widget').length === 0;
918
    }
919
920
    my.config = {
921
        WIDGET_MARGIN_X: 20,
922
        WIDGET_MARGIN_Y: 60
923
    };
924
    my.loadDashboard = loadDashboard;
925
    my.handlers = {};
926
    my.util = {};
927
    my.loader = loader;
928
    my.unload = unload;
929
    my.addDomEvents = addDomEvents;
930
    my.getActiveConfig = getParsedFormConfig;
931
    my.layout = VIEW_BUILDER.length > 0 ? VIEW_BUILDER.data().layout : null;
932
    my.widgets = new Widgets();
933
    return my;
934
}();
935