Completed
Push — master ( b7f945...edf8e8 )
by Chris
01:12
created

app.js ➔ fitGrid   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 9
rs 9.6666
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('select 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 data = $(e.relatedTarget).closest('.item.widget').data();
95
        var guid = data.guid;
96
        var module = getModuleByGUID(guid);
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
    }
108
109
    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...
110
        var module_form = $($MODULE_FORM);
111
        // Updates the module input fields with new data by rewriting them all.
112
        var guid = module_form.attr('data-guid');
113
        var active = getModuleByGUID(guid);
114
        // Update the modules values to the current input values.
115
        module_form.find('input').each(function(_, input){
116
            var name = $(input).attr('name');
117
            if(name) {
118
                if(name === 'override' || name === 'refresh') {
119
                    // Convert checkbox to json friendly format.
120
                    active[name] = $(input).is(':checked');
121
                } else {
122
                    active[name] = $(input).val();
123
                }
124
            }
125
        });
126
        // Update bar chart type
127
        var chart_type = module_form.find('select');
128
        active[chart_type.attr('name')] = chart_type.val();
129
        // Clear out module input values
130
        $('.modules').empty();
131
        $.each(dashboard_data.modules, function(i, module){
132
            var val = JSON.stringify(module, module);
133
            var input = $('<input type="text" name="module_' + i + '" class="form-control">');
134
            input.val(val);
135
            $('.modules').append(input);
136
        });
137
        updateWidget(active);
138
        $($EDIT_CONTAINER).collapse();
139
        // Refit the grid
140
        fitGrid();
141
    }
142
143
    function updateWidget(config) {
144
        // Trigger update form into view since data is dirty
145
        // Update visual size to existing widget.
146
        var widget = getModuleWidgetByGUID(config.guid);
147
        loader(widget);
148
        widget.style({
149
            height: config.height + 'px',
150
            width: config.width + 'px'
151
        });
152
        widget.select('.widget-title .widget-title-text').text(config.name);
153
        loadWidgetData(widget, config);
154
    }
155
156
    function refreshWidget(e) {
157
        e.preventDefault();
158
        var container = $(this).closest('.widget');
159
        var guid = container.attr('data-guid');
160
        var config = getModuleByGUID(guid);
161
        var widget = addWidget($MAIN_CONTAINER, config);
162
        loadWidgetData(widget, config);
163
        fitGrid();
164
    }
165
166
    function addChartContainers(container, data) {
167
        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...
168
            // Closure to maintain each chart data value in loop
169
            (function(config){
170
                var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 167. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
171
                // Add div wrappers for js grid layout library,
172
                // and add title, icons, and buttons
173
                var widget = addWidget(container, config);
174
                // Determine how to load this widget
175
                loadWidgetData(widget, config);
176
            })(data.modules[name]);
177
        }
178
        fitGrid();
179
    }
180
181
    function getModuleWidgetByGUID(guid) {
182
        return d3.select('.item.widget[data-guid="' + guid + '"]');
183
    }
184
185
    function getModuleByGUID(guid) {
186
        return dashboard_data.modules.find(function(n){return n['guid'] === guid});
187
    }
188
189
    function deleteModule(e) {
190
        e.preventDefault();
191
        if(!confirm('Are you sure?')) {return;}
192
        var guid = $($MODULE_FORM).attr('data-guid');
193
        // Remove form input and visual widget
194
        $('.modules').find('#' + guid).remove();
195
        $('.item.widget[data-guid="' + guid + '"]').remove();
196
        $($EDIT_MODAL).modal('hide');
197
        // Redraw wall to replace visual 'hole'
198
        fitGrid();
199
        // Trigger update form into view since data is dirty
200
        $($EDIT_CONTAINER).collapse('show');
201
    }
202
203
    function addDomEvents() {
204
        // TODO: debounce/throttle
205
        $($API_ROUTE_URL).on('change.charts', previewAPIRoute);
206
        $($API_PREVIEW_BTN).on('click.charts', previewAPIRoute);
207
        // Save module popup form
208
        $($SAVE_MODULE).on('click.charts.module', saveModule);
209
        // Edit existing modules
210
        $($EDIT_MODAL).on('show.bs.modal', updateEditForm);
211
        $('#update-module').on('click.charts.module', updateModule);
212
        // Allow swapping of edit/update events
213
        // for the add module button and form modal
214
        $($ADD_MODULE).on('click.charts', function(){
215
            $('#update-module')
216
            .attr('id', $SAVE_MODULE.replace('#', ''))
217
            .text('Save module')
218
            .off('click.charts.module')
219
            .on('click.charts', saveModule);
220
        });
221
        // Allow swapping of edit/update events
222
        // for the edit button and form modal
223
        $('.widget-edit').on('click.charts', function(){
224
            $($SAVE_MODULE)
225
            .attr('id', 'update-module')
226
            .text('Update module')
227
            .off('click.charts.module')
228
            .on('click.charts', updateModule);
229
        });
230
        // Add delete button for existing widgets.
231
        $($DELETE_BTN).on('click.charts', deleteModule);
232
        // Add delete confirm for dashboards.
233
        $($DELETE_DASHBOARD).on('submit.charts', function(e){
234
            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...
235
        });
236
    }
237
238
    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...
239
        fitGrid({
240
            columnWidth: 5,
241
            itemSelector: '.item',
242
            transitionDuration: 0,
243
            fitWidth: true
244
        }, true);
245
        $('.item.widget').removeClass('hidden');
246
    }
247
248
    function fitGrid(opts, init) {
249
        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...
250
        var options = $.extend({}, opts, {});
251
        if(init) {
252
            my.chart_wall = $('#container').packery(options);
253
        } else {
254
            my.chart_wall.packery(options);
255
        }
256
    }
257
258
    function loader(container) {
259
        container.select('.loader-overlay').classed({hidden: false});
260
        container.select('.widget-loader').classed({hidden: false});
261
    }
262
263
    function unload(container) {
264
        container.select('.loader-overlay').classed({hidden: true});
265
        container.select('.widget-loader').classed({hidden: true});
266
    }
267
268
    function handleInputs(widget, config) {
269
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
270
        // Load event handlers for these newly created forms.
271
        $(inputs_selector).find('form').on('submit', function(e){
272
            e.stopImmediatePropagation();
273
            e.preventDefault();
274
            // Just create a new url for this, but use existing config.
275
            // The global object config will not be altered.
276
            // The first {} here is important, as it enforces a deep copy,
277
            // not a mutation of the original object.
278
            var url = config.dataSource;
279
            // Ensure we don't lose params already save on this endpoint url.
280
            var existing_params = url.split('?')[1];
281
            var params = getValidParamString($(this).serializeArray());
282
            var _config = $.extend({}, config, {
283
                dataSource: url.replace(/\?.+/, '') + '?' + existing_params + '&' + params
284
            });
285
            // Otherwise reload like normal.
286
            loadWidgetData(widget, _config);
287
            // Hide the form again
288
            $(inputs_selector).removeClass('in');
289
        });
290
    }
291
292
    function getValidParamString(arr) {
293
        // Jquery $.serialize and $.serializeArray will
294
        // return empty query parameters, which is undesirable and can
295
        // be error prone for RESTFUL endpoints.
296
        // e.g. `foo=bar&bar=` becomes `foo=bar`
297
        var param_str = '';
298
        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...
299
        $.each(arr, function(i, param){
300
            param_str += (param.name + '=' + param.value);
301
            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...
302
        });
303
        return param_str;
304
    }
305
306
    function loadWidgetData(widget, config) {
307
        loader(widget);
308
        try {
309
            // Handle any custom inputs the user specified for this module.
310
            // They map to standard form inputs and correspond to query
311
            // arguments for this dataSource.
312
            if(config.inputs) {handleInputs(widget, config);}
313
314
            if(config.type === 'datatable') {
315
                jsondash.handlers.handleDataTable(widget, config);
316
            }
317
            else if(jsondash.util.isSparkline(config.type)) {
318
                jsondash.handlers.handleSparkline(widget, config);
319
            }
320
            else if(config.type === 'iframe') {
321
                jsondash.handlers.handleIframe(widget, config);
322
            }
323
            else if(config.type === 'timeline') {
324
                jsondash.handlers.handleTimeline(widget, config);
325
            }
326
            else if(config.type === 'venn') {
327
                jsondash.handlers.handleVenn(widget, config);
328
            }
329
            else if(config.type === 'number') {
330
                jsondash.handlers.handleSingleNum(widget, config);
331
            }
332
            else if(config.type === 'youtube') {
333
                jsondash.handlers.handleYoutube(widget, config);
334
            }
335
            else if(config.type === 'graph'){
336
                jsondash.handlers.handleGraph(widget, config);
337
            }
338
            else if(config.type === 'custom') {
339
                jsondash.handlers.handleCustom(widget, config);
340
            }
341
            else if(config.type === 'plotly-any') {
342
                jsondash.handlers.handlePlotly(widget, config);
343
            }
344
            else if(jsondash.util.isD3Subtype(config)) {
345
                jsondash.handlers.handleD3(widget, config);
346
            } else {
347
                jsondash.handlers.handleC3(widget, config);
348
            }
349
        } catch(e) {
350
            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...
351
            unload(widget);
352
        }
353
        addResizeEvent(widget, config);
354
    }
355
356
    function addResizeEvent(widget, config) {
357
        // Add resize event
358
        $(widget[0]).resizable({
359
            helper: 'resizable-helper',
360
            minWidth: 200,
361
            minHeight: 200,
362
            stop: function(event, ui) {
363
                // Update the configs dimensions.
364
                config = $.extend(config, {width: ui.size.width, height: ui.size.height});
365
                updateModuleInput(config);
366
                loadWidgetData(widget, config);
367
                fitGrid();
368
                // Open save panel
369
                $($EDIT_CONTAINER).collapse('show');
370
            }
371
        });
372
    }
373
374
    function updateModuleInput(config) {
375
        $('input[id="' + config.guid + '"]').val(JSON.stringify(config));
376
    }
377
378
    function prettyCode(code) {
379
        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...
380
        return JSON.stringify(JSON.parse(code), null, 4);
381
    }
382
383
    function addRefreshers(modules) {
384
        $.each(modules, function(_, module){
385
            if(module.refresh && module.refreshInterval) {
386
                var container = d3.select('[data-guid="' + module.guid + '"]');
387
                setInterval(function(){
388
                    loadWidgetData(container, module);
389
                }, parseInt(module.refreshInterval, 10));
390
            }
391
        });
392
    }
393
394
    function loadDashboard(data) {
395
        // Load the grid before rendering the ajax, since the DOM
396
        // is rendered server side.
397
        initGrid($MAIN_CONTAINER);
398
        // Add actual ajax data.
399
        addChartContainers($MAIN_CONTAINER, data);
400
        dashboard_data = data;
401
402
        // Add event handlers for widget UI
403
        $('.widget-refresh').on('click.charts', refreshWidget);
404
405
        // Setup refresh intervals for all widgets that specify it.
406
        addRefreshers(data.modules);
407
408
        // Format json config display
409
        $('#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...
410
            var code = $(this).find('code').text();
411
            $(this).find('code').text(prettyCode(code));
412
        });
413
414
        // Reformat the code inside of the raw json field, to pretty print
415
        // for the user.
416
        $('#raw-config').text(prettyCode($('#raw-config').text()));
417
418
        // Setup responsive handlers
419
        var jres = jRespond([
420
        {
421
            label: 'handheld',
422
            enter: 0,
423
            exit: 767
424
        }
425
        ]);
426
        jres.addFunc({
427
            breakpoint: 'handheld',
428
            enter: function() {
429
                $('.widget').css({
430
                    'max-width': '100%',
431
                    'width': '100%',
432
                    'position': 'static'
433
                });
434
            }
435
        });
436
    }
437
    my.config = {
438
        WIDGET_MARGIN_X: 20,
439
        WIDGET_MARGIN_Y: 60
440
    };
441
    my.loadDashboard = loadDashboard;
442
    my.handlers = {};
443
    my.util = {};
444
    my.loader = loader;
445
    my.unload = unload;
446
    my.addDomEvents = addDomEvents;
447
    return my;
448
}();
449