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