[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 // This file is part of Moodle - http://moodle.org/ 2 // 3 // Moodle is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // (at your option) any later version. 7 // 8 // Moodle is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU General Public License for more details. 12 // 13 // You should have received a copy of the GNU General Public License 14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 15 16 /** 17 * Implement an accessible aria tree widget, from a nested unordered list. 18 * Based on http://oaa-accessibility.org/example/41/ 19 * 20 * To respond to selection changed events - use tree.on("selectionchanged", handler). 21 * The handler will receive an array of nodes, which are the list items that are currently 22 * selected. (Or a single node if multiselect is disabled). 23 * 24 * @module tool_lp/tree 25 * @package core 26 * @copyright 2015 Damyon Wiese <damyon@moodle.com> 27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 */ 29 define(['jquery', 'core/url', 'core/log'], function($, url, log) { 30 // Private variables and functions. 31 /** @var {String} expandedImage The html for an expanded tree node twistie. */ 32 var expandedImage = $('<img alt="" src="' + url.imageUrl('t/expanded') + '"/>'); 33 /** @var {String} collapsedImage The html for a collapsed tree node twistie. */ 34 var collapsedImage = $('<img alt="" src="' + url.imageUrl('t/collapsed') + '"/>'); 35 36 /** 37 * Constructor 38 * 39 * @param {String} selector 40 * @param {Boolean} multiSelect 41 */ 42 var Tree = function(selector, multiSelect) { 43 this.treeRoot = $(selector); 44 this.multiSelect = (typeof multiSelect === 'undefined' || multiSelect === true); 45 46 this.items = this.treeRoot.find('li'); 47 this.expandAll = this.items.length < 20; 48 this.parents = this.treeRoot.find('li:has(ul)'); 49 50 if (multiSelect) { 51 this.treeRoot.attr('aria-multiselectable', 'true'); 52 } 53 54 this.items.attr('aria-selected', 'false'); 55 56 this.visibleItems = null; 57 this.activeItem = null; 58 this.lastActiveItem = null; 59 60 this.keys = { 61 tab: 9, 62 enter: 13, 63 space: 32, 64 pageup: 33, 65 pagedown: 34, 66 end: 35, 67 home: 36, 68 left: 37, 69 up: 38, 70 right: 39, 71 down: 40, 72 eight: 56, 73 asterisk: 106 74 }; 75 76 this.init(); 77 78 this.bindEventHandlers(); 79 }; 80 // Public variables and functions. 81 82 /** 83 * Init this tree 84 * @method init 85 */ 86 Tree.prototype.init = function() { 87 this.parents.attr('aria-expanded', 'true'); 88 this.parents.prepend(expandedImage.clone()); 89 90 this.items.attr('role', 'tree-item'); 91 this.items.attr('tabindex', '-1'); 92 this.parents.attr('role', 'group'); 93 this.treeRoot.attr('role', 'tree'); 94 95 this.visibleItems = this.treeRoot.find('li'); 96 97 var thisObj = this; 98 if (!this.expandAll) { 99 this.parents.each(function() { 100 thisObj.collapseGroup($(this)); 101 }); 102 this.expandGroup(this.parents.first()); 103 } 104 }; 105 106 /** 107 * Expand a collapsed group. 108 * 109 * @method expandGroup 110 * @param {Object} item is the jquery id of the parent item of the group 111 */ 112 Tree.prototype.expandGroup = function(item) { 113 // Find the first child ul node. 114 var group = item.children('ul'); 115 116 // Expand the group. 117 group.show().attr('aria-hidden', 'false'); 118 119 item.attr('aria-expanded', 'true'); 120 121 item.children('img').attr('src', expandedImage.attr('src')); 122 123 // Update the list of visible items. 124 this.visibleItems = this.treeRoot.find('li:visible'); 125 }; 126 127 /** 128 * Collapse an expanded group. 129 * 130 * @method collapseGroup 131 * @param {Object} item is the jquery id of the parent item of the group 132 */ 133 Tree.prototype.collapseGroup = function(item) { 134 var group = item.children('ul'); 135 136 // Collapse the group. 137 group.hide().attr('aria-hidden', 'true'); 138 139 item.attr('aria-expanded', 'false'); 140 141 item.children('img').attr('src', collapsedImage.attr('src')); 142 143 // Update the list of visible items. 144 this.visibleItems = this.treeRoot.find('li:visible'); 145 }; 146 147 /** 148 * Expand or collapse a group. 149 * 150 * @method toggleGroup 151 * @param {Object} item is the jquery id of the parent item of the group 152 */ 153 Tree.prototype.toggleGroup = function(item) { 154 if (item.attr('aria-expanded') == 'true') { 155 this.collapseGroup(item); 156 } else { 157 this.expandGroup(item); 158 } 159 }; 160 161 /** 162 * Whenever the currently selected node has changed, trigger an event using this function. 163 * 164 * @method triggerChange 165 */ 166 Tree.prototype.triggerChange = function() { 167 var allSelected = this.items.filter('[aria-selected=true]'); 168 if (!this.multiSelect) { 169 allSelected = allSelected.first(); 170 } 171 this.treeRoot.trigger('selectionchanged', {selected: allSelected}); 172 }; 173 174 /** 175 * Select all the items between the last focused item and this currently focused item. 176 * 177 * @method multiSelectItem 178 * @param {Object} item is the jquery id of the newly selected item. 179 */ 180 Tree.prototype.multiSelectItem = function(item) { 181 if (!this.multiSelect) { 182 this.items.attr('aria-selected', 'false'); 183 } else if (this.lastActiveItem !== null) { 184 var lastIndex = this.visibleItems.index(this.lastActiveItem); 185 var currentIndex = this.visibleItems.index(this.activeItem); 186 var oneItem = null; 187 188 while (lastIndex < currentIndex) { 189 oneItem = $(this.visibleItems.get(lastIndex)); 190 oneItem.attr('aria-selected', 'true'); 191 lastIndex++; 192 } 193 while (lastIndex > currentIndex) { 194 oneItem = $(this.visibleItems.get(lastIndex)); 195 oneItem.attr('aria-selected', 'true'); 196 lastIndex--; 197 } 198 } 199 200 item.attr('aria-selected', 'true'); 201 this.triggerChange(); 202 }; 203 204 /** 205 * Select a single item. Make sure all the parents are expanded. De-select all other items. 206 * 207 * @method selectItem 208 * @param {Object} item is the jquery id of the newly selected item. 209 */ 210 Tree.prototype.selectItem = function(item) { 211 // Expand all nodes up the tree. 212 var walk = item.parent(); 213 while (walk.attr('role') != 'tree') { 214 walk = walk.parent(); 215 if (walk.attr('aria-expanded') == 'false') { 216 this.expandGroup(walk); 217 } 218 walk = walk.parent(); 219 } 220 this.items.attr('aria-selected', 'false'); 221 item.attr('aria-selected', 'true'); 222 this.triggerChange(); 223 }; 224 225 /** 226 * Toggle the selected state for an item back and forth. 227 * 228 * @method toggleItem 229 * @param {Object} item is the jquery id of the item to toggle. 230 */ 231 Tree.prototype.toggleItem = function(item) { 232 if (!this.multiSelect) { 233 this.selectItem(item); 234 return; 235 } 236 237 var current = item.attr('aria-selected'); 238 if (current === 'true') { 239 current = 'false'; 240 } else { 241 current = 'true'; 242 } 243 item.attr('aria-selected', current); 244 this.triggerChange(); 245 }; 246 247 /** 248 * Set the focus to this item. 249 * 250 * @method updateFocus 251 * @param {Object} item is the jquery id of the parent item of the group 252 */ 253 Tree.prototype.updateFocus = function(item) { 254 this.lastActiveItem = this.activeItem; 255 this.activeItem = item; 256 // Expand all nodes up the tree. 257 var walk = item.parent(); 258 while (walk.attr('role') != 'tree') { 259 walk = walk.parent(); 260 if (walk.attr('aria-expanded') == 'false') { 261 this.expandGroup(walk); 262 } 263 walk = walk.parent(); 264 } 265 this.items.attr('tabindex', '-1'); 266 item.attr('tabindex', 0); 267 }; 268 269 /** 270 * Handle a key down event - ie navigate the tree. 271 * 272 * @method handleKeyDown 273 * @param {Object} item is the jquery id of the parent item of the group 274 * @param {Event} e The event. 275 * @return {Boolean} 276 */ 277 // This function should be simplified. In the meantime.. 278 // eslint-disable-next-line complexity 279 Tree.prototype.handleKeyDown = function(item, e) { 280 var currentIndex = this.visibleItems.index(item); 281 var newItem = null; 282 var hasKeyModifier = e.shiftKey || e.ctrlKey || e.metaKey || e.altKey; 283 var thisObj = this; 284 285 switch (e.keyCode) { 286 case this.keys.home: { 287 // Jump to first item in tree. 288 newItem = this.parents.first(); 289 newItem.focus(); 290 if (e.shiftKey) { 291 this.multiSelectItem(newItem); 292 } else if (!hasKeyModifier) { 293 this.selectItem(newItem); 294 } 295 296 e.stopPropagation(); 297 return false; 298 } 299 case this.keys.end: { 300 // Jump to last visible item. 301 newItem = this.visibleItems.last(); 302 newItem.focus(); 303 if (e.shiftKey) { 304 this.multiSelectItem(newItem); 305 } else if (!hasKeyModifier) { 306 this.selectItem(newItem); 307 } 308 309 e.stopPropagation(); 310 return false; 311 } 312 case this.keys.enter: 313 case this.keys.space: { 314 315 if (e.shiftKey) { 316 this.multiSelectItem(item); 317 } else if (e.metaKey || e.ctrlKey) { 318 this.toggleItem(item); 319 } else { 320 this.selectItem(item); 321 } 322 323 e.stopPropagation(); 324 return false; 325 } 326 case this.keys.left: { 327 if (item.has('ul') && item.attr('aria-expanded') == 'true') { 328 this.collapseGroup(item); 329 } else { 330 // Move up to the parent. 331 var itemUL = item.parent(); 332 var itemParent = itemUL.parent(); 333 if (itemParent.is('li')) { 334 itemParent.focus(); 335 if (e.shiftKey) { 336 this.multiSelectItem(itemParent); 337 } else if (!hasKeyModifier) { 338 this.selectItem(itemParent); 339 } 340 } 341 } 342 343 e.stopPropagation(); 344 return false; 345 } 346 case this.keys.right: { 347 if (item.has('ul') && item.attr('aria-expanded') == 'false') { 348 this.expandGroup(item); 349 } else { 350 // Move to the first item in the child group. 351 newItem = item.children('ul').children('li').first(); 352 if (newItem.length > 0) { 353 newItem.focus(); 354 if (e.shiftKey) { 355 this.multiSelectItem(newItem); 356 } else if (!hasKeyModifier) { 357 this.selectItem(newItem); 358 } 359 } 360 } 361 362 e.stopPropagation(); 363 return false; 364 } 365 case this.keys.up: { 366 367 if (currentIndex > 0) { 368 var prev = this.visibleItems.eq(currentIndex - 1); 369 prev.focus(); 370 if (e.shiftKey) { 371 this.multiSelectItem(prev); 372 } else if (!hasKeyModifier) { 373 this.selectItem(prev); 374 } 375 } 376 377 e.stopPropagation(); 378 return false; 379 } 380 case this.keys.down: { 381 382 if (currentIndex < this.visibleItems.length - 1) { 383 var next = this.visibleItems.eq(currentIndex + 1); 384 next.focus(); 385 if (e.shiftKey) { 386 this.multiSelectItem(next); 387 } else if (!hasKeyModifier) { 388 this.selectItem(next); 389 } 390 } 391 e.stopPropagation(); 392 return false; 393 } 394 case this.keys.asterisk: { 395 // Expand all groups. 396 this.parents.each(function() { 397 thisObj.expandGroup($(this)); 398 }); 399 400 e.stopPropagation(); 401 return false; 402 } 403 case this.keys.eight: { 404 if (e.shiftKey) { 405 // Expand all groups. 406 this.parents.each(function() { 407 thisObj.expandGroup($(this)); 408 }); 409 410 e.stopPropagation(); 411 } 412 413 return false; 414 } 415 } 416 417 return true; 418 }; 419 420 /** 421 * Handle a key press event - ie navigate the tree. 422 * 423 * @method handleKeyPress 424 * @param {Object} item is the jquery id of the parent item of the group 425 * @param {Event} e The event. 426 * @return {Boolean} 427 */ 428 Tree.prototype.handleKeyPress = function(item, e) { 429 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 430 // Do nothing. 431 return true; 432 } 433 434 switch (e.keyCode) { 435 case this.keys.tab: { 436 return true; 437 } 438 case this.keys.enter: 439 case this.keys.home: 440 case this.keys.end: 441 case this.keys.left: 442 case this.keys.right: 443 case this.keys.up: 444 case this.keys.down: { 445 e.stopPropagation(); 446 return false; 447 } 448 default : { 449 var chr = String.fromCharCode(e.which); 450 var match = false; 451 var itemIndex = this.visibleItems.index(item); 452 var itemCount = this.visibleItems.length; 453 var currentIndex = itemIndex + 1; 454 455 // Check if the active item was the last one on the list. 456 if (currentIndex == itemCount) { 457 currentIndex = 0; 458 } 459 460 // Iterate through the menu items (starting from the current item and wrapping) until a match is found 461 // or the loop returns to the current menu item. 462 while (currentIndex != itemIndex) { 463 464 var currentItem = this.visibleItems.eq(currentIndex); 465 var titleChr = currentItem.text().charAt(0); 466 467 if (currentItem.has('ul')) { 468 titleChr = currentItem.find('span').text().charAt(0); 469 } 470 471 if (titleChr.toLowerCase() == chr) { 472 match = true; 473 break; 474 } 475 476 currentIndex = currentIndex + 1; 477 if (currentIndex == itemCount) { 478 // Reached the end of the list, start again at the beginning. 479 currentIndex = 0; 480 } 481 } 482 483 if (match === true) { 484 this.updateFocus(this.visibleItems.eq(currentIndex)); 485 } 486 e.stopPropagation(); 487 return false; 488 } 489 } 490 491 // eslint-disable-next-line no-unreachable 492 return true; 493 }; 494 495 /** 496 * Attach an event listener to the tree. 497 * 498 * @method on 499 * @param {String} eventname This is the name of the event to listen for. Only 'selectionchanged' is supported for now. 500 * @param {Function} handler The function to call when the event is triggered. 501 */ 502 Tree.prototype.on = function(eventname, handler) { 503 if (eventname !== 'selectionchanged') { 504 log.warning('Invalid custom event name for tree. Only "selectionchanged" is supported.'); 505 } else { 506 this.treeRoot.on(eventname, handler); 507 } 508 }; 509 510 /** 511 * Handle a double click (expand/collapse). 512 * 513 * @method handleDblClick 514 * @param {Object} item is the jquery id of the parent item of the group 515 * @param {Event} e The event. 516 * @return {Boolean} 517 */ 518 Tree.prototype.handleDblClick = function(item, e) { 519 520 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 521 // Do nothing. 522 return true; 523 } 524 525 // Apply the focus markup. 526 this.updateFocus(item); 527 528 // Expand or collapse the group. 529 this.toggleGroup(item); 530 531 e.stopPropagation(); 532 return false; 533 }; 534 535 /** 536 * Handle a click (select). 537 * 538 * @method handleExpandCollapseClick 539 * @param {Object} item is the jquery id of the parent item of the group 540 * @param {Event} e The event. 541 * @return {Boolean} 542 */ 543 Tree.prototype.handleExpandCollapseClick = function(item, e) { 544 545 // Do not shift the focus. 546 this.toggleGroup(item); 547 e.stopPropagation(); 548 return false; 549 }; 550 551 552 /** 553 * Handle a click (select). 554 * 555 * @method handleClick 556 * @param {Object} item is the jquery id of the parent item of the group 557 * @param {Event} e The event. 558 * @return {Boolean} 559 */ 560 Tree.prototype.handleClick = function(item, e) { 561 562 if (e.shiftKey) { 563 this.multiSelectItem(item); 564 } else if (e.metaKey || e.ctrlKey) { 565 this.toggleItem(item); 566 } else { 567 this.selectItem(item); 568 } 569 this.updateFocus(item); 570 e.stopPropagation(); 571 return false; 572 }; 573 574 /** 575 * Handle a blur event 576 * 577 * @method handleBlur 578 * @param {Object} item item is the jquery id of the parent item of the group 579 * @param {Event} e The event. 580 * @return {Boolean} 581 */ 582 Tree.prototype.handleBlur = function() { 583 return true; 584 }; 585 586 /** 587 * Handle a focus event 588 * 589 * @method handleFocus 590 * @param {Object} item item is the jquery id of the parent item of the group 591 * @param {Event} e The event. 592 * @return {Boolean} 593 */ 594 Tree.prototype.handleFocus = function(item) { 595 596 this.updateFocus(item); 597 598 return true; 599 }; 600 601 /** 602 * Bind the event listeners we require. 603 * 604 * @method bindEventHandlers 605 */ 606 Tree.prototype.bindEventHandlers = function() { 607 var thisObj = this; 608 609 // Bind a dblclick handler to the parent items. 610 this.parents.dblclick(function(e) { 611 return thisObj.handleDblClick($(this), e); 612 }); 613 614 // Bind a click handler. 615 this.items.click(function(e) { 616 return thisObj.handleClick($(this), e); 617 }); 618 619 // Bind a toggle handler to the expand/collapse icons. 620 this.items.children('img').click(function(e) { 621 return thisObj.handleExpandCollapseClick($(this).parent(), e); 622 }); 623 624 // Bind a keydown handler. 625 this.items.keydown(function(e) { 626 return thisObj.handleKeyDown($(this), e); 627 }); 628 629 // Bind a keypress handler. 630 this.items.keypress(function(e) { 631 return thisObj.handleKeyPress($(this), e); 632 }); 633 634 // Bind a focus handler. 635 this.items.focus(function(e) { 636 return thisObj.handleFocus($(this), e); 637 }); 638 639 // Bind a blur handler. 640 this.items.blur(function(e) { 641 return thisObj.handleBlur($(this), e); 642 }); 643 644 }; 645 646 return /** @alias module:tool_lp/tree */ Tree; 647 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |