[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 // This file is part of Moodle - http://moodle.org/ 2 // 3 // Moodle is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // (at your option) any later version. 7 // 8 // Moodle is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU General Public License for more details. 12 // 13 // You should have received a copy of the GNU General Public License 14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 15 16 /** 17 * Autocomplete wrapper for select2 library. 18 * 19 * @module core/form-autocomplete 20 * @class autocomplete 21 * @package core 22 * @copyright 2015 Damyon Wiese <damyon@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 * @since 3.0 25 */ 26 /* globals require: false */ 27 define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'], function($, log, str, templates, notification) { 28 29 // Private functions and variables. 30 /** @var {Object} KEYS - List of keycode constants. */ 31 var KEYS = { 32 DOWN: 40, 33 ENTER: 13, 34 SPACE: 32, 35 ESCAPE: 27, 36 COMMA: 188, 37 UP: 38 38 }; 39 40 /** 41 * Make an item in the selection list "active". 42 * 43 * @method activateSelection 44 * @private 45 * @param {Number} index The index in the current (visible) list of selection. 46 * @param {Object} state State variables for this autocomplete element. 47 */ 48 var activateSelection = function(index, state) { 49 // Find the elements in the DOM. 50 var selectionElement = $(document.getElementById(state.selectionId)); 51 52 // Count the visible items. 53 var length = selectionElement.children('[aria-selected=true]').length; 54 // Limit the index to the upper/lower bounds of the list (wrap in both directions). 55 index = index % length; 56 while (index < 0) { 57 index += length; 58 } 59 // Find the specified element. 60 var element = $(selectionElement.children('[aria-selected=true]').get(index)); 61 // Create an id we can assign to this element. 62 var itemId = state.selectionId + '-' + index; 63 64 // Deselect all the selections. 65 selectionElement.children().attr('data-active-selection', false).attr('id', ''); 66 // Select only this suggestion and assign it the id. 67 element.attr('data-active-selection', true).attr('id', itemId); 68 // Tell the input field it has a new active descendant so the item is announced. 69 selectionElement.attr('aria-activedescendant', itemId); 70 }; 71 72 /** 73 * Update the element that shows the currently selected items. 74 * 75 * @method updateSelectionList 76 * @private 77 * @param {Object} options Original options for this autocomplete element. 78 * @param {Object} state State variables for this autocomplete element. 79 * @param {JQuery} originalSelect The JQuery object matching the hidden select list. 80 */ 81 var updateSelectionList = function(options, state, originalSelect) { 82 // Build up a valid context to re-render the template. 83 var items = []; 84 var newSelection = $(document.getElementById(state.selectionId)); 85 var activeId = newSelection.attr('aria-activedescendant'); 86 var activeValue = false; 87 88 if (activeId) { 89 activeValue = $(document.getElementById(activeId)).attr('data-value'); 90 } 91 originalSelect.children('option').each(function(index, ele) { 92 if ($(ele).prop('selected')) { 93 items.push({label: $(ele).html(), value: $(ele).attr('value')}); 94 } 95 }); 96 var context = $.extend({items: items}, options, state); 97 98 // Render the template. 99 templates.render('core/form_autocomplete_selection', context).done(function(newHTML) { 100 // Add it to the page. 101 newSelection.empty().append($(newHTML).html()); 102 103 if (activeValue !== false) { 104 // Reselect any previously selected item. 105 newSelection.children('[aria-selected=true]').each(function(index, ele) { 106 if ($(ele).attr('data-value') === activeValue) { 107 activateSelection(index, state); 108 } 109 }); 110 } 111 }).fail(notification.exception); 112 }; 113 114 /** 115 * Notify of a change in the selection. 116 * 117 * @param {jQuery} originalSelect The jQuery object matching the hidden select list. 118 */ 119 var notifyChange = function(originalSelect) { 120 if (typeof M.core_formchangechecker !== 'undefined') { 121 M.core_formchangechecker.set_form_changed(); 122 } 123 originalSelect.change(); 124 }; 125 126 /** 127 * Remove the given item from the list of selected things. 128 * 129 * @method deselectItem 130 * @private 131 * @param {Object} options Original options for this autocomplete element. 132 * @param {Object} state State variables for this autocomplete element. 133 * @param {Element} item The item to be deselected. 134 * @param {Element} originalSelect The original select list. 135 */ 136 var deselectItem = function(options, state, item, originalSelect) { 137 var selectedItemValue = $(item).attr('data-value'); 138 139 // We can only deselect items if this is a multi-select field. 140 if (options.multiple) { 141 // Look for a match, and toggle the selected property if there is a match. 142 originalSelect.children('option').each(function(index, ele) { 143 if ($(ele).attr('value') == selectedItemValue) { 144 $(ele).prop('selected', false); 145 // We remove newly created custom tags from the suggestions list when they are deselected. 146 if ($(ele).attr('data-iscustom')) { 147 $(ele).remove(); 148 } 149 } 150 }); 151 } 152 // Rerender the selection list. 153 updateSelectionList(options, state, originalSelect); 154 // Notifiy that the selection changed. 155 notifyChange(originalSelect); 156 }; 157 158 /** 159 * Make an item in the suggestions "active" (about to be selected). 160 * 161 * @method activateItem 162 * @private 163 * @param {Number} index The index in the current (visible) list of suggestions. 164 * @param {Object} state State variables for this instance of autocomplete. 165 */ 166 var activateItem = function(index, state) { 167 // Find the elements in the DOM. 168 var inputElement = $(document.getElementById(state.inputId)); 169 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 170 171 // Count the visible items. 172 var length = suggestionsElement.children('[aria-hidden=false]').length; 173 // Limit the index to the upper/lower bounds of the list (wrap in both directions). 174 index = index % length; 175 while (index < 0) { 176 index += length; 177 } 178 // Find the specified element. 179 var element = $(suggestionsElement.children('[aria-hidden=false]').get(index)); 180 // Find the index of this item in the full list of suggestions (including hidden). 181 var globalIndex = $(suggestionsElement.children('[role=option]')).index(element); 182 // Create an id we can assign to this element. 183 var itemId = state.suggestionsId + '-' + globalIndex; 184 185 // Deselect all the suggestions. 186 suggestionsElement.children().attr('aria-selected', false).attr('id', ''); 187 // Select only this suggestion and assign it the id. 188 element.attr('aria-selected', true).attr('id', itemId); 189 // Tell the input field it has a new active descendant so the item is announced. 190 inputElement.attr('aria-activedescendant', itemId); 191 192 // Scroll it into view. 193 var scrollPos = element.offset().top 194 - suggestionsElement.offset().top 195 + suggestionsElement.scrollTop() 196 - (suggestionsElement.height() / 2); 197 suggestionsElement.animate({ 198 scrollTop: scrollPos 199 }, 100); 200 }; 201 202 /** 203 * Find the index of the current active suggestion, and activate the next one. 204 * 205 * @method activateNextItem 206 * @private 207 * @param {Object} state State variable for this auto complete element. 208 */ 209 var activateNextItem = function(state) { 210 // Find the list of suggestions. 211 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 212 // Find the active one. 213 var element = suggestionsElement.children('[aria-selected=true]'); 214 // Find it's index. 215 var current = suggestionsElement.children('[aria-hidden=false]').index(element); 216 // Activate the next one. 217 activateItem(current + 1, state); 218 }; 219 220 /** 221 * Find the index of the current active selection, and activate the previous one. 222 * 223 * @method activatePreviousSelection 224 * @private 225 * @param {Object} state State variables for this instance of autocomplete. 226 */ 227 var activatePreviousSelection = function(state) { 228 // Find the list of selections. 229 var selectionsElement = $(document.getElementById(state.selectionId)); 230 // Find the active one. 231 var element = selectionsElement.children('[data-active-selection=true]'); 232 if (!element) { 233 activateSelection(0, state); 234 return; 235 } 236 // Find it's index. 237 var current = selectionsElement.children('[aria-selected=true]').index(element); 238 // Activate the next one. 239 activateSelection(current - 1, state); 240 }; 241 /** 242 * Find the index of the current active selection, and activate the next one. 243 * 244 * @method activateNextSelection 245 * @private 246 * @param {Object} state State variables for this instance of autocomplete. 247 */ 248 var activateNextSelection = function(state) { 249 // Find the list of selections. 250 var selectionsElement = $(document.getElementById(state.selectionId)); 251 // Find the active one. 252 var element = selectionsElement.children('[data-active-selection=true]'); 253 if (!element) { 254 activateSelection(0, state); 255 return; 256 } 257 // Find it's index. 258 var current = selectionsElement.children('[aria-selected=true]').index(element); 259 // Activate the next one. 260 activateSelection(current + 1, state); 261 }; 262 263 /** 264 * Find the index of the current active suggestion, and activate the previous one. 265 * 266 * @method activatePreviousItem 267 * @private 268 * @param {Object} state State variables for this autocomplete element. 269 */ 270 var activatePreviousItem = function(state) { 271 // Find the list of suggestions. 272 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 273 // Find the active one. 274 var element = suggestionsElement.children('[aria-selected=true]'); 275 // Find it's index. 276 var current = suggestionsElement.children('[aria-hidden=false]').index(element); 277 // Activate the next one. 278 activateItem(current - 1, state); 279 }; 280 281 /** 282 * Close the list of suggestions. 283 * 284 * @method closeSuggestions 285 * @private 286 * @param {Object} state State variables for this autocomplete element. 287 */ 288 var closeSuggestions = function(state) { 289 // Find the elements in the DOM. 290 var inputElement = $(document.getElementById(state.inputId)); 291 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 292 293 // Announce the list of suggestions was closed, and read the current list of selections. 294 inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId); 295 // Hide the suggestions list (from screen readers too). 296 suggestionsElement.hide().attr('aria-hidden', true); 297 }; 298 299 /** 300 * Rebuild the list of suggestions based on the current values in the select list, and the query. 301 * 302 * @method updateSuggestions 303 * @private 304 * @param {Object} options The original options for this autocomplete. 305 * @param {Object} state The state variables for this autocomplete. 306 * @param {String} query The current text for the search string. 307 * @param {JQuery} originalSelect The JQuery object matching the hidden select list. 308 */ 309 var updateSuggestions = function(options, state, query, originalSelect) { 310 // Find the elements in the DOM. 311 var inputElement = $(document.getElementById(state.inputId)); 312 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 313 314 // Used to track if we found any visible suggestions. 315 var matchingElements = false; 316 // Options is used by the context when rendering the suggestions from a template. 317 var suggestions = []; 318 originalSelect.children('option').each(function(index, option) { 319 if ($(option).prop('selected') !== true) { 320 suggestions[suggestions.length] = {label: option.innerHTML, value: $(option).attr('value')}; 321 } 322 }); 323 324 // Re-render the list of suggestions. 325 var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase(); 326 var context = $.extend({options: suggestions}, options, state); 327 templates.render( 328 'core/form_autocomplete_suggestions', 329 context 330 ).done(function(newHTML) { 331 // We have the new template, insert it in the page. 332 suggestionsElement.replaceWith(newHTML); 333 // Get the element again. 334 suggestionsElement = $(document.getElementById(state.suggestionsId)); 335 // Show it if it is hidden. 336 suggestionsElement.show().attr('aria-hidden', false); 337 // For each option in the list, hide it if it doesn't match the query. 338 suggestionsElement.children().each(function(index, node) { 339 node = $(node); 340 if ((options.caseSensitive && node.text().indexOf(searchquery) > -1) || 341 (!options.caseSensitive && node.text().toLocaleLowerCase().indexOf(searchquery) > -1)) { 342 node.show().attr('aria-hidden', false); 343 matchingElements = true; 344 } else { 345 node.hide().attr('aria-hidden', true); 346 } 347 }); 348 // If we found any matches, show the list. 349 inputElement.attr('aria-expanded', true); 350 if (matchingElements) { 351 // We only activate the first item in the list if tags is false, 352 // because otherwise "Enter" would select the first item, instead of 353 // creating a new tag. 354 if (!options.tags) { 355 activateItem(0, state); 356 } 357 } else { 358 // Nothing matches. Tell them that. 359 str.get_string('nosuggestions', 'form').done(function(nosuggestionsstr) { 360 suggestionsElement.html(nosuggestionsstr); 361 }); 362 } 363 }).fail(notification.exception); 364 365 }; 366 367 /** 368 * Create a new item for the list (a tag). 369 * 370 * @method createItem 371 * @private 372 * @param {Object} options The original options for the autocomplete. 373 * @param {Object} state State variables for the autocomplete. 374 * @param {JQuery} originalSelect The JQuery object matching the hidden select list. 375 */ 376 var createItem = function(options, state, originalSelect) { 377 // Find the element in the DOM. 378 var inputElement = $(document.getElementById(state.inputId)); 379 // Get the current text in the input field. 380 var query = inputElement.val(); 381 var tags = query.split(','); 382 var found = false; 383 384 $.each(tags, function(tagindex, tag) { 385 // If we can only select one at a time, deselect any current value. 386 tag = tag.trim(); 387 if (tag !== '') { 388 if (!options.multiple) { 389 originalSelect.children('option').prop('selected', false); 390 } 391 // Look for an existing option in the select list that matches this new tag. 392 originalSelect.children('option').each(function(index, ele) { 393 if ($(ele).attr('value') == tag) { 394 found = true; 395 $(ele).prop('selected', true); 396 } 397 }); 398 // Only create the item if it's new. 399 if (!found) { 400 var option = $('<option>'); 401 option.append(tag); 402 option.attr('value', tag); 403 originalSelect.append(option); 404 option.prop('selected', true); 405 // We mark newly created custom options as we handle them differently if they are "deselected". 406 option.attr('data-iscustom', true); 407 } 408 } 409 }); 410 411 updateSelectionList(options, state, originalSelect); 412 // Notifiy that the selection changed. 413 notifyChange(originalSelect); 414 // Clear the input field. 415 inputElement.val(''); 416 // Close the suggestions list. 417 closeSuggestions(state); 418 }; 419 420 /** 421 * Select the currently active item from the suggestions list. 422 * 423 * @method selectCurrentItem 424 * @private 425 * @param {Object} options The original options for the autocomplete. 426 * @param {Object} state State variables for the autocomplete. 427 * @param {JQuery} originalSelect The JQuery object matching the hidden select list. 428 */ 429 var selectCurrentItem = function(options, state, originalSelect) { 430 // Find the elements in the page. 431 var inputElement = $(document.getElementById(state.inputId)); 432 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 433 // Here loop through suggestions and set val to join of all selected items. 434 435 var selectedItemValue = suggestionsElement.children('[aria-selected=true]').attr('data-value'); 436 // The select will either be a single or multi select, so the following will either 437 // select one or more items correctly. 438 // Take care to use 'prop' and not 'attr' for selected properties. 439 // If only one can be selected at a time, start by deselecting everything. 440 if (!options.multiple) { 441 originalSelect.children('option').prop('selected', false); 442 } 443 // Look for a match, and toggle the selected property if there is a match. 444 originalSelect.children('option').each(function(index, ele) { 445 if ($(ele).attr('value') == selectedItemValue) { 446 $(ele).prop('selected', true); 447 } 448 }); 449 // Rerender the selection list. 450 updateSelectionList(options, state, originalSelect); 451 // Notifiy that the selection changed. 452 notifyChange(originalSelect); 453 // Clear the input element. 454 inputElement.val(''); 455 // Close the list of suggestions. 456 closeSuggestions(state); 457 }; 458 459 /** 460 * Fetch a new list of options via ajax. 461 * 462 * @method updateAjax 463 * @private 464 * @param {Event} e The event that triggered this update. 465 * @param {Object} options The original options for the autocomplete. 466 * @param {Object} state The state variables for the autocomplete. 467 * @param {JQuery} originalSelect The JQuery object matching the hidden select list. 468 * @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results. 469 */ 470 var updateAjax = function(e, options, state, originalSelect, ajaxHandler) { 471 // Get the query to pass to the ajax function. 472 var query = $(e.currentTarget).val(); 473 // Call the transport function to do the ajax (name taken from Select2). 474 ajaxHandler.transport(options.selector, query, function(results) { 475 // We got a result - pass it through the translator before using it. 476 var processedResults = ajaxHandler.processResults(options.selector, results); 477 var existingValues = []; 478 479 // Now destroy all options that are not currently selected. 480 originalSelect.children('option').each(function(optionIndex, option) { 481 option = $(option); 482 if (!option.prop('selected')) { 483 option.remove(); 484 } else { 485 existingValues.push(String(option.attr('value'))); 486 } 487 }); 488 489 if (!options.multiple && originalSelect.children('option').length === 0) { 490 // If this is a single select - and there are no current options 491 // the first option added will be selected by the browser. This causes a bug! 492 // We need to insert an empty option so that none of the real options are selected. 493 var option = $('<option>'); 494 originalSelect.append(option); 495 } 496 // And add all the new ones returned from ajax. 497 $.each(processedResults, function(resultIndex, result) { 498 if (existingValues.indexOf(String(result.value)) === -1) { 499 var option = $('<option>'); 500 option.append(result.label); 501 option.attr('value', result.value); 502 originalSelect.append(option); 503 } 504 }); 505 // Update the list of suggestions now from the new values in the select list. 506 updateSuggestions(options, state, '', originalSelect); 507 }, notification.exception); 508 }; 509 510 /** 511 * Add all the event listeners required for keyboard nav, blur clicks etc. 512 * 513 * @method addNavigation 514 * @private 515 * @param {Object} options The options used to create this autocomplete element. 516 * @param {Object} state State variables for this autocomplete element. 517 * @param {JQuery} originalSelect The JQuery object matching the hidden select list. 518 */ 519 var addNavigation = function(options, state, originalSelect) { 520 // Start with the input element. 521 var inputElement = $(document.getElementById(state.inputId)); 522 // Add keyboard nav with keydown. 523 inputElement.on('keydown', function(e) { 524 switch (e.keyCode) { 525 case KEYS.DOWN: 526 // If the suggestion list is open, move to the next item. 527 if (!options.showSuggestions) { 528 // Do not consume this event. 529 return true; 530 } else if (inputElement.attr('aria-expanded') === "true") { 531 activateNextItem(state); 532 } else { 533 // Handle ajax population of suggestions. 534 if (!inputElement.val() && options.ajax) { 535 require([options.ajax], function(ajaxHandler) { 536 updateAjax(e, options, state, originalSelect, ajaxHandler); 537 }); 538 } else { 539 // Else - open the suggestions list. 540 updateSuggestions(options, state, inputElement.val(), originalSelect); 541 } 542 } 543 // We handled this event, so prevent it. 544 e.preventDefault(); 545 return false; 546 case KEYS.COMMA: 547 if (options.tags) { 548 // If we are allowing tags, comma should create a tag (or enter). 549 createItem(options, state, originalSelect); 550 } 551 // We handled this event, so prevent it. 552 e.preventDefault(); 553 return false; 554 case KEYS.UP: 555 // Choose the previous active item. 556 activatePreviousItem(state); 557 // We handled this event, so prevent it. 558 e.preventDefault(); 559 return false; 560 case KEYS.ENTER: 561 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 562 if ((inputElement.attr('aria-expanded') === "true") && 563 (suggestionsElement.children('[aria-selected=true]').length > 0)) { 564 // If the suggestion list has an active item, select it. 565 selectCurrentItem(options, state, originalSelect); 566 } else if (options.tags) { 567 // If tags are enabled, create a tag. 568 createItem(options, state, originalSelect); 569 } 570 // We handled this event, so prevent it. 571 e.preventDefault(); 572 return false; 573 case KEYS.ESCAPE: 574 if (inputElement.attr('aria-expanded') === "true") { 575 // If the suggestion list is open, close it. 576 closeSuggestions(state); 577 } 578 // We handled this event, so prevent it. 579 e.preventDefault(); 580 return false; 581 } 582 return true; 583 }); 584 // Handler used to force set the value from behat. 585 inputElement.on('behat:set-value', function() { 586 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 587 if ((inputElement.attr('aria-expanded') === "true") && 588 (suggestionsElement.children('[aria-selected=true]').length > 0)) { 589 // If the suggestion list has an active item, select it. 590 selectCurrentItem(options, state, originalSelect); 591 } else if (options.tags) { 592 // If tags are enabled, create a tag. 593 createItem(options, state, originalSelect); 594 } 595 }); 596 inputElement.on('blur', function() { 597 window.setTimeout(function() { 598 // Get the current element with focus. 599 var focusElement = $(document.activeElement); 600 // Only close the menu if the input hasn't regained focus. 601 if (focusElement.attr('id') != inputElement.attr('id')) { 602 if (options.tags) { 603 createItem(options, state, originalSelect); 604 } 605 closeSuggestions(state); 606 } 607 }, 500); 608 }); 609 if (options.showSuggestions) { 610 var arrowElement = $(document.getElementById(state.downArrowId)); 611 arrowElement.on('click', function() { 612 // Prevent the close timer, or we will open, then close the suggestions. 613 inputElement.focus(); 614 // Show the suggestions list. 615 updateSuggestions(options, state, inputElement.val(), originalSelect); 616 }); 617 } 618 619 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 620 suggestionsElement.parent().on('click', '[role=option]', function(e) { 621 // Handle clicks on suggestions. 622 var element = $(e.currentTarget).closest('[role=option]'); 623 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 624 // Find the index of the clicked on suggestion. 625 var current = suggestionsElement.children('[aria-hidden=false]').index(element); 626 // Activate it. 627 activateItem(current, state); 628 // And select it. 629 selectCurrentItem(options, state, originalSelect); 630 }); 631 var selectionElement = $(document.getElementById(state.selectionId)); 632 // Handle clicks on the selected items (will unselect an item). 633 selectionElement.on('click', '[role=listitem]', function(e) { 634 // Get the item that was clicked. 635 var item = $(e.currentTarget); 636 // Remove it from the selection. 637 deselectItem(options, state, item, originalSelect); 638 }); 639 // Keyboard navigation for the selection list. 640 selectionElement.on('keydown', function(e) { 641 switch (e.keyCode) { 642 case KEYS.DOWN: 643 // Choose the next selection item. 644 activateNextSelection(state); 645 // We handled this event, so prevent it. 646 e.preventDefault(); 647 return false; 648 case KEYS.UP: 649 // Choose the previous selection item. 650 activatePreviousSelection(state); 651 // We handled this event, so prevent it. 652 e.preventDefault(); 653 return false; 654 case KEYS.SPACE: 655 case KEYS.ENTER: 656 // Get the item that is currently selected. 657 var selectedItem = $(document.getElementById(state.selectionId)).children('[data-active-selection=true]'); 658 if (selectedItem) { 659 // Unselect this item. 660 deselectItem(options, state, selectedItem, originalSelect); 661 // We handled this event, so prevent it. 662 e.preventDefault(); 663 } 664 return false; 665 } 666 return true; 667 }); 668 // Whenever the input field changes, update the suggestion list. 669 if (options.showSuggestions) { 670 inputElement.on('input', function(e) { 671 var query = $(e.currentTarget).val(); 672 var last = $(e.currentTarget).data('last-value'); 673 // IE11 fires many more input events than required - even when the value has not changed. 674 // We need to only do this for real value changed events or the suggestions will be 675 // unclickable on IE11 (because they will be rebuilt before the click event fires). 676 // Note - because of this we cannot close the list when the query is empty or it will break 677 // on IE11. 678 if (last !== query) { 679 updateSuggestions(options, state, query, originalSelect); 680 } 681 $(e.currentTarget).data('last-value', query); 682 }); 683 } 684 }; 685 686 return /** @alias module:core/form-autocomplete */ { 687 // Public variables and functions. 688 /** 689 * Turn a boring select box into an auto-complete beast. 690 * 691 * @method enhance 692 * @param {string} selector The selector that identifies the select box. 693 * @param {boolean} tags Whether to allow support for tags (can define new entries). 694 * @param {string} ajax Name of an AMD module to handle ajax requests. If specified, the AMD 695 * module must expose 2 functions "transport" and "processResults". 696 * These are modeled on Select2 see: https://select2.github.io/options.html#ajax 697 * @param {String} placeholder - The text to display before a selection is made. 698 * @param {Boolean} caseSensitive - If search has to be made case sensitive. 699 * @param {Boolean} showSuggestions - If suggestions should be shown 700 * @param {String} noSelectionString - Text to display when there is no selection 701 */ 702 enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString) { 703 // Set some default values. 704 var options = { 705 selector: selector, 706 tags: false, 707 ajax: false, 708 placeholder: placeholder, 709 caseSensitive: false, 710 showSuggestions: true, 711 noSelectionString: noSelectionString 712 }; 713 if (typeof tags !== "undefined") { 714 options.tags = tags; 715 } 716 if (typeof ajax !== "undefined") { 717 options.ajax = ajax; 718 } 719 if (typeof caseSensitive !== "undefined") { 720 options.caseSensitive = caseSensitive; 721 } 722 if (typeof showSuggestions !== "undefined") { 723 options.showSuggestions = showSuggestions; 724 } 725 if (typeof noSelectionString === "undefined") { 726 str.get_string('noselection', 'form').done(function(result) { 727 options.noSelectionString = result; 728 }).fail(notification.exception); 729 } 730 731 // Look for the select element. 732 var originalSelect = $(selector); 733 if (!originalSelect) { 734 log.debug('Selector not found: ' + selector); 735 return; 736 } 737 738 // Hide the original select. 739 originalSelect.hide().attr('aria-hidden', true); 740 741 // Find or generate some ids. 742 var state = { 743 selectId: originalSelect.attr('id'), 744 inputId: 'form_autocomplete_input-' + $.now(), 745 suggestionsId: 'form_autocomplete_suggestions-' + $.now(), 746 selectionId: 'form_autocomplete_selection-' + $.now(), 747 downArrowId: 'form_autocomplete_downarrow-' + $.now() 748 }; 749 options.multiple = originalSelect.attr('multiple'); 750 751 var originalLabel = $('[for=' + state.selectId + ']'); 752 // Create the new markup and insert it after the select. 753 var suggestions = []; 754 originalSelect.children('option').each(function(index, option) { 755 suggestions[index] = {label: option.innerHTML, value: $(option).attr('value')}; 756 }); 757 758 // Render all the parts of our UI. 759 var context = $.extend({}, options, state); 760 context.options = suggestions; 761 context.items = []; 762 763 var renderInput = templates.render('core/form_autocomplete_input', context); 764 var renderDatalist = templates.render('core/form_autocomplete_suggestions', context); 765 var renderSelection = templates.render('core/form_autocomplete_selection', context); 766 767 $.when(renderInput, renderDatalist, renderSelection).done(function(input, suggestions, selection) { 768 // Add our new UI elements to the page. 769 originalSelect.after(suggestions); 770 originalSelect.after(input); 771 originalSelect.after(selection); 772 // Update the form label to point to the text input. 773 originalLabel.attr('for', state.inputId); 774 // Add the event handlers. 775 addNavigation(options, state, originalSelect); 776 777 var inputElement = $(document.getElementById(state.inputId)); 778 var suggestionsElement = $(document.getElementById(state.suggestionsId)); 779 // Hide the suggestions by default. 780 suggestionsElement.hide().attr('aria-hidden', true); 781 782 // If this field uses ajax, set it up. 783 if (options.ajax) { 784 require([options.ajax], function(ajaxHandler) { 785 var throttleTimeout = null; 786 var handler = function(e) { 787 updateAjax(e, options, state, originalSelect, ajaxHandler); 788 }; 789 790 // For input events, we do not want to trigger many, many updates. 791 var throttledHandler = function(e) { 792 if (throttleTimeout !== null) { 793 window.clearTimeout(throttleTimeout); 794 throttleTimeout = null; 795 } 796 throttleTimeout = window.setTimeout(handler.bind(this, e), 300); 797 }; 798 // Trigger an ajax update after the text field value changes. 799 inputElement.on("input", throttledHandler); 800 var arrowElement = $(document.getElementById(state.downArrowId)); 801 arrowElement.on("click", handler); 802 }); 803 } 804 // Show the current values in the selection list. 805 updateSelectionList(options, state, originalSelect); 806 }); 807 } 808 }; 809 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |