Completed
Push — master ( a5a3aa...a855fe )
by Chris
01:15
created

app.js ➔ ... ➔ parseNum   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
nc 2
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 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
        populateOrderField(widget);
381
        // Update form for specific row if row button was caller
382
        // Trigger event for select dropdown to ensure any UI is consistent.
383
        // This is done AFTER the fields have been pre-populated.
384
        WIDGET_FORM.find('[name="type"]').change();
385
        // A trigger for 3rd-party/external js to use to listen to.
386
        WIDGET_FORM.trigger(EVENTS.edit_form_loaded);
387
    }
388
389
    function populateRowField(row) {
390
        var rows_field = $('[name="row"]');
391
        var num_rows = $('.grid-row').not('.grid-row-template').length;
392
        // Don't try and populate if not in freeform mode.
393
        if(my.layout === 'freeform') {return;}
394
        if(num_rows === 0){
395
            addNewRow();
396
        }
397
        rows_field.find('option').remove();
398
        // Add new option fields - d3 range is exclusive so we add one
399
        d3.map(d3.range(1, num_rows + 1), function(i){
400
            var option = $('<option></option>');
401
            option.val(i).text('row ' + i);
402
            rows_field.append(option);
403
        });
404
        // Update current value
405
        if(row) {rows_field.val(row)};
406
    }
407
408
    /**
409
     * [populateOrderField Destroy and re-create order dropdown input based on number of items in a row, or in a dashboard.]
410
     * @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...
411
     */
412
    function populateOrderField(widget) {
413
        // Add the number of items to order field.
414
        var order_field = WIDGET_FORM.find('[name="order"]');
415
        var max_options = 0;
416
        if(my.layout === 'grid') {
417
            if(!widget) {
418
                var row = WIDGET_FORM.find('[name="row"]').val();
419
                // Get the max options based on the currently selected value in the row dropdown
420
                // We also add one since this is "adding" a new item so the order should include
421
                // one more than is currently there.
422
                max_options = $('.grid-row').eq(row - 1).find('.item.widget').length + 1;
423
            } else {
424
                // Get parent row and find number of widget children for this rows' order max
425
                max_options = $(widget.el[0]).closest('.grid-row').find('.item.widget').length;
426
            }
427
        } else {
428
            var widgets = $('.item.widget');
429
            max_options = widgets.length > 0 ? widgets.length: 2;
430
        }
431
        order_field.find('option').remove();
432
        // Add empty option.
433
        order_field.append('<option value=""></option>');
434
        d3.map(d3.range(1, max_options + 1), function(i){
435
            var option = $('<option></option>');
436
            option.val(i).text(i);
437
            order_field.append(option);
438
        });
439
        order_field.val(widget && widget.config ? widget.config.order : '');
440
    }
441
442
    /**
443
     * [getParsedFormConfig Get a config usable for each json widget based on the forms active values.]
444
     * @return {[object]} [The serialized config]
445
     */
446
    function getParsedFormConfig() {
447
        function parseNum(num) {
448
            // Like parseInt, but always returns a Number.
449
            if(isNaN(parseInt(num, 10))) {
450
                return 0;
451
            }
452
            return parseInt(num, 10);
453
        }
454
        var form = WIDGET_FORM;
455
        var conf = {
456
            name: form.find('[name="name"]').val(),
457
            type: form.find('[name="type"]').val(),
458
            family: form.find('[name="type"]').find('option:checked').data() ? form.find('[name="type"]').find('option:checked').data().family : null,
459
            row: form.find('[name="row"]').val(),
460
            width: form.find('[name="width"]').val(),
461
            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...
462
            dataSource: form.find('[name="dataSource"]').val(),
463
            override: form.find('[name="override"]').is(':checked'),
464
            order: parseNum(form.find('[name="order"]').val(), 10),
465
            refresh: form.find('[name="refresh"]').is(':checked'),
466
            refreshInterval: jsondash.util.intervalStrToMS(form.find('[name="refreshInterval"]').val()),
467
        };
468
        return conf;
469
    }
470
471
    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...
472
        var guid = WIDGET_FORM.attr('data-guid');
473
        var widget = my.widgets.get(guid);
474
        var conf = getParsedFormConfig();
475
        widget.update(conf);
476
    }
477
478
    function refreshWidget(e) {
479
        e.preventDefault();
480
        loadWidgetData(my.widgets.getByEl($(this).closest('.widget')));
481
        fitGrid();
482
    }
483
484
    /**
485
     * [isPreviewableType Determine if a chart type can be previewed in the 'preview api' section of the modal]
486
     * @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...
487
     * @return {Boolean}      [Whether or not it's previewable]
488
     */
489
    function isPreviewableType(type) {
490
        if(type === 'iframe') {return false;}
491
        if(type === 'youtube') {return false;}
492
        if(type === 'custom') {return false;}
493
        return true;
494
    }
495
496
    /**
497
     * [chartsTypeChanged Event handler for onChange event for chart type field]
498
     */
499
    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...
500
        var active_conf = getParsedFormConfig();
501
        var previewable = isPreviewableType(active_conf.type);
502
        togglePreviewOutput(previewable);
503
    }
504
505
    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...
506
        // Update the order field based on the current rows item length.
507
        populateOrderField();
508
    }
509
510
    function loader(container) {
511
        container.select('.loader-overlay').classed({hidden: false});
512
        container.select('.widget-loader').classed({hidden: false});
513
    }
514
515
    function unload(container) {
516
        container.select('.loader-overlay').classed({hidden: true});
517
        container.select('.widget-loader').classed({hidden: true});
518
    }
519
520
    /**
521
     * [addDomEvents Add all dom event handlers here]
522
     */
523
    function addDomEvents() {
524
        WIDGET_FORM.find('[name="row"]').on('change.charts.row', chartsRowChanged);
525
        // Chart type change
526
        WIDGET_FORM.find('[name="type"]').on('change.charts.type', chartsTypeChanged);
527
        // TODO: debounce/throttle
528
        API_PREVIEW_BTN.on('click.charts', previewAPIRoute);
529
        // Save module popup form
530
        SAVE_WIDGET_BTN.on('click.charts.save', saveWidget);
531
        // Edit existing modules
532
        EDIT_MODAL.on('show.bs.modal', populateEditForm);
533
        UPDATE_FORM_BTN.on('click.charts.save', onUpdateWidget);
534
535
        // Allow swapping of edit/update events
536
        // for the add module button and form modal
537
        ADD_MODULE.on('click.charts', function(){
538
            UPDATE_FORM_BTN
539
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
540
            .text('Save widget')
541
            .off('click.charts.save')
542
            .on('click.charts.save', saveWidget);
543
        });
544
545
        // Allow swapping of edit/update events
546
        // for the add module per row button and form modal
547
        VIEW_BUILDER.on('click.charts', '.grid-row-label', function(){
548
            UPDATE_FORM_BTN
549
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
550
            .text('Save widget')
551
            .off('click.charts.save')
552
            .on('click.charts.save', saveWidget);
553
        });
554
555
        // Add delete button for existing widgets.
556
        DELETE_BTN.on('click.charts', function(e){
557
            e.preventDefault();
558
            var guid = WIDGET_FORM.attr('data-guid');
559
            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...
560
        });
561
        // Add delete confirm for dashboards.
562
        DELETE_DASHBOARD.on('submit.charts', function(e){
563
            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...
564
        });
565
566
        // Format json config display
567
        $('#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...
568
            var code = $(this).find('code').text();
569
            $(this).find('code').text(prettyCode(code));
570
        });
571
572
        // Add event for downloading json config raw.
573
        // Will provide decent support but still not major: http://caniuse.com/#search=download
574
        $('[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...
575
            var datestr = new Date().toString().replace(/ /gi, '-');
576
            var data = encodeURIComponent(JSON.stringify(JSON_DATA.val(), null, 4));
577
            data = "data:text/json;charset=utf-8," + data;
578
            $(this).attr('href', data);
579
            $(this).attr('download', 'charts-config-raw-' + datestr + '.json');
580
        });
581
582
        // For fixed grid, add events for making new rows.
583
        ADD_ROW_CONTS.find('.btn').on('click', addNewRow);
584
585
        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...
586
            $('body').toggleClass('jsondash-editing');
587
            updateRowControls();
588
        });
589
590
        $('.delete-row').on('click', function(e){
591
            e.preventDefault();
592
            var row = $(this).closest('.grid-row');
593
            if(row.find('.item.widget').length > 0) {
594
                if(!confirm('Are you sure?')) {
595
                    return;
596
                }
597
            }
598
            deleteRow(row);
599
        });
600
    }
601
602
    function initFixedDragDrop(options) {
603
        var grid_drag_opts = {
604
            connectToSortable: '.grid-row'
605
        };
606
        $('.grid-row').droppable({
607
            drop: function(event, ui) {
608
                // update the widgets location
609
                var idx    = $(this).index();
610
                var el     = $(ui.draggable);
611
                var widget = my.widgets.getByEl(el);
612
                widget.update({row: idx}, true);
613
                // Actually move the dom element, and reset
614
                // the dragging css so it snaps into the row container
615
                el.parent().appendTo($(this));
616
                el.css({
617
                    position: 'relative',
618
                    top: 0,
619
                    left: 0
620
                });
621
            }
622
        });
623
        $('.item.widget').draggable($.extend(grid_drag_opts, options));
624
    }
625
626
    function fitGrid(grid_packer_opts, init) {
627
        var packer_options = $.isPlainObject(grid_packer_opts) ? grid_packer_opts : {};
628
        var grid_packer_options = $.extend({}, packer_options, {});
629
        var drag_options = {
630
            scroll: true,
631
            handle: '.dragger',
632
            start: function() {
633
                $('.grid-row').addClass('drag-target');
634
            },
635
            stop: function(){
636
                $('.grid-row').removeClass('drag-target');
637
                EDIT_CONTAINER.collapse('show');
638
                if(my.layout === 'grid') {
639
                    // Update row order.
640
                    updateChartsRowOrder();
641
                } else {
642
                    my.chart_wall.packery(grid_packer_options);
643
                    updateChartsOrder();
644
                }
645
            }
646
        };
647
        if(my.layout === 'grid' && $('.grid-row').length > 1) {
648
            initFixedDragDrop(drag_options);
649
            return;
650
        }
651
        if(init) {
652
            my.chart_wall = $('#container').packery(grid_packer_options);
653
            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...
654
            my.chart_wall.packery('bindUIDraggableEvents', items);
655
        } else {
656
            my.chart_wall.packery(grid_packer_options);
657
        }
658
    }
659
660
    function updateChartsOrder() {
661
        // Update the order and order value of each chart
662
        var items = my.chart_wall.packery('getItemElements');
663
        // Update module order
664
        $.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...
665
            var widget = my.widgets.getByEl($(this));
666
            widget.update({order: i}, true);
667
        });
668
    }
669
670
    function handleInputs(widget, config) {
671
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
672
        // Load event handlers for these newly created forms.
673
        $(inputs_selector).find('form').on('submit', function(e){
674
            e.stopImmediatePropagation();
675
            e.preventDefault();
676
            // Just create a new url for this, but use existing config.
677
            // The global object config will not be altered.
678
            // The first {} here is important, as it enforces a deep copy,
679
            // not a mutation of the original object.
680
            var url = config.dataSource;
681
            // Ensure we don't lose params already save on this endpoint url.
682
            var existing_params = url.split('?')[1];
683
            var params = jsondash.util.getValidParamString($(this).serializeArray());
684
            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...
685
                dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params
686
            });
687
            // Otherwise reload like normal.
688
            loadWidgetData(my.widgets.get(config.guid));
689
            // Hide the form again
690
            $(inputs_selector).removeClass('in');
691
        });
692
    }
693
694
    /**
695
     * [loadWidgetData Load a widgets data source/re-render]
696
     * @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...
697
     * @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...
698
     */
699
    function loadWidgetData(widg) {
700
        var widget = widg.el;
701
        var config = widg.config;
702
703
        widget.classed({error: false});
704
        widget.select('.error-overlay')
705
            .classed({hidden: true})
706
            .select('.alert')
707
            .text('');
708
709
        loader(widget);
710
        try {
711
            // Handle any custom inputs the user specified for this module.
712
            // They map to standard form inputs and correspond to query
713
            // arguments for this dataSource.
714
            if(config.inputs) {handleInputs(widg, config);}
715
716
            if(config.type === 'datatable') {
717
                jsondash.handlers.handleDataTable(widget, config);
718
            }
719
            else if(jsondash.util.isSparkline(config.type)) {
720
                jsondash.handlers.handleSparkline(widget, config);
721
            }
722
            else if(config.type === 'iframe') {
723
                jsondash.handlers.handleIframe(widget, config);
724
            }
725
            else if(config.type === 'timeline') {
726
                jsondash.handlers.handleTimeline(widget, config);
727
            }
728
            else if(config.type === 'venn') {
729
                jsondash.handlers.handleVenn(widget, config);
730
            }
731
            else if(config.type === 'number') {
732
                jsondash.handlers.handleSingleNum(widget, config);
733
            }
734
            else if(config.type === 'youtube') {
735
                jsondash.handlers.handleYoutube(widget, config);
736
            }
737
            else if(config.type === 'graph'){
738
                jsondash.handlers.handleGraph(widget, config);
739
            }
740
            else if(config.type === 'custom') {
741
                jsondash.handlers.handleCustom(widget, config);
742
            }
743
            else if(config.type === 'wordcloud') {
744
                jsondash.handlers.handleWordCloud(widget, config);
745
            }
746
            else if(config.type === 'vega-lite') {
747
                jsondash.handlers.handleVegaLite(widget, config);
748
            }
749
            else if(config.type === 'plotly-any') {
750
                jsondash.handlers.handlePlotly(widget, config);
751
            }
752
            else if(jsondash.util.isD3Subtype(config)) {
753
                jsondash.handlers.handleD3(widget, config);
754
            } else {
755
                jsondash.handlers.handleC3(widget, config);
756
            }
757
        } catch(e) {
758
            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...
759
            widget.classed({error: true});
760
            widget.select('.error-overlay')
761
                .classed({hidden: false})
762
                .select('.alert')
763
                .text('Loading error: "' + e + '"');
764
            unload(widget);
765
        }
766
        addResizeEvent(widg);
767
    }
768
769
    function addResizeEvent(widg) {
770
        // Add resize event
771
        var resize_opts = {
772
            helper: 'resizable-helper',
773
            minWidth: MIN_CHART_SIZE,
774
            minHeight: MIN_CHART_SIZE,
775
            maxWidth: VIEW_BUILDER.width(),
776
            handles: my.layout === 'grid' ? 's' : 'e, s, se',
777
            stop: function(event, ui) {
778
                var newconf = {height: ui.size.height};
779
                if(my.layout !== 'grid') {
780
                    newconf['width'] = ui.size.width;
781
                }
782
                // Update the configs dimensions.
783
                widg.update(newconf);
784
                fitGrid();
785
                // Open save panel
786
                EDIT_CONTAINER.collapse('show');
787
            }
788
        };
789
        // Add snap to grid (vertical only) in fixed grid mode.
790
        // This makes aligning charts easier because the snap points
791
        // are more likely to be consistent.
792
        if(my.layout === 'grid') {resize_opts['grid'] = 20;}
793
        $(widg.el[0]).resizable(resize_opts);
794
    }
795
796
    function prettyCode(code) {
797
        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...
798
        return JSON.stringify(JSON.parse(code), null, 4);
799
    }
800
801
    function prettifyJSONPreview() {
802
        // Reformat the code inside of the raw json field,
803
        // to pretty print for the user.
804
        JSON_DATA.text(prettyCode(JSON_DATA.text()));
805
    }
806
807
    function addNewRow(e) {
808
        // Add a new row with a toggleable label that indicates
809
        // which row it is for user editing.
810
        var placement = 'top';
811
        if(e) {
812
            e.preventDefault();
813
            placement = $(this).closest('.row').data().rowPlacement;
814
        }
815
        var el = ROW_TEMPLATE.clone(true);
816
        el.removeClass('grid-row-template');
817
        if(placement === 'top') {
818
            VIEW_BUILDER.find('.add-new-row-container:first').after(el);
819
        } else {
820
            VIEW_BUILDER.find('.add-new-row-container:last').before(el);
821
        }
822
        // Update the row ordering text
823
        updateRowOrder();
824
        // Add new events for dragging/dropping
825
        fitGrid();
826
    }
827
828
    function updateChartsRowOrder() {
829
        // Update the row order for each chart.
830
        // This is necessary for cases like adding a new row,
831
        // where the order is updated (before or after) the current row.
832
        // NOTE: This function assumes the row order has been recalculated in advance!
833
        $('.grid-row').each(function(i, row){
834
            $(row).find('.item.widget').each(function(j, item){
835
                var widget = my.widgets.getByEl($(item));
836
                widget.update({row: i + 1, order: j + 1}, true);
837
            });
838
        });
839
    }
840
841
    function updateRowOrder() {
842
        $('.grid-row').not('.grid-row-template').each(function(i, row){
843
            var idx = $(row).index();
844
            $(row).find('.grid-row-label').attr('data-row', idx);
845
            $(row).find('.rownum').text(idx);
846
        });
847
        updateChartsRowOrder();
848
    }
849
850
    function loadDashboard(data) {
851
        // Load the grid before rendering the ajax, since the DOM
852
        // is rendered server side.
853
        fitGrid({
854
            columnWidth: 5,
855
            itemSelector: '.item',
856
            transitionDuration: 0,
857
            fitWidth: true
858
        }, true);
859
        $('.item.widget').removeClass('hidden');
860
861
        // Populate widgets with the config data.
862
        my.widgets.populate(data);
863
864
        // Load all widgets, adding actual ajax data.
865
        for(var guid in my.widgets.all()){
866
            loadWidgetData(my.widgets.get(guid));
867
        }
868
869
        // Setup responsive handlers
870
        var jres = jRespond([{
871
            label: 'handheld',
872
            enter: 0,
873
            exit: 767
874
        }]);
875
        jres.addFunc({
876
            breakpoint: 'handheld',
877
            enter: function() {
878
                $('.widget').css({
879
                    'max-width': '100%',
880
                    'width': '100%',
881
                    'position': 'static'
882
                });
883
            }
884
        });
885
        prettifyJSONPreview();
886
        populateRowField();
887
        fitGrid();
888
        if(isEmptyDashboard()) {EDIT_TOGGLE_BTN.click();}
889
    }
890
891
    /**
892
     * [updateRowControls Check each row's buttons and disable the "add" button if that row
893
     * is at the maximum colcount (12)]
894
     */
895
    function updateRowControls() {
896
        $('.grid-row').not('.grid-row-template').each(function(i, row){
897
            var count = getRowColCount($(row));
898
            if(count >= 12) {
899
                $(row).find('.grid-row-label').addClass('disabled');
900
            } else {
901
                $(row).find('.grid-row-label').removeClass('disabled');
902
            }
903
        });
904
    }
905
906
    /**
907
     * [getRowColCount Return the column count of a row.]
908
     * @param  {[dom selection]} row [The row selection]
909
     */
910
    function getRowColCount(row) {
911
        var count = 0;
912
        row.find('.item.widget').each(function(j, item){
913
            var classes = $(item).parent().attr('class').split(/\s+/);
914
            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...
915
                if(classes[i].startsWith('col-md-')) {
916
                    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 916. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
917
                }
918
            }
919
        });
920
        return count;
921
    }
922
923
    function isEmptyDashboard() {
924
        return $('.item.widget').length === 0;
925
    }
926
927
    my.config = {
928
        WIDGET_MARGIN_X: 20,
929
        WIDGET_MARGIN_Y: 60
930
    };
931
    my.loadDashboard = loadDashboard;
932
    my.handlers = {};
933
    my.util = {};
934
    my.loader = loader;
935
    my.unload = unload;
936
    my.addDomEvents = addDomEvents;
937
    my.getActiveConfig = getParsedFormConfig;
938
    my.layout = VIEW_BUILDER.length > 0 ? VIEW_BUILDER.data().layout : null;
939
    my.widgets = new Widgets();
940
    return my;
941
}();
942