Completed
Push — master ( c4c141...16569e )
by Chris
01:28
created

app.js ➔ Widgets   B

Complexity

Conditions 1
Paths 8

Size

Total Lines 99

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 1
dl 0
loc 99
rs 8.3103
c 6
b 0
f 0
nc 8
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        self.getAllOfProp = function(propname) {
72
            var props = [];
73
            $.each(self.all(), function(i, widg){
74
                props.push(widg.config[propname]);
75
            });
76
            return props;
77
        };
78
        self.loadAll = function() {
79
            var unique_urls = d3.set(self.getAllOfProp('dataSource')).values();
80
            var cached = {};
81
            var proms = [];
82
            // Build out promises.
83
            $.each(unique_urls, function(_, url){
84
                proms.push($.getJSON(url));
85
            });
86
            // Retrieve and gather the promises
87
            $.when.apply($, proms).done(whenAllDone).then(thenAfter, thenAfterFailed);
88
89
            function whenAllDone() {
90
                if(arguments.length === 3) {
91
                    var ref_url = unique_urls[0];
92
                    var data = arguments[0];
93
                    if(ref_url) {
94
                        cached[ref_url] = data;
95
                    }
96
                } else {
97
                    $.each(arguments, function(index, prom){
98
                        var ref_url = unique_urls[index];
99
                        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...
100
                        if(ref_url) {
101
                            data = prom[0];
102
                            cached[ref_url] = data;
103
                        }
104
                    });
105
                }
106
                // Inject a cached value on the config for use down the road
107
                // (this is done so little is changed with the architecture of getting and loading).
108
                for(var guid in self.all()){
109
                    // Don't refresh, just update config with new key value for cached data.
110
                    var widg = self.get(guid);
111
                    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 92. 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...
112
                    // Grab data from specific `key` key, if it exists (for shared data on a single endpoint).
113
                    var cachedData = widg.config.key && data.multicharts ? data.multicharts[widg.config.key] : data;
114
                    widg.update({cachedData: cachedData}, true);
115
                    // Actually load them all
116
                    widg.load();
117
                }
118
            }
119
120
            function thenAfter() {}
121
            function thenAfterFailed(error) {}
0 ignored issues
show
Unused Code introduced by
The parameter error 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...
122
        };
123
        self.newModel = function() {
124
            var config = getParsedFormConfig();
125
            var guid   = jsondash.util.guid();
126
            config['guid'] = guid;
127
            if(!config.refresh || !refreshableType(config.type)) {
128
                config['refresh'] = false;
129
            }
130
            return config;
131
        };
132
        self.populate = function(data) {
133
            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...
134
                // Closure to maintain each chart data value in loop
135
                (function(config){
136
                    var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 133. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
137
                    // Add div wrappers for js grid layout library,
138
                    // and add title, icons, and buttons
139
                    // This is the widget "model"/object used throughout.
140
                    self.add(config);
141
                })(data.modules[name]);
142
            }
143
        };
144
    }
145
146
    function Widget(container, config) {
147
        // model for a chart widget
148
        var self = this;
149
        self.config = config;
150
        self.guid = self.config.guid;
151
        self.container = container;
152
        self._refreshInterval = null;
153
        self._makeWidget = function(config) {
154
            if(document.querySelector('[data-guid="' + config.guid + '"]')){
155
                return d3.select('[data-guid="' + config.guid + '"]');
156
            }
157
            return d3.select(self.container).select('div')
158
                .append('div')
159
                .classed({item: true, widget: true})
160
                .attr('data-guid', config.guid)
161
                .attr('data-refresh', config.refresh)
162
                .attr('data-refresh-interval', config.refreshInterval)
163
                .style('width', config.width + 'px')
164
                .style('height', config.height + 'px')
165
                .html(d3.select(CHART_TEMPLATE.selector).html())
166
                .select('.widget-title .widget-title-text').text(config.name);
167
        };
168
        // d3 el
169
        self.el = self._makeWidget(config);
170
        // Jquery el
171
        self.$el = $(self.el[0]);
172
        self.init = function() {
173
            // Add event handlers for widget UI
174
            self.$el.find('.widget-refresh').on('click.charts', refreshWidget);
175
            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...
176
                self.delete();
177
            });
178
            // Allow swapping of edit/update events
179
            // for the edit button and form modal
180
            self.$el.find('.widget-edit').on('click.charts', function(){
181
                SAVE_WIDGET_BTN
182
                .attr('id', UPDATE_FORM_BTN.selector.replace('#', ''))
183
                .text('Update widget')
184
                .off('click.charts.save')
185
                .on('click.charts', onUpdateWidget);
186
            });
187
            if(self.config.refresh && self.config.refreshInterval) {
188
                self._refreshInterval = setInterval(function(){
189
                    self.load();
190
                }, parseInt(self.config.refreshInterval, 10));
191
            }
192
            if(my.layout === 'grid') {
193
                updateRowControls();
194
            }
195
        };
196
        self.getInput = function() {
197
            // Get the form input for this widget.
198
            return $('input[id="' + self.guid + '"]');
199
        };
200
        self.delete = function(bypass_confirm) {
201
            if(!bypass_confirm){
202
                if(!confirm('Are you sure?')) {
203
                    return;
204
                }
205
            }
206
            var row = self.$el.closest('.grid-row');
207
            clearInterval(self._refreshInterval);
208
            // Delete the input
209
            self.getInput().remove();
210
            self.$el.trigger(EVENTS.delete_widget, [self]);
211
            // Delete the widget
212
            self.el.remove();
213
            // Remove reference to the collection by guid
214
            my.widgets._delete(self.guid);
215
            EDIT_MODAL.modal('hide');
216
            // Redraw wall to replace visual 'hole'
217
            if(my.layout === 'grid') {
218
                // Fill empty holes in this charts' row
219
                fillEmptyCols(row);
220
                updateRowControls();
221
            }
222
            // Trigger update form into view since data is dirty
223
            EDIT_CONTAINER.collapse('show');
224
            // Refit grid - this should be last.
225
            fitGrid();
226
        };
227
        self.addGridClasses = function(sel, classes) {
228
            d3.map(classes, function(colcount){
229
                var classlist = {};
230
                classlist['col-md-' + colcount] = true;
231
                classlist['col-lg-' + colcount] = true;
232
                sel.classed(classlist);
233
            });
234
        };
235
        self.removeGridClasses = function(sel) {
236
            var bootstrap_classes = d3.range(1, 13);
237
            d3.map(bootstrap_classes, function(i){
238
                var classes = {};
239
                classes['col-md-' + i] = false;
240
                classes['col-lg-' + i] = false;
241
                sel.classed(classes);
242
            });
243
        };
244
        self.update = function(conf, dont_refresh) {
245
                /**
246
             * Single source to update all aspects of a widget - in DOM, in model, etc...
247
             */
248
            var widget = self.el;
249
            // Update model data
250
            self.config = $.extend(self.config, conf);
251
            // Trigger update form into view since data is dirty
252
            // Update visual size to existing widget.
253
            loader(widget);
254
            widget.style({
255
                height: self.config.height + 'px',
256
                width: my.layout === 'grid' ? '100%' : self.config.width + 'px'
257
            });
258
            if(my.layout === 'grid') {
259
                // Extract col number from config: format is "col-N"
260
                var colcount = self.config.width.split('-')[1];
261
                var parent = d3.select(widget.node().parentNode);
262
                // Reset all other grid classes and then add new one.
263
                self.removeGridClasses(parent);
264
                self.addGridClasses(parent, [colcount]);
265
                // Update row buttons based on current state
266
                updateRowControls();
267
            }
268
            widget.select('.widget-title .widget-title-text').text(self.config.name);
269
            // Update the form input for this widget.
270
            self._updateForm();
271
272
            if(!dont_refresh) {
273
                self.load();
274
                EDIT_CONTAINER.collapse();
275
                // Refit the grid
276
                fitGrid();
277
            } else {
278
                unload(widget);
279
            }
280
            $(widget[0]).trigger(EVENTS.update_widget);
281
        };
282
        self.load = function() {
283
            var widg      = my.widgets.get(self.guid);
284
            var widget    = self.el;
285
            var $widget   = self.$el;
286
            var config    = widg.config;
287
            var inputs    = $widget.find('.chart-inputs');
288
            var container = $('<div></div>').addClass('chart-container');
289
            var family    = config.family.toLowerCase();
290
291
            widget.classed({error: false});
292
            widget.select('.error-overlay')
293
                .classed({hidden: true})
294
                .select('.alert')
295
                .text('');
296
297
            loader(widget);
298
299
            try {
300
                // Cleanup for all widgets.
301
                widget.selectAll('.chart-container').remove();
302
                // Ensure the chart inputs comes AFTER any chart container.
303
                if(inputs.length > 0) {
304
                    inputs.before(container);
305
                } else {
306
                    $widget.append(container);
307
                }
308
                // Handle any custom inputs the user specified for this module.
309
                // They map to standard form inputs and correspond to query
310
                // arguments for this dataSource.
311
                if(config.inputs) {
312
                    handleInputs(widg, config);
313
                }
314
315
                // Retrieve and immediately call the appropriate handler.
316
                getHandler(family)(widget, config);
317
318
            } catch(e) {
319
                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...
320
                widget.classed({error: true});
321
                widget.select('.error-overlay')
322
                    .classed({hidden: false})
323
                    .select('.alert')
324
                    .text('Loading error: "' + e + '"');
325
                unload(widget);
326
            }
327
            addResizeEvent(widg);
328
        };
329
        self._updateForm = function() {
330
            self.getInput().val(JSON.stringify(self.config));
331
        };
332
333
        // Run init script on creation
334
        self.init();
335
    }
336
337
    /**
338
     * [fillEmptyCols Fill in gaps in a row when an item has been deleted (fixed grid only)]
339
     */
340
    function fillEmptyCols(row) {
341
        row.each(function(_, row){
342
            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...
343
            var cols = $(row).find('> div');
344
            cols.filter(function(i, col){
345
                return $(col).find('.item.widget').length === 0;
346
            }).remove();
347
        });
348
    }
349
350
    function togglePreviewOutput(is_on) {
351
        if(is_on) {
352
            API_PREVIEW_CONT.show();
353
            return;
354
        }
355
        API_PREVIEW_CONT.hide();
356
    }
357
358
    function previewAPIRoute(e) {
359
        e.preventDefault();
360
        // Shows the response of the API field as a json payload, inline.
361
        $.ajax({
362
            type: 'GET',
363
            url: API_ROUTE_URL.val().trim(),
364
            success: function(data) {
365
                API_PREVIEW.html(prettyCode(data));
366
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: false}]);
367
            },
368
            error: function(data, status, error) {
369
                API_PREVIEW.html(error);
370
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: true}]);
371
            }
372
        });
373
    }
374
375
    function refreshableType(type) {
376
        if(type === 'youtube') {return false;}
377
        return true;
378
    }
379
380
    function validateWidgetForm() {
381
        var is_valid = true;
382
        var url_field = WIDGET_FORM.find('[name="dataSource"]');
383
        WIDGET_FORM.find('[required]').each(function(i, el){
384
            if($(el).val() === '') {
385
                $(el).parent().addClass('has-error').removeClass('has-success');
386
                is_valid = false;
387
                return false;
388
            } else {
389
                $(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...
390
            }
391
        });
392
        // Validate youtube videos
393
        if(WIDGET_FORM.find('[name="type"]').val() === 'youtube') {
394
            if(!url_field.val().startsWith('<iframe')) {
395
                url_field.parent().addClass('has-error');
396
                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...
397
                return false;
398
            }
399
        }
400
        return is_valid;
401
    }
402
403
    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...
404
        if(!(validateWidgetForm())) {
405
            return false;
406
        }
407
        var new_config = my.widgets.newModel();
408
        // Remove empty rows and then update the order so it's consecutive.
409
        $('.grid-row').not('.grid-row-template').each(function(i, row){
410
            // Delete empty rows - except any empty rows that have been created
411
            // for the purpose of this new chart.
412
            if($(row).find('.item.widget').length === 0 && new_config.row !== i + 1) {
413
                $(row).remove();
414
            }
415
        });
416
        // Update the row orders after deleting empty ones
417
        updateRowOrder();
418
        var newfield = $('<input class="form-control" type="text">');
419
        // Add a unique guid for referencing later.
420
        newfield.attr('name', 'module_' + new_config.id);
421
        newfield.val(JSON.stringify(new_config));
422
        $('.modules').append(newfield);
423
        // Save immediately.
424
        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...
425
    }
426
427
    function isModalButton(e) {
428
        return e.relatedTarget.id === ADD_MODULE.selector.replace('#', '');
429
    }
430
431
    function isRowButton(e) {
432
        return $(e.relatedTarget).hasClass('grid-row-label');
433
    }
434
435
    function clearForm() {
436
        WIDGET_FORM.find('label')
437
        .removeClass('has-error')
438
        .removeClass('has-success')
439
        .find('input, select')
440
        .each(function(_, input){
441
            $(input).val('');
442
        });
443
    }
444
445
    function deleteRow(row) {
446
        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...
447
        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...
448
            var guid = $(this).data().guid;
449
            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...
450
        });
451
        // Remove AFTER removing the charts contained within
452
        row.remove();
453
        updateRowOrder();
454
        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...
455
    }
456
457
    function populateEditForm(e) {
458
        // If the modal caller was the add modal button, skip populating the field.
459
        API_PREVIEW.text('...');
460
        clearForm();
461
        if(isModalButton(e) || isRowButton(e)) {
462
            DELETE_BTN.hide();
463
            if(isRowButton(e)) {
464
                var row = $(e.relatedTarget).data().row;
465
                populateRowField(row);
466
                // Trigger the order field update based on the current row
467
                WIDGET_FORM.find('[name="row"]').change();
468
            } else {
469
                populateRowField();
470
            }
471
            return;
472
        }
473
        DELETE_BTN.show();
474
        // Updates the fields in the edit form to the active widgets values.
475
        var item = $(e.relatedTarget).closest('.item.widget');
476
        var guid = item.data().guid;
477
        var widget = my.widgets.get(guid);
478
        var conf = widget.config;
479
        populateRowField(conf.row);
480
        // Update the modal fields with this widgets' value.
481
        $.each(conf, function(field, val){
482
            if(field === 'override' || field === 'refresh') {
483
                WIDGET_FORM.find('[name="' + field + '"]').prop('checked', val);
484
            } else if(field === 'classes') {
485
                WIDGET_FORM.find('[name="' + field + '"]').val(val.join(','));
486
            } else {
487
                WIDGET_FORM.find('[name="' + field + '"]').val(val);
488
            }
489
        });
490
        // Update with current guid for referencing the module.
491
        WIDGET_FORM.attr('data-guid', guid);
492
        // Populate visual GUID
493
        $('[data-view-chart-guid]').find('.guid-text').text(guid);
494
        populateOrderField(widget);
495
        // Update form for specific row if row button was caller
496
        // Trigger event for select dropdown to ensure any UI is consistent.
497
        // This is done AFTER the fields have been pre-populated.
498
        WIDGET_FORM.find('[name="type"]').change();
499
        // A trigger for 3rd-party/external js to use to listen to.
500
        WIDGET_FORM.trigger(EVENTS.edit_form_loaded);
501
    }
502
503
    function populateRowField(row) {
504
        var rows_field = $('[name="row"]');
505
        var num_rows = $('.grid-row').not('.grid-row-template').length;
506
        // Don't try and populate if not in freeform mode.
507
        if(my.layout === 'freeform') {return;}
508
        if(num_rows === 0){
509
            addNewRow();
510
        }
511
        rows_field.find('option').remove();
512
        // Add new option fields - d3 range is exclusive so we add one
513
        d3.map(d3.range(1, num_rows + 1), function(i){
514
            var option = $('<option></option>');
515
            option.val(i).text('row ' + i);
516
            rows_field.append(option);
517
        });
518
        // Update current value
519
        if(row) {rows_field.val(row)};
520
    }
521
522
    /**
523
     * [populateOrderField Destroy and re-create order dropdown input based on number of items in a row, or in a dashboard.]
524
     * @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...
525
     */
526
    function populateOrderField(widget) {
527
        // Add the number of items to order field.
528
        var order_field = WIDGET_FORM.find('[name="order"]');
529
        var max_options = 0;
530
        if(my.layout === 'grid') {
531
            if(!widget) {
532
                var row = WIDGET_FORM.find('[name="row"]').val();
533
                // Get the max options based on the currently selected value in the row dropdown
534
                // We also add one since this is "adding" a new item so the order should include
535
                // one more than is currently there.
536
                max_options = $('.grid-row').eq(row - 1).find('.item.widget').length + 1;
537
            } else {
538
                // Get parent row and find number of widget children for this rows' order max
539
                max_options = $(widget.el[0]).closest('.grid-row').find('.item.widget').length;
540
            }
541
        } else {
542
            var widgets = $('.item.widget');
543
            max_options = widgets.length > 0 ? widgets.length: 2;
544
        }
545
        order_field.find('option').remove();
546
        // Add empty option.
547
        order_field.append('<option value=""></option>');
548
        d3.map(d3.range(1, max_options + 1), function(i){
549
            var option = $('<option></option>');
550
            option.val(i).text(i);
551
            order_field.append(option);
552
        });
553
        order_field.val(widget && widget.config ? widget.config.order : '');
554
    }
555
556
    /**
557
     * [getParsedFormConfig Get a config usable for each json widget based on the forms active values.]
558
     * @return {[object]} [The serialized config]
559
     */
560
    function getParsedFormConfig() {
561
        function parseNum(num) {
562
            // Like parseInt, but always returns a Number.
563
            if(isNaN(parseInt(num, 10))) {
564
                return 0;
565
            }
566
            return parseInt(num, 10);
567
        }
568
        var form = WIDGET_FORM;
569
        var conf = {
570
            name: form.find('[name="name"]').val(),
571
            type: form.find('[name="type"]').val(),
572
            family: form.find('[name="type"]').find('option:checked').data() ? form.find('[name="type"]').find('option:checked').data().family : null,
573
            width: form.find('[name="width"]').val(),
574
            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...
575
            dataSource: form.find('[name="dataSource"]').val(),
576
            override: form.find('[name="override"]').is(':checked'),
577
            order: parseNum(form.find('[name="order"]').val(), 10),
578
            refresh: form.find('[name="refresh"]').is(':checked'),
579
            refreshInterval: jsondash.util.intervalStrToMS(form.find('[name="refreshInterval"]').val()),
580
            classes: getClasses(form),
581
        };
582
        if(my.layout === 'grid') {
583
            conf['row'] = parseNum(form.find('[name="row"]').val());
584
        }
585
        return conf;
586
    }
587
588
    function getClasses(form) {
589
        var classes = form.find('[name="classes"]').val().replace(/\ /gi, '').split(',');
590
        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...
591
            return el !== '';
592
        });
593
    }
594
595
    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...
596
        var guid = WIDGET_FORM.attr('data-guid');
597
        var widget = my.widgets.get(guid);
598
        var conf = getParsedFormConfig();
599
        widget.update(conf);
600
    }
601
602
    function refreshWidget(e) {
603
        e.preventDefault();
604
        var el = my.widgets.getByEl($(this).closest('.widget'));
605
        el.$el.trigger(EVENTS.refresh_widget);
606
        el.load();
607
        fitGrid();
608
    }
609
610
    /**
611
     * [isPreviewableType Determine if a chart type can be previewed in the 'preview api' section of the modal]
612
     * @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...
613
     * @return {Boolean}      [Whether or not it's previewable]
614
     */
615
    function isPreviewableType(type) {
616
        if(type === 'iframe') {return false;}
617
        if(type === 'youtube') {return false;}
618
        if(type === 'custom') {return false;}
619
        if(type === 'image') {return false;}
620
        return true;
621
    }
622
623
    /**
624
     * [chartsTypeChanged Event handler for onChange event for chart type field]
625
     */
626
    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...
627
        var active_conf = getParsedFormConfig();
628
        var previewable = isPreviewableType(active_conf.type);
629
        togglePreviewOutput(previewable);
630
    }
631
632
    function populateGridWidthDropdown() {
633
        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...
634
        var form = d3.select(WIDGET_FORM.selector);
635
        form.select('[name="width"]').remove();
636
        form
637
            .append('select')
638
            .attr('name', 'width')
639
            .selectAll('option')
640
            .data(cols)
641
            .enter()
642
            .append('option')
643
            .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...
644
                return i;
645
            })
646
            .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...
647
                return i;
648
            });
649
    }
650
651
    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...
652
        var mode = MAIN_FORM.find('[name="mode"]').val();
653
        if(mode === 'grid') {
654
            populateGridWidthDropdown();
655
        }
656
    }
657
658
    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...
659
        // Update the order field based on the current rows item length.
660
        populateOrderField();
661
    }
662
663
    function loader(container) {
664
        container.select('.loader-overlay').classed({hidden: false});
665
        container.select('.widget-loader').classed({hidden: false});
666
    }
667
668
    function unload(container) {
669
        container.select('.loader-overlay').classed({hidden: true});
670
        container.select('.widget-loader').classed({hidden: true});
671
    }
672
673
    /**
674
     * [addDomEvents Add all dom event handlers here]
675
     */
676
    function addDomEvents() {
677
        MAIN_FORM.find('[name="mode"]').on('change.charts.row', chartsModeChanged);
678
        WIDGET_FORM.find('[name="row"]').on('change.charts.row', chartsRowChanged);
679
        // Chart type change
680
        WIDGET_FORM.find('[name="type"]').on('change.charts.type', chartsTypeChanged);
681
        // TODO: debounce/throttle
682
        API_PREVIEW_BTN.on('click.charts', previewAPIRoute);
683
        // Save module popup form
684
        SAVE_WIDGET_BTN.on('click.charts.save', saveWidget);
685
        // Edit existing modules
686
        EDIT_MODAL.on('show.bs.modal', populateEditForm);
687
        UPDATE_FORM_BTN.on('click.charts.save', onUpdateWidget);
688
689
        // Allow swapping of edit/update events
690
        // for the add module button and form modal
691
        ADD_MODULE.on('click.charts', function(){
692
            UPDATE_FORM_BTN
693
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
694
            .text('Save widget')
695
            .off('click.charts.save')
696
            .on('click.charts.save', saveWidget);
697
        });
698
699
        // Allow swapping of edit/update events
700
        // for the add module per row button and form modal
701
        VIEW_BUILDER.on('click.charts', '.grid-row-label', function(){
702
            UPDATE_FORM_BTN
703
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
704
            .text('Save widget')
705
            .off('click.charts.save')
706
            .on('click.charts.save', saveWidget);
707
        });
708
709
        // Add delete button for existing widgets.
710
        DELETE_BTN.on('click.charts', function(e){
711
            e.preventDefault();
712
            var guid = WIDGET_FORM.attr('data-guid');
713
            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...
714
        });
715
        // Add delete confirm for dashboards.
716
        DELETE_DASHBOARD.on('submit.charts', function(e){
717
            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...
718
        });
719
720
        // Format json config display
721
        $('#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...
722
            var code = $(this).find('code').text();
723
            $(this).find('code').text(prettyCode(code));
724
        });
725
726
        // Add event for downloading json config raw.
727
        // Will provide decent support but still not major: http://caniuse.com/#search=download
728
        $('[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...
729
            var datestr = new Date().toString().replace(/ /gi, '-');
730
            var data = encodeURIComponent(JSON.stringify(JSON_DATA.val(), null, 4));
731
            data = "data:text/json;charset=utf-8," + data;
732
            $(this).attr('href', data);
733
            $(this).attr('download', 'charts-config-raw-' + datestr + '.json');
734
        });
735
736
        // For fixed grid, add events for making new rows.
737
        ADD_ROW_CONTS.find('.btn').on('click', addNewRow);
738
739
        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...
740
            $('body').toggleClass('jsondash-editing');
741
            updateRowControls();
742
        });
743
744
        $('.delete-row').on('click', function(e){
745
            e.preventDefault();
746
            var row = $(this).closest('.grid-row');
747
            if(row.find('.item.widget').length > 0) {
748
                if(!confirm('Are you sure?')) {
749
                    return;
750
                }
751
            }
752
            deleteRow(row);
753
        });
754
    }
755
756
    function initFixedDragDrop(options) {
757
        var grid_drag_opts = {
758
            connectToSortable: '.grid-row'
759
        };
760
        $('.grid-row').droppable({
761
            drop: function(event, ui) {
762
                // update the widgets location
763
                var idx    = $(this).index();
764
                var el     = $(ui.draggable);
765
                var widget = my.widgets.getByEl(el);
766
                widget.update({row: idx}, true);
767
                // Actually move the dom element, and reset
768
                // the dragging css so it snaps into the row container
769
                el.parent().appendTo($(this));
770
                el.css({
771
                    position: 'relative',
772
                    top: 0,
773
                    left: 0
774
                });
775
            }
776
        });
777
        $('.item.widget').draggable($.extend(grid_drag_opts, options));
778
    }
779
780
    function fitGrid(grid_packer_opts, init) {
781
        var packer_options = $.isPlainObject(grid_packer_opts) ? grid_packer_opts : {};
782
        var grid_packer_options = $.extend({}, packer_options, {});
783
        var drag_options = {
784
            scroll: true,
785
            handle: '.dragger',
786
            start: function() {
787
                $('.grid-row').addClass('drag-target');
788
            },
789
            stop: function(){
790
                $('.grid-row').removeClass('drag-target');
791
                EDIT_CONTAINER.collapse('show');
792
                if(my.layout === 'grid') {
793
                    // Update row order.
794
                    updateChartsRowOrder();
795
                } else {
796
                    my.chart_wall.packery(grid_packer_options);
797
                    updateChartsOrder();
798
                }
799
            }
800
        };
801
        if(my.layout === 'grid' && $('.grid-row').length > 1) {
802
            initFixedDragDrop(drag_options);
803
            return;
804
        }
805
        if(init) {
806
            my.chart_wall = $('#container').packery(grid_packer_options);
807
            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...
808
            my.chart_wall.packery('bindUIDraggableEvents', items);
809
        } else {
810
            my.chart_wall.packery(grid_packer_options);
811
        }
812
    }
813
814
    function updateChartsOrder() {
815
        // Update the order and order value of each chart
816
        var items = my.chart_wall.packery('getItemElements');
817
        // Update module order
818
        $.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...
819
            var widget = my.widgets.getByEl($(this));
820
            widget.update({order: i}, true);
821
        });
822
    }
823
824
    function handleInputs(widget, config) {
825
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
826
        // Load event handlers for these newly created forms.
827
        $(inputs_selector).find('form').on('submit', function(e){
828
            e.stopImmediatePropagation();
829
            e.preventDefault();
830
            // Just create a new url for this, but use existing config.
831
            // The global object config will not be altered.
832
            // The first {} here is important, as it enforces a deep copy,
833
            // not a mutation of the original object.
834
            var url = config.dataSource;
835
            // Ensure we don't lose params already save on this endpoint url.
836
            var existing_params = url.split('?')[1];
837
            var params = jsondash.util.getValidParamString($(this).serializeArray());
838
            params = jsondash.util.reformatQueryParams(existing_params, params);
839
            var _config = $.extend({}, config, {
840
                dataSource: url.replace(/\?.+/, '') + '?' + params
841
            });
842
            my.widgets.get(config.guid).update(_config, true);
843
            // Otherwise reload like normal.
844
            my.widgets.get(config.guid).load();
845
            // Hide the form again
846
            $(inputs_selector).removeClass('in');
847
        });
848
    }
849
850
    function getHandler(family) {
851
        var handlers  = {
852
            basic          : jsondash.handlers.handleBasic,
853
            datatable      : jsondash.handlers.handleDataTable,
854
            sparkline      : jsondash.handlers.handleSparkline,
855
            timeline       : jsondash.handlers.handleTimeline,
856
            venn           : jsondash.handlers.handleVenn,
857
            graph          : jsondash.handlers.handleGraph,
858
            wordcloud      : jsondash.handlers.handleWordCloud,
859
            vega           : jsondash.handlers.handleVegaLite,
860
            plotlystandard : jsondash.handlers.handlePlotly,
861
            cytoscape      : jsondash.handlers.handleCytoscape,
862
            sigmajs        : jsondash.handlers.handleSigma,
863
            c3             : jsondash.handlers.handleC3,
864
            d3             : jsondash.handlers.handleD3
865
        };
866
        return handlers[family];
867
    }
868
869
    function addResizeEvent(widg) {
870
        // Add resize event
871
        var resize_opts = {
872
            helper: 'resizable-helper',
873
            minWidth: MIN_CHART_SIZE,
874
            minHeight: MIN_CHART_SIZE,
875
            maxWidth: VIEW_BUILDER.width(),
876
            handles: my.layout === 'grid' ? 's' : 'e, s, se',
877
            stop: function(event, ui) {
878
                var newconf = {height: ui.size.height};
879
                if(my.layout !== 'grid') {
880
                    newconf['width'] = ui.size.width;
881
                }
882
                // Update the configs dimensions.
883
                widg.update(newconf);
884
                fitGrid();
885
                // Open save panel
886
                EDIT_CONTAINER.collapse('show');
887
            }
888
        };
889
        // Add snap to grid (vertical only) in fixed grid mode.
890
        // This makes aligning charts easier because the snap points
891
        // are more likely to be consistent.
892
        if(my.layout === 'grid') {resize_opts['grid'] = 20;}
893
        $(widg.el[0]).resizable(resize_opts);
894
    }
895
896
    function prettyCode(code) {
897
        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...
898
        return JSON.stringify(JSON.parse(code), null, 4);
899
    }
900
901
    function prettifyJSONPreview() {
902
        // Reformat the code inside of the raw json field,
903
        // to pretty print for the user.
904
        JSON_DATA.text(prettyCode(JSON_DATA.text()));
905
    }
906
907
    function addNewRow(e) {
908
        // Add a new row with a toggleable label that indicates
909
        // which row it is for user editing.
910
        var placement = 'top';
911
        if(e) {
912
            e.preventDefault();
913
            placement = $(this).closest('.row').data().rowPlacement;
914
        }
915
        var el = ROW_TEMPLATE.clone(true);
916
        el.removeClass('grid-row-template');
917
        if(placement === 'top') {
918
            VIEW_BUILDER.find('.add-new-row-container:first').after(el);
919
        } else {
920
            VIEW_BUILDER.find('.add-new-row-container:last').before(el);
921
        }
922
        // Update the row ordering text
923
        updateRowOrder();
924
        // Add new events for dragging/dropping
925
        fitGrid();
926
        el.trigger(EVENTS.add_row);
927
    }
928
929
    function updateChartsRowOrder() {
930
        // Update the row order for each chart.
931
        // This is necessary for cases like adding a new row,
932
        // where the order is updated (before or after) the current row.
933
        // NOTE: This function assumes the row order has been recalculated in advance!
934
        $('.grid-row').each(function(i, row){
935
            $(row).find('.item.widget').each(function(j, item){
936
                var widget = my.widgets.getByEl($(item));
937
                widget.update({row: i + 1, order: j + 1}, true);
938
            });
939
        });
940
    }
941
942
    function updateRowOrder() {
943
        $('.grid-row').not('.grid-row-template').each(function(i, row){
944
            var idx = $(row).index();
945
            $(row).find('.grid-row-label').attr('data-row', idx);
946
            $(row).find('.rownum').text(idx);
947
        });
948
        updateChartsRowOrder();
949
    }
950
951
    function loadDashboard(data) {
952
        // Load the grid before rendering the ajax, since the DOM
953
        // is rendered server side.
954
        fitGrid({
955
            columnWidth: 5,
956
            itemSelector: '.item',
957
            transitionDuration: 0,
958
            fitWidth: true
959
        }, true);
960
        $('.item.widget').removeClass('hidden');
961
962
        // Populate widgets with the config data.
963
        my.widgets.populate(data);
964
965
        // Load all widgets, adding actual ajax data.
966
        my.widgets.loadAll();
967
968
        // Setup responsive handlers
969
        var jres = jRespond([{
970
            label: 'handheld',
971
            enter: 0,
972
            exit: 767
973
        }]);
974
        jres.addFunc({
975
            breakpoint: 'handheld',
976
            enter: function() {
977
                $('.widget').css({
978
                    'max-width': '100%',
979
                    'width': '100%',
980
                    'position': 'static'
981
                });
982
            }
983
        });
984
        prettifyJSONPreview();
985
        populateRowField();
986
        fitGrid();
987
        if(isEmptyDashboard()) {EDIT_TOGGLE_BTN.click();}
988
        MAIN_CONTAINER.trigger(EVENTS.init);
989
    }
990
991
    /**
992
     * [updateRowControls Check each row's buttons and disable the "add" button if that row
993
     * is at the maximum colcount (12)]
994
     */
995
    function updateRowControls() {
996
        $('.grid-row').not('.grid-row-template').each(function(i, row){
997
            var count = getRowColCount($(row));
998
            if(count >= 12) {
999
                $(row).find('.grid-row-label').addClass('disabled');
1000
            } else {
1001
                $(row).find('.grid-row-label').removeClass('disabled');
1002
            }
1003
        });
1004
    }
1005
1006
    /**
1007
     * [getRowColCount Return the column count of a row.]
1008
     * @param  {[dom selection]} row [The row selection]
1009
     */
1010
    function getRowColCount(row) {
1011
        var count = 0;
1012
        row.find('.item.widget').each(function(j, item){
1013
            var classes = $(item).parent().attr('class').split(/\s+/);
1014
            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...
1015
                if(classes[i].startsWith('col-md-')) {
1016
                    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 1016. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
1017
                }
1018
            }
1019
        });
1020
        return count;
1021
    }
1022
1023
    function isEmptyDashboard() {
1024
        return $('.item.widget').length === 0;
1025
    }
1026
1027
    my.config = {
1028
        WIDGET_MARGIN_X: 20,
1029
        WIDGET_MARGIN_Y: 60
1030
    };
1031
    my.loadDashboard = loadDashboard;
1032
    my.handlers = {};
1033
    my.util = {};
1034
    my.loader = loader;
1035
    my.unload = unload;
1036
    my.addDomEvents = addDomEvents;
1037
    my.getActiveConfig = getParsedFormConfig;
1038
    my.layout = VIEW_BUILDER.length > 0 ? VIEW_BUILDER.data().layout : null;
1039
    my.widgets = new Widgets();
1040
    return my;
1041
}();
1042