Completed
Push — master ( 29c86e...db1e3f )
by Chris
57s
created

app.js ➔ ... ➔ self.getAllOfPropUnless   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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