Completed
Push — master ( 99c046...8312b6 )
by Chris
01:20
created

app.js ➔ loadWidgetData   F

Complexity

Conditions 22
Paths 348

Size

Total Lines 90

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 22
c 6
b 0
f 0
nc 348
nop 1
dl 0
loc 90
rs 3.5977

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like app.js ➔ loadWidgetData often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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