Completed
Push — master ( 16569e...29c86e )
by Chris
50s
created

app.js ➔ ... ➔ $.each   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 5
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
    /**
44
     * [Widgets A singleton manager for all widgets.]
45
     */
46
    function Widgets() {
47
        var self = this;
48
        self.widgets = {};
49
        self.url_cache = {};
50
        self.container = MAIN_CONTAINER.selector;
51
        self.all = function() {
52
            return self.widgets;
53
        };
54
        self.add = function(config) {
55
            self.widgets[config.guid] = new Widget(self.container, config);
56
            self.widgets[config.guid].$el.trigger(EVENTS.add_widget);
57
            return self.widgets[config.guid];
58
        };
59
        self.addFromForm = function() {
60
            return self.add(self.newModel());
61
        };
62
        self._delete = function(guid) {
63
            delete self.widgets[guid];
64
        };
65
        self.get = function(guid) {
66
            return self.widgets[guid];
67
        };
68
        self.getByEl = function(el) {
69
            return self.get(el.data().guid);
70
        };
71
        /**
72
         * [getAllMatchingProp Get widget guids matching a givne propname and val]
73
         */
74
        self.getAllMatchingProp = function(propname, val) {
75
            var matches = [];
76
            $.each(self.all(), function(i, widg){
77
                if(widg.config[propname] === val) {
78
                    matches.push(widg.config.guid);
79
                }
80
            });
81
            return matches;
82
        };
83
        /**
84
         * [getAllOfProp Get all the widgets' config values of a specified property]
85
         */
86
        self.getAllOfProp = function(propname) {
87
            var props = [];
88
            $.each(self.all(), function(i, widg){
89
                props.push(widg.config[propname]);
90
            });
91
            return props;
92
        };
93
        /**
94
         * [loadAll Load all widgets at once in succession]
95
         */
96
        self.loadAll = function() {
97
            var unique_urls = d3.set(self.getAllOfProp('dataSource')).values();
98
            var cached = {};
99
            var proms = [];
100
            // Build out promises.
101
            $.each(unique_urls, function(_, url){
102
                var req = $.ajax({
103
                    url: url,
104
                    type: 'GET',
105
                    dataType: 'json',
106
                    error: function(error){
107
                        var matches = self.getAllMatchingProp('dataSource', url);
108
                        $.each(matches, function(_, guid){
109
                            var widg = self.get(guid);
110
                            jsondash.handleRes(error, null, widg.el);
111
                        });
112
                    }
113
                });
114
                proms.push(req);
115
            });
116
            // Retrieve and gather the promises
117
            $.when.apply($, proms).done(whenAllDone);
118
119
            function whenAllDone() {
120
                if(arguments.length === 3) {
121
                    var ref_url = unique_urls[0];
122
                    var data = arguments[0];
123
                    if(ref_url) {
124
                        cached[ref_url] = data;
125
                    }
126
                } else {
127
                    $.each(arguments, function(index, prom){
128
                        var ref_url = unique_urls[index];
129
                        var data = null;
0 ignored issues
show
Unused Code introduced by
The assignment to data seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
130
                        if(ref_url) {
131
                            data = prom[0];
132
                            cached[ref_url] = data;
133
                        }
134
                    });
135
                }
136
                // Inject a cached value on the config for use down the road
137
                // (this is done so little is changed with the architecture of getting and loading).
138
                for(var guid in self.all()){
139
                    // Don't refresh, just update config with new key value for cached data.
140
                    var widg = self.get(guid);
141
                    var data = cached[widg.config.dataSource];
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable data already seems to be declared on line 122. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
142
                    // Grab data from specific `key` key, if it exists (for shared data on a single endpoint).
143
                    var cachedData = widg.config.key && data.multicharts ? data.multicharts[widg.config.key] : data;
144
                    widg.update({cachedData: cachedData}, true);
145
                    // Actually load them all
146
                    widg.load();
147
                }
148
            }
149
        };
150
        self.newModel = function() {
151
            var config = getParsedFormConfig();
152
            var guid   = jsondash.util.guid();
153
            config['guid'] = guid;
154
            if(!config.refresh || !refreshableType(config.type)) {
155
                config['refresh'] = false;
156
            }
157
            return config;
158
        };
159
        self.populate = function(data) {
160
            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...
161
                // Closure to maintain each chart data value in loop
162
                (function(config){
163
                    var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 160. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
164
                    // Add div wrappers for js grid layout library,
165
                    // and add title, icons, and buttons
166
                    // This is the widget "model"/object used throughout.
167
                    self.add(config);
168
                })(data.modules[name]);
169
            }
170
        };
171
    }
172
173
    function Widget(container, config) {
174
        // model for a chart widget
175
        var self = this;
176
        self.config = config;
177
        self.guid = self.config.guid;
178
        self.container = container;
179
        self._refreshInterval = null;
180
        self._makeWidget = function(config) {
181
            if(document.querySelector('[data-guid="' + config.guid + '"]')){
182
                return d3.select('[data-guid="' + config.guid + '"]');
183
            }
184
            return d3.select(self.container).select('div')
185
                .append('div')
186
                .classed({item: true, widget: true})
187
                .attr('data-guid', config.guid)
188
                .attr('data-refresh', config.refresh)
189
                .attr('data-refresh-interval', config.refreshInterval)
190
                .style('width', config.width + 'px')
191
                .style('height', config.height + 'px')
192
                .html(d3.select(CHART_TEMPLATE.selector).html())
193
                .select('.widget-title .widget-title-text').text(config.name);
194
        };
195
        // d3 el
196
        self.el = self._makeWidget(config);
197
        // Jquery el
198
        self.$el = $(self.el[0]);
199
        self.init = function() {
200
            // Add event handlers for widget UI
201
            self.$el.find('.widget-refresh').on('click.charts', refreshWidget);
202
            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...
203
                self.delete();
204
            });
205
            // Allow swapping of edit/update events
206
            // for the edit button and form modal
207
            self.$el.find('.widget-edit').on('click.charts', function(){
208
                SAVE_WIDGET_BTN
209
                .attr('id', UPDATE_FORM_BTN.selector.replace('#', ''))
210
                .text('Update widget')
211
                .off('click.charts.save')
212
                .on('click.charts', onUpdateWidget);
213
            });
214
            if(self.config.refresh && self.config.refreshInterval) {
215
                self._refreshInterval = setInterval(function(){
216
                    self.load();
217
                }, parseInt(self.config.refreshInterval, 10));
218
            }
219
            if(my.layout === 'grid') {
220
                updateRowControls();
221
            }
222
        };
223
        self.getInput = function() {
224
            // Get the form input for this widget.
225
            return $('input[id="' + self.guid + '"]');
226
        };
227
        self.delete = function(bypass_confirm) {
228
            if(!bypass_confirm){
229
                if(!confirm('Are you sure?')) {
230
                    return;
231
                }
232
            }
233
            var row = self.$el.closest('.grid-row');
234
            clearInterval(self._refreshInterval);
235
            // Delete the input
236
            self.getInput().remove();
237
            self.$el.trigger(EVENTS.delete_widget, [self]);
238
            // Delete the widget
239
            self.el.remove();
240
            // Remove reference to the collection by guid
241
            my.widgets._delete(self.guid);
242
            EDIT_MODAL.modal('hide');
243
            // Redraw wall to replace visual 'hole'
244
            if(my.layout === 'grid') {
245
                // Fill empty holes in this charts' row
246
                fillEmptyCols(row);
247
                updateRowControls();
248
            }
249
            // Trigger update form into view since data is dirty
250
            EDIT_CONTAINER.collapse('show');
251
            // Refit grid - this should be last.
252
            fitGrid();
253
        };
254
        self.addGridClasses = function(sel, classes) {
255
            d3.map(classes, function(colcount){
256
                var classlist = {};
257
                classlist['col-md-' + colcount] = true;
258
                classlist['col-lg-' + colcount] = true;
259
                sel.classed(classlist);
260
            });
261
        };
262
        self.removeGridClasses = function(sel) {
263
            var bootstrap_classes = d3.range(1, 13);
264
            d3.map(bootstrap_classes, function(i){
265
                var classes = {};
266
                classes['col-md-' + i] = false;
267
                classes['col-lg-' + i] = false;
268
                sel.classed(classes);
269
            });
270
        };
271
        self.update = function(conf, dont_refresh) {
272
                /**
273
             * Single source to update all aspects of a widget - in DOM, in model, etc...
274
             */
275
            var widget = self.el;
276
            // Update model data
277
            self.config = $.extend(self.config, conf);
278
            // Trigger update form into view since data is dirty
279
            // Update visual size to existing widget.
280
            loader(widget);
281
            widget.style({
282
                height: self.config.height + 'px',
283
                width: my.layout === 'grid' ? '100%' : self.config.width + 'px'
284
            });
285
            if(my.layout === 'grid') {
286
                // Extract col number from config: format is "col-N"
287
                var colcount = self.config.width.split('-')[1];
288
                var parent = d3.select(widget.node().parentNode);
289
                // Reset all other grid classes and then add new one.
290
                self.removeGridClasses(parent);
291
                self.addGridClasses(parent, [colcount]);
292
                // Update row buttons based on current state
293
                updateRowControls();
294
            }
295
            widget.select('.widget-title .widget-title-text').text(self.config.name);
296
            // Update the form input for this widget.
297
            self._updateForm();
298
299
            if(!dont_refresh) {
300
                self.load();
301
                EDIT_CONTAINER.collapse();
302
                // Refit the grid
303
                fitGrid();
304
            } else {
305
                unload(widget);
306
            }
307
            $(widget[0]).trigger(EVENTS.update_widget);
308
        };
309
        self.load = function() {
310
            var widg      = my.widgets.get(self.guid);
311
            var widget    = self.el;
312
            var $widget   = self.$el;
313
            var config    = widg.config;
314
            var inputs    = $widget.find('.chart-inputs');
315
            var container = $('<div></div>').addClass('chart-container');
316
            var family    = config.family.toLowerCase();
317
318
            widget.classed({error: false});
319
            widget.select('.error-overlay')
320
                .classed({hidden: true})
321
                .select('.alert')
322
                .text('');
323
324
            loader(widget);
325
326
            try {
327
                // Cleanup for all widgets.
328
                widget.selectAll('.chart-container').remove();
329
                // Ensure the chart inputs comes AFTER any chart container.
330
                if(inputs.length > 0) {
331
                    inputs.before(container);
332
                } else {
333
                    $widget.append(container);
334
                }
335
                // Handle any custom inputs the user specified for this module.
336
                // They map to standard form inputs and correspond to query
337
                // arguments for this dataSource.
338
                if(config.inputs) {
339
                    handleInputs(widg, config);
340
                }
341
342
                // Retrieve and immediately call the appropriate handler.
343
                getHandler(family)(widget, config);
344
345
            } catch(e) {
346
                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...
347
                widget.classed({error: true});
348
                widget.select('.error-overlay')
349
                    .classed({hidden: false})
350
                    .select('.alert')
351
                    .text('Loading error: "' + e + '"');
352
                unload(widget);
353
            }
354
            addResizeEvent(widg);
355
        };
356
        self._updateForm = function() {
357
            self.getInput().val(JSON.stringify(self.config));
358
        };
359
360
        // Run init script on creation
361
        self.init();
362
    }
363
364
    /**
365
     * [fillEmptyCols Fill in gaps in a row when an item has been deleted (fixed grid only)]
366
     */
367
    function fillEmptyCols(row) {
368
        row.each(function(_, row){
369
            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...
370
            var cols = $(row).find('> div');
371
            cols.filter(function(i, col){
372
                return $(col).find('.item.widget').length === 0;
373
            }).remove();
374
        });
375
    }
376
377
    function togglePreviewOutput(is_on) {
378
        if(is_on) {
379
            API_PREVIEW_CONT.show();
380
            return;
381
        }
382
        API_PREVIEW_CONT.hide();
383
    }
384
385
    function previewAPIRoute(e) {
386
        e.preventDefault();
387
        // Shows the response of the API field as a json payload, inline.
388
        $.ajax({
389
            type: 'GET',
390
            url: API_ROUTE_URL.val().trim(),
391
            success: function(data) {
392
                API_PREVIEW.html(prettyCode(data));
393
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: false}]);
394
            },
395
            error: function(data, status, error) {
396
                API_PREVIEW.html(error);
397
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: true}]);
398
            }
399
        });
400
    }
401
402
    function refreshableType(type) {
403
        if(type === 'youtube') {return false;}
404
        return true;
405
    }
406
407
    function validateWidgetForm() {
408
        var is_valid = true;
409
        var url_field = WIDGET_FORM.find('[name="dataSource"]');
410
        WIDGET_FORM.find('[required]').each(function(i, el){
411
            if($(el).val() === '') {
412
                $(el).parent().addClass('has-error').removeClass('has-success');
413
                is_valid = false;
414
                return false;
415
            } else {
416
                $(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...
417
            }
418
        });
419
        // Validate youtube videos
420
        if(WIDGET_FORM.find('[name="type"]').val() === 'youtube') {
421
            if(!url_field.val().startsWith('<iframe')) {
422
                url_field.parent().addClass('has-error');
423
                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...
424
                return false;
425
            }
426
        }
427
        return is_valid;
428
    }
429
430
    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...
431
        if(!(validateWidgetForm())) {
432
            return false;
433
        }
434
        var new_config = my.widgets.newModel();
435
        // Remove empty rows and then update the order so it's consecutive.
436
        $('.grid-row').not('.grid-row-template').each(function(i, row){
437
            // Delete empty rows - except any empty rows that have been created
438
            // for the purpose of this new chart.
439
            if($(row).find('.item.widget').length === 0 && new_config.row !== i + 1) {
440
                $(row).remove();
441
            }
442
        });
443
        // Update the row orders after deleting empty ones
444
        updateRowOrder();
445
        var newfield = $('<input class="form-control" type="text">');
446
        // Add a unique guid for referencing later.
447
        newfield.attr('name', 'module_' + new_config.id);
448
        newfield.val(JSON.stringify(new_config));
449
        $('.modules').append(newfield);
450
        // Save immediately.
451
        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...
452
    }
453
454
    function isModalButton(e) {
455
        return e.relatedTarget.id === ADD_MODULE.selector.replace('#', '');
456
    }
457
458
    function isRowButton(e) {
459
        return $(e.relatedTarget).hasClass('grid-row-label');
460
    }
461
462
    function clearForm() {
463
        WIDGET_FORM.find('label')
464
        .removeClass('has-error')
465
        .removeClass('has-success')
466
        .find('input, select')
467
        .each(function(_, input){
468
            $(input).val('');
469
        });
470
    }
471
472
    function deleteRow(row) {
473
        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...
474
        row.find('.item.widget').each(function(i, widget){
0 ignored issues
show
Unused Code introduced by
The parameter widget is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter i is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

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