Completed
Push — master ( 973247...b98c45 )
by Chris
01:18
created

app.js ➔ getHandler   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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