Completed
Push — master ( b4cf96...451523 )
by Chris
01:38
created

app.js ➔ populateGridWidthDropdown   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
nc 1
nop 0
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 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
        if(type === 'image') {return false;}
524
        return true;
525
    }
526
527
    /**
528
     * [chartsTypeChanged Event handler for onChange event for chart type field]
529
     */
530
    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...
531
        var active_conf = getParsedFormConfig();
532
        var previewable = isPreviewableType(active_conf.type);
533
        togglePreviewOutput(previewable);
534
    }
535
536
    function populateGridWidthDropdown() {
537
        var cols = d3.range(1, 13).map(function(i, v){return 'col-' + i;});;
0 ignored issues
show
Unused Code introduced by
The parameter v 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...
538
        var form = d3.select(WIDGET_FORM.selector);
539
        form.select('[name="width"]').remove();
540
        form
541
            .append('select')
542
            .attr('name', 'width')
543
            .selectAll('option')
544
            .data(cols)
545
            .enter()
546
            .append('option')
547
            .value(function(i, v){
0 ignored issues
show
Unused Code introduced by
The parameter v 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...
548
                return i;
549
            })
550
            .text(function(i, v){
0 ignored issues
show
Unused Code introduced by
The parameter v 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...
551
                return i;
552
            });
553
    }
554
555
    function chartsModeChanged(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...
556
        var mode = MAIN_FORM.find('[name="mode"]').val();
557
        if(mode === 'grid') {
558
            populateGridWidthDropdown();
559
        }
560
    }
561
562
    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...
563
        // Update the order field based on the current rows item length.
564
        populateOrderField();
565
    }
566
567
    function loader(container) {
568
        container.select('.loader-overlay').classed({hidden: false});
569
        container.select('.widget-loader').classed({hidden: false});
570
    }
571
572
    function unload(container) {
573
        container.select('.loader-overlay').classed({hidden: true});
574
        container.select('.widget-loader').classed({hidden: true});
575
    }
576
577
    /**
578
     * [addDomEvents Add all dom event handlers here]
579
     */
580
    function addDomEvents() {
581
        MAIN_FORM.find('[name="mode"]').on('change.charts.row', chartsModeChanged);
582
        WIDGET_FORM.find('[name="row"]').on('change.charts.row', chartsRowChanged);
583
        // Chart type change
584
        WIDGET_FORM.find('[name="type"]').on('change.charts.type', chartsTypeChanged);
585
        // TODO: debounce/throttle
586
        API_PREVIEW_BTN.on('click.charts', previewAPIRoute);
587
        // Save module popup form
588
        SAVE_WIDGET_BTN.on('click.charts.save', saveWidget);
589
        // Edit existing modules
590
        EDIT_MODAL.on('show.bs.modal', populateEditForm);
591
        UPDATE_FORM_BTN.on('click.charts.save', onUpdateWidget);
592
593
        // Allow swapping of edit/update events
594
        // for the add module button and form modal
595
        ADD_MODULE.on('click.charts', function(){
596
            UPDATE_FORM_BTN
597
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
598
            .text('Save widget')
599
            .off('click.charts.save')
600
            .on('click.charts.save', saveWidget);
601
        });
602
603
        // Allow swapping of edit/update events
604
        // for the add module per row button and form modal
605
        VIEW_BUILDER.on('click.charts', '.grid-row-label', function(){
606
            UPDATE_FORM_BTN
607
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
608
            .text('Save widget')
609
            .off('click.charts.save')
610
            .on('click.charts.save', saveWidget);
611
        });
612
613
        // Add delete button for existing widgets.
614
        DELETE_BTN.on('click.charts', function(e){
615
            e.preventDefault();
616
            var guid = WIDGET_FORM.attr('data-guid');
617
            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...
618
        });
619
        // Add delete confirm for dashboards.
620
        DELETE_DASHBOARD.on('submit.charts', function(e){
621
            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...
622
        });
623
624
        // Format json config display
625
        $('#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...
626
            var code = $(this).find('code').text();
627
            $(this).find('code').text(prettyCode(code));
628
        });
629
630
        // Add event for downloading json config raw.
631
        // Will provide decent support but still not major: http://caniuse.com/#search=download
632
        $('[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...
633
            var datestr = new Date().toString().replace(/ /gi, '-');
634
            var data = encodeURIComponent(JSON.stringify(JSON_DATA.val(), null, 4));
635
            data = "data:text/json;charset=utf-8," + data;
636
            $(this).attr('href', data);
637
            $(this).attr('download', 'charts-config-raw-' + datestr + '.json');
638
        });
639
640
        // For fixed grid, add events for making new rows.
641
        ADD_ROW_CONTS.find('.btn').on('click', addNewRow);
642
643
        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...
644
            $('body').toggleClass('jsondash-editing');
645
            updateRowControls();
646
        });
647
648
        $('.delete-row').on('click', function(e){
649
            e.preventDefault();
650
            var row = $(this).closest('.grid-row');
651
            if(row.find('.item.widget').length > 0) {
652
                if(!confirm('Are you sure?')) {
653
                    return;
654
                }
655
            }
656
            deleteRow(row);
657
        });
658
    }
659
660
    function initFixedDragDrop(options) {
661
        var grid_drag_opts = {
662
            connectToSortable: '.grid-row'
663
        };
664
        $('.grid-row').droppable({
665
            drop: function(event, ui) {
666
                // update the widgets location
667
                var idx    = $(this).index();
668
                var el     = $(ui.draggable);
669
                var widget = my.widgets.getByEl(el);
670
                widget.update({row: idx}, true);
671
                // Actually move the dom element, and reset
672
                // the dragging css so it snaps into the row container
673
                el.parent().appendTo($(this));
674
                el.css({
675
                    position: 'relative',
676
                    top: 0,
677
                    left: 0
678
                });
679
            }
680
        });
681
        $('.item.widget').draggable($.extend(grid_drag_opts, options));
682
    }
683
684
    function fitGrid(grid_packer_opts, init) {
685
        var packer_options = $.isPlainObject(grid_packer_opts) ? grid_packer_opts : {};
686
        var grid_packer_options = $.extend({}, packer_options, {});
687
        var drag_options = {
688
            scroll: true,
689
            handle: '.dragger',
690
            start: function() {
691
                $('.grid-row').addClass('drag-target');
692
            },
693
            stop: function(){
694
                $('.grid-row').removeClass('drag-target');
695
                EDIT_CONTAINER.collapse('show');
696
                if(my.layout === 'grid') {
697
                    // Update row order.
698
                    updateChartsRowOrder();
699
                } else {
700
                    my.chart_wall.packery(grid_packer_options);
701
                    updateChartsOrder();
702
                }
703
            }
704
        };
705
        if(my.layout === 'grid' && $('.grid-row').length > 1) {
706
            initFixedDragDrop(drag_options);
707
            return;
708
        }
709
        if(init) {
710
            my.chart_wall = $('#container').packery(grid_packer_options);
711
            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...
712
            my.chart_wall.packery('bindUIDraggableEvents', items);
713
        } else {
714
            my.chart_wall.packery(grid_packer_options);
715
        }
716
    }
717
718
    function updateChartsOrder() {
719
        // Update the order and order value of each chart
720
        var items = my.chart_wall.packery('getItemElements');
721
        // Update module order
722
        $.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...
723
            var widget = my.widgets.getByEl($(this));
724
            widget.update({order: i}, true);
725
        });
726
    }
727
728
    function handleInputs(widget, config) {
729
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
730
        // Load event handlers for these newly created forms.
731
        $(inputs_selector).find('form').on('submit', function(e){
732
            e.stopImmediatePropagation();
733
            e.preventDefault();
734
            // Just create a new url for this, but use existing config.
735
            // The global object config will not be altered.
736
            // The first {} here is important, as it enforces a deep copy,
737
            // not a mutation of the original object.
738
            var url = config.dataSource;
739
            // Ensure we don't lose params already save on this endpoint url.
740
            var existing_params = url.split('?')[1];
741
            var params = jsondash.util.getValidParamString($(this).serializeArray());
742
            params = jsondash.util.reformatQueryParams(existing_params, params);
743
            var _config = $.extend({}, config, {
744
                dataSource: url.replace(/\?.+/, '') + '?' + params
745
            });
746
            my.widgets.get(config.guid).update(_config, true);
747
            // Otherwise reload like normal.
748
            loadWidgetData(my.widgets.get(config.guid));
749
            // Hide the form again
750
            $(inputs_selector).removeClass('in');
751
        });
752
    }
753
754
    function getHandler(family) {
755
        var handlers  = {
756
            basic          : jsondash.handlers.handleBasic,
757
            datatable      : jsondash.handlers.handleDataTable,
758
            sparkline      : jsondash.handlers.handleSparkline,
759
            timeline       : jsondash.handlers.handleTimeline,
760
            venn           : jsondash.handlers.handleVenn,
761
            graph          : jsondash.handlers.handleGraph,
762
            wordcloud      : jsondash.handlers.handleWordCloud,
763
            vega           : jsondash.handlers.handleVegaLite,
764
            plotlystandard : jsondash.handlers.handlePlotly,
765
            cytoscape      : jsondash.handlers.handleCytoscape,
766
            sigmajs        : jsondash.handlers.handleSigma,
767
            c3             : jsondash.handlers.handleC3,
768
            d3             : jsondash.handlers.handleD3
769
        };
770
        return handlers[family];
771
    }
772
773
    /**
774
     * [loadWidgetData Load a widgets data source/re-render]
775
     * @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...
776
     * @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...
777
     */
778
    function loadWidgetData(widg) {
779
        var widget    = widg.el;
780
        var $widget   = $(widget[0]);
781
        var config    = widg.config;
782
        var inputs    = $widget.find('.chart-inputs');
783
        var container = $('<div></div>').addClass('chart-container');
784
        var family    = config.family.toLowerCase();
785
786
        widget.classed({error: false});
787
        widget.select('.error-overlay')
788
            .classed({hidden: true})
789
            .select('.alert')
790
            .text('');
791
792
        loader(widget);
793
794
        try {
795
            // Cleanup for all widgets.
796
            widget.selectAll('.chart-container').remove();
797
            // Ensure the chart inputs comes AFTER any chart container.
798
            if(inputs.length > 0) {
799
                inputs.before(container);
800
            } else {
801
                $widget.append(container);
802
            }
803
            // Handle any custom inputs the user specified for this module.
804
            // They map to standard form inputs and correspond to query
805
            // arguments for this dataSource.
806
            if(config.inputs) {
807
                handleInputs(widg, config);
808
            }
809
810
            // Retrieve and immediately call the appropriate handler.
811
            getHandler(family)(widget, config);
812
813
        } catch(e) {
814
            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...
815
            widget.classed({error: true});
816
            widget.select('.error-overlay')
817
                .classed({hidden: false})
818
                .select('.alert')
819
                .text('Loading error: "' + e + '"');
820
            unload(widget);
821
        }
822
        addResizeEvent(widg);
823
    }
824
825
    function addResizeEvent(widg) {
826
        // Add resize event
827
        var resize_opts = {
828
            helper: 'resizable-helper',
829
            minWidth: MIN_CHART_SIZE,
830
            minHeight: MIN_CHART_SIZE,
831
            maxWidth: VIEW_BUILDER.width(),
832
            handles: my.layout === 'grid' ? 's' : 'e, s, se',
833
            stop: function(event, ui) {
834
                var newconf = {height: ui.size.height};
835
                if(my.layout !== 'grid') {
836
                    newconf['width'] = ui.size.width;
837
                }
838
                // Update the configs dimensions.
839
                widg.update(newconf);
840
                fitGrid();
841
                // Open save panel
842
                EDIT_CONTAINER.collapse('show');
843
            }
844
        };
845
        // Add snap to grid (vertical only) in fixed grid mode.
846
        // This makes aligning charts easier because the snap points
847
        // are more likely to be consistent.
848
        if(my.layout === 'grid') {resize_opts['grid'] = 20;}
849
        $(widg.el[0]).resizable(resize_opts);
850
    }
851
852
    function prettyCode(code) {
853
        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...
854
        return JSON.stringify(JSON.parse(code), null, 4);
855
    }
856
857
    function prettifyJSONPreview() {
858
        // Reformat the code inside of the raw json field,
859
        // to pretty print for the user.
860
        JSON_DATA.text(prettyCode(JSON_DATA.text()));
861
    }
862
863
    function addNewRow(e) {
864
        // Add a new row with a toggleable label that indicates
865
        // which row it is for user editing.
866
        var placement = 'top';
867
        if(e) {
868
            e.preventDefault();
869
            placement = $(this).closest('.row').data().rowPlacement;
870
        }
871
        var el = ROW_TEMPLATE.clone(true);
872
        el.removeClass('grid-row-template');
873
        if(placement === 'top') {
874
            VIEW_BUILDER.find('.add-new-row-container:first').after(el);
875
        } else {
876
            VIEW_BUILDER.find('.add-new-row-container:last').before(el);
877
        }
878
        // Update the row ordering text
879
        updateRowOrder();
880
        // Add new events for dragging/dropping
881
        fitGrid();
882
        el.trigger(EVENTS.add_row);
883
    }
884
885
    function updateChartsRowOrder() {
886
        // Update the row order for each chart.
887
        // This is necessary for cases like adding a new row,
888
        // where the order is updated (before or after) the current row.
889
        // NOTE: This function assumes the row order has been recalculated in advance!
890
        $('.grid-row').each(function(i, row){
891
            $(row).find('.item.widget').each(function(j, item){
892
                var widget = my.widgets.getByEl($(item));
893
                widget.update({row: i + 1, order: j + 1}, true);
894
            });
895
        });
896
    }
897
898
    function updateRowOrder() {
899
        $('.grid-row').not('.grid-row-template').each(function(i, row){
900
            var idx = $(row).index();
901
            $(row).find('.grid-row-label').attr('data-row', idx);
902
            $(row).find('.rownum').text(idx);
903
        });
904
        updateChartsRowOrder();
905
    }
906
907
    function loadDashboard(data) {
908
        // Load the grid before rendering the ajax, since the DOM
909
        // is rendered server side.
910
        fitGrid({
911
            columnWidth: 5,
912
            itemSelector: '.item',
913
            transitionDuration: 0,
914
            fitWidth: true
915
        }, true);
916
        $('.item.widget').removeClass('hidden');
917
918
        // Populate widgets with the config data.
919
        my.widgets.populate(data);
920
921
        // Load all widgets, adding actual ajax data.
922
        for(var guid in my.widgets.all()){
923
            loadWidgetData(my.widgets.get(guid));
924
        }
925
926
        // Setup responsive handlers
927
        var jres = jRespond([{
928
            label: 'handheld',
929
            enter: 0,
930
            exit: 767
931
        }]);
932
        jres.addFunc({
933
            breakpoint: 'handheld',
934
            enter: function() {
935
                $('.widget').css({
936
                    'max-width': '100%',
937
                    'width': '100%',
938
                    'position': 'static'
939
                });
940
            }
941
        });
942
        prettifyJSONPreview();
943
        populateRowField();
944
        fitGrid();
945
        if(isEmptyDashboard()) {EDIT_TOGGLE_BTN.click();}
946
        MAIN_CONTAINER.trigger(EVENTS.init);
947
    }
948
949
    /**
950
     * [updateRowControls Check each row's buttons and disable the "add" button if that row
951
     * is at the maximum colcount (12)]
952
     */
953
    function updateRowControls() {
954
        $('.grid-row').not('.grid-row-template').each(function(i, row){
955
            var count = getRowColCount($(row));
956
            if(count >= 12) {
957
                $(row).find('.grid-row-label').addClass('disabled');
958
            } else {
959
                $(row).find('.grid-row-label').removeClass('disabled');
960
            }
961
        });
962
    }
963
964
    /**
965
     * [getRowColCount Return the column count of a row.]
966
     * @param  {[dom selection]} row [The row selection]
967
     */
968
    function getRowColCount(row) {
969
        var count = 0;
970
        row.find('.item.widget').each(function(j, item){
971
            var classes = $(item).parent().attr('class').split(/\s+/);
972
            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...
973
                if(classes[i].startsWith('col-md-')) {
974
                    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 974. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
975
                }
976
            }
977
        });
978
        return count;
979
    }
980
981
    function isEmptyDashboard() {
982
        return $('.item.widget').length === 0;
983
    }
984
985
    my.config = {
986
        WIDGET_MARGIN_X: 20,
987
        WIDGET_MARGIN_Y: 60
988
    };
989
    my.loadDashboard = loadDashboard;
990
    my.handlers = {};
991
    my.util = {};
992
    my.loader = loader;
993
    my.unload = unload;
994
    my.addDomEvents = addDomEvents;
995
    my.getActiveConfig = getParsedFormConfig;
996
    my.layout = VIEW_BUILDER.length > 0 ? VIEW_BUILDER.data().layout : null;
997
    my.widgets = new Widgets();
998
    return my;
999
}();
1000