Completed
Push — master ( 684a71...5716d7 )
by Chris
01:36
created

app.js ➔ populateOrderField   B

Complexity

Conditions 6
Paths 16

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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