Current Path : C:/xampp/htdocs/moodle/admin/tool/lp/amd/src/ |
Current File : C:/xampp/htdocs/moodle/admin/tool/lp/amd/src/tree.js |
// 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/>. /** * Implement an accessible aria tree widget, from a nested unordered list. * Based on http://oaa-accessibility.org/example/41/ * * To respond to selection changed events - use tree.on("selectionchanged", handler). * The handler will receive an array of nodes, which are the list items that are currently * selected. (Or a single node if multiselect is disabled). * * @module tool_lp/tree * @package core * @copyright 2015 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/url', 'core/log'], function($, url, log) { // Private variables and functions. /** @var {String} expandedImage The html for an expanded tree node twistie. */ var expandedImage = $('<img alt="" src="' + url.imageUrl('t/expanded') + '"/>'); /** @var {String} collapsedImage The html for a collapsed tree node twistie. */ var collapsedImage = $('<img alt="" src="' + url.imageUrl('t/collapsed') + '"/>'); /** * Constructor * * @param {String} selector * @param {Boolean} multiSelect */ var Tree = function(selector, multiSelect) { this.treeRoot = $(selector); this.multiSelect = (typeof multiSelect === 'undefined' || multiSelect === true); this.items = this.treeRoot.find('li'); this.expandAll = this.items.length < 20; this.parents = this.treeRoot.find('li:has(ul)'); if (multiSelect) { this.treeRoot.attr('aria-multiselectable', 'true'); } this.items.attr('aria-selected', 'false'); this.visibleItems = null; this.activeItem = null; this.lastActiveItem = null; this.keys = { tab: 9, enter: 13, space: 32, pageup: 33, pagedown: 34, end: 35, home: 36, left: 37, up: 38, right: 39, down: 40, eight: 56, asterisk: 106 }; this.init(); this.bindEventHandlers(); }; // Public variables and functions. /** * Init this tree * @method init */ Tree.prototype.init = function() { this.parents.attr('aria-expanded', 'true'); this.parents.prepend(expandedImage.clone()); this.items.attr('role', 'tree-item'); this.items.attr('tabindex', '-1'); this.parents.attr('role', 'group'); this.treeRoot.attr('role', 'tree'); this.visibleItems = this.treeRoot.find('li'); var thisObj = this; if (!this.expandAll) { this.parents.each(function() { thisObj.collapseGroup($(this)); }); this.expandGroup(this.parents.first()); } }; /** * Expand a collapsed group. * * @method expandGroup * @param {Object} item is the jquery id of the parent item of the group */ Tree.prototype.expandGroup = function(item) { // Find the first child ul node. var group = item.children('ul'); // Expand the group. group.show().attr('aria-hidden', 'false'); item.attr('aria-expanded', 'true'); item.children('img').attr('src', expandedImage.attr('src')); // Update the list of visible items. this.visibleItems = this.treeRoot.find('li:visible'); }; /** * Collapse an expanded group. * * @method collapseGroup * @param {Object} item is the jquery id of the parent item of the group */ Tree.prototype.collapseGroup = function(item) { var group = item.children('ul'); // Collapse the group. group.hide().attr('aria-hidden', 'true'); item.attr('aria-expanded', 'false'); item.children('img').attr('src', collapsedImage.attr('src')); // Update the list of visible items. this.visibleItems = this.treeRoot.find('li:visible'); }; /** * Expand or collapse a group. * * @method toggleGroup * @param {Object} item is the jquery id of the parent item of the group */ Tree.prototype.toggleGroup = function(item) { if (item.attr('aria-expanded') == 'true') { this.collapseGroup(item); } else { this.expandGroup(item); } }; /** * Whenever the currently selected node has changed, trigger an event using this function. * * @method triggerChange */ Tree.prototype.triggerChange = function() { var allSelected = this.items.filter('[aria-selected=true]'); if (!this.multiSelect) { allSelected = allSelected.first(); } this.treeRoot.trigger('selectionchanged', {selected: allSelected}); }; /** * Select all the items between the last focused item and this currently focused item. * * @method multiSelectItem * @param {Object} item is the jquery id of the newly selected item. */ Tree.prototype.multiSelectItem = function(item) { if (!this.multiSelect) { this.items.attr('aria-selected', 'false'); } else if (this.lastActiveItem !== null) { var lastIndex = this.visibleItems.index(this.lastActiveItem); var currentIndex = this.visibleItems.index(this.activeItem); var oneItem = null; while (lastIndex < currentIndex) { oneItem = $(this.visibleItems.get(lastIndex)); oneItem.attr('aria-selected', 'true'); lastIndex++; } while (lastIndex > currentIndex) { oneItem = $(this.visibleItems.get(lastIndex)); oneItem.attr('aria-selected', 'true'); lastIndex--; } } item.attr('aria-selected', 'true'); this.triggerChange(); }; /** * Select a single item. Make sure all the parents are expanded. De-select all other items. * * @method selectItem * @param {Object} item is the jquery id of the newly selected item. */ Tree.prototype.selectItem = function(item) { // Expand all nodes up the tree. var walk = item.parent(); while (walk.attr('role') != 'tree') { walk = walk.parent(); if (walk.attr('aria-expanded') == 'false') { this.expandGroup(walk); } walk = walk.parent(); } this.items.attr('aria-selected', 'false'); item.attr('aria-selected', 'true'); this.triggerChange(); }; /** * Toggle the selected state for an item back and forth. * * @method toggleItem * @param {Object} item is the jquery id of the item to toggle. */ Tree.prototype.toggleItem = function(item) { if (!this.multiSelect) { this.selectItem(item); return; } var current = item.attr('aria-selected'); if (current === 'true') { current = 'false'; } else { current = 'true'; } item.attr('aria-selected', current); this.triggerChange(); }; /** * Set the focus to this item. * * @method updateFocus * @param {Object} item is the jquery id of the parent item of the group */ Tree.prototype.updateFocus = function(item) { this.lastActiveItem = this.activeItem; this.activeItem = item; // Expand all nodes up the tree. var walk = item.parent(); while (walk.attr('role') != 'tree') { walk = walk.parent(); if (walk.attr('aria-expanded') == 'false') { this.expandGroup(walk); } walk = walk.parent(); } this.items.attr('tabindex', '-1'); item.attr('tabindex', 0); }; /** * Handle a key down event - ie navigate the tree. * * @method handleKeyDown * @param {Object} item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ // This function should be simplified. In the meantime.. // eslint-disable-next-line complexity Tree.prototype.handleKeyDown = function(item, e) { var currentIndex = this.visibleItems.index(item); var newItem = null; var hasKeyModifier = e.shiftKey || e.ctrlKey || e.metaKey || e.altKey; var thisObj = this; switch (e.keyCode) { case this.keys.home: { // Jump to first item in tree. newItem = this.parents.first(); newItem.focus(); if (e.shiftKey) { this.multiSelectItem(newItem); } else if (!hasKeyModifier) { this.selectItem(newItem); } e.stopPropagation(); return false; } case this.keys.end: { // Jump to last visible item. newItem = this.visibleItems.last(); newItem.focus(); if (e.shiftKey) { this.multiSelectItem(newItem); } else if (!hasKeyModifier) { this.selectItem(newItem); } e.stopPropagation(); return false; } case this.keys.enter: case this.keys.space: { if (e.shiftKey) { this.multiSelectItem(item); } else if (e.metaKey || e.ctrlKey) { this.toggleItem(item); } else { this.selectItem(item); } e.stopPropagation(); return false; } case this.keys.left: { if (item.has('ul') && item.attr('aria-expanded') == 'true') { this.collapseGroup(item); } else { // Move up to the parent. var itemUL = item.parent(); var itemParent = itemUL.parent(); if (itemParent.is('li')) { itemParent.focus(); if (e.shiftKey) { this.multiSelectItem(itemParent); } else if (!hasKeyModifier) { this.selectItem(itemParent); } } } e.stopPropagation(); return false; } case this.keys.right: { if (item.has('ul') && item.attr('aria-expanded') == 'false') { this.expandGroup(item); } else { // Move to the first item in the child group. newItem = item.children('ul').children('li').first(); if (newItem.length > 0) { newItem.focus(); if (e.shiftKey) { this.multiSelectItem(newItem); } else if (!hasKeyModifier) { this.selectItem(newItem); } } } e.stopPropagation(); return false; } case this.keys.up: { if (currentIndex > 0) { var prev = this.visibleItems.eq(currentIndex - 1); prev.focus(); if (e.shiftKey) { this.multiSelectItem(prev); } else if (!hasKeyModifier) { this.selectItem(prev); } } e.stopPropagation(); return false; } case this.keys.down: { if (currentIndex < this.visibleItems.length - 1) { var next = this.visibleItems.eq(currentIndex + 1); next.focus(); if (e.shiftKey) { this.multiSelectItem(next); } else if (!hasKeyModifier) { this.selectItem(next); } } e.stopPropagation(); return false; } case this.keys.asterisk: { // Expand all groups. this.parents.each(function() { thisObj.expandGroup($(this)); }); e.stopPropagation(); return false; } case this.keys.eight: { if (e.shiftKey) { // Expand all groups. this.parents.each(function() { thisObj.expandGroup($(this)); }); e.stopPropagation(); } return false; } } return true; }; /** * Handle a key press event - ie navigate the tree. * * @method handleKeyPress * @param {Object} item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ Tree.prototype.handleKeyPress = function(item, e) { if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { // Do nothing. return true; } switch (e.keyCode) { case this.keys.tab: { return true; } case this.keys.enter: case this.keys.home: case this.keys.end: case this.keys.left: case this.keys.right: case this.keys.up: case this.keys.down: { e.stopPropagation(); return false; } default : { var chr = String.fromCharCode(e.which); var match = false; var itemIndex = this.visibleItems.index(item); var itemCount = this.visibleItems.length; var currentIndex = itemIndex + 1; // Check if the active item was the last one on the list. if (currentIndex == itemCount) { currentIndex = 0; } // Iterate through the menu items (starting from the current item and wrapping) until a match is found // or the loop returns to the current menu item. while (currentIndex != itemIndex) { var currentItem = this.visibleItems.eq(currentIndex); var titleChr = currentItem.text().charAt(0); if (currentItem.has('ul')) { titleChr = currentItem.find('span').text().charAt(0); } if (titleChr.toLowerCase() == chr) { match = true; break; } currentIndex = currentIndex + 1; if (currentIndex == itemCount) { // Reached the end of the list, start again at the beginning. currentIndex = 0; } } if (match === true) { this.updateFocus(this.visibleItems.eq(currentIndex)); } e.stopPropagation(); return false; } } // eslint-disable-next-line no-unreachable return true; }; /** * Attach an event listener to the tree. * * @method on * @param {String} eventname This is the name of the event to listen for. Only 'selectionchanged' is supported for now. * @param {Function} handler The function to call when the event is triggered. */ Tree.prototype.on = function(eventname, handler) { if (eventname !== 'selectionchanged') { log.warning('Invalid custom event name for tree. Only "selectionchanged" is supported.'); } else { this.treeRoot.on(eventname, handler); } }; /** * Handle a double click (expand/collapse). * * @method handleDblClick * @param {Object} item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ Tree.prototype.handleDblClick = function(item, e) { if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { // Do nothing. return true; } // Apply the focus markup. this.updateFocus(item); // Expand or collapse the group. this.toggleGroup(item); e.stopPropagation(); return false; }; /** * Handle a click (select). * * @method handleExpandCollapseClick * @param {Object} item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ Tree.prototype.handleExpandCollapseClick = function(item, e) { // Do not shift the focus. this.toggleGroup(item); e.stopPropagation(); return false; }; /** * Handle a click (select). * * @method handleClick * @param {Object} item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ Tree.prototype.handleClick = function(item, e) { if (e.shiftKey) { this.multiSelectItem(item); } else if (e.metaKey || e.ctrlKey) { this.toggleItem(item); } else { this.selectItem(item); } this.updateFocus(item); e.stopPropagation(); return false; }; /** * Handle a blur event * * @method handleBlur * @param {Object} item item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ Tree.prototype.handleBlur = function() { return true; }; /** * Handle a focus event * * @method handleFocus * @param {Object} item item is the jquery id of the parent item of the group * @param {Event} e The event. * @return {Boolean} */ Tree.prototype.handleFocus = function(item) { this.updateFocus(item); return true; }; /** * Bind the event listeners we require. * * @method bindEventHandlers */ Tree.prototype.bindEventHandlers = function() { var thisObj = this; // Bind a dblclick handler to the parent items. this.parents.dblclick(function(e) { return thisObj.handleDblClick($(this), e); }); // Bind a click handler. this.items.click(function(e) { return thisObj.handleClick($(this), e); }); // Bind a toggle handler to the expand/collapse icons. this.items.children('img').click(function(e) { return thisObj.handleExpandCollapseClick($(this).parent(), e); }); // Bind a keydown handler. this.items.keydown(function(e) { return thisObj.handleKeyDown($(this), e); }); // Bind a keypress handler. this.items.keypress(function(e) { return thisObj.handleKeyPress($(this), e); }); // Bind a focus handler. this.items.focus(function(e) { return thisObj.handleFocus($(this), e); }); // Bind a blur handler. this.items.blur(function(e) { return thisObj.handleBlur($(this), e); }); }; return /** @alias module:tool_lp/tree */ Tree; });