mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 12:32:08 +02:00
MDL-51247 forms: All new aria-pimped autocomplete mform element.
Supports static list of options - or options fetched via ajax. Has options for single,multi and tags support.
This commit is contained in:
parent
cd83fae089
commit
60a1ea56d9
@ -49,6 +49,7 @@ $string['month'] = 'Month';
|
||||
$string['mustbeoverriden'] = 'Abstract form_definition() method in class {$a} must be overridden, please fix the code.';
|
||||
$string['nomethodforaddinghelpbutton'] = 'There is no method for adding a help button to form element {$a->name} (class {$a->classname})';
|
||||
$string['nonexistentformelements'] = 'Trying to add help buttons to non-existent form elements : {$a}';
|
||||
$string['noselection'] = 'No selection';
|
||||
$string['optional'] = 'Optional';
|
||||
$string['othersettings'] = 'Other settings';
|
||||
$string['requiredelement'] = 'Required field';
|
||||
@ -56,6 +57,7 @@ $string['revealpassword'] = 'Reveal';
|
||||
$string['security'] = 'Security';
|
||||
$string['selectallornone'] = 'Select all/none';
|
||||
$string['selected'] = 'Selected';
|
||||
$string['selecteditems'] = 'Selected items:';
|
||||
$string['showadvanced'] = 'Show advanced';
|
||||
$string['showless'] = 'Show less...';
|
||||
$string['showmore'] = 'Show more...';
|
||||
|
1
lib/amd/build/form-autocomplete.min.js
vendored
Normal file
1
lib/amd/build/form-autocomplete.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
760
lib/amd/src/form-autocomplete.js
Normal file
760
lib/amd/src/form-autocomplete.js
Normal file
@ -0,0 +1,760 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Autocomplete wrapper for select2 library.
|
||||
*
|
||||
* @module core/form-autocomplete
|
||||
* @class autocomplete
|
||||
* @package core
|
||||
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 3.0
|
||||
*/
|
||||
/* globals require: false */
|
||||
define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'], function($, log, str, templates, notification) {
|
||||
|
||||
// Private functions and variables.
|
||||
/** @var {Object} KEYS - List of keycode constants. */
|
||||
var KEYS = {
|
||||
DOWN: 40,
|
||||
ENTER: 13,
|
||||
SPACE: 32,
|
||||
ESCAPE: 27,
|
||||
COMMA: 188,
|
||||
UP: 38
|
||||
};
|
||||
|
||||
/** @var {Number} closeSuggestionsTimer - integer used to cancel window.setTimeout. */
|
||||
var closeSuggestionsTimer = null;
|
||||
|
||||
/**
|
||||
* Make an item in the selection list "active".
|
||||
*
|
||||
* @method activateSelection
|
||||
* @private
|
||||
* @param {Number} index The index in the current (visible) list of selection.
|
||||
* @param {String} selectionId The id of the selection element for this instance of the autocomplete.
|
||||
*/
|
||||
var activateSelection = function(index, selectionId) {
|
||||
// Find the elements in the DOM.
|
||||
var selectionElement = $(document.getElementById(selectionId));
|
||||
|
||||
// Count the visible items.
|
||||
var length = selectionElement.children('[aria-selected=true]').length;
|
||||
// Limit the index to the upper/lower bounds of the list (wrap in both directions).
|
||||
index = index % length;
|
||||
while (index < 0) {
|
||||
index += length;
|
||||
}
|
||||
// Find the specified element.
|
||||
var element = $(selectionElement.children('[aria-selected=true]').get(index));
|
||||
// Create an id we can assign to this element.
|
||||
var itemId = selectionId + '-' + index;
|
||||
|
||||
// Deselect all the selections.
|
||||
selectionElement.children().attr('data-active-selection', false).attr('id', '');
|
||||
// Select only this suggestion and assign it the id.
|
||||
element.attr('data-active-selection', true).attr('id', itemId);
|
||||
// Tell the input field it has a new active descendant so the item is announced.
|
||||
selectionElement.attr('aria-activedescendant', itemId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the current item from the list of selected things.
|
||||
*
|
||||
* @method deselectCurrentSelection
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
* @param {String} selectionId The id of the selection element for this instance of the autocomplete.
|
||||
* @param {Element} originalSelect The original select list.
|
||||
* @param {Boolean} multiple Is this a multi select.
|
||||
* @param {Boolean} tags Is this a tags select.
|
||||
*/
|
||||
var deselectCurrentSelection = function(inputId, suggestionsId, selectionId, originalSelect, multiple, tags) {
|
||||
var selectionElement = $(document.getElementById(selectionId));
|
||||
var selectedItemValue = selectionElement.children('[data-active-selection=true]').attr('data-value');
|
||||
// The select will either be a single or multi select, so the following will either
|
||||
// select one or more items correctly.
|
||||
// Take care to use 'prop' and not 'attr' for selected properties.
|
||||
// If only one can be selected at a time, start by deselecting everything.
|
||||
if (!multiple) {
|
||||
originalSelect.children('option').prop('selected', false);
|
||||
}
|
||||
// Look for a match, and toggle the selected property if there is a match.
|
||||
originalSelect.children('option').each(function(index, ele) {
|
||||
if ($(ele).attr('value') == selectedItemValue) {
|
||||
$(ele).prop('selected', false);
|
||||
if (tags) {
|
||||
$(ele).remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Rerender the selection list.
|
||||
updateSelectionList(selectionId, inputId, originalSelect, multiple);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make an item in the suggestions "active" (about to be selected).
|
||||
*
|
||||
* @method activateItem
|
||||
* @private
|
||||
* @param {Number} index The index in the current (visible) list of suggestions.
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
*/
|
||||
var activateItem = function(index, inputId, suggestionsId) {
|
||||
// Find the elements in the DOM.
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
|
||||
// Count the visible items.
|
||||
var length = suggestionsElement.children('[aria-hidden=false]').length;
|
||||
// Limit the index to the upper/lower bounds of the list (wrap in both directions).
|
||||
index = index % length;
|
||||
while (index < 0) {
|
||||
index += length;
|
||||
}
|
||||
// Find the specified element.
|
||||
var element = $(suggestionsElement.children('[aria-hidden=false]').get(index));
|
||||
// Find the index of this item in the full list of suggestions (including hidden).
|
||||
var globalIndex = $(suggestionsElement.children('[role=option]')).index(element);
|
||||
// Create an id we can assign to this element.
|
||||
var itemId = suggestionsId + '-' + globalIndex;
|
||||
|
||||
// Deselect all the suggestions.
|
||||
suggestionsElement.children().attr('aria-selected', false).attr('id', '');
|
||||
// Select only this suggestion and assign it the id.
|
||||
element.attr('aria-selected', true).attr('id', itemId);
|
||||
// Tell the input field it has a new active descendant so the item is announced.
|
||||
inputElement.attr('aria-activedescendant', itemId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the index of the current active suggestion, and activate the next one.
|
||||
*
|
||||
* @method activateNextItem
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
*/
|
||||
var activateNextItem = function(inputId, suggestionsId) {
|
||||
// Find the list of suggestions.
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
// Find the active one.
|
||||
var element = suggestionsElement.children('[aria-selected=true]');
|
||||
// Find it's index.
|
||||
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
|
||||
// Activate the next one.
|
||||
activateItem(current+1, inputId, suggestionsId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the index of the current active selection, and activate the previous one.
|
||||
*
|
||||
* @method activatePreviousSelection
|
||||
* @private
|
||||
* @param {String} selectionId The id of the selection element for this instance of the autocomplete.
|
||||
*/
|
||||
var activatePreviousSelection = function(selectionId) {
|
||||
// Find the list of selections.
|
||||
var selectionsElement = $(document.getElementById(selectionId));
|
||||
// Find the active one.
|
||||
var element = selectionsElement.children('[data-active-selection=true]');
|
||||
if (!element) {
|
||||
activateSelection(0, selectionId);
|
||||
return;
|
||||
}
|
||||
// Find it's index.
|
||||
var current = selectionsElement.children('[aria-selected=true]').index(element);
|
||||
// Activate the next one.
|
||||
activateSelection(current-1, selectionId);
|
||||
};
|
||||
/**
|
||||
* Find the index of the current active selection, and activate the next one.
|
||||
*
|
||||
* @method activateNextSelection
|
||||
* @private
|
||||
* @param {String} selectionId The id of the selection element for this instance of the autocomplete.
|
||||
*/
|
||||
var activateNextSelection = function(selectionId) {
|
||||
// Find the list of selections.
|
||||
var selectionsElement = $(document.getElementById(selectionId));
|
||||
// Find the active one.
|
||||
var element = selectionsElement.children('[data-active-selection=true]');
|
||||
if (!element) {
|
||||
activateSelection(0, selectionId);
|
||||
return;
|
||||
}
|
||||
// Find it's index.
|
||||
var current = selectionsElement.children('[aria-selected=true]').index(element);
|
||||
// Activate the next one.
|
||||
activateSelection(current+1, selectionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the index of the current active suggestion, and activate the previous one.
|
||||
*
|
||||
* @method activatePreviousItem
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
*/
|
||||
var activatePreviousItem = function(inputId, suggestionsId) {
|
||||
// Find the list of suggestions.
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
// Find the active one.
|
||||
var element = suggestionsElement.children('[aria-selected=true]');
|
||||
// Find it's index.
|
||||
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
|
||||
// Activate the next one.
|
||||
activateItem(current-1, inputId, suggestionsId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the list of suggestions.
|
||||
*
|
||||
* @method closeSuggestions
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
*/
|
||||
var closeSuggestions = function(inputId, suggestionsId, selectionId) {
|
||||
// Find the elements in the DOM.
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
|
||||
// Announce the list of suggestions was closed, and read the current list of selections.
|
||||
inputElement.attr('aria-expanded', false).attr('aria-activedescendant', selectionId);
|
||||
// Hide the suggestions list (from screen readers too).
|
||||
suggestionsElement.hide().attr('aria-hidden', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rebuild the list of suggestions based on the current values in the select list, and the query.
|
||||
*
|
||||
* @method updateSuggestions
|
||||
* @private
|
||||
* @param {String} query The current query typed in the input field.
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
|
||||
* @param {Boolean} multiple Are multiple items allowed to be selected?
|
||||
* @param {Boolean} tags Are we allowed to create new items on the fly?
|
||||
*/
|
||||
var updateSuggestions = function(query, inputId, suggestionsId, originalSelect, multiple, tags) {
|
||||
// Find the elements in the DOM.
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
|
||||
// Used to track if we found any visible suggestions.
|
||||
var matchingElements = false;
|
||||
// Options is used by the context when rendering the suggestions from a template.
|
||||
var options = [];
|
||||
originalSelect.children('option').each(function(index, option) {
|
||||
if ($(option).prop('selected') !== true) {
|
||||
options[options.length] = { label: option.innerHTML, value: $(option).attr('value') };
|
||||
}
|
||||
});
|
||||
|
||||
// Re-render the list of suggestions.
|
||||
templates.render(
|
||||
'core/form_autocomplete_suggestions',
|
||||
{ inputId: inputId, suggestionsId: suggestionsId, options: options, multiple: multiple}
|
||||
).done(function(newHTML) {
|
||||
// We have the new template, insert it in the page.
|
||||
suggestionsElement.replaceWith(newHTML);
|
||||
// Get the element again.
|
||||
suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
// Show it if it is hidden.
|
||||
suggestionsElement.show().attr('aria-hidden', false);
|
||||
// For each option in the list, hide it if it doesn't match the query.
|
||||
suggestionsElement.children().each(function(index, node) {
|
||||
node = $(node);
|
||||
if (node.text().indexOf(query) > -1) {
|
||||
node.show().attr('aria-hidden', false);
|
||||
matchingElements = true;
|
||||
} else {
|
||||
node.hide().attr('aria-hidden', true);
|
||||
}
|
||||
});
|
||||
// If we found any matches, show the list.
|
||||
if (matchingElements) {
|
||||
inputElement.attr('aria-expanded', true);
|
||||
// We only activate the first item in the list if tags is false,
|
||||
// because otherwise "Enter" would select the first item, instead of
|
||||
// creating a new tag.
|
||||
if (!tags) {
|
||||
activateItem(0, inputId, suggestionsId);
|
||||
}
|
||||
} else {
|
||||
// Abort - nothing matches. Hide the suggestions properly.
|
||||
suggestionsElement.hide();
|
||||
suggestionsElement.attr('aria-hidden', true);
|
||||
inputElement.attr('aria-expanded', false);
|
||||
}
|
||||
}).fail(notification.exception);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new item for the list (a tag).
|
||||
*
|
||||
* @method createItem
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
* @param {Boolean} multiple Are multiple items allowed to be selected?
|
||||
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
|
||||
*/
|
||||
var createItem = function(inputId, suggestionsId, selectionId, multiple, originalSelect) {
|
||||
// Find the element in the DOM.
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
// Get the current text in the input field.
|
||||
var query = inputElement.val();
|
||||
var tags = query.split(',');
|
||||
var found = false;
|
||||
|
||||
$.each(tags, function(tagindex, tag) {
|
||||
// If we can only select one at a time, deselect any current value.
|
||||
tag = tag.trim();
|
||||
if (tag !== '') {
|
||||
if (!multiple) {
|
||||
originalSelect.children('option').prop('selected', false);
|
||||
}
|
||||
// Look for an existing option in the select list that matches this new tag.
|
||||
originalSelect.children('option').each(function(index, ele) {
|
||||
if ($(ele).attr('value') == tag) {
|
||||
found = true;
|
||||
$(ele).prop('selected', true);
|
||||
}
|
||||
});
|
||||
// Only create the item if it's new.
|
||||
if (!found) {
|
||||
var option = $('<option>');
|
||||
option.append(tag);
|
||||
option.attr('value', tag);
|
||||
originalSelect.append(option);
|
||||
option.prop('selected', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Get the selection element.
|
||||
var newSelection = $(document.getElementById(selectionId));
|
||||
// Build up a valid context to re-render the selection.
|
||||
var items = [];
|
||||
originalSelect.children('option').each(function(index, ele) {
|
||||
if ($(ele).prop('selected')) {
|
||||
items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
|
||||
}
|
||||
});
|
||||
var context = {
|
||||
selectionId: selectionId,
|
||||
items: items,
|
||||
multiple: multiple
|
||||
};
|
||||
// Re-render the selection.
|
||||
templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
|
||||
// Update the page.
|
||||
newSelection.empty().append($(newHTML).html());
|
||||
}).fail(notification.exception);
|
||||
// Clear the input field.
|
||||
inputElement.val('');
|
||||
// Close the suggestions list.
|
||||
closeSuggestions(inputId, suggestionsId, selectionId);
|
||||
// Trigger a change event so that the mforms javascript can check for required fields etc.
|
||||
originalSelect.change();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the element that shows the currently selected items.
|
||||
*
|
||||
* @method updateSelectionList
|
||||
* @private
|
||||
* @param {String} selectionId The id of the selections element for this instance of the autocomplete.
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
|
||||
* @param {Boolean} multiple Does this element support multiple selections.
|
||||
*/
|
||||
var updateSelectionList = function(selectionId, inputId, originalSelect, multiple) {
|
||||
// Build up a valid context to re-render the template.
|
||||
var items = [];
|
||||
var newSelection = $(document.getElementById(selectionId));
|
||||
originalSelect.children('option').each(function(index, ele) {
|
||||
if ($(ele).prop('selected')) {
|
||||
items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
|
||||
}
|
||||
});
|
||||
var context = {
|
||||
selectionId: selectionId,
|
||||
items: items,
|
||||
multiple: multiple
|
||||
};
|
||||
// Render the template.
|
||||
templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
|
||||
// Add it to the page.
|
||||
newSelection.empty().append($(newHTML).html());
|
||||
}).fail(notification.exception);
|
||||
// Because this function get's called after changing the selection, this is a good place
|
||||
// to trigger a change notification.
|
||||
originalSelect.change();
|
||||
};
|
||||
|
||||
/**
|
||||
* Select the currently active item from the suggestions list.
|
||||
*
|
||||
* @method selectCurrentItem
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
* @param {String} selectionId The id of the selection element for this instance of the autocomplete.
|
||||
* @param {Boolean} multiple Are multiple items allowed to be selected?
|
||||
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
|
||||
*/
|
||||
var selectCurrentItem = function(inputId, suggestionsId, selectionId, multiple, originalSelect) {
|
||||
// Find the elements in the page.
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
// Here loop through suggestions and set val to join of all selected items.
|
||||
|
||||
var selectedItemValue = suggestionsElement.children('[aria-selected=true]').attr('data-value');
|
||||
// The select will either be a single or multi select, so the following will either
|
||||
// select one or more items correctly.
|
||||
// Take care to use 'prop' and not 'attr' for selected properties.
|
||||
// If only one can be selected at a time, start by deselecting everything.
|
||||
if (!multiple) {
|
||||
originalSelect.children('option').prop('selected', false);
|
||||
}
|
||||
// Look for a match, and toggle the selected property if there is a match.
|
||||
originalSelect.children('option').each(function(index, ele) {
|
||||
if ($(ele).attr('value') == selectedItemValue) {
|
||||
$(ele).prop('selected', true);
|
||||
}
|
||||
});
|
||||
// Rerender the selection list.
|
||||
updateSelectionList(selectionId, inputId, originalSelect, multiple);
|
||||
// Clear the input element.
|
||||
inputElement.val('');
|
||||
// Close the list of suggestions.
|
||||
closeSuggestions(inputId, suggestionsId, selectionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch a new list of options via ajax.
|
||||
*
|
||||
* @method updateAjax
|
||||
* @private
|
||||
* @param {Event} e The event that triggered this update.
|
||||
* @param {String} selector The selector pointing to the original select.
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
|
||||
* @param {Boolean} multiple Are multiple items allowed to be selected?
|
||||
* @param {Boolean} tags Are we allowed to create new items on the fly?
|
||||
* @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
|
||||
*/
|
||||
var updateAjax = function(e, selector, inputId, suggestionsId, originalSelect, multiple, tags, ajaxHandler) {
|
||||
// Get the query to pass to the ajax function.
|
||||
var query = $(e.currentTarget).val();
|
||||
// Call the transport function to do the ajax (name taken from Select2).
|
||||
ajaxHandler.transport(selector, query, function(results) {
|
||||
// We got a result - pass it through the translator before using it.
|
||||
var processedResults = ajaxHandler.processResults(selector, results);
|
||||
var existingValues = [];
|
||||
|
||||
// Now destroy all options that are not currently selected.
|
||||
originalSelect.children('option').each(function(optionIndex, option) {
|
||||
option = $(option);
|
||||
if (!option.prop('selected')) {
|
||||
option.remove();
|
||||
} else {
|
||||
existingValues.push(option.attr('value'));
|
||||
}
|
||||
});
|
||||
// And add all the new ones returned from ajax.
|
||||
$.each(processedResults, function(resultIndex, result) {
|
||||
if (existingValues.indexOf(result.value) === -1) {
|
||||
var option = $('<option>');
|
||||
option.append(result.label);
|
||||
option.attr('value', result.value);
|
||||
originalSelect.append(option);
|
||||
}
|
||||
});
|
||||
// Update the list of suggestions now from the new values in the select list.
|
||||
updateSuggestions('', inputId, suggestionsId, originalSelect, multiple, tags);
|
||||
}, notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add all the event listeners required for keyboard nav, blur clicks etc.
|
||||
*
|
||||
* @method addNavigation
|
||||
* @private
|
||||
* @param {String} inputId The id of the input element for this instance of the autocomplete.
|
||||
* @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
|
||||
* @param {String} downArrowId The id of arrow to open the suggestions list.
|
||||
* @param {String} selectionId The id of element that shows the current selections.
|
||||
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
|
||||
* @param {Boolean} multiple Are multiple items allowed to be selected?
|
||||
* @param {Boolean} tags Are we allowed to create new items on the fly?
|
||||
*/
|
||||
var addNavigation = function(inputId, suggestionsId, downArrowId, selectionId, originalSelect, multiple, tags) {
|
||||
// Start with the input element.
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
// Add keyboard nav with keydown.
|
||||
inputElement.on('keydown', function(e) {
|
||||
switch (e.keyCode) {
|
||||
case KEYS.DOWN:
|
||||
// If the suggestion list is open, move to the next item.
|
||||
if (inputElement.attr('aria-expanded') === "true") {
|
||||
activateNextItem(inputId, suggestionsId);
|
||||
} else {
|
||||
// Else - open the suggestions list.
|
||||
updateSuggestions(inputElement.val(), inputId, suggestionsId, originalSelect, multiple, tags);
|
||||
}
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
case KEYS.COMMA:
|
||||
if (tags) {
|
||||
// If we are allowing tags, comma should create a tag (or enter).
|
||||
createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
|
||||
}
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
case KEYS.UP:
|
||||
// Choose the previous active item.
|
||||
activatePreviousItem(inputId, suggestionsId);
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
case KEYS.ENTER:
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
if ((inputElement.attr('aria-expanded') === "true") &&
|
||||
(suggestionsElement.children('[aria-selected=true]').length > 0)) {
|
||||
// If the suggestion list has an active item, select it.
|
||||
selectCurrentItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
|
||||
} else if (tags) {
|
||||
// If tags are enabled, create a tag.
|
||||
createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
|
||||
}
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
case KEYS.ESCAPE:
|
||||
if (inputElement.attr('aria-expanded') === "true") {
|
||||
// If the suggestion list is open, close it.
|
||||
closeSuggestions(inputId, suggestionsId, selectionId);
|
||||
}
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Handler used to force set the value from behat.
|
||||
inputElement.on('behat:set-value', function() {
|
||||
if (tags) {
|
||||
createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
|
||||
}
|
||||
});
|
||||
inputElement.on('blur focus', function(e) {
|
||||
// We may be blurring because we have clicked on the suggestion list. We
|
||||
// dont want to close the selection list before the click event fires, so
|
||||
// we have to delay.
|
||||
if (closeSuggestionsTimer) {
|
||||
window.clearTimeout(closeSuggestionsTimer);
|
||||
}
|
||||
closeSuggestionsTimer = window.setTimeout(function() {
|
||||
if ((e.type == 'blur') && tags) {
|
||||
createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
|
||||
}
|
||||
closeSuggestions(inputId, suggestionsId, selectionId);
|
||||
}, 500);
|
||||
});
|
||||
var arrowElement = $(document.getElementById(downArrowId));
|
||||
arrowElement.on('click', function() {
|
||||
// Prevent the close timer, or we will open, then close the suggestions.
|
||||
inputElement.focus();
|
||||
if (closeSuggestionsTimer) {
|
||||
window.clearTimeout(closeSuggestionsTimer);
|
||||
}
|
||||
// Show the suggestions list.
|
||||
updateSuggestions(inputElement.val(), inputId, suggestionsId, originalSelect, multiple, tags);
|
||||
});
|
||||
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
suggestionsElement.parent().on('click', '[role=option]', function(e) {
|
||||
// Handle clicks on suggestions.
|
||||
var element = $(e.currentTarget).closest('[role=option]');
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
// Find the index of the clicked on suggestion.
|
||||
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
|
||||
// Activate it.
|
||||
activateItem(current, inputId, suggestionsId);
|
||||
// And select it.
|
||||
selectCurrentItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
|
||||
});
|
||||
var selectionElement = $(document.getElementById(selectionId));
|
||||
// Handle clicks on the selected items (will unselect an item).
|
||||
selectionElement.parent().on('click', '[role=listitem]', function(e) {
|
||||
var value = $(e.currentTarget).attr('data-value');
|
||||
|
||||
// Only allow deselect if we allow multiple selections.
|
||||
if (multiple) {
|
||||
// Find the matching element and deselect it.
|
||||
originalSelect.children('option').each(function(index, ele) {
|
||||
if ($(ele).attr('value') == value) {
|
||||
$(ele).prop('selected', !$(ele).prop('selected'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Re-render the selection list.
|
||||
updateSelectionList(selectionId, inputId, originalSelect, multiple);
|
||||
});
|
||||
// Keyboard navigation for the selection list.
|
||||
selectionElement.parent().on('keydown', function(e) {
|
||||
switch (e.keyCode) {
|
||||
case KEYS.DOWN:
|
||||
// Choose the next selection item.
|
||||
activateNextSelection(selectionId);
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
case KEYS.UP:
|
||||
// Choose the previous selection item.
|
||||
activatePreviousSelection(selectionId);
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
case KEYS.SPACE:
|
||||
case KEYS.ENTER:
|
||||
// Unselect this item.
|
||||
deselectCurrentSelection(inputId, suggestionsId, selectionId, originalSelect, multiple, tags);
|
||||
// We handled this event, so prevent it.
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Whenever the input field changes, update the suggestion list.
|
||||
inputElement.on('input', function(e) {
|
||||
var query = $(e.currentTarget).val();
|
||||
updateSuggestions(query, inputId, suggestionsId, originalSelect, multiple, tags);
|
||||
});
|
||||
};
|
||||
|
||||
return /** @alias module:core/form-autocomplete */ {
|
||||
// Public variables and functions.
|
||||
/**
|
||||
* Turn a boring select box into an auto-complete beast.
|
||||
*
|
||||
* @method enhance
|
||||
* @param {string} select The selector that identifies the select box.
|
||||
* @param {boolean} tags Whether to allow support for tags (can define new entries).
|
||||
* @param {string} ajax Name of an AMD module to handle ajax requests. If specified, the AMD
|
||||
* module must expose 2 functions "transport" and "processResults".
|
||||
* These are modeled on Select2 see: https://select2.github.io/options.html#ajax
|
||||
* @param {String} placeholder - The text to display before a selection is made.
|
||||
*/
|
||||
enhance: function(selector, tags, ajax, placeholder) {
|
||||
// Set some default values.
|
||||
if (typeof tags === "undefined") {
|
||||
tags = false;
|
||||
}
|
||||
if (typeof ajax === "undefined") {
|
||||
ajax = false;
|
||||
}
|
||||
|
||||
// Look for the select element.
|
||||
var originalSelect = $(selector);
|
||||
if (!originalSelect) {
|
||||
log.debug('Selector not found: ' + selector);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide the original select.
|
||||
originalSelect.hide().attr('aria-hidden', true);
|
||||
|
||||
// Find or generate some ids.
|
||||
var selectId = originalSelect.attr('id');
|
||||
var multiple = originalSelect.attr('multiple');
|
||||
var inputId = 'form_autocomplete_input-' + $.now();
|
||||
var suggestionsId = 'form_autocomplete_suggestions-' + $.now();
|
||||
var selectionId = 'form_autocomplete_selection-' + $.now();
|
||||
var downArrowId = 'form_autocomplete_downarrow-' + $.now();
|
||||
|
||||
var originalLabel = $('[for=' + selectId + ']');
|
||||
// Create the new markup and insert it after the select.
|
||||
var options = [];
|
||||
originalSelect.children('option').each(function(index, option) {
|
||||
options[index] = { label: option.innerHTML, value: $(option).attr('value') };
|
||||
});
|
||||
|
||||
// Render all the parts of our UI.
|
||||
var renderInput = templates.render(
|
||||
'core/form_autocomplete_input',
|
||||
{ downArrowId: downArrowId,
|
||||
inputId: inputId,
|
||||
suggestionsId: suggestionsId,
|
||||
selectionId: selectionId,
|
||||
placeholder: placeholder,
|
||||
multiple: multiple }
|
||||
);
|
||||
var renderDatalist = templates.render(
|
||||
'core/form_autocomplete_suggestions',
|
||||
{ inputId: inputId, suggestionsId: suggestionsId, options: options, multiple: multiple}
|
||||
);
|
||||
var renderSelection = templates.render(
|
||||
'core/form_autocomplete_selection',
|
||||
{ selectionId: selectionId, items: [], multiple: multiple}
|
||||
);
|
||||
|
||||
$.when(renderInput, renderDatalist, renderSelection).done(function(input, suggestions, selection) {
|
||||
// Add our new UI elements to the page.
|
||||
originalSelect.after(suggestions);
|
||||
originalSelect.after(input);
|
||||
originalSelect.after(selection);
|
||||
// Update the form label to point to the text input.
|
||||
originalLabel.attr('for', inputId);
|
||||
// Add the event handlers.
|
||||
addNavigation(inputId, suggestionsId, downArrowId, selectionId, originalSelect, multiple, tags);
|
||||
|
||||
var inputElement = $(document.getElementById(inputId));
|
||||
var suggestionsElement = $(document.getElementById(suggestionsId));
|
||||
// Hide the suggestions by default.
|
||||
suggestionsElement.hide().attr('aria-hidden', true);
|
||||
|
||||
// If this field uses ajax, set it up.
|
||||
if (ajax) {
|
||||
require([ajax], function(ajaxHandler) {
|
||||
var handler = function(e) {
|
||||
updateAjax(e, selector, inputId, suggestionsId, originalSelect, multiple, tags, ajaxHandler);
|
||||
};
|
||||
// Trigger an ajax update after the text field value changes.
|
||||
inputElement.on("input keypress", handler);
|
||||
var arrowElement = $(document.getElementById(downArrowId));
|
||||
arrowElement.on("click", handler);
|
||||
});
|
||||
}
|
||||
// Show the current values in the selection list.
|
||||
updateSelectionList(selectionId, inputId, originalSelect, multiple);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
55
lib/behat/form_field/behat_form_autocomplete.php
Normal file
55
lib/behat/form_field/behat_form_autocomplete.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Auto complete form field class.
|
||||
*
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2015 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/behat_form_text.php');
|
||||
|
||||
/**
|
||||
* Auto complete form field.
|
||||
*
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2015 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_form_autocomplete extends behat_form_text {
|
||||
|
||||
/**
|
||||
* Sets the value to a field.
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_value($value) {
|
||||
if (!$this->running_javascript()) {
|
||||
throw new coding_exception('Setting the valid of an autocomplete field requires javascript.');
|
||||
}
|
||||
$this->field->setValue($value);
|
||||
$id = $this->field->getAttribute('id');
|
||||
$js = ' require(["jquery"], function($) { $(document.getElementById("'.$id.'")).trigger("behat:set-value"); }); ';
|
||||
$this->session->executeScript($js);
|
||||
}
|
||||
}
|
176
lib/form/autocomplete.php
Normal file
176
lib/form/autocomplete.php
Normal file
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
/**
|
||||
* autocomplete type form element
|
||||
*
|
||||
* Contains HTML class for a autocomplete type element
|
||||
*
|
||||
* @package core_form
|
||||
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->libdir . '/form/select.php');
|
||||
|
||||
/**
|
||||
* Autocomplete as you type form element
|
||||
*
|
||||
* HTML class for a autocomplete type element
|
||||
*
|
||||
* @package core_form
|
||||
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
|
||||
|
||||
/** @var boolean $tags Should we allow typing new entries to the field? */
|
||||
protected $tags = false;
|
||||
/** @var string $ajax Name of an AMD module to send/process ajax requests. */
|
||||
protected $ajax = '';
|
||||
/** @var string $placeholder Placeholder text for an empty list. */
|
||||
protected $placeholder = '';
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param string $elementName Select name attribute
|
||||
* @param mixed $elementLabel Label(s) for the select
|
||||
* @param mixed $options Data to be used to populate options
|
||||
* @param mixed $attributes Either a typical HTML attribute string or an associative array. Special options
|
||||
* "tags", "placeholder", "ajax", "multiple" are supported.
|
||||
*/
|
||||
function MoodleQuickForm_autocomplete($elementName=null, $elementLabel=null, $options=null, $attributes=null) {
|
||||
// Even if the constructor gets called twice we do not really want 2x options (crazy forms!).
|
||||
$this->_options = array();
|
||||
if ($attributes === null) {
|
||||
$attributes = array();
|
||||
}
|
||||
if (isset($attributes['tags'])) {
|
||||
$this->tags = $attributes['tags'];
|
||||
unset($attributes['tags']);
|
||||
}
|
||||
$this->placeholder = get_string('search');
|
||||
if (isset($attributes['placeholder'])) {
|
||||
$this->placeholder = $attributes['placeholder'];
|
||||
unset($attributes['placeholder']);
|
||||
}
|
||||
if (isset($attributes['ajax'])) {
|
||||
$this->ajax = $attributes['ajax'];
|
||||
unset($attributes['ajax']);
|
||||
}
|
||||
parent::HTML_QuickForm_select($elementName, $elementLabel, $options, $attributes);
|
||||
|
||||
$this->_type = 'autocomplete';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML for select form element.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function toHtml(){
|
||||
global $PAGE;
|
||||
|
||||
// Enhance the select with javascript.
|
||||
$this->_generateId();
|
||||
$id = $this->getAttribute('id');
|
||||
$PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#' . $id, $this->tags, $this->ajax, $this->placeholder));
|
||||
|
||||
return parent::toHTML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the current list of options to see if there are any options with this value.
|
||||
* @param string $value to search
|
||||
* @return boolean
|
||||
*/
|
||||
function optionExists($value) {
|
||||
foreach ($this->_options as $option) {
|
||||
if (isset($option['attr']['value']) && ($option['attr']['value'] == $value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of this element. If values can be added or are unknown, we will
|
||||
* make sure they exist in the options array.
|
||||
* @param mixed string|array $value The value to set.
|
||||
* @return boolean
|
||||
*/
|
||||
function setValue($value) {
|
||||
$values = (array) $value;
|
||||
foreach ($values as $onevalue) {
|
||||
if (($this->tags || $this->ajax) &&
|
||||
(!$this->optionExists($onevalue)) &&
|
||||
($onevalue !== '_qf__force_multiselect_submission')) {
|
||||
$this->addOption($onevalue, $onevalue);
|
||||
}
|
||||
}
|
||||
return parent::setValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 'safe' element's value
|
||||
*
|
||||
* @param array array of submitted values to search
|
||||
* @param bool whether to return the value as associative array
|
||||
* @access public
|
||||
* @return mixed
|
||||
*/
|
||||
function exportValue(&$submitValues, $assoc = false) {
|
||||
if ($this->ajax || $this->tags) {
|
||||
// When this was an ajax request, we do not know the allowed list of values.
|
||||
$value = $this->_findValue($submitValues);
|
||||
if (null === $value) {
|
||||
$value = $this->getValue();
|
||||
}
|
||||
// Quickforms inserts a duplicate element in the form with
|
||||
// this value so that a value is always submitted for this form element.
|
||||
// Normally this is cleaned as a side effect of it not being a valid option,
|
||||
// but in this case we need to detect and skip it manually.
|
||||
if ($value === '_qf__force_multiselect_submission' || $value === null) {
|
||||
$value = '';
|
||||
}
|
||||
return $this->_prepareValue($value, $assoc);
|
||||
} else {
|
||||
return parent::exportValue($submitValues, $assoc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by HTML_QuickForm whenever form event is made on this element
|
||||
*
|
||||
* @param string $event Name of event
|
||||
* @param mixed $arg event arguments
|
||||
* @param object $caller calling object
|
||||
* @return bool
|
||||
*/
|
||||
function onQuickFormEvent($event, $arg, &$caller)
|
||||
{
|
||||
switch ($event) {
|
||||
case 'createElement':
|
||||
$caller->setType($arg[0], PARAM_TAGLIST);
|
||||
break;
|
||||
}
|
||||
return parent::onQuickFormEvent($event, $arg, $caller);
|
||||
}
|
||||
}
|
66
lib/form/tests/autocomplete_test.php
Normal file
66
lib/form/tests/autocomplete_test.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for autocomplete forms element.
|
||||
*
|
||||
* This file contains all unit test related to autocomplete forms element.
|
||||
*
|
||||
* @package core_form
|
||||
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/form/autocomplete.php');
|
||||
|
||||
/**
|
||||
* Unit tests for MoodleQuickForm_autocomplete
|
||||
*
|
||||
* Contains test cases for testing MoodleQuickForm_autocomplete
|
||||
*
|
||||
* @package core_form
|
||||
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class core_form_autocomplete_testcase extends basic_testcase {
|
||||
/**
|
||||
* Testcase for validation
|
||||
*/
|
||||
public function test_validation() {
|
||||
// A default select with single values validates the data.
|
||||
$options = array('1' => 'One', 2 => 'Two');
|
||||
$element = new MoodleQuickForm_autocomplete('testel', null, $options);
|
||||
$submission = array('testel' => 2);
|
||||
$this->assertEquals($element->exportValue($submission), 2);
|
||||
$submission = array('testel' => 3);
|
||||
$this->assertNull($element->exportValue($submission));
|
||||
|
||||
// A select with multiple values validates the data.
|
||||
$options = array('1' => 'One', 2 => 'Two');
|
||||
$element = new MoodleQuickForm_autocomplete('testel', null, $options, array('multiple'=>'multiple'));
|
||||
$submission = array('testel' => array(2, 3));
|
||||
$this->assertEquals($element->exportValue($submission), array(2));
|
||||
|
||||
// A select where the values are fetched via ajax does not validate the data.
|
||||
$element = new MoodleQuickForm_autocomplete('testel', null, array(), array('multiple'=>'multiple', 'ajax'=>'anything'));
|
||||
$submission = array('testel' => array(2, 3));
|
||||
$this->assertEquals($element->exportValue($submission), array(2, 3));
|
||||
}
|
||||
|
||||
}
|
@ -2931,6 +2931,7 @@ $GLOBALS['_HTML_QuickForm_default_renderer'] = new MoodleQuickForm_Renderer();
|
||||
|
||||
/** Please keep this list in alphabetical order. */
|
||||
MoodleQuickForm::registerElementType('advcheckbox', "$CFG->libdir/form/advcheckbox.php", 'MoodleQuickForm_advcheckbox');
|
||||
MoodleQuickForm::registerElementType('autocomplete', "$CFG->libdir/form/autocomplete.php", 'MoodleQuickForm_autocomplete');
|
||||
MoodleQuickForm::registerElementType('button', "$CFG->libdir/form/button.php", 'MoodleQuickForm_button');
|
||||
MoodleQuickForm::registerElementType('cancel', "$CFG->libdir/form/cancel.php", 'MoodleQuickForm_cancel');
|
||||
MoodleQuickForm::registerElementType('searchableselector', "$CFG->libdir/form/searchableselector.php", 'MoodleQuickForm_searchableselector');
|
||||
|
@ -218,7 +218,7 @@ class HTML_QuickForm_RuleRegistry
|
||||
$value = " _qfGroups['{$elementName}'] = {";
|
||||
$elements =& $element->getElements();
|
||||
for ($i = 0, $count = count($elements); $i < $count; $i++) {
|
||||
$append = ($elements[$i]->getType() == 'select' && $elements[$i]->getMultiple())? '[]': '';
|
||||
$append = (($elements[$i]->getType() == 'select' || $element->getType() == 'autocomplete') && $elements[$i]->getMultiple())? '[]': '';
|
||||
$value .= "'" . $element->getElementName($i) . $append . "': true" .
|
||||
($i < $count - 1? ', ': '');
|
||||
}
|
||||
@ -281,7 +281,7 @@ class HTML_QuickForm_RuleRegistry
|
||||
" }\n";
|
||||
}
|
||||
|
||||
} elseif ($element->getType() == 'select') {
|
||||
} elseif ($element->getType() == 'select' || $element->getType() == 'autocomplete') {
|
||||
if ($element->getMultiple()) {
|
||||
$elementName .= '[]';
|
||||
$value =
|
||||
|
38
lib/templates/form_autocomplete_input.mustache
Normal file
38
lib/templates/form_autocomplete_input.mustache
Normal file
@ -0,0 +1,38 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/form_autocomplete_input
|
||||
|
||||
Moodle template for the input field in an autocomplate form element.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* inputId The dom id of this input field.
|
||||
* suggestionsId The dom id of the suggestions list.
|
||||
* selectionId The dom id of the current selection list.
|
||||
* downArrowId The dom id of the down arrow to open the suggestions.
|
||||
* placeholder The place holder text when the field is empty
|
||||
|
||||
Example context (json):
|
||||
{ "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
|
||||
}}
|
||||
<input type="text" id="{{inputId}}" list="{{suggestionsId}}" placeholder="{{placeholder}}" role="combobox" aria-expanded="false" autocomplete="off" autocorrect="off" autocapitalize="off" aria-autocomplete="list" aria-owns="{{suggestionsId}} {{selectionId}}"/><span class="form-autocomplete-downarrow" id="{{downArrowId}}">▼</span>
|
48
lib/templates/form_autocomplete_selection.mustache
Normal file
48
lib/templates/form_autocomplete_selection.mustache
Normal file
@ -0,0 +1,48 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/form_autocomplete_selection
|
||||
|
||||
Moodle template for the currently selected items in an autocomplate form element.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* multiple True if this field allows multiple selections
|
||||
* selectionId The dom id of the current selection list.
|
||||
* items List of items with label and value fields.
|
||||
|
||||
Example context (json):
|
||||
{ "multiple": true, "selectionId": 1, "items": [
|
||||
{ "label": "Item label with <strong>tags</strong>", "value": "5" },
|
||||
{ "label": "Another item label with <strong>tags</strong>", "value": "4" }
|
||||
]}
|
||||
}}
|
||||
<div class="form-autocomplete-selection {{#multiple}}form-autocomplete-multiple{{/multiple}}" id="{{selectionId}}" role="list" aria-atomic="true" tabindex="0" aria-multiselectable="true">
|
||||
<span class="accesshide">{{#str}}selecteditems, form{{/str}}</span>
|
||||
{{#items}}
|
||||
<span role="listitem" data-value="{{value}}" aria-selected="true" class="label label-info"><span aria-hidden="true">×</span> {{{label}}}</span>
|
||||
{{/items}}
|
||||
{{^items}}
|
||||
<span>{{#str}}noselection,form{{/str}}</span>
|
||||
{{/items}}
|
||||
</div>
|
||||
</div>
|
42
lib/templates/form_autocomplete_suggestions.mustache
Normal file
42
lib/templates/form_autocomplete_suggestions.mustache
Normal file
@ -0,0 +1,42 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/form_autocomplete_suggestions
|
||||
|
||||
Moodle template for the list of valid options in an autocomplate form element.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* suggestionsId The dom id of the current suggestions list.
|
||||
* options List of options with label and value fields.
|
||||
|
||||
Example context (json):
|
||||
{ "suggestionsId": 1, "options": [
|
||||
{ "label": "Item label with <strong>tags</strong>", "value": "5" },
|
||||
{ "label": "Another item label with <strong>tags</strong>", "value": "4" }
|
||||
]}
|
||||
}}
|
||||
<ul class="form-autocomplete-suggestions" id="{{suggestionsId}}" role="listbox" aria-hidden="true">
|
||||
{{#options}}
|
||||
<li role="option" data-value="{{value}}">{{{label}}}</li>
|
||||
{{/options}}
|
||||
</ul>
|
@ -50,7 +50,8 @@ $THEME->sheets = array(
|
||||
'user',
|
||||
'tabs',
|
||||
'filemanager',
|
||||
'templates'
|
||||
'templates',
|
||||
'autocomplete'
|
||||
);
|
||||
|
||||
$THEME->editor_sheets = array('editor');
|
||||
|
68
theme/base/style/autocomplete.css
Normal file
68
theme/base/style/autocomplete.css
Normal file
@ -0,0 +1,68 @@
|
||||
/* Custom styles for autocomplete form element */
|
||||
.form-autocomplete-selection {
|
||||
margin: 0.2em;
|
||||
min-height: 21px;
|
||||
}
|
||||
|
||||
.form-autocomplete-multiple [role=listitem].label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-autocomplete-selection [role=listitem].label {
|
||||
color: #333;
|
||||
background-color: #00E;
|
||||
border: 4px solid #00E;
|
||||
color: #FFF;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.form-autocomplete-suggestions {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 2px solid #EEE;
|
||||
border-radius: 3px;
|
||||
min-width: 206px;
|
||||
max-height: 20em;
|
||||
overflow: auto;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
margin-top: -0.2em;
|
||||
z-index: 1;
|
||||
}
|
||||
.form-autocomplete-suggestions li {
|
||||
list-style-type: none;
|
||||
padding: 0.2em;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
.form-autocomplete-suggestions li:hover {
|
||||
background-color: #00E;
|
||||
color: #FFF;
|
||||
}
|
||||
.form-autocomplete-suggestions li[aria-selected=true] {
|
||||
background-color: #555;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.form-autocomplete-downarrow {
|
||||
position: relative;
|
||||
top: -0.1em;
|
||||
left: -1.5em;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
}
|
||||
.dir-rtl .form-autocomplete-downarrow {
|
||||
right: -1.5em;
|
||||
left: inherit;
|
||||
}
|
||||
.form-autocomplete-selection:focus {
|
||||
outline: none;
|
||||
}
|
||||
.form-autocomplete-selection [data-active-selection=true] {
|
||||
padding: 0.5em;
|
||||
font-size: large;
|
||||
}
|
@ -657,3 +657,61 @@ textarea[cols],
|
||||
input[size] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Custom styles for autocomplete form element */
|
||||
.form-autocomplete-selection {
|
||||
margin: 0.2em;
|
||||
min-height: 21px;
|
||||
}
|
||||
.form-autocomplete-multiple [role=listitem] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-autocomplete-suggestions {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 2px solid @grayLighter;
|
||||
border-radius: 3px;
|
||||
min-width: 206px;
|
||||
max-height: 20em;
|
||||
overflow: auto;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
margin-top: -0.2em;
|
||||
z-index: 1;
|
||||
}
|
||||
.form-autocomplete-suggestions li {
|
||||
list-style-type: none;
|
||||
padding: 0.2em;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
color: @textColor;
|
||||
}
|
||||
.form-autocomplete-suggestions li:hover {
|
||||
background-color: lighten(@dropdownLinkBackgroundActive, 15%);
|
||||
color: @dropdownLinkColorActive;
|
||||
}
|
||||
.form-autocomplete-suggestions li[aria-selected=true] {
|
||||
background-color: darken(@navbarBackground, 5%);
|
||||
color: @gray;
|
||||
}
|
||||
|
||||
.form-autocomplete-downarrow {
|
||||
color: @textColor;
|
||||
position: relative;
|
||||
top: -0.3em;
|
||||
left: -1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dir-rtl .form-autocomplete-downarrow {
|
||||
right: -1.5em;
|
||||
left: inherit;
|
||||
}
|
||||
|
||||
.form-autocomplete-selection:focus {
|
||||
outline: none;
|
||||
}
|
||||
.form-autocomplete-selection [data-active-selection=true] {
|
||||
padding: 0.5em;
|
||||
font-size: large;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user