Completed
Push — master ( 86adbf...92b61e )
by
unknown
02:54 queued 10s
created

application.js ➔ setupNamespaceSelector   A

Complexity

Conditions 1
Paths 2

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 47
rs 9.0303
c 1
b 1
f 0
cc 1
nc 2
nop 0

1 Function

Rating   Name   Duplication   Size   Complexity  
B application.js ➔ ... ➔ $(ꞌ#project_inputꞌ).change 0 36 1
1
(function () {
2
    var sortDirection, sortColumn, $tocClone, tocHeight, sectionOffset = {},
3
        toggleTableData, apiPath, lastProject;
4
5
    // Load translations with 'en.json' as a fallback
6
    var messagesToLoad = {};
7
    messagesToLoad[i18nLang] = assetPath + 'static/i18n/' + i18nLang + '.json';
0 ignored issues
show
Bug introduced by
The variable assetPath seems to be never declared. If this is a global, consider adding a /** global: assetPath */ 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...
Bug introduced by
The variable i18nLang seems to be never declared. If this is a global, consider adding a /** global: i18nLang */ 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...
8
    if (i18nLang !== 'en') {
9
        messagesToLoad.en = assetPath + 'static/i18n/en.json';
10
    }
11
    $.i18n({
12
        locale: i18nLang
13
    }).load(messagesToLoad);
14
15
    $(document).ready(function () {
16
        $('.xt-hide').on('click', function () {
17
            $(this).hide();
18
            $(this).siblings('.xt-show').show();
19
            $(this).parents('.panel-heading').siblings('.panel-body').hide();
20
        });
21
        $('.xt-show').on('click', function () {
22
            $(this).hide();
23
            $(this).siblings('.xt-hide').show();
24
            $(this).parents('.panel-heading').siblings('.panel-body').show();
25
        });
26
27
        setupColumnSorting();
28
        setupTOC();
29
        setupProjectListener();
30
        setupAutocompletion();
31
    });
32
33
    /**
34
     * Script to make interactive toggle table and pie chart.
35
     * For visual example, see the "Semi-automated edits" section of the AutoEdits tool.
36
     *
37
     * Example usage (see autoEdits/result.html.twig and js/autoedits.js for more):
38
     *     <table class="table table-bordered table-hover table-striped toggle-table">
39
     *         <thead>...</thead>
40
     *         <tbody>
41
     *             {% for tool, values in semi_automated %}
42
     *             <tr>
43
     *                 <!-- use the 'linked' class here because the cell contains a link -->
44
     *                 <td class="sort-entry--tool linked" data-value="{{ tool }}">
45
     *                     <span class="toggle-table--toggle" data-index="{{ loop.index0 }}" data-key="{{ tool }}">
46
     *                         <span class="glyphicon glyphicon-remove"></span>
47
     *                         <span class="color-icon" style="background:{{ chartColor(loop.index0) }}"></span>
48
     *                     </span>
49
     *                     {{ wiki.pageLink(...) }}
50
     *                 </td>
51
     *                 <td class="sort-entry--count" data-value="{{ values.count }}">
52
     *                     {{ values.count }}
53
     *                 </td>
54
     *             </tr>
55
     *             {% endfor %}
56
     *             ...
57
     *         </tbody>
58
     *     </table>
59
     *     <div class="toggle-table--chart">
60
     *         <canvas id="tool_chart" width="400" height="400"></canvas>
61
     *     </div>
62
     *     <script>
63
     *         window.toolsChart = new Chart($('#tool_chart'), { ... });
64
     *         window.countsByTool = {{ semi_automated | json_encode() | raw }};
65
     *         ...
66
     *
67
     *         // See autoedits.js for more
68
     *         window.setupToggleTable(window.countsByTool, window.toolsChart, 'count', function (newData) {
69
     *             // update the totals in toggle table based on newData
70
     *         });
71
     *     </script>
72
     *
73
     * @param  {Object}   dataSource     Object of data that makes up the chart
74
     * @param  {Chart}    chartObj       Reference to the pie chart associated with the .toggle-table
75
     * @param  {String}   [valueKey]     The name of the key within entries of dataSource,
76
     *                                   where the value is what's shown in the chart.
77
     *                                   If omitted or null, `dataSource` is assumed to be of the structure:
78
     *                                   { 'a' => 123, 'b' => 456 }
79
     * @param  {Function} updateCallback Callback to update the .toggle-table totals. `toggleTableData`
80
     *                                   is passed in which contains the new data, you just need to
81
     *                                   format it (maybe need to use i18n, update multiple cells, etc.)
82
     */
83
    window.setupToggleTable = function (dataSource, chartObj, valueKey, updateCallback) {
84
        $('.toggle-table').on('click', '.toggle-table--toggle', function () {
85
            if (!toggleTableData) {
86
                // must be cloned
87
                toggleTableData = Object.assign({}, dataSource);
88
            }
89
90
            var index = $(this).data('index'),
91
                key = $(this).data('key'),
92
                $row = $(this).parents('tr');
0 ignored issues
show
Unused Code introduced by
The variable $row seems to be never used. Consider removing it.
Loading history...
93
94
            // must use .attr instead of .prop as sorting script will clone DOM elements
95
            if ($(this).attr('data-disabled') === 'true') {
96
                toggleTableData[key] = dataSource[key];
97
                var oldValue = parseInt(valueKey ? toggleTableData[key][valueKey] : toggleTableData[key], 10);
98
                chartObj.data.datasets[0].data[index] = oldValue;
99
                $(this).attr('data-disabled', 'false');
100
            } else {
101
                delete toggleTableData[key];
102
                chartObj.data.datasets[0].data[index] = null;
103
                $(this).attr('data-disabled', 'true');
104
            }
105
106
            // gray out row in table
107
            $(this).parents('tr').toggleClass('excluded');
108
109
            // change the hover icon from a 'x' to a '+'
110
            $(this).find('.glyphicon').toggleClass('glyphicon-remove').toggleClass('glyphicon-plus');
111
112
            // update stats
113
            updateCallback(toggleTableData);
114
115
            chartObj.update();
116
        });
117
    }
118
119
    /**
120
     * Sorting of columns
121
     *
122
     *  Example usage:
123
     *   {% for key in ['username', 'edits', 'minor', 'date'] %}
124
     *      <th>
125
     *         <span class="sort-link sort-link--{{ key }}" data-column="{{ key }}">
126
     *            {{ msg(key) | capitalize }}
127
     *            <span class="glyphicon glyphicon-sort"></span>
128
     *         </span>
129
     *      </th>
130
     *  {% endfor %}
131
     *   <th class="sort-link" data-column="username">Username</th>
132
     *   ...
133
     *   <td class="sort-entry--username" data-value="{{ username }}">{{ username }}</td>
134
     *   ...
135
     *
136
     * Data type is automatically determined, with support for integer,
137
     *   floats, and strings, including date strings (e.g. "2016-01-01 12:59")
138
     */
139
    function setupColumnSorting()
140
    {
141
        $('.sort-link').on('click', function () {
142
            sortDirection = sortColumn === $(this).data('column') ? -sortDirection : 1;
143
144
            $('.sort-link .glyphicon').removeClass('glyphicon-sort-by-alphabet-alt glyphicon-sort-by-alphabet').addClass('glyphicon-sort');
145
            var newSortClassName = sortDirection === 1 ? 'glyphicon-sort-by-alphabet-alt' : 'glyphicon-sort-by-alphabet';
146
            $(this).find('.glyphicon').addClass(newSortClassName).removeClass('glyphicon-sort');
147
148
            sortColumn = $(this).data('column');
149
            var $table = $(this).parents('table');
150
            var entries = $table.find('.sort-entry--' + sortColumn).parent();
151
152
            if (!entries.length) {
153
                return; }
154
155
            entries.sort(function (a, b) {
156
                var before = $(a).find('.sort-entry--' + sortColumn).data('value'),
157
                after = $(b).find('.sort-entry--' + sortColumn).data('value');
158
159
                // test data type, assumed to be string if can't be parsed as float
160
                if (!isNaN(parseFloat(before, 10))) {
161
                    before = parseFloat(before, 10);
162
                    after = parseFloat(after, 10);
163
                }
164
165
                if (before < after) {
166
                    return sortDirection;
167
                } else if (before > after) {
168
                    return -sortDirection;
169
                } else {
170
                    return 0;
171
                }
172
            });
173
174
            $table.find('tbody').html($(entries));
175
        });
176
    }
177
178
    /**
179
     * Floating table of contents
180
     *
181
     * Example usage (see articleInfo/result.html.twig for more):
182
     *     <p class="text-center xt-heading-subtitle">
183
     *         ...
184
     *     </p>
185
     *     <div class="text-center xt-toc">
186
     *         {% set sections = ['generalstats', 'usertable', 'yearcounts', 'monthcounts'] %}
187
     *         {% for section in sections %}
188
     *             <span>
189
     *                 <a href="#{{ section }}" data-section="{{ section }}">{{ msg(section) }}</a>
190
     *             </span>
191
     *         {% endfor %}
192
     *     </div>
193
     *     ...
194
     *     {% set content %}
195
     *         ...content for general stats...
196
     *     {% endset %}
197
     *     {{ layout.content_block('generalstats', content) }}
198
     *     ...
199
     */
200
    function setupTOC()
201
    {
202
        var $toc = $('.xt-toc');
203
204
        if (!$toc || !$toc[0]) {
205
            return;
206
        }
207
208
        tocHeight = $toc.height();
209
210
        // listeners on the section links
211
        var setupTocListeners = function () {
212
            $('.xt-toc').find('a').off('click').on('click', function (e) {
213
                document.activeElement.blur();
214
                var $newSection = $('#' + $(e.target).data('section'));
215
                $(window).scrollTop($newSection.offset().top - tocHeight);
216
217
                $(this).parents('.xt-toc').find('a').removeClass('bold');
218
219
                createTocClone();
220
                $tocClone.addClass('bold');
221
            });
222
        };
223
224
        // clone the TOC and add position:fixed
225
        var createTocClone = function () {
226
            if ($tocClone) {
227
                return;
228
            }
229
            $tocClone = $toc.clone();
230
            $tocClone.addClass('fixed');
231
            $toc.after($tocClone);
232
            setupTocListeners();
233
        };
234
235
        // build object containing offsets of each section
236
        var buildSectionOffsets = function () {
237
            $.each($toc.find('a'), function (index, tocMember) {
238
                var id = $(tocMember).data('section');
239
                sectionOffset[id] = $('#' + id).offset().top;
240
            });
241
        }
242
243
        // rebuild section offsets when sections are shown/hidden
244
        $('.xt-show, .xt-hide').on('click', buildSectionOffsets);
245
246
        buildSectionOffsets();
247
        setupTocListeners();
248
249
        var tocOffsetTop = $toc.offset().top;
250
        $(window).on('scroll', function (e) {
251
            var windowOffset = $(e.target).scrollTop();
252
            var inRange = windowOffset > tocOffsetTop;
253
254
            if (inRange) {
255
                if (!$tocClone) {
256
                    createTocClone();
257
                }
258
259
                // bolden the link for whichever section we're in
260
                var $activeMember;
261
                Object.keys(sectionOffset).forEach(function (section) {
262
                    if (windowOffset > sectionOffset[section] - tocHeight - 1) {
263
                        $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...
264
                    }
265
                });
266
                $tocClone.find('a').removeClass('bold');
267
                if ($activeMember) {
268
                    $activeMember.addClass('bold');
269
                }
270
            } else if (!inRange && $tocClone) {
271
                // remove the clone once we're out of range
272
                $tocClone.remove();
273
                $tocClone = null;
274
            }
275
        });
276
    }
277
278
    /**
279
     * Add listener to the project input field to update any
280
     * namespace selectors and autocompletion fields.
281
     */
282
    function setupProjectListener()
283
    {
284
        // Stop here if there is no project field
285
        if (!$("#project_input")) {
286
            return;
287
        }
288
289
        // If applicable, setup namespace selector with real time updates when changing projects.
290
        // This will also set `apiPath` so that autocompletion will query the right wiki.
291
        if ($('#project_input').length && $('#namespace_select').length) {
292
            setupNamespaceSelector();
293
        // Otherwise, if there's a user or page input field, we still need to update `apiPath`
294
        // for the user input autocompletion when the project is changed.
295
        } else if ($('#user_input')[0] || $('#article_input')[0]) {
296
            // keep track of last valid project
297
            lastProject = $('#project_input').val();
298
299
            $('#project_input').on('change', function () {
300
                var newProject = this.value;
301
302
                $.get(xtBaseUrl + 'api/normalizeProject/' + newProject).done(function (data) {
0 ignored issues
show
Bug introduced by
The variable xtBaseUrl seems to be never declared. If this is a global, consider adding a /** global: xtBaseUrl */ 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...
303
                    // Keep track of project API path for use in page title autocompletion
304
                    apiPath = data.api;
305
                    lastProject = newProject;
306
                    setupAutocompletion();
307
                }).fail(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 371 does not use this.
Loading history...
308
            });
309
        }
310
    }
311
312
    /**
313
     * Use the wiki input field to populate the namespace selector.
314
     * This also updates `apiPath` and calls setupAutocompletion()
315
     */
316
    function setupNamespaceSelector()
317
    {
318
        // keep track of last valid project
319
        lastProject = $('#project_input').val();
320
321
        $('#project_input').on('change', function () {
322
            // Disable the namespace selector and show a spinner while the data loads.
323
            $('#namespace_select').prop('disabled', true);
324
            $loader = $('span.loader');
0 ignored issues
show
Bug introduced by
The variable $loader 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.$loader.
Loading history...
325
            $('label[for="namespace_select"]').append($loader);
326
            $loader.removeClass('hidden');
327
328
            var newProject = this.value;
329
330
            $.get(xtBaseUrl + 'api/namespaces/' + newProject).done(function (data) {
0 ignored issues
show
Bug introduced by
The variable xtBaseUrl seems to be never declared. If this is a global, consider adding a /** global: xtBaseUrl */ 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...
331
                // Clone the 'all' option (even if there isn't one),
332
                // and replace the current option list with this.
333
                var $allOption = $('#namespace_select option[value="all"]').eq(0).clone();
334
                $("#namespace_select").html($allOption);
335
336
                // Keep track of project API path for use in page title autocompletion
337
                apiPath = data.api;
338
339
                // Add all of the new namespace options.
340
                for (var ns in data.namespaces) {
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...
341
                    var nsName = parseInt(ns, 10) === 0 ? $.i18n('mainspace') : data.namespaces[ns];
342
                    $('#namespace_select').append(
343
                        "<option value=" + ns + ">" + nsName + "</option>"
344
                    );
345
                }
346
                // Default to mainspace being selected.
347
                $("#namespace_select").val(0);
348
                lastProject = newProject;
349
350
                // Re-init autocompletion
351
                setupAutocompletion();
352
            }).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 371 does not use this.
Loading history...
353
                $('#namespace_select').prop('disabled', false);
354
                $loader.addClass('hidden');
355
            });
356
        });
357
358
        // If they change the namespace, update autocompletion,
359
        // which will ensure only pages in the selected namespace
360
        // show up in the autocompletion
361
        $('#namespace_select').on('change', setupAutocompletion);
362
    }
363
364
    /**
365
     * Called by setupNamespaceSelector or setupProjectListener
366
     *   when the user changes to a project that doesn't exist.
367
     * This throws a warning message and reverts back to the
368
     *   last valid project.
369
     * @param {string} newProject - project they attempted to add
370
     */
371
    function revertToValidProject(newProject)
372
    {
373
        $('#project_input').val(lastProject);
374
        $('.site-notice').append(
375
            "<div class='alert alert-warning alert-dismissible' role='alert'>" +
376
                $.i18n('invalid-project', "<strong>" + newProject + "</strong>") +
377
                "<button class='close' data-dismiss='alert' aria-label='Close'>" +
378
                    "<span aria-hidden='true'>&times;</span>" +
379
                "</button>" +
380
            "</div>"
381
        );
382
    }
383
384
    /**
385
     * Setup autocompletion of pages if a page input field is present.
386
     */
387
    function setupAutocompletion()
388
    {
389
        var $articleInput = $('#article_input'),
390
            $userInput = $('#user_input'),
391
            $namespaceInput = $("#namespace_select");
392
393
        // Make sure typeahead-compatible fields are present
394
        if (!$articleInput[0] && !$userInput[0] && !$('#project_input')[0]) {
395
            return;
396
        }
397
398
        // Destroy any existing instances
399
        if ($articleInput.data('typeahead')) {
400
            $articleInput.data('typeahead').destroy();
401
        }
402
        if ($userInput.data('typeahead')) {
403
            $userInput.data('typeahead').destroy();
404
        }
405
406
        // set initial value for the API url, which is put as a data attribute in forms.html.twig
407
        if (!apiPath) {
408
            apiPath = $('#article_input').data('api') || $('#user_input').data('api');
409
        }
410
411
        // Defaults for typeahead options. preDispatch and preProcess will be
412
        // set accordingly for each typeahead instance
413
        var typeaheadOpts = {
414
            url: apiPath,
415
            timeout: 200,
416
            triggerLength: 1,
417
            method: 'get',
418
            preDispatch: null,
419
            preProcess: null,
420
        };
421
422
        if ($articleInput[0]) {
423
            $articleInput.typeahead({
424
                ajax: Object.assign(typeaheadOpts, {
425
                    preDispatch: function (query) {
426
                        // If there is a namespace selector, make sure we search
427
                        // only within that namespace
428
                        if ($namespaceInput[0] && $namespaceInput.val() !== '0') {
429
                            var nsName = $namespaceInput.find('option:selected').text().trim();
430
                            query = nsName + ':' + query;
431
                        }
432
                        return {
433
                            action: 'query',
434
                            list: 'prefixsearch',
435
                            format: 'json',
436
                            pssearch: query
437
                        };
438
                    },
439
                    preProcess: function (data) {
440
                        var nsName = '';
441
                        // Strip out namespace name if applicable
442
                        if ($namespaceInput[0] && $namespaceInput.val() !== '0') {
443
                            nsName = $namespaceInput.find('option:selected').text().trim();
444
                        }
445
                        return data.query.prefixsearch.map(function (elem) {
446
                            return elem.title.replace(new RegExp('^' + nsName + ':'), '');
447
                        });
448
                    },
449
                })
450
            });
451
        }
452
453
        if ($userInput[0]) {
454
            $userInput.typeahead({
455
                ajax: Object.assign(typeaheadOpts, {
456
                    preDispatch: function (query) {
457
                        return {
458
                            action: 'query',
459
                            list: 'prefixsearch',
460
                            format: 'json',
461
                            pssearch: 'User:' + query
462
                        };
463
                    },
464
                    preProcess: function (data) {
465
                        var results = data.query.prefixsearch.map(function (elem) {
466
                            return elem.title.split('/')[0].substr(elem.title.indexOf(':') + 1);
467
                        });
468
469
                        return results.filter(function (value, index, array) {
470
                            return array.indexOf(value) === index;
471
                        });
472
                    },
473
                })
474
            });
475
        }
476
    }
477
})();
478