Completed
Push — master ( 0b398e...ee1b20 )
by Chris
01:20
created

app.js ➔ getClasses   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
nc 1
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
        init:             'jsondash.init',
33
        edit_form_loaded: 'jsondash.editform.loaded',
34
        add_widget:       'jsondash.widget.added',
35
        update_widget:    'jsondash.widget.updated',
36
        delete_widget:    'jsondash.widget.deleted',
37
        refresh_widget:   'jsondash.widget.refresh',
38
        add_row:          'jsondash.row.add',
39
        delete_row:       'jsondash.row.delete',
40
        preview_api:      'jsondash.preview',
41
    }
42
43
    function Widgets() {
44
        var self = this;
45
        self.widgets = {};
46
        self.container = MAIN_CONTAINER.selector;
47
        self.all = function() {
48
            return self.widgets;
49
        };
50
        self.add = function(config) {
51
            self.widgets[config.guid] = new Widget(self.container, config);
52
            self.widgets[config.guid].$el.trigger(EVENTS.add_widget);
53
            return self.widgets[config.guid];
54
        };
55
        self.addFromForm = function() {
56
            return self.add(self.newModel());
57
        };
58
        self._delete = function(guid) {
59
            delete self.widgets[guid];
60
        };
61
        self.get = function(guid) {
62
            return self.widgets[guid];
63
        };
64
        self.getByEl = function(el) {
65
            return self.get(el.data().guid);
66
        };
67
        self.getAllOfProp = function(propname) {
68
            var props = [];
69
            $.each(self.all(), function(i, widg){
70
                props.push(widg.config[propname]);
71
            });
72
            return props;
73
        };
74
        self.newModel = function() {
75
            var config = getParsedFormConfig();
76
            var guid   = jsondash.util.guid();
77
            config['guid'] = guid;
78
            if(!config.refresh || !refreshableType(config.type)) {
79
                config['refresh'] = false;
80
            }
81
            return config;
82
        };
83
        self.populate = function(data) {
84
            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...
85
                // Closure to maintain each chart data value in loop
86
                (function(config){
87
                    var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 84. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
88
                    // Add div wrappers for js grid layout library,
89
                    // and add title, icons, and buttons
90
                    // This is the widget "model"/object used throughout.
91
                    self.add(config);
92
                })(data.modules[name]);
93
            }
94
        };
95
    }
96
97
    function Widget(container, config) {
98
        // model for a chart widget
99
        var self = this;
100
        self.config = config;
101
        self.guid = self.config.guid;
102
        self.container = container;
103
        self._refreshInterval = null;
104
        self._makeWidget = function(config) {
105
            if(document.querySelector('[data-guid="' + config.guid + '"]')){
106
                return d3.select('[data-guid="' + config.guid + '"]');
107
            }
108
            return d3.select(self.container).select('div')
109
                .append('div')
110
                .classed({item: true, widget: true})
111
                .attr('data-guid', config.guid)
112
                .attr('data-refresh', config.refresh)
113
                .attr('data-refresh-interval', config.refreshInterval)
114
                .style('width', config.width + 'px')
115
                .style('height', config.height + 'px')
116
                .html(d3.select(CHART_TEMPLATE.selector).html())
117
                .select('.widget-title .widget-title-text').text(config.name);
118
        };
119
        // d3 el
120
        self.el = self._makeWidget(config);
121
        // Jquery el
122
        self.$el = $(self.el[0]);
123
        self.init = function() {
124
            // Add event handlers for widget UI
125
            self.$el.find('.widget-refresh').on('click.charts', refreshWidget);
126
            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...
127
                self.delete();
128
            });
129
            // Allow swapping of edit/update events
130
            // for the edit button and form modal
131
            self.$el.find('.widget-edit').on('click.charts', function(){
132
                SAVE_WIDGET_BTN
133
                .attr('id', UPDATE_FORM_BTN.selector.replace('#', ''))
134
                .text('Update widget')
135
                .off('click.charts.save')
136
                .on('click.charts', onUpdateWidget);
137
            });
138
            if(self.config.refresh && self.config.refreshInterval) {
139
                self._refreshInterval = setInterval(function(){
140
                    loadWidgetData(my.widgets.get(self.config.guid));
141
                }, parseInt(self.config.refreshInterval, 10));
142
            }
143
            if(my.layout === 'grid') {
144
                updateRowControls();
145
            }
146
        };
147
        self.getInput = function() {
148
            // Get the form input for this widget.
149
            return $('input[id="' + self.guid + '"]');
150
        };
151
        self.delete = function(bypass_confirm) {
152
            if(!bypass_confirm){
153
                if(!confirm('Are you sure?')) {
154
                    return;
155
                }
156
            }
157
            var row = self.$el.closest('.grid-row');
158
            clearInterval(self._refreshInterval);
159
            // Delete the input
160
            self.getInput().remove();
161
            self.$el.trigger(EVENTS.delete_widget, [self]);
162
            // Delete the widget
163
            self.el.remove();
164
            // Remove reference to the collection by guid
165
            my.widgets._delete(self.guid);
166
            EDIT_MODAL.modal('hide');
167
            // Redraw wall to replace visual 'hole'
168
            if(my.layout === 'grid') {
169
                // Fill empty holes in this charts' row
170
                fillEmptyCols(row);
171
                updateRowControls();
172
            }
173
            // Trigger update form into view since data is dirty
174
            EDIT_CONTAINER.collapse('show');
175
            // Refit grid - this should be last.
176
            fitGrid();
177
        };
178
        self.addGridClasses = function(sel, classes) {
179
            d3.map(classes, function(colcount){
180
                var classlist = {};
181
                classlist['col-md-' + colcount] = true;
182
                classlist['col-lg-' + colcount] = true;
183
                sel.classed(classlist);
184
            });
185
        };
186
        self.removeGridClasses = function(sel) {
187
            var bootstrap_classes = d3.range(1, 13);
188
            d3.map(bootstrap_classes, function(i){
189
                var classes = {};
190
                classes['col-md-' + i] = false;
191
                classes['col-lg-' + i] = false;
192
                sel.classed(classes);
193
            });
194
        };
195
        self.update = function(conf, dont_refresh) {
196
                /**
197
             * Single source to update all aspects of a widget - in DOM, in model, etc...
198
             */
199
            var widget = self.el;
200
            // Update model data
201
            self.config = $.extend(self.config, conf);
202
            // Trigger update form into view since data is dirty
203
            // Update visual size to existing widget.
204
            loader(widget);
205
            widget.style({
206
                height: self.config.height + 'px',
207
                width: my.layout === 'grid' ? '100%' : self.config.width + 'px'
208
            });
209
            if(my.layout === 'grid') {
210
                // Extract col number from config: format is "col-N"
211
                var colcount = self.config.width.split('-')[1];
212
                var parent = d3.select(widget.node().parentNode);
213
                // Reset all other grid classes and then add new one.
214
                self.removeGridClasses(parent);
215
                self.addGridClasses(parent, [colcount]);
216
                // Update row buttons based on current state
217
                updateRowControls();
218
            }
219
            widget.select('.widget-title .widget-title-text').text(self.config.name);
220
            // Update the form input for this widget.
221
            self._updateForm();
222
223
            if(!dont_refresh) {
224
                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...
225
                EDIT_CONTAINER.collapse();
226
                // Refit the grid
227
                fitGrid();
228
            } else {
229
                unload(widget);
230
            }
231
            $(widget[0]).trigger(EVENTS.update_widget);
232
        };
233
        self._updateForm = function() {
234
            self.getInput().val(JSON.stringify(self.config));
235
        };
236
237
        // Run init script on creation
238
        self.init();
239
    }
240
241
    /**
242
     * [fillEmptyCols Fill in gaps in a row when an item has been deleted (fixed grid only)]
243
     */
244
    function fillEmptyCols(row) {
245
        row.each(function(_, row){
246
            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...
247
            var cols = $(row).find('> div');
248
            cols.filter(function(i, col){
249
                return $(col).find('.item.widget').length === 0;
250
            }).remove();
251
        });
252
    }
253
254
    function togglePreviewOutput(is_on) {
255
        if(is_on) {
256
            API_PREVIEW_CONT.show();
257
            return;
258
        }
259
        API_PREVIEW_CONT.hide();
260
    }
261
262
    function previewAPIRoute(e) {
263
        e.preventDefault();
264
        // Shows the response of the API field as a json payload, inline.
265
        $.ajax({
266
            type: 'GET',
267
            url: API_ROUTE_URL.val().trim(),
268
            success: function(data) {
269
                API_PREVIEW.html(prettyCode(data));
270
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: false}]);
271
            },
272
            error: function(data, status, error) {
273
                API_PREVIEW.html(error);
274
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: true}]);
275
            }
276
        });
277
    }
278
279
    function refreshableType(type) {
280
        if(type === 'youtube') {return false;}
281
        return true;
282
    }
283
284
    function validateWidgetForm() {
285
        var is_valid = true;
286
        var url_field = WIDGET_FORM.find('[name="dataSource"]');
287
        WIDGET_FORM.find('[required]').each(function(i, el){
288
            if($(el).val() === '') {
289
                $(el).parent().addClass('has-error').removeClass('has-success');
290
                is_valid = false;
291
                return false;
292
            } else {
293
                $(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...
294
            }
295
        });
296
        // Validate youtube videos
297
        if(WIDGET_FORM.find('[name="type"]').val() === 'youtube') {
298
            if(!url_field.val().startsWith('<iframe')) {
299
                url_field.parent().addClass('has-error');
300
                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...
301
                return false;
302
            }
303
        }
304
        return is_valid;
305
    }
306
307
    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...
308
        if(!(validateWidgetForm())) {
309
            return false;
310
        }
311
        var new_config = my.widgets.newModel();
312
        // Remove empty rows and then update the order so it's consecutive.
313
        $('.grid-row').not('.grid-row-template').each(function(i, row){
314
            // Delete empty rows - except any empty rows that have been created
315
            // for the purpose of this new chart.
316
            if($(row).find('.item.widget').length === 0 && new_config.row !== i + 1) {
317
                $(row).remove();
318
            }
319
        });
320
        // Update the row orders after deleting empty ones
321
        updateRowOrder();
322
        var newfield = $('<input class="form-control" type="text">');
323
        // Add a unique guid for referencing later.
324
        newfield.attr('name', 'module_' + new_config.id);
325
        newfield.val(JSON.stringify(new_config));
326
        $('.modules').append(newfield);
327
        // Save immediately.
328
        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...
329
    }
330
331
    function isModalButton(e) {
332
        return e.relatedTarget.id === ADD_MODULE.selector.replace('#', '');
333
    }
334
335
    function isRowButton(e) {
336
        return $(e.relatedTarget).hasClass('grid-row-label');
337
    }
338
339
    function clearForm() {
340
        WIDGET_FORM.find('label')
341
        .removeClass('has-error')
342
        .removeClass('has-success')
343
        .find('input, select')
344
        .each(function(_, input){
345
            $(input).val('');
346
        });
347
    }
348
349
    function deleteRow(row) {
350
        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...
351
        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...
352
            var guid = $(this).data().guid;
353
            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...
354
        });
355
        // Remove AFTER removing the charts contained within
356
        row.remove();
357
        updateRowOrder();
358
        el.trigger(EVENTS.delete_row);
0 ignored issues
show
Bug introduced by
The variable el seems to be never declared. If this is a global, consider adding a /** global: el */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

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