Completed
Push — master ( edf8e8...b02095 )
by Chris
01:05
created

app.js ➔ ... ➔ $(ꞌ.item.widgetꞌ).each   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 5
rs 9.4285
nc 1
nop 2
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 dashboard_data = null;
11
    var $API_ROUTE_URL = '[name="dataSource"]';
12
    var $API_PREVIEW = '#api-output';
13
    var $API_PREVIEW_BTN = '#api-output-preview';
14
    var $MODULE_FORM = '#module-form';
15
    var $VIEW_BUILDER = '#view-builder';
16
    var $ADD_MODULE = '#add-module';
17
    var $MAIN_CONTAINER = '#container';
18
    var $EDIT_MODAL = '#chart-options';
19
    var $DELETE_BTN = '#delete-widget';
20
    var $DELETE_DASHBOARD = '.delete-dashboard';
21
    var $SAVE_MODULE = '#save-module';
22
    var $EDIT_CONTAINER = '#edit-view-container';
23
24
    function addWidget(container, model) {
25
        if(document.querySelector('[data-guid="' + model.guid + '"]')) return d3.select('[data-guid="' + model.guid + '"]');
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...
26
        return d3.select(container).select('div')
27
            .append('div')
28
            .classed({item: true, widget: true})
29
            .attr('data-guid', model.guid)
30
            .attr('data-refresh', model.refresh)
31
            .attr('data-refresh-interval', model.refreshInterval)
32
            .style('width', model.width + 'px')
33
            .style('height', model.height + 'px')
34
            .html(d3.select('#chart-template').html())
35
            .select('.widget-title .widget-title-text').text(model.name);
36
    }
37
38
    function previewAPIRoute(e) {
39
        e.preventDefault();
40
        // Shows the response of the API field as a json payload, inline.
41
        $.ajax({
42
            type: 'get',
43
            url: $($API_ROUTE_URL).val().trim(),
44
            success: function(d) {
45
               $($API_PREVIEW).html(prettyCode(d));
46
            },
47
            error: function(d, status, error) {
48
                $($API_PREVIEW).html(error);
49
            }
50
        });
51
    }
52
53
    function refreshableType(type) {
54
        if(type === 'youtube') {return false;}
55
        return true;
56
    }
57
58
    function saveModule(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...
59
        var data     = jsondash.util.serializeToJSON($($MODULE_FORM).serializeArray());
60
        var newfield = $('<input class="form-control" type="text">');
61
        var id       = jsondash.util.guid();
62
        // Add a unique guid for referencing later.
63
        data['guid'] = id;
64
        // Add family for lookups
65
        data['family'] = $($MODULE_FORM).find('[name="type"]').find('option:selected').data().family;
66
        if(!data.refresh || !refreshableType(data.type)) {data['refresh'] = false;}
67
        if(!data.override) {data['override'] = false;}
68
        newfield.attr('name', 'module_' + id);
69
        newfield.val(JSON.stringify(data));
70
        $('.modules').append(newfield);
71
        // Add new visual block to view grid
72
        addWidget($VIEW_BUILDER, data);
73
        // Refit the grid
74
        fitGrid();
75
    }
76
77
    function isModalButton(e) {
78
        return e.relatedTarget.id === $ADD_MODULE.replace('#', '');
79
    }
80
81
    function updateEditForm(e) {
82
        var module_form = $($MODULE_FORM);
83
        // If the modal caller was the add modal button, skip populating the field.
84
        if(isModalButton(e)) {
85
            module_form.find('input').each(function(_, input){
86
                $(input).val('');
87
            });
88
            $($API_PREVIEW).empty();
89
            $($DELETE_BTN).hide();
90
            return;
91
        }
92
        $($DELETE_BTN).show();
93
        // Updates the fields in the edit form to the active widgets values.
94
        var item = $(e.relatedTarget).closest('.item.widget');
95
        var guid = item.data().guid;
96
        var module = getModule(item);
97
        // Update the modal window fields with this one's value.
98
        $.each(module, function(field, val){
99
            if(field === 'override' || field === 'refresh') {
100
                module_form.find('[name="' + field + '"]').prop('checked', val);
101
            } else {
102
                module_form.find('[name="' + field + '"]').val(val);
103
            }
104
        });
105
        // Update with current guid for referencing the module.
106
        module_form.attr('data-guid', guid);
107
        populateOrderField(module);
108
    }
109
110
    function populateOrderField(module) {
111
        var module_form = $($MODULE_FORM);
112
        // Add the number of items to order field.
113
        var order_field = module_form.find('[name="order"]');
114
        order_field.find('option').remove();
115
        order_field.append('<option value=""></option>');
116
        $('.item.widget').each(function(i, _){
0 ignored issues
show
Unused Code introduced by
The parameter _ 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...
117
            var option = $('<option></option>');
118
            option.val(i).text(i);
119
            order_field.append(option);
120
        });
121
        order_field.val(module.order || '');
122
    }
123
124
    function updateModule(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...
125
        var module_form = $($MODULE_FORM);
126
        // Updates the module input fields with new data by rewriting them all.
127
        var guid = module_form.attr('data-guid');
128
        var active = getModuleByGUID(guid);
129
        // Update the modules values to the current input values.
130
        module_form.find('input').each(function(_, input){
131
            var name = $(input).attr('name');
132
            if(name) {
133
                if(name === 'override' || name === 'refresh') {
134
                    // Convert checkbox to json friendly format.
135
                    active[name] = $(input).is(':checked');
136
                } else {
137
                    active[name] = $(input).val();
138
                }
139
            }
140
        });
141
        // Update bar chart type
142
        active['type'] = module_form.find('[name="type"]').val();
143
        // Update order
144
        active['order'] = parseInt(module_form.find('[name="order"]').val(), 10);
145
        // Clear out module input values
146
        $('.modules').empty();
147
        $.each(dashboard_data.modules, function(i, module){
148
            var val = JSON.stringify(module, module);
149
            var input = $('<input type="text" name="module_' + i + '" class="form-control">');
150
            input.val(val);
151
            $('.modules').append(input);
152
        });
153
        updateWidget(active);
154
        $($EDIT_CONTAINER).collapse();
155
        // Refit the grid
156
        fitGrid();
157
    }
158
159
    function updateWidget(config) {
160
        // Trigger update form into view since data is dirty
161
        // Update visual size to existing widget.
162
        var widget = getModuleWidgetByGUID(config.guid);
163
        loader(widget);
164
        widget.style({
165
            height: config.height + 'px',
166
            width: config.width + 'px'
167
        });
168
        widget.select('.widget-title .widget-title-text').text(config.name);
169
        loadWidgetData(widget, config);
170
    }
171
172
    function refreshWidget(e) {
173
        e.preventDefault();
174
        var config = getModule($(this).closest('.widget'));
175
        var widget = addWidget($MAIN_CONTAINER, config);
176
        loadWidgetData(widget, config);
177
        fitGrid();
178
    }
179
180
    function addChartContainers(container, data) {
181
        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...
182
            // Closure to maintain each chart data value in loop
183
            (function(config){
184
                var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 181. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
185
                // Add div wrappers for js grid layout library,
186
                // and add title, icons, and buttons
187
                var widget = addWidget(container, config);
188
                // Determine how to load this widget
189
                loadWidgetData(widget, config);
190
            })(data.modules[name]);
191
        }
192
        fitGrid();
193
    }
194
195
    function getModuleWidgetByGUID(guid) {
196
        return d3.select('.item.widget[data-guid="' + guid + '"]');
197
    }
198
199
    function getModuleByGUID(guid) {
200
        return dashboard_data.modules.find(function(n){return n['guid'] === guid});
201
    }
202
203
    function deleteModule(e) {
204
        e.preventDefault();
205
        if(!confirm('Are you sure?')) {return;}
206
        var guid = $($MODULE_FORM).attr('data-guid');
207
        // Remove form input and visual widget
208
        $('.modules').find('#' + guid).remove();
209
        $('.item.widget[data-guid="' + guid + '"]').remove();
210
        $($EDIT_MODAL).modal('hide');
211
        // Redraw wall to replace visual 'hole'
212
        fitGrid();
213
        // Trigger update form into view since data is dirty
214
        $($EDIT_CONTAINER).collapse('show');
215
    }
216
217
    function addDomEvents() {
218
        // TODO: debounce/throttle
219
        $($API_ROUTE_URL).on('change.charts', previewAPIRoute);
220
        $($API_PREVIEW_BTN).on('click.charts', previewAPIRoute);
221
        // Save module popup form
222
        $($SAVE_MODULE).on('click.charts.module', saveModule);
223
        // Edit existing modules
224
        $($EDIT_MODAL).on('show.bs.modal', updateEditForm);
225
        $('#update-module').on('click.charts.module', updateModule);
226
        // Allow swapping of edit/update events
227
        // for the add module button and form modal
228
        $($ADD_MODULE).on('click.charts', function(){
229
            $('#update-module')
230
            .attr('id', $SAVE_MODULE.replace('#', ''))
231
            .text('Save module')
232
            .off('click.charts.module')
233
            .on('click.charts', saveModule);
234
        });
235
        // Allow swapping of edit/update events
236
        // for the edit button and form modal
237
        $('.widget-edit').on('click.charts', function(){
238
            $($SAVE_MODULE)
239
            .attr('id', 'update-module')
240
            .text('Update module')
241
            .off('click.charts.module')
242
            .on('click.charts', updateModule);
243
        });
244
        // Add delete button for existing widgets.
245
        $($DELETE_BTN).on('click.charts', deleteModule);
246
        // Add delete confirm for dashboards.
247
        $($DELETE_DASHBOARD).on('submit.charts', function(e){
248
            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...
249
        });
250
    }
251
252
    function initGrid(container) {
0 ignored issues
show
Unused Code introduced by
The parameter container 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...
253
        fitGrid({
254
            columnWidth: 5,
255
            itemSelector: '.item',
256
            transitionDuration: 0,
257
            fitWidth: true
258
        }, true);
259
        $('.item.widget').removeClass('hidden');
260
    }
261
262
    function fitGrid(opts, init) {
263
        var valid_options = $.isPlainObject(opts);
0 ignored issues
show
Unused Code introduced by
The variable valid_options seems to be never used. Consider removing it.
Loading history...
264
        var options = $.extend({}, opts, {});
265
        if(init) {
266
            my.chart_wall = $('#container').packery(options);
267
            items = my.chart_wall.find('.item').draggable({
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...
268
                scroll: true,
269
                handle: '.dragger',
270
                stop: function(){
271
                    $($EDIT_CONTAINER).collapse('show');
272
                    updateModuleOrder();
273
                    my.chart_wall.packery(options);
274
                }
275
            });
276
            my.chart_wall.packery('bindUIDraggableEvents', items);
277
        } else {
278
            my.chart_wall.packery(options);
279
        }
280
    }
281
282
    function updateModuleOrder() {
283
        var items = my.chart_wall.packery('getItemElements');
284
        // Update module order
285
        $.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...
286
            var module = getModule($(this));
287
            var config = $.extend(module, {order: i});
288
            updateModuleInput(config);
289
        });
290
    }
291
292
    function getModule(el) {
293
        // Return module by element
294
        var data = el.data();
295
        var guid = data.guid;
296
        var module = getModuleByGUID(guid);
297
        return module;
298
    }
299
300
    function loader(container) {
301
        container.select('.loader-overlay').classed({hidden: false});
302
        container.select('.widget-loader').classed({hidden: false});
303
    }
304
305
    function unload(container) {
306
        container.select('.loader-overlay').classed({hidden: true});
307
        container.select('.widget-loader').classed({hidden: true});
308
    }
309
310
    function handleInputs(widget, config) {
311
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
312
        // Load event handlers for these newly created forms.
313
        $(inputs_selector).find('form').on('submit', function(e){
314
            e.stopImmediatePropagation();
315
            e.preventDefault();
316
            // Just create a new url for this, but use existing config.
317
            // The global object config will not be altered.
318
            // The first {} here is important, as it enforces a deep copy,
319
            // not a mutation of the original object.
320
            var url = config.dataSource;
321
            // Ensure we don't lose params already save on this endpoint url.
322
            var existing_params = url.split('?')[1];
323
            var params = getValidParamString($(this).serializeArray());
324
            var _config = $.extend({}, config, {
325
                dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params
326
            });
327
            // Otherwise reload like normal.
328
            loadWidgetData(widget, _config);
329
            // Hide the form again
330
            $(inputs_selector).removeClass('in');
331
        });
332
    }
333
334
    function getValidParamString(arr) {
335
        // Jquery $.serialize and $.serializeArray will
336
        // return empty query parameters, which is undesirable and can
337
        // be error prone for RESTFUL endpoints.
338
        // e.g. `foo=bar&bar=` becomes `foo=bar`
339
        var param_str = '';
340
        arr = arr.filter(function(param, i){return param.value !== '';});
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...
341
        $.each(arr, function(i, param){
342
            param_str += (param.name + '=' + param.value);
343
            if(i < arr.length - 1 && arr.length > 1) param_str += '&';
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...
344
        });
345
        return param_str;
346
    }
347
348
    function loadWidgetData(widget, config) {
349
        loader(widget);
350
        try {
351
            // Handle any custom inputs the user specified for this module.
352
            // They map to standard form inputs and correspond to query
353
            // arguments for this dataSource.
354
            if(config.inputs) {handleInputs(widget, config);}
355
356
            if(config.type === 'datatable') {
357
                jsondash.handlers.handleDataTable(widget, config);
358
            }
359
            else if(jsondash.util.isSparkline(config.type)) {
360
                jsondash.handlers.handleSparkline(widget, config);
361
            }
362
            else if(config.type === 'iframe') {
363
                jsondash.handlers.handleIframe(widget, config);
364
            }
365
            else if(config.type === 'timeline') {
366
                jsondash.handlers.handleTimeline(widget, config);
367
            }
368
            else if(config.type === 'venn') {
369
                jsondash.handlers.handleVenn(widget, config);
370
            }
371
            else if(config.type === 'number') {
372
                jsondash.handlers.handleSingleNum(widget, config);
373
            }
374
            else if(config.type === 'youtube') {
375
                jsondash.handlers.handleYoutube(widget, config);
376
            }
377
            else if(config.type === 'graph'){
378
                jsondash.handlers.handleGraph(widget, config);
379
            }
380
            else if(config.type === 'custom') {
381
                jsondash.handlers.handleCustom(widget, config);
382
            }
383
            else if(config.type === 'plotly-any') {
384
                jsondash.handlers.handlePlotly(widget, config);
385
            }
386
            else if(jsondash.util.isD3Subtype(config)) {
387
                jsondash.handlers.handleD3(widget, config);
388
            } else {
389
                jsondash.handlers.handleC3(widget, config);
390
            }
391
        } catch(e) {
392
            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...
393
            unload(widget);
394
        }
395
        addResizeEvent(widget, config);
396
    }
397
398
    function addResizeEvent(widget, config) {
399
        // Add resize event
400
        $(widget[0]).resizable({
401
            helper: 'resizable-helper',
402
            minWidth: 200,
403
            minHeight: 200,
404
            stop: function(event, ui) {
405
                // Update the configs dimensions.
406
                config = $.extend(config, {width: ui.size.width, height: ui.size.height});
407
                updateModuleInput(config);
408
                loadWidgetData(widget, config);
409
                fitGrid();
410
                // Open save panel
411
                $($EDIT_CONTAINER).collapse('show');
412
            }
413
        });
414
    }
415
416
    function updateModuleInput(config) {
417
        $('input[id="' + config.guid + '"]').val(JSON.stringify(config));
418
    }
419
420
    function prettyCode(code) {
421
        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...
422
        return JSON.stringify(JSON.parse(code), null, 4);
423
    }
424
425
    function addRefreshers(modules) {
426
        $.each(modules, function(_, module){
427
            if(module.refresh && module.refreshInterval) {
428
                var container = d3.select('[data-guid="' + module.guid + '"]');
429
                setInterval(function(){
430
                    loadWidgetData(container, module);
431
                }, parseInt(module.refreshInterval, 10));
432
            }
433
        });
434
    }
435
436
    function loadDashboard(data) {
437
        // Load the grid before rendering the ajax, since the DOM
438
        // is rendered server side.
439
        initGrid($MAIN_CONTAINER);
440
        // Add actual ajax data.
441
        addChartContainers($MAIN_CONTAINER, data);
442
        dashboard_data = data;
443
444
        // Add event handlers for widget UI
445
        $('.widget-refresh').on('click.charts', refreshWidget);
446
447
        // Setup refresh intervals for all widgets that specify it.
448
        addRefreshers(data.modules);
449
450
        // Format json config display
451
        $('#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...
452
            var code = $(this).find('code').text();
453
            $(this).find('code').text(prettyCode(code));
454
        });
455
456
        // Reformat the code inside of the raw json field, to pretty print
457
        // for the user.
458
        $('#raw-config').text(prettyCode($('#raw-config').text()));
459
460
        // Setup responsive handlers
461
        var jres = jRespond([
462
        {
463
            label: 'handheld',
464
            enter: 0,
465
            exit: 767
466
        }
467
        ]);
468
        jres.addFunc({
469
            breakpoint: 'handheld',
470
            enter: function() {
471
                $('.widget').css({
472
                    'max-width': '100%',
473
                    'width': '100%',
474
                    'position': 'static'
475
                });
476
            }
477
        });
478
    }
479
    my.config = {
480
        WIDGET_MARGIN_X: 20,
481
        WIDGET_MARGIN_Y: 60
482
    };
483
    my.loadDashboard = loadDashboard;
484
    my.handlers = {};
485
    my.util = {};
486
    my.loader = loader;
487
    my.unload = unload;
488
    my.addDomEvents = addDomEvents;
489
    return my;
490
}();
491