Passed
Push — master ( 3d4ba3...a5dea4 )
by MusikAnimal
05:14
created

window.setupColumnSorting   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 6
nop 2
dl 0
loc 18
rs 9.2
c 0
b 0
f 0
1
(function () {
2
    var $tocClone, tocHeight, sectionOffset = {}, apiPath, lastProject;
3
4
    /** global: i18nLang */
5
    /** global: i18nPaths */
6
    $.i18n({
7
        locale: i18nLang
8
    }).load(i18nPaths);
9
10
    $(document).ready(function () {
11
        // TODO: move these listeners to a setup function and document how to use it.
12
        $('.xt-hide').on('click', function () {
13
            $(this).hide();
14
            $(this).siblings('.xt-show').show();
15
16
            if ($(this).parents('.panel-heading').length) {
17
                $(this).parents('.panel-heading').siblings('.panel-body').hide();
18
            } else {
19
                $(this).parents('.xt-show-hide--parent').next('.xt-show-hide--target').hide();
20
            }
21
        });
22
        $('.xt-show').on('click', function () {
23
            $(this).hide();
24
            $(this).siblings('.xt-hide').show();
25
26
            if ($(this).parents('.panel-heading').length) {
27
                $(this).parents('.panel-heading').siblings('.panel-body').show();
28
            } else {
29
                $(this).parents('.xt-show-hide--parent').next('.xt-show-hide--target').show();
30
            }
31
        });
32
33
        setupNavCollapsing();
34
        setupColumnSorting();
35
        setupTOC();
36
        setupStickyHeader();
37
        setupProjectListener();
38
        setupAutocompletion();
39
        displayWaitingNoticeOnSubmission();
40
41
        // Re-init forms, workaround for issues with Safari and Firefox.
42
        // See displayWaitingNoticeOnSubmission() for more.
43
        window.onpageshow = function (e) {
44
            if (e.persisted) {
45
                displayWaitingNoticeOnSubmission(true);
46
            }
47
        };
48
    });
49
50
    /**
51
     * Script to make interactive toggle table and pie chart.
52
     * For visual example, see the "Semi-automated edits" section of the AutoEdits tool.
53
     *
54
     * Example usage (see autoEdits/result.html.twig and js/autoedits.js for more):
55
     *     <table class="table table-bordered table-hover table-striped toggle-table">
56
     *         <thead>...</thead>
57
     *         <tbody>
58
     *             {% for tool, values in semi_automated %}
59
     *             <tr>
60
     *                 <!-- use the 'linked' class here because the cell contains a link -->
61
     *                 <td class="sort-entry--tool linked" data-value="{{ tool }}">
62
     *                     <span class="toggle-table--toggle" data-index="{{ loop.index0 }}" data-key="{{ tool }}">
63
     *                         <span class="glyphicon glyphicon-remove"></span>
64
     *                         <span class="color-icon" style="background:{{ chartColor(loop.index0) }}"></span>
65
     *                     </span>
66
     *                     {{ wiki.pageLink(...) }}
67
     *                 </td>
68
     *                 <td class="sort-entry--count" data-value="{{ values.count }}">
69
     *                     {{ values.count }}
70
     *                 </td>
71
     *             </tr>
72
     *             {% endfor %}
73
     *             ...
74
     *         </tbody>
75
     *     </table>
76
     *     <div class="toggle-table--chart">
77
     *         <canvas id="tool_chart" width="400" height="400"></canvas>
78
     *     </div>
79
     *     <script>
80
     *         window.toolsChart = new Chart($('#tool_chart'), { ... });
81
     *         window.countsByTool = {{ semi_automated | json_encode() | raw }};
82
     *         ...
83
     *
84
     *         // See autoedits.js for more
85
     *         window.setupToggleTable(window.countsByTool, window.toolsChart, 'count', function (newData) {
86
     *             // update the totals in toggle table based on newData
87
     *         });
88
     *     </script>
89
     *
90
     * @param  {Object}   dataSource     Object of data that makes up the chart
91
     * @param  {Chart}    chartObj       Reference to the pie chart associated with the .toggle-table
92
     * @param  {String}   [valueKey]     The name of the key within entries of dataSource,
93
     *                                   where the value is what's shown in the chart.
94
     *                                   If omitted or null, `dataSource` is assumed to be of the structure:
95
     *                                   { 'a' => 123, 'b' => 456 }
96
     * @param  {Function} updateCallback Callback to update the .toggle-table totals. `toggleTableData`
97
     *                                   is passed in which contains the new data, you just need to
98
     *                                   format it (maybe need to use i18n, update multiple cells, etc.).
99
     *                                   The second parameter that is passed back is the 'key' of the toggled
100
     *                                   item, and the third is the index of the item.
101
     */
102
    window.setupToggleTable = function (dataSource, chartObj, valueKey, updateCallback) {
103
        var toggleTableData;
104
105
        $('.toggle-table').on('click', '.toggle-table--toggle', function () {
106
            if (!toggleTableData) {
107
                // must be cloned
108
                toggleTableData = Object.assign({}, dataSource);
109
            }
110
111
            var index = $(this).data('index'),
112
                key = $(this).data('key');
113
114
            // must use .attr instead of .prop as sorting script will clone DOM elements
115
            if ($(this).attr('data-disabled') === 'true') {
116
                toggleTableData[key] = dataSource[key];
117
                var oldValue = parseInt(valueKey ? toggleTableData[key][valueKey] : toggleTableData[key], 10);
118
                chartObj.data.datasets[0].data[index] = oldValue;
119
                $(this).attr('data-disabled', 'false');
120
            } else {
121
                delete toggleTableData[key];
122
                chartObj.data.datasets[0].data[index] = null;
123
                $(this).attr('data-disabled', 'true');
124
            }
125
126
            // gray out row in table
127
            $(this).parents('tr').toggleClass('excluded');
128
129
            // change the hover icon from a 'x' to a '+'
130
            $(this).find('.glyphicon').toggleClass('glyphicon-remove').toggleClass('glyphicon-plus');
131
132
            // update stats
133
            updateCallback(toggleTableData, key, index);
134
135
            chartObj.update();
136
        });
137
    }
138
139
    /**
140
     * If there are more tool links in the nav than will fit in the viewport,
141
     *   move the last entry to the More menu, one at a time, until it all fits.
142
     * This does not listen for window resize events.
143
     */
144
    function setupNavCollapsing()
145
    {
146
        var windowWidth = $(window).width(),
147
            toolNavWidth = $('.tool-links').outerWidth(),
148
            navRightWidth = $('.nav-buttons').outerWidth();
149
150
        // Ignore if in mobile responsive view
151
        if (windowWidth < 768) {
152
            return;
153
        }
154
155
        // Do this first so we account for the space the More menu takes up
156
        if (toolNavWidth + navRightWidth > windowWidth) {
157
            $('.tool-links--more').removeClass('hidden');
158
        }
159
160
        // Don't loop more than there are links in the nav.
161
        // This more just a safeguard against an infinite loop should something go wrong.
162
        var numLinks = $('.tool-links--entry').length;
163
        while (numLinks > 0 && toolNavWidth + navRightWidth > windowWidth) {
164
            // Remove the last tool link that is not the current tool being used
165
            var $link = $('.tool-links--nav > .tool-links--entry:not(.active)').last().remove();
166
            $('.tool-links--more .dropdown-menu').append($link);
167
            toolNavWidth = $('.tool-links').outerWidth();
168
            numLinks--;
169
        }
170
    }
171
172
    /**
173
     * Sorting of columns
174
     *
175
     *  Example usage:
176
     *   {% for key in ['username', 'edits', 'minor', 'date'] %}
177
     *      <th>
178
     *         <span class="sort-link sort-link--{{ key }}" data-column="{{ key }}">
179
     *            {{ msg(key) | capitalize }}
180
     *            <span class="glyphicon glyphicon-sort"></span>
181
     *         </span>
182
     *      </th>
183
     *  {% endfor %}
184
     *   <th class="sort-link" data-column="username">Username</th>
185
     *   ...
186
     *   <td class="sort-entry--username" data-value="{{ username }}">{{ username }}</td>
187
     *   ...
188
     *
189
     * Data type is automatically determined, with support for integer,
190
     *   floats, and strings, including date strings (e.g. "2016-01-01 12:59")
191
     */
192
    window.setupColumnSorting = function () {
193
        var sortDirection, sortColumn;
194
195
        $('.sort-link').on('click', function () {
196
            sortDirection = sortColumn === $(this).data('column') ? -sortDirection : 1;
197
198
            $('.sort-link .glyphicon').removeClass('glyphicon-sort-by-alphabet-alt glyphicon-sort-by-alphabet').addClass('glyphicon-sort');
199
            var newSortClassName = sortDirection === 1 ? 'glyphicon-sort-by-alphabet-alt' : 'glyphicon-sort-by-alphabet';
200
            $(this).find('.glyphicon').addClass(newSortClassName).removeClass('glyphicon-sort');
201
202
            sortColumn = $(this).data('column');
203
            var $table = $(this).parents('table');
204
            var $entries = $table.find('.sort-entry--' + sortColumn).parent();
205
206
            if (!$entries.length) {
207
                return;
208
            }
209
210
            $entries.sort(function (a, b) {
211
                var before = $(a).find('.sort-entry--' + sortColumn).data('value'),
212
                    after = $(b).find('.sort-entry--' + sortColumn).data('value');
213
214
                // test data type, assumed to be string if can't be parsed as float
215
                if (!isNaN(parseFloat(before, 10))) {
216
                    before = parseFloat(before, 10);
217
                    after = parseFloat(after, 10);
218
                }
219
220
                if (before < after) {
221
                    return sortDirection;
222
                } else if (before > after) {
223
                    return -sortDirection;
224
                } else {
225
                    return 0;
226
                }
227
            });
228
229
            // Re-fill the rank column, if applicable.
230
            if ($('.sort-entry--rank').length > 0) {
231
                $.each($entries, function (index, entry) {
232
                    $(entry).find('.sort-entry--rank').text(index + 1);
233
                });
234
            }
235
236
            $table.find('tbody').html($entries);
237
        });
238
    }
239
240
    /**
241
     * Floating table of contents
242
     *
243
     * Example usage (see articleInfo/result.html.twig for more):
244
     *     <p class="text-center xt-heading-subtitle">
245
     *         ...
246
     *     </p>
247
     *     <div class="text-center xt-toc">
248
     *         {% set sections = ['generalstats', 'usertable', 'yearcounts', 'monthcounts'] %}
249
     *         {% for section in sections %}
250
     *             <span>
251
     *                 <a href="#{{ section }}" data-section="{{ section }}">{{ msg(section) }}</a>
252
     *             </span>
253
     *         {% endfor %}
254
     *     </div>
255
     *     ...
256
     *     {% set content %}
257
     *         ...content for general stats...
258
     *     {% endset %}
259
     *     {{ layout.content_block('generalstats', content) }}
260
     *     ...
261
     */
262
    function setupTOC()
263
    {
264
        var $toc = $('.xt-toc');
265
266
        if (!$toc || !$toc[0]) {
267
            return;
268
        }
269
270
        tocHeight = $toc.height();
271
272
        // listeners on the section links
273
        var setupTocListeners = function () {
274
            $('.xt-toc').find('a').off('click').on('click', function (e) {
275
                document.activeElement.blur();
276
                var $newSection = $('#' + $(e.target).data('section'));
277
                $(window).scrollTop($newSection.offset().top - tocHeight);
278
279
                $(this).parents('.xt-toc').find('a').removeClass('bold');
280
281
                createTocClone();
282
                $tocClone.addClass('bold');
283
            });
284
        };
285
        window.setupTocListeners = setupTocListeners;
286
287
        // clone the TOC and add position:fixed
288
        var createTocClone = function () {
289
            if ($tocClone) {
290
                return;
291
            }
292
            $tocClone = $toc.clone();
293
            $tocClone.addClass('fixed');
294
            $toc.after($tocClone);
295
            setupTocListeners();
296
        };
297
298
        // build object containing offsets of each section
299
        window.buildSectionOffsets = function () {
300
            $.each($toc.find('a'), function (index, tocMember) {
301
                var id = $(tocMember).data('section');
302
                sectionOffset[id] = $('#' + id).offset().top;
303
            });
304
        }
305
306
        // rebuild section offsets when sections are shown/hidden
307
        $('.xt-show, .xt-hide').on('click', buildSectionOffsets);
0 ignored issues
show
Bug introduced by
The variable buildSectionOffsets seems to be never declared. If this is a global, consider adding a /** global: buildSectionOffsets */ 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...
308
309
        buildSectionOffsets();
310
        setupTocListeners();
311
312
        var tocOffsetTop = $toc.offset().top;
313
        $(window).on('scroll.toc', function (e) {
314
            var windowOffset = $(e.target).scrollTop();
315
            var inRange = windowOffset > tocOffsetTop;
316
317
            if (inRange) {
318
                if (!$tocClone) {
319
                    createTocClone();
320
                }
321
322
                // bolden the link for whichever section we're in
323
                var $activeMember;
324
                Object.keys(sectionOffset).forEach(function (section) {
325
                    if (windowOffset > sectionOffset[section] - tocHeight - 1) {
326
                        $activeMember = $tocClone.find('a[data-section="' + section + '"]');
0 ignored issues
show
Bug introduced by
The variable $tocClone seems to not be initialized for all possible execution paths.
Loading history...
327
                    }
328
                });
329
                $tocClone.find('a').removeClass('bold');
330
                if ($activeMember) {
331
                    $activeMember.addClass('bold');
332
                }
333
            } else if (!inRange && $tocClone) {
334
                // remove the clone once we're out of range
335
                $tocClone.remove();
336
                $tocClone = null;
337
            }
338
        });
339
    }
340
341
    /**
342
     * Make any tables with the class 'table-sticky-header' have sticky headers.
343
     * E.g. as you scroll the heading row will be fixed at the top for reference.
344
     */
345
    function setupStickyHeader()
346
    {
347
        var $header = $('.table-sticky-header');
348
349
        if (!$header || !$header[0]) {
350
            return;
351
        }
352
353
        var headerHeight = $header.height(),
0 ignored issues
show
Unused Code introduced by
The variable headerHeight seems to be never used. Consider removing it.
Loading history...
354
            $headerRow = $header.find('thead tr').eq(0),
355
            $headerClone;
356
357
        // Make a clone of the header to maintain placement of the original header,
358
        // making the original header the sticky one. This way event listeners on it
359
        // (such as column sorting) will still work.
360
        var cloneHeader = function () {
361
            if ($headerClone) {
362
                return;
363
            }
364
365
            $headerClone = $headerRow.clone();
366
            $headerRow.addClass('sticky-heading');
367
            $headerRow.before($headerClone);
368
369
            // Explicitly set widths of each column, which are lost with position:absolute.
370
            $headerRow.find('th').each(function (index) {
371
                $(this).css('width', $headerClone.find('th').eq(index).outerWidth());
372
            });
373
            $headerRow.css('width', $headerClone.outerWidth() + 1);
374
        };
375
376
        var headerOffsetTop = $header.offset().top;
377
        $(window).on('scroll.stickyHeader', function (e) {
378
            var windowOffset = $(e.target).scrollTop();
379
            var inRange = windowOffset > headerOffsetTop;
380
381
            if (inRange && !$headerClone) {
382
                cloneHeader();
383
            } else if (!inRange && $headerClone) {
384
                // Remove the clone once we're out of range,
385
                // and make the original un-sticky.
386
                $headerRow.removeClass('sticky-heading');
387
                $headerClone.remove();
388
                $headerClone = null;
389
            } else if ($headerClone) {
390
                // The header is position:absolute so it will follow with X scrolling,
391
                // but for Y we must go by the window scroll position.
392
                $headerRow.css(
393
                    'top',
394
                    $(window).scrollTop() - $header.offset().top
395
                );
396
            }
397
        });
398
    }
399
400
    /**
401
     * Add listener to the project input field to update any
402
     * namespace selectors and autocompletion fields.
403
     */
404
    function setupProjectListener()
405
    {
406
        // Stop here if there is no project field
407
        if (!$("#project_input")) {
408
            return;
409
        }
410
411
        // If applicable, setup namespace selector with real time updates when changing projects.
412
        // This will also set `apiPath` so that autocompletion will query the right wiki.
413
        if ($('#project_input').length && $('#namespace_select').length) {
414
            setupNamespaceSelector();
415
        // Otherwise, if there's a user or page input field, we still need to update `apiPath`
416
        // for the user input autocompletion when the project is changed.
417
        } else if ($('#user_input')[0] || $('#article_input')[0]) {
418
            // keep track of last valid project
419
            lastProject = $('#project_input').val();
420
421
            $('#project_input').on('change', function () {
422
                var newProject = this.value;
423
424
                // Show the spinner.
425
                $(this).addClass('show-loader');
426
427
                /** global: xtBaseUrl */
428
                $.get(xtBaseUrl + 'api/project/normalize/' + newProject).done(function (data) {
429
                    // Keep track of project API path for use in page title autocompletion
430
                    apiPath = data.api;
431
                    lastProject = newProject;
432
                    setupAutocompletion();
433
                }).fail(
434
                    revertToValidProject.bind(this, newProject)
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function revertToValidProject declared on line 504 does not use this.
Loading history...
435
                ).always(function () {
436
                    $('#project_input').removeClass('show-loader');
437
                });
438
            });
439
        }
440
    }
441
442
    /**
443
     * Use the wiki input field to populate the namespace selector.
444
     * This also updates `apiPath` and calls setupAutocompletion()
445
     */
446
    function setupNamespaceSelector()
447
    {
448
        // keep track of last valid project
449
        lastProject = $('#project_input').val();
450
451
        $('#project_input').on('change', function () {
452
            // Disable the namespace selector and show a spinner while the data loads.
453
            $('#namespace_select').prop('disabled', true);
454
            $(this).addClass('show-loader');
455
456
            var newProject = this.value;
457
458
            /** global: xtBaseUrl */
459
            $.get(xtBaseUrl + 'api/project/namespaces/' + newProject).done(function (data) {
460
                // Clone the 'all' option (even if there isn't one),
461
                // and replace the current option list with this.
462
                var $allOption = $('#namespace_select option[value="all"]').eq(0).clone();
463
                $("#namespace_select").html($allOption);
464
465
                // Keep track of project API path for use in page title autocompletion
466
                apiPath = data.api;
467
468
                // Add all of the new namespace options.
469
                for (var ns in data.namespaces) {
470
                    if (!data.namespaces.hasOwnProperty(ns)) {
471
                        continue; // Skip keys from the prototype.
472
                    }
473
474
                    var nsName = parseInt(ns, 10) === 0 ? $.i18n('mainspace') : data.namespaces[ns];
475
                    $('#namespace_select').append(
476
                        "<option value=" + ns + ">" + nsName + "</option>"
477
                    );
478
                }
479
                // Default to mainspace being selected.
480
                $("#namespace_select").val(0);
481
                lastProject = newProject;
482
483
                // Re-init autocompletion
484
                setupAutocompletion();
485
            }).fail(revertToValidProject.bind(this, newProject)).always(function () {
0 ignored issues
show
Unused Code introduced by
The call to bind does not seem necessary since the function revertToValidProject declared on line 504 does not use this.
Loading history...
486
                $('#namespace_select').prop('disabled', false);
487
                $('#project_input').removeClass('show-loader');
488
            });
489
        });
490
491
        // If they change the namespace, update autocompletion,
492
        // which will ensure only pages in the selected namespace
493
        // show up in the autocompletion
494
        $('#namespace_select').on('change', setupAutocompletion);
495
    }
496
497
    /**
498
     * Called by setupNamespaceSelector or setupProjectListener
499
     *   when the user changes to a project that doesn't exist.
500
     * This throws a warning message and reverts back to the
501
     *   last valid project.
502
     * @param {string} newProject - project they attempted to add
503
     */
504
    function revertToValidProject(newProject)
505
    {
506
        $('#project_input').val(lastProject);
507
        $('.site-notice').append(
508
            "<div class='alert alert-warning alert-dismissible' role='alert'>" +
509
                $.i18n('invalid-project', "<strong>" + newProject + "</strong>") +
510
                "<button class='close' data-dismiss='alert' aria-label='Close'>" +
511
                    "<span aria-hidden='true'>&times;</span>" +
512
                "</button>" +
513
            "</div>"
514
        );
515
    }
516
517
    /**
518
     * Setup autocompletion of pages if a page input field is present.
519
     */
520
    function setupAutocompletion()
521
    {
522
        var $articleInput = $('#article_input'),
523
            $userInput = $('#user_input'),
524
            $namespaceInput = $("#namespace_select");
525
526
        // Make sure typeahead-compatible fields are present
527
        if (!$articleInput[0] && !$userInput[0] && !$('#project_input')[0]) {
528
            return;
529
        }
530
531
        // Destroy any existing instances
532
        if ($articleInput.data('typeahead')) {
533
            $articleInput.data('typeahead').destroy();
534
        }
535
        if ($userInput.data('typeahead')) {
536
            $userInput.data('typeahead').destroy();
537
        }
538
539
        // set initial value for the API url, which is put as a data attribute in forms.html.twig
540
        if (!apiPath) {
541
            apiPath = $('#article_input').data('api') || $('#user_input').data('api');
542
        }
543
544
        // Defaults for typeahead options. preDispatch and preProcess will be
545
        // set accordingly for each typeahead instance
546
        var typeaheadOpts = {
547
            url: apiPath,
548
            timeout: 200,
549
            triggerLength: 1,
550
            method: 'get',
551
            loadingClass: 'show-loader',
552
            preDispatch: null,
553
            preProcess: null,
554
        };
555
556
        if ($articleInput[0]) {
557
            $articleInput.typeahead({
558
                ajax: Object.assign(typeaheadOpts, {
559
                    preDispatch: function (query) {
560
                        // If there is a namespace selector, make sure we search
561
                        // only within that namespace
562
                        if ($namespaceInput[0] && $namespaceInput.val() !== '0') {
563
                            var nsName = $namespaceInput.find('option:selected').text().trim();
564
                            query = nsName + ':' + query;
565
                        }
566
                        return {
567
                            action: 'query',
568
                            list: 'prefixsearch',
569
                            format: 'json',
570
                            pssearch: query
571
                        };
572
                    },
573
                    preProcess: function (data) {
574
                        var nsName = '';
575
                        // Strip out namespace name if applicable
576
                        if ($namespaceInput[0] && $namespaceInput.val() !== '0') {
577
                            nsName = $namespaceInput.find('option:selected').text().trim();
578
                        }
579
                        return data.query.prefixsearch.map(function (elem) {
580
                            return elem.title.replace(new RegExp('^' + nsName + ':'), '');
581
                        });
582
                    },
583
                })
584
            });
585
        }
586
587
        if ($userInput[0]) {
588
            $userInput.typeahead({
589
                ajax: Object.assign(typeaheadOpts, {
590
                    preDispatch: function (query) {
591
                        return {
592
                            action: 'query',
593
                            list: 'prefixsearch',
594
                            format: 'json',
595
                            pssearch: 'User:' + query
596
                        };
597
                    },
598
                    preProcess: function (data) {
599
                        var results = data.query.prefixsearch.map(function (elem) {
600
                            return elem.title.split('/')[0].substr(elem.title.indexOf(':') + 1);
601
                        });
602
603
                        return results.filter(function (value, index, array) {
604
                            return array.indexOf(value) === index;
605
                        });
606
                    },
607
                })
608
            });
609
        }
610
    }
611
612
    /**
613
     * For any form submission, this disables the submit button and replaces its text with
614
     * a loading message and a counting timer.
615
     * @param {boolean} [undo] Revert the form back to the initial state.
616
     *                         This is used on page load to solve an issue with Safari and Firefox
617
     *                         where after browsing back to the form, the "loading" state persists.
618
     */
619
    function displayWaitingNoticeOnSubmission(undo)
620
    {
621
        if (undo) {
622
            // Re-enable form
623
            $('.form-control').prop('readonly', false);
624
            $('.form-submit').prop('disabled', false);
625
            $('.form-submit').text($.i18n('submit')).prop('disabled', false);
626
        } else {
627
            $('#content form').on('submit', function () {
628
                // Remove focus from any active element
629
                document.activeElement.blur();
630
631
                // Disable the form so they can't hit Enter to re-submit
632
                $('.form-control').prop('readonly', true);
633
634
                // Change the submit button text.
635
                $('.form-submit').prop('disabled', true)
636
                    .html($.i18n('loading') + " <span id='submit_timer'></span>");
637
638
                // Add the counter.
639
                var startTime = Date.now();
640
                setInterval(function () {
641
                    var elapsedSeconds = Math.round((Date.now() - startTime) / 1000);
642
                    var minutes = Math.floor(elapsedSeconds / 60);
643
                    var seconds = ('00' + (elapsedSeconds - (minutes * 60))).slice(-2);
644
                    $('#submit_timer').text(minutes + ":" + seconds);
645
                }, 1000);
646
            });
647
        }
648
    }
649
650
})();
651