Merge branch 'wip-MDL-51486-master' of git://github.com/abgreeve/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 4 Nov 2015 03:35:38 +0000 (11:35 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 4 Nov 2015 03:35:38 +0000 (11:35 +0800)
grade/report/grader/lib.php
grade/report/grader/module.js
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/form/autocomplete.php
lib/form/tags.php
lib/templates/form_autocomplete_input.mustache

index 85d3a95..aba460f 100644 (file)
@@ -1109,6 +1109,14 @@ class grade_report_grader extends grade_report {
                     }
                 }
 
+                // Enable keyboard navigation if the grade is editable (not locked, not in a unoverridable category, etc).
+                if ($enableajax && $grade->is_editable()) {
+                    // If a grade item is type text, and we don't have show quick feedback on, it can't be edited.
+                    if ($item->gradetype != GRADE_TYPE_TEXT || $showquickfeedback) {
+                        $itemcell->attributes['class'] .= ' gbnavigable';
+                    }
+                }
+
                 if (!empty($this->gradeserror[$item->id][$userid])) {
                     $itemcell->text .= $this->gradeserror[$item->id][$userid];
                 }
index 2fac542..0199090 100644 (file)
@@ -251,6 +251,7 @@ M.gradereport_grader.classes.ajax.prototype.keypress_enter = function(e) {
  * @param {Bool} ignoreshift If true and shift is pressed then don't exec
  */
 M.gradereport_grader.classes.ajax.prototype.keypress_tab = function(e, ignoreshift) {
+    e.preventDefault();
     var next = null;
     if (e.shiftKey) {
         if (ignoreshift) {
@@ -328,8 +329,8 @@ M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) {
     if (!next) {
         return this.current.node;
     }
-    // Continue on until we find a clickable cell
-    if (!next.hasClass('clickable')) {
+    // Continue on until we find a navigable cell
+    if (!next.hasClass('gbnavigable')) {
         return this.get_next_cell(next);
     }
     return next;
@@ -352,8 +353,8 @@ M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) {
     if (!next) {
         return this.current.node;
     }
-    // Continue on until we find a clickable cell
-    if (!next.hasClass('clickable')) {
+    // Continue on until we find a navigable cell
+    if (!next.hasClass('gbnavigable')) {
         return this.get_prev_cell(next);
     }
     return next;
@@ -380,8 +381,8 @@ M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) {
     if (!next) {
         return this.current.node;
     }
-    // Continue on until we find a clickable cell
-    if (!next.hasClass('clickable')) {
+    // Continue on until we find a navigable cell
+    if (!next.hasClass('gbnavigable')) {
         return this.get_above_cell(next);
     }
     return next;
@@ -408,8 +409,8 @@ M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) {
     if (!next) {
         return this.current.node;
     }
-    // Continue on until we find a clickable cell
-    if (!next.hasClass('clickable')) {
+    // Continue on until we find a navigable cell
+    if (!next.hasClass('gbnavigable')) {
         return this.get_below_cell(next);
     }
     return next;
@@ -700,13 +701,13 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
         }
     } else if (this.grade) {
         // Handle Tab and Shift+Tab.
-        this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));
+        this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'down:9', this));
     }
     if (this.grade) {
         // Handle the Enter key being pressed.
-        this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));
+        this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'up:13', this));
         // Handle CTRL + arrow keys.
-        this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));
+        this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'down:37,38,39,40+ctrl', this));
     }
 };
 /**
@@ -759,6 +760,7 @@ M.gradereport_grader.classes.existingfield.prototype.keypress_tab = function(e,
  * @param {Event} e
  */
 M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(e) {
+    e.preventDefault();
     var next = null;
     switch (e.keyCode) {
         case 37:    // Left
@@ -785,6 +787,7 @@ M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(
 M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node) {
     if (node) {
         var properties = this.report.get_cell_info(node);
+        this.report.ajax.current = node;
         switch(properties.itemtype) {
             case 'scale':
                 properties.cell.one('select.select').focus();
@@ -1078,16 +1081,16 @@ M.gradereport_grader.classes.textfield.prototype.attach_key_events = function()
     if (this.editfeedback) {
         if (this.grade) {
             // Handle Shift+Tab.
-            this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));
+            this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'down:9+shift', a));
         }
         // Handle Tab.
-        this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));
+        this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'down:9', a, true));
         // Handle the Enter key being pressed.
-        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));
+        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'up:13', a));
     } else {
         if (this.grade) {
             // Handle Tab and Shift+Tab.
-            this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));
+            this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'down:9', a));
         }
     }
 
@@ -1097,7 +1100,7 @@ M.gradereport_grader.classes.textfield.prototype.attach_key_events = function()
 
     if (this.grade) {
         // Handle the Enter key being pressed.
-        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));
+        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'up:13', a));
         // Prevent the default key action on all fields for arrow keys on all key events!
         // Note: this still does not work in FF!!!!!
         this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
index c3b3a5f..b6ca068 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 05cdc6c..9f68d42 100644 (file)
@@ -46,11 +46,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      * @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.
+     * @param {Object} state State variables for this autocomplete element.
      */
-    var activateSelection = function(index, selectionId) {
+    var activateSelection = function(index, state) {
         // Find the elements in the DOM.
-        var selectionElement = $(document.getElementById(selectionId));
+        var selectionElement = $(document.getElementById(state.selectionId));
 
         // Count the visible items.
         var length = selectionElement.children('[aria-selected=true]').length;
@@ -62,7 +62,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         // 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;
+        var itemId = state.selectionId + '-' + index;
 
         // Deselect all the selections.
         selectionElement.children().attr('data-active-selection', false).attr('id', '');
@@ -77,17 +77,16 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @method deselectItem
      * @private
-     * @param {String} inputId The id of the input element for this instance of the autocomplete.
-     * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
+     * @param {Object} options Original options for this autocomplete element.
+     * @param {Object} state State variables for this autocomplete element.
      * @param {Element} The item to be deselected.
      * @param {Element} originalSelect The original select list.
-     * @param {Boolean} multiple Is this a multi select.
      */
-    var deselectItem = function(inputId, selectionId, item, originalSelect, multiple) {
+    var deselectItem = function(options, state, item, originalSelect) {
         var selectedItemValue = $(item).attr('data-value');
 
         // We can only deselect items if this is a multi-select field.
-        if (multiple) {
+        if (options.multiple) {
             // 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) {
@@ -100,7 +99,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             });
         }
         // Rerender the selection list.
-        updateSelectionList(selectionId, inputId, originalSelect, multiple);
+        updateSelectionList(options, state, originalSelect);
     };
 
     /**
@@ -109,13 +108,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      * @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.
+     * @param {Object} state State variables for this instance of autocomplete.
      */
-    var activateItem = function(index, inputId, suggestionsId) {
+    var activateItem = function(index, state) {
         // Find the elements in the DOM.
-        var inputElement = $(document.getElementById(inputId));
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var inputElement = $(document.getElementById(state.inputId));
+        var suggestionsElement = $(document.getElementById(state.suggestionsId));
 
         // Count the visible items.
         var length = suggestionsElement.children('[aria-hidden=false]').length;
@@ -129,7 +127,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         // 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;
+        var itemId = state.suggestionsId + '-' + globalIndex;
 
         // Deselect all the suggestions.
         suggestionsElement.children().attr('aria-selected', false).attr('id', '');
@@ -153,18 +151,17 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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.
+     * @param {Object} state State variable for this auto complete element.
      */
-    var activateNextItem = function(inputId, suggestionsId) {
+    var activateNextItem = function(state) {
         // Find the list of suggestions.
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var suggestionsElement = $(document.getElementById(state.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);
+        activateItem(current+1, state);
     };
 
     /**
@@ -172,42 +169,42 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @method activatePreviousSelection
      * @private
-     * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
+     * @param {Object} state State variables for this instance of autocomplete.
      */
-    var activatePreviousSelection = function(selectionId) {
+    var activatePreviousSelection = function(state) {
         // Find the list of selections.
-        var selectionsElement = $(document.getElementById(selectionId));
+        var selectionsElement = $(document.getElementById(state.selectionId));
         // Find the active one.
         var element = selectionsElement.children('[data-active-selection=true]');
         if (!element) {
-            activateSelection(0, selectionId);
+            activateSelection(0, state);
             return;
         }
         // Find it's index.
         var current = selectionsElement.children('[aria-selected=true]').index(element);
         // Activate the next one.
-        activateSelection(current-1, selectionId);
+        activateSelection(current-1, state);
     };
     /**
      * 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.
+     * @param {Object} state State variables for this instance of autocomplete.
      */
-    var activateNextSelection = function(selectionId) {
+    var activateNextSelection = function(state) {
         // Find the list of selections.
-        var selectionsElement = $(document.getElementById(selectionId));
+        var selectionsElement = $(document.getElementById(state.selectionId));
         // Find the active one.
         var element = selectionsElement.children('[data-active-selection=true]');
         if (!element) {
-            activateSelection(0, selectionId);
+            activateSelection(0, state);
             return;
         }
         // Find it's index.
         var current = selectionsElement.children('[aria-selected=true]').index(element);
         // Activate the next one.
-        activateSelection(current+1, selectionId);
+        activateSelection(current+1, state);
     };
 
     /**
@@ -215,18 +212,17 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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.
+     * @param {Object} state State variables for this autocomplete element.
      */
-    var activatePreviousItem = function(inputId, suggestionsId) {
+    var activatePreviousItem = function(state) {
         // Find the list of suggestions.
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var suggestionsElement = $(document.getElementById(state.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);
+        activateItem(current-1, state);
     };
 
     /**
@@ -234,16 +230,15 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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.
+     * @param {Object} state State variables for this autocomplete element.
      */
-    var closeSuggestions = function(inputId, suggestionsId, selectionId) {
+    var closeSuggestions = function(state) {
         // Find the elements in the DOM.
-        var inputElement = $(document.getElementById(inputId));
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var inputElement = $(document.getElementById(state.inputId));
+        var suggestionsElement = $(document.getElementById(state.suggestionsId));
 
         // Announce the list of suggestions was closed, and read the current list of selections.
-        inputElement.attr('aria-expanded', false).attr('aria-activedescendant', selectionId);
+        inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId);
         // Hide the suggestions list (from screen readers too).
         suggestionsElement.hide().attr('aria-hidden', true);
     };
@@ -253,46 +248,44 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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 {Object} options The original options for this autocomplete.
+     * @param {Object} state The state variables for this autocomplete.
+     * @param {String} query The current text for the search string.
      * @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 {Boolean} caseSensitive - If search has to be made case sensitive.
      */
-    var updateSuggestions = function(query, inputId, suggestionsId, originalSelect, multiple, tags, caseSensitive) {
+    var updateSuggestions = function(options, state, query, originalSelect) {
         // Find the elements in the DOM.
-        var inputElement = $(document.getElementById(inputId));
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var inputElement = $(document.getElementById(state.inputId));
+        var suggestionsElement = $(document.getElementById(state.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 = [];
+        var suggestions = [];
         originalSelect.children('option').each(function(index, option) {
             if ($(option).prop('selected') !== true) {
-                options[options.length] = { label: option.innerHTML, value: $(option).attr('value') };
+                suggestions[suggestions.length] = { label: option.innerHTML, value: $(option).attr('value') };
             }
         });
 
         // Re-render the list of suggestions.
-        var searchquery = caseSensitive ? query : query.toLocaleLowerCase();
+        var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
+        var context = $.extend({ options: suggestions}, options, state);
         templates.render(
             'core/form_autocomplete_suggestions',
-            { inputId: inputId, suggestionsId: suggestionsId, options: options, multiple: multiple}
+            context
         ).done(function(newHTML) {
             // We have the new template, insert it in the page.
             suggestionsElement.replaceWith(newHTML);
             // Get the element again.
-            suggestionsElement = $(document.getElementById(suggestionsId));
+            suggestionsElement = $(document.getElementById(state.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 ((caseSensitive && node.text().indexOf(searchquery) > -1) ||
-                        (!caseSensitive && node.text().toLocaleLowerCase().indexOf(searchquery) > -1)) {
+                if ((options.caseSensitive && node.text().indexOf(searchquery) > -1) ||
+                        (!options.caseSensitive && node.text().toLocaleLowerCase().indexOf(searchquery) > -1)) {
                     node.show().attr('aria-hidden', false);
                     matchingElements = true;
                 } else {
@@ -305,8 +298,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 // 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);
+                if (!options.tags) {
+                    activateItem(0, state);
                 }
             } else {
                 // Nothing matches. Tell them that.
@@ -323,14 +316,13 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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 {Object} options The original options for the autocomplete.
+     * @param {Object} state State variables for the autocomplete.
      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
      */
-    var createItem = function(inputId, suggestionsId, selectionId, multiple, originalSelect) {
+    var createItem = function(options, state, originalSelect) {
         // Find the element in the DOM.
-        var inputElement = $(document.getElementById(inputId));
+        var inputElement = $(document.getElementById(state.inputId));
         // Get the current text in the input field.
         var query = inputElement.val();
         var tags = query.split(',');
@@ -340,7 +332,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             // If we can only select one at a time, deselect any current value.
             tag = tag.trim();
             if (tag !== '') {
-                if (!multiple) {
+                if (!options.multiple) {
                     originalSelect.children('option').prop('selected', false);
                 }
                 // Look for an existing option in the select list that matches this new tag.
@@ -363,12 +355,12 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             }
         });
 
-        updateSelectionList(selectionId, inputId, originalSelect, multiple);
+        updateSelectionList(options, state, originalSelect);
 
         // Clear the input field.
         inputElement.val('');
         // Close the suggestions list.
-        closeSuggestions(inputId, suggestionsId, selectionId);
+        closeSuggestions(state);
     };
 
     /**
@@ -376,15 +368,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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 {Object} options Original options for this autocomplete element.
+     * @param {Object} state State variables for this autocomplete element.
      * @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) {
+    var updateSelectionList = function(options, state, originalSelect) {
         // Build up a valid context to re-render the template.
         var items = [];
-        var newSelection = $(document.getElementById(selectionId));
+        var newSelection = $(document.getElementById(state.selectionId));
         var activeId = newSelection.attr('aria-activedescendant');
         var activeValue = false;
 
@@ -396,11 +387,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
             }
         });
-        var context = {
-            selectionId: selectionId,
-            items: items,
-            multiple: multiple
-        };
+        var context = $.extend({ items: items }, options, state);
+
         // Render the template.
         templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
             // Add it to the page.
@@ -410,7 +398,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 // Reselect any previously selected item.
                 newSelection.children('[aria-selected=true]').each(function(index, ele) {
                     if ($(ele).attr('data-value') === activeValue) {
-                        activateSelection(index, selectionId);
+                        activateSelection(index, state);
                     }
                 });
             }
@@ -425,16 +413,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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 {Object} options The original options for the autocomplete.
+     * @param {Object} state State variables for the autocomplete.
      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
      */
-    var selectCurrentItem = function(inputId, suggestionsId, selectionId, multiple, originalSelect) {
+    var selectCurrentItem = function(options, state, originalSelect) {
         // Find the elements in the page.
-        var inputElement = $(document.getElementById(inputId));
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var inputElement = $(document.getElementById(state.inputId));
+        var suggestionsElement = $(document.getElementById(state.suggestionsId));
         // Here loop through suggestions and set val to join of all selected items.
 
         var selectedItemValue = suggestionsElement.children('[aria-selected=true]').attr('data-value');
@@ -442,7 +428,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         // 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) {
+        if (!options.multiple) {
             originalSelect.children('option').prop('selected', false);
         }
         // Look for a match, and toggle the selected property if there is a match.
@@ -452,11 +438,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             }
         });
         // Rerender the selection list.
-        updateSelectionList(selectionId, inputId, originalSelect, multiple);
+        updateSelectionList(options, state, originalSelect);
         // Clear the input element.
         inputElement.val('');
         // Close the list of suggestions.
-        closeSuggestions(inputId, suggestionsId, selectionId);
+        closeSuggestions(state);
     };
 
     /**
@@ -465,22 +451,18 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      * @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 {Object} options The original options for the autocomplete.
+     * @param {Object} state The state variables for 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.
-     * @param {Boolean} caseSensitive - If search has to be made case sensitive.
      */
-    var updateAjax = function(e, selector, inputId, suggestionsId, originalSelect, multiple, tags, ajaxHandler, caseSensitive) {
+    var updateAjax = function(e, options, state, originalSelect, 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) {
+        ajaxHandler.transport(options.selector, query, function(results) {
             // We got a result - pass it through the translator before using it.
-            var processedResults = ajaxHandler.processResults(selector, results);
+            var processedResults = ajaxHandler.processResults(options.selector, results);
             var existingValues = [];
 
             // Now destroy all options that are not currently selected.
@@ -502,7 +484,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 }
             });
             // Update the list of suggestions now from the new values in the select list.
-            updateSuggestions('', inputId, suggestionsId, originalSelect, multiple, tags, caseSensitive);
+            updateSuggestions(options, state, '', originalSelect);
         }, notification.exception);
     };
 
@@ -511,88 +493,60 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      *
      * @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 {Object} options The options used to create this autocomplete element.
+     * @param {Object} state State variables for this autocomplete element.
      * @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 {String} selector The selector for this select list.
-     * @param {String} ajax Name of an AMD module to handle ajax requests. If specified, the AMD
-     *                      module must expose 2 functions "transport" and "processResults".
-     * @param {Boolean} caseSensitive - If search has to be made case sensitive.
      */
-    var addNavigation = function(inputId,
-                                 suggestionsId,
-                                 downArrowId,
-                                 selectionId,
-                                 originalSelect,
-                                 multiple,
-                                 tags,
-                                 selector,
-                                 ajax,
-                                 caseSensitive) {
+    var addNavigation = function(options, state, originalSelect) {
         // Start with the input element.
-        var inputElement = $(document.getElementById(inputId));
+        var inputElement = $(document.getElementById(state.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);
+                    if (!options.showSuggestions) {
+                        // Do not consume this event.
+                        return true;
+                    } else if (inputElement.attr('aria-expanded') === "true") {
+                        activateNextItem(state);
                     } else {
                         // Handle ajax population of suggestions.
-                        if (!inputElement.val() && ajax) {
-                            require([ajax], function(ajaxHandler) {
-                                updateAjax(e,
-                                    selector,
-                                    inputId,
-                                    suggestionsId,
-                                    originalSelect,
-                                    multiple,
-                                    tags,
-                                    ajaxHandler,
-                                    caseSensitive);
+                        if (!inputElement.val() && options.ajax) {
+                            require([options.ajax], function(ajaxHandler) {
+                                updateAjax(e, options, state, originalSelect, ajaxHandler);
                             });
                         } else {
                             // Else - open the suggestions list.
-                            updateSuggestions(inputElement.val(),
-                                inputId,
-                                suggestionsId,
-                                originalSelect,
-                                multiple,
-                                tags,
-                                caseSensitive);
+                            updateSuggestions(options, state, inputElement.val(), originalSelect);
                         }
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
                 case KEYS.COMMA:
-                    if (tags) {
+                    if (options.tags) {
                         // If we are allowing tags, comma should create a tag (or enter).
-                        createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
+                        createItem(options, state, originalSelect);
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
                 case KEYS.UP:
                     // Choose the previous active item.
-                    activatePreviousItem(inputId, suggestionsId);
+                    activatePreviousItem(state);
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
                 case KEYS.ENTER:
-                    var suggestionsElement = $(document.getElementById(suggestionsId));
+                    var suggestionsElement = $(document.getElementById(state.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) {
+                        selectCurrentItem(options, state, originalSelect);
+                    } else if (options.tags) {
                         // If tags are enabled, create a tag.
-                        createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
+                        createItem(options, state, originalSelect);
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
@@ -600,7 +554,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 case KEYS.ESCAPE:
                     if (inputElement.attr('aria-expanded') === "true") {
                         // If the suggestion list is open, close it.
-                        closeSuggestions(inputId, suggestionsId, selectionId);
+                        closeSuggestions(state);
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
@@ -610,8 +564,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         });
         // Handler used to force set the value from behat.
         inputElement.on('behat:set-value', function() {
-            if (tags) {
-                createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
+            if (options.tags) {
+                createItem(options, state, originalSelect);
             }
         });
         inputElement.on('blur focus', function(e) {
@@ -623,66 +577,68 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             }
             closeSuggestionsTimer = window.setTimeout(function() {
                 if (e.type == 'blur') {
-                    if (tags) {
-                        createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
+                    if (options.tags) {
+                        createItem(options, state, originalSelect);
                     }
-                    closeSuggestions(inputId, suggestionsId, selectionId);
+                    closeSuggestions(state);
                 }
             }, 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, caseSensitive);
-        });
+        if (options.showSuggestions) {
+            var arrowElement = $(document.getElementById(state.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(options, state, inputElement.val(), originalSelect);
+            });
+        }
 
-        var suggestionsElement = $(document.getElementById(suggestionsId));
+        var suggestionsElement = $(document.getElementById(state.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));
+            var suggestionsElement = $(document.getElementById(state.suggestionsId));
             // Find the index of the clicked on suggestion.
             var current = suggestionsElement.children('[aria-hidden=false]').index(element);
             // Activate it.
-            activateItem(current, inputId, suggestionsId);
+            activateItem(current, state);
             // And select it.
-            selectCurrentItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
+            selectCurrentItem(options, state, originalSelect);
         });
-        var selectionElement = $(document.getElementById(selectionId));
+        var selectionElement = $(document.getElementById(state.selectionId));
         // Handle clicks on the selected items (will unselect an item).
         selectionElement.on('click', '[role=listitem]', function(e) {
             // Get the item that was clicked.
             var item = $(e.currentTarget);
             // Remove it from the selection.
-            deselectItem(inputId, selectionId, item, originalSelect, multiple);
+            deselectItem(options, state, item, originalSelect);
         });
         // Keyboard navigation for the selection list.
         selectionElement.on('keydown', function(e) {
             switch (e.keyCode) {
                 case KEYS.DOWN:
                     // Choose the next selection item.
-                    activateNextSelection(selectionId);
+                    activateNextSelection(state);
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
                 case KEYS.UP:
                     // Choose the previous selection item.
-                    activatePreviousSelection(selectionId);
+                    activatePreviousSelection(state);
                     // We handled this event, so prevent it.
                     e.preventDefault();
                     return false;
                 case KEYS.SPACE:
                 case KEYS.ENTER:
                     // Get the item that is currently selected.
-                    var selectedItem = $(document.getElementById(selectionId)).children('[data-active-selection=true]');
+                    var selectedItem = $(document.getElementById(state.selectionId)).children('[data-active-selection=true]');
                     if (selectedItem) {
                         // Unselect this item.
-                        deselectItem(inputId, selectionId, selectedItem, originalSelect, multiple);
+                        deselectItem(options, state, selectedItem, originalSelect);
                         // We handled this event, so prevent it.
                         e.preventDefault();
                     }
@@ -691,20 +647,22 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             return true;
         });
         // Whenever the input field changes, update the suggestion list.
-        inputElement.on('input', function(e) {
-            var query = $(e.currentTarget).val();
-            var last = $(e.currentTarget).data('last-value');
-            if (typeof last === 'undefined') {
-                last = query;
-            }
-            // IE11 fires many more input events than required - even when the value has not changed.
-            // We need to only do this for real value changed events or the suggestions will be
-            // unclickable on IE11 (because they will be rebuilt before the click event fires).
-            if (last != query) {
-                updateSuggestions(query, inputId, suggestionsId, originalSelect, multiple, tags, caseSensitive);
-                $(e.currentTarget).data('last-value', query);
-            }
-        });
+        if (options.showSuggestions) {
+            inputElement.on('input', function(e) {
+                var query = $(e.currentTarget).val();
+                var last = $(e.currentTarget).data('last-value');
+                if (typeof last === 'undefined') {
+                    last = query;
+                }
+                // IE11 fires many more input events than required - even when the value has not changed.
+                // We need to only do this for real value changed events or the suggestions will be
+                // unclickable on IE11 (because they will be rebuilt before the click event fires).
+                if (last != query) {
+                    updateSuggestions(options, state, query, originalSelect);
+                    $(e.currentTarget).data('last-value', query);
+                }
+            });
+        }
     };
 
     return /** @alias module:core/form-autocomplete */ {
@@ -721,16 +679,27 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
          * @param {String} placeholder - The text to display before a selection is made.
          * @param {Boolean} caseSensitive - If search has to be made case sensitive.
          */
-        enhance: function(selector, tags, ajax, placeholder, caseSensitive) {
+        enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions) {
             // Set some default values.
-            if (typeof tags === "undefined") {
-                tags = false;
+            var options = {
+                selector: selector,
+                tags: false,
+                ajax: false,
+                placeholder: placeholder,
+                caseSensitive: false,
+                showSuggestions: true
+            };
+            if (typeof tags !== "undefined") {
+                options.tags = tags;
+            }
+            if (typeof ajax !== "undefined") {
+                options.ajax = ajax;
             }
-            if (typeof ajax === "undefined") {
-                ajax = false;
+            if (typeof caseSensitive !== "undefined") {
+                options.caseSensitive = caseSensitive;
             }
-            if (typeof caseSensitive === "undefined") {
-                caseSensitive = false;
+            if (typeof showSuggestions !== "undefined") {
+                options.showSuggestions = showSuggestions;
             }
 
             // Look for the select element.
@@ -744,38 +713,30 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             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 + ']');
+            var state = {
+                selectId: originalSelect.attr('id'),
+                inputId: 'form_autocomplete_input-' + $.now(),
+                suggestionsId: 'form_autocomplete_suggestions-' + $.now(),
+                selectionId: 'form_autocomplete_selection-' + $.now(),
+                downArrowId: 'form_autocomplete_downarrow-' + $.now()
+            };
+            options.multiple = originalSelect.attr('multiple');
+
+            var originalLabel = $('[for=' + state.selectId + ']');
             // Create the new markup and insert it after the select.
-            var options = [];
+            var suggestions = [];
             originalSelect.children('option').each(function(index, option) {
-                options[index] = { label: option.innerHTML, value: $(option).attr('value') };
+                suggestions[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}
-            );
+            var context = $.extend({}, options, state);
+            context.options = suggestions;
+            context.items = [];
+
+            var renderInput = templates.render('core/form_autocomplete_input', context);
+            var renderDatalist = templates.render('core/form_autocomplete_suggestions', context);
+            var renderSelection = templates.render('core/form_autocomplete_selection', context);
 
             $.when(renderInput, renderDatalist, renderSelection).done(function(input, suggestions, selection) {
                 // Add our new UI elements to the page.
@@ -783,46 +744,29 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 originalSelect.after(input);
                 originalSelect.after(selection);
                 // Update the form label to point to the text input.
-                originalLabel.attr('for', inputId);
+                originalLabel.attr('for', state.inputId);
                 // Add the event handlers.
-                addNavigation(inputId,
-                    suggestionsId,
-                    downArrowId,
-                    selectionId,
-                    originalSelect,
-                    multiple,
-                    tags,
-                    selector,
-                    ajax,
-                    caseSensitive);
-
-                var inputElement = $(document.getElementById(inputId));
-                var suggestionsElement = $(document.getElementById(suggestionsId));
+                addNavigation(options, state, originalSelect);
+
+                var inputElement = $(document.getElementById(state.inputId));
+                var suggestionsElement = $(document.getElementById(state.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) {
+                if (options.ajax) {
+                    require([options.ajax], function(ajaxHandler) {
                         var handler = function(e) {
-                            updateAjax(e,
-                                selector,
-                                inputId,
-                                suggestionsId,
-                                originalSelect,
-                                multiple,
-                                tags,
-                                ajaxHandler,
-                                caseSensitive);
+                            updateAjax(e, options, state, originalSelect, ajaxHandler);
                         };
                         // Trigger an ajax update after the text field value changes.
                         inputElement.on("input keypress", handler);
-                        var arrowElement = $(document.getElementById(downArrowId));
+                        var arrowElement = $(document.getElementById(state.downArrowId));
                         arrowElement.on("click", handler);
                     });
                 }
                 // Show the current values in the selection list.
-                updateSelectionList(selectionId, inputId, originalSelect, multiple);
+                updateSelectionList(options, state, originalSelect);
             });
         }
     };
index 22e0739..206bc67 100644 (file)
@@ -48,6 +48,8 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
     protected $placeholder = '';
     /** @var bool $casesensitive Whether the search has to be case-sensitive. */
     protected $casesensitive = false;
+    /** @var bool $showsuggestions Show suggestions by default - but this can be turned off. */
+    protected $showsuggestions = true;
 
     /**
      * constructor
@@ -68,6 +70,10 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
             $this->tags = $attributes['tags'];
             unset($attributes['tags']);
         }
+        if (isset($attributes['showsuggestions'])) {
+            $this->showsuggestions = $attributes['showsuggestions'];
+            unset($attributes['showsuggestions']);
+        }
         $this->placeholder = get_string('search');
         if (isset($attributes['placeholder'])) {
             $this->placeholder = $attributes['placeholder'];
@@ -98,7 +104,7 @@ class MoodleQuickForm_autocomplete extends MoodleQuickForm_select {
         $this->_generateId();
         $id = $this->getAttribute('id');
         $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#' . $id, $this->tags, $this->ajax,
-            $this->placeholder, $this->casesensitive));
+            $this->placeholder, $this->casesensitive, $this->showsuggestions));
 
         return parent::toHTML();
     }
index 545f1f8..961a262 100644 (file)
@@ -90,6 +90,7 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
         }
         $attributes['multiple'] = 'multiple';
         $attributes['placeholder'] = get_string('entertags', 'tag');
+        $attributes['showsuggestions'] = $this->showingofficial;
 
         parent::MoodleQuickForm_autocomplete($elementName, $elementLabel, $validoptions, $attributes);
     }
index 077ed15..ad064f2 100644 (file)
@@ -35,4 +35,9 @@
     Example context (json):
     { "inputID": 1, "suggestionsId": 2, "selectionId": 3, "downArrowId": 4, "placeholder": "Select something" }
 }}
+{{#showSuggestions}}
 <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}}">&#x25BC;</span>
+{{/showSuggestions}}
+{{^showSuggestions}}
+<input type="text" id="{{inputId}}" placeholder="{{placeholder}}" role="textbox" aria-owns="{{selectionId}}"/>
+{{/showSuggestions}}