[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 /** 2 * Provides drop down menus for list of action links. 3 * 4 * @module moodle-core-actionmenu 5 */ 6 7 var BODY = Y.one(Y.config.doc.body), 8 CSS = { 9 MENUSHOWN : 'action-menu-shown' 10 }, 11 SELECTOR = { 12 CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', 13 MENU : '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]', 14 MENUBAR: '[role="menubar"]', 15 MENUITEM: '[role="menuitem"]', 16 MENUCONTENT : '.menu[data-rel=menu-content]', 17 MENUCONTENTCHILD: 'li a', 18 MENUCHILD: '.menu li a', 19 TOGGLE : '.toggle-display', 20 KEEPOPEN: '[data-keepopen="1"]', 21 MENUBARITEMS: [ 22 '[role="menubar"] > [role="menuitem"]', 23 '[role="menubar"] > [role="presentation"] > [role="menuitem"]' 24 ], 25 MENUITEMS: [ 26 '> [role="menuitem"]', 27 '> [role="presentation"] > [role="menuitem"]' 28 ] 29 }, 30 ACTIONMENU, 31 ALIGN = { 32 TL : 'tl', 33 TR : 'tr', 34 BL : 'bl', 35 BR : 'br' 36 }; 37 38 /** 39 * Action menu support. 40 * This converts a generic list of links into a drop down menu opened by hovering or clicking 41 * on a menu icon. 42 * 43 * @namespace M.core.actionmenu 44 * @class ActionMenu 45 * @constructor 46 * @extends Base 47 */ 48 ACTIONMENU = function() { 49 ACTIONMENU.superclass.constructor.apply(this, arguments); 50 }; 51 ACTIONMENU.prototype = { 52 53 /** 54 * The dialogue used for all action menu displays. 55 * @property type 56 * @type M.core.dialogue 57 * @protected 58 */ 59 dialogue : null, 60 61 /** 62 * An array of events attached during the display of the dialogue. 63 * @property events 64 * @type Object 65 * @protected 66 */ 67 events : [], 68 69 /** 70 * The node that owns the currently displayed menu. 71 * 72 * @property owner 73 * @type Node 74 * @default null 75 */ 76 owner : null, 77 78 /** 79 * The menu button that toggles this open. 80 * 81 * @property menulink 82 * @type Node 83 * @protected 84 */ 85 menulink: null, 86 87 /** 88 * The set of menu nodes. 89 * 90 * @property menuChildren 91 * @type NodeList 92 * @protected 93 */ 94 menuChildren: null, 95 96 /** 97 * The first menu item. 98 * 99 * @property firstMenuChild 100 * @type Node 101 * @protected 102 */ 103 firstMenuChild: null, 104 105 /** 106 * The last menu item. 107 * 108 * @property lastMenuChild 109 * @type Node 110 * @protected 111 */ 112 lastMenuChild: null, 113 114 /** 115 * Called during the initialisation process of the object. 116 * 117 * @method initializer 118 */ 119 initializer : function() { 120 Y.log('Initialising the action menu manager', 'debug', ACTIONMENU.NAME); 121 Y.all(SELECTOR.MENU).each(this.enhance, this); 122 BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this); 123 124 BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); 125 BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); 126 127 // Ensure that we toggle on menuitems when the spacebar is pressed. 128 BODY.delegate('key', function(e) { 129 e.currentTarget.simulate('click'); 130 e.preventDefault(); 131 }, 'down:32', SELECTOR.MENUBARITEMS.join(',')); 132 }, 133 134 /** 135 * Enhances a menu adding aria attributes and flagging it as functional. 136 * 137 * @method enhance 138 * @param {Node} menu 139 * @return boolean 140 */ 141 enhance : function(menu) { 142 var menucontent = menu.one(SELECTOR.MENUCONTENT), 143 align; 144 if (!menucontent) { 145 return false; 146 } 147 align = menucontent.getData('align') || this.get('align').join('-'); 148 menu.one(SELECTOR.TOGGLE).set('aria-haspopup', true); 149 menucontent.set('aria-hidden', true); 150 if (!menucontent.hasClass('align-'+align)) { 151 menucontent.addClass('align-'+align); 152 } 153 if (menucontent.hasChildNodes()) { 154 menu.setAttribute('data-enhanced', '1'); 155 } 156 }, 157 158 /** 159 * Handle movement between menu items in a menubar. 160 * 161 * @method moveMenuItem 162 * @param {EventFacade} e The event generating the move request 163 * @chainable 164 */ 165 moveMenuItem: function(e) { 166 var nextFocus, 167 menuitem = e.target.ancestor(SELECTOR.MENUITEM, true); 168 169 if (e.keyCode === 37) { 170 nextFocus = this.getMenuItem(menuitem, true); 171 } else if (e.keyCode === 39) { 172 nextFocus = this.getMenuItem(menuitem); 173 } 174 175 if (nextFocus) { 176 nextFocus.focus(); 177 } 178 return this; 179 }, 180 181 /** 182 * Get the next menuitem in a menubar. 183 * 184 * @method getMenuItem 185 * @param {Node} currentItem The currently focused item in the menubar 186 * @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards 187 * @return {Node|null} The next item, or null if none was found 188 */ 189 getMenuItem: function(currentItem, previous) { 190 var menubar = currentItem.ancestor(SELECTOR.MENUBAR), 191 menuitems; 192 193 if (!menubar) { 194 return null; 195 } 196 197 menuitems = menubar.all(SELECTOR.MENUITEMS.join(',')); 198 199 if (!menuitems) { 200 return null; 201 } 202 203 var childCount = menuitems.size(); 204 205 if (childCount === 1) { 206 // Only one item, exit now because we should already be on it. 207 return null; 208 } 209 210 // Determine the next child. 211 var index = 0, 212 direction = 1, 213 checkCount = 0; 214 215 // Work out the index of the currently selected item. 216 for (index = 0; index < childCount; index++) { 217 if (menuitems.item(index) === currentItem) { 218 break; 219 } 220 } 221 222 // Check that the menu item was found - otherwise return null. 223 if (menuitems.item(index) !== currentItem) { 224 return null; 225 } 226 227 // Reverse the direction if we want the previous item. 228 if (previous) { 229 direction = -1; 230 } 231 232 do { 233 // Update the index in the direction of travel. 234 index += direction; 235 236 next = menuitems.item(index); 237 238 // Check that we don't loop multiple times. 239 checkCount++; 240 } while (next && next.hasAttribute('hidden')); 241 242 return next; 243 }, 244 245 /** 246 * Hides the menu if it is visible. 247 * @method hideMenu 248 */ 249 hideMenu : function() { 250 if (this.dialogue) { 251 Y.log('Hiding an action menu', 'debug', ACTIONMENU.NAME); 252 this.dialogue.removeClass('show'); 253 this.dialogue.one(SELECTOR.MENUCONTENT).set('aria-hidden', true); 254 this.dialogue = null; 255 } 256 for (var i in this.events) { 257 if (this.events[i].detach) { 258 this.events[i].detach(); 259 } 260 } 261 this.events = []; 262 if (this.owner) { 263 this.owner.removeClass(CSS.MENUSHOWN); 264 this.owner = null; 265 } 266 267 if (this.menulink) { 268 this.menulink.focus(); 269 this.menulink = null; 270 } 271 }, 272 273 showIfHidden: function(e) { 274 var menu = e.target.ancestor(SELECTOR.MENU), 275 menuvisible = (menu.hasClass('show')); 276 277 if (!menuvisible) { 278 e.preventDefault(); 279 this.showMenu(e, menu); 280 } 281 return this; 282 }, 283 284 /** 285 * Toggles the display of the menu. 286 * @method toggleMenu 287 * @param {EventFacade} e 288 */ 289 toggleMenu : function(e) { 290 var menu = e.target.ancestor(SELECTOR.MENU), 291 menuvisible = (menu.hasClass('show')); 292 293 // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations. 294 e.halt(true); 295 this.hideMenu(); 296 if (menuvisible) { 297 // The menu was visible and the user has clicked to toggle it again. 298 return; 299 } 300 this.showMenu(e, menu); 301 }, 302 303 /** 304 * Handle keyboard events when the menu is open. We respond to: 305 * * escape (exit) 306 * * tab (move to next menu item) 307 * * up/down (move to previous/next menu item) 308 * 309 * @method handleKeyboardEvent 310 * @param {EventFacade} e The key event 311 */ 312 handleKeyboardEvent: function(e) { 313 var next; 314 315 // Handle when the menu is still selected. 316 if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) { 317 if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) { 318 this.firstMenuChild.focus(); 319 e.preventDefault(); 320 } else if (e.keyCode === 38 && this.lastMenuChild) { 321 this.lastMenuChild.focus(); 322 e.preventDefault(); 323 } else if (e.keyCode === 9 && e.shiftKey) { 324 this.hideMenu(); 325 e.preventDefault(); 326 } 327 return this; 328 } 329 330 if (e.keyCode === 27) { 331 // The escape key was pressed so close the menu. 332 this.hideMenu(); 333 e.preventDefault(); 334 335 } else if (e.keyCode === 32) { 336 // The space bar was pressed. Trigger a click. 337 e.preventDefault(); 338 e.currentTarget.simulate('click'); 339 } else if (e.keyCode === 9) { 340 // The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options. 341 // We only override the Shift + Tab on the first option, and Tab on the last option to change where the focus is moved to. 342 if (e.target === this.firstMenuChild && e.shiftKey) { 343 this.hideMenu(); 344 e.preventDefault(); 345 } else if (e.target === this.lastMenuChild && !e.shiftKey) { 346 if (this.hideMenu()) { 347 // Determine the next selector and focus on it. 348 next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR); 349 if (next) { 350 next.focus(); 351 } 352 } 353 } 354 355 } else if (e.keyCode === 38 || e.keyCode === 40) { 356 // The up (38) or down (40) key was pushed. 357 // On cursor moves we loops through the menu rather than exiting it as in the tab behaviour. 358 var found = false, 359 index = 0, 360 direction = 1, 361 checkCount = 0; 362 363 // Determine which menu item is currently selected. 364 while (!found && index < this.menuChildren.size()) { 365 if (this.menuChildren.item(index) === e.currentTarget) { 366 found = true; 367 } else { 368 index++; 369 } 370 } 371 372 if (!found) { 373 Y.log("Unable to find this menu item in the list of menu children", 'debug', 'moodle-core-actionmenu'); 374 return; 375 } 376 377 if (e.keyCode === 38) { 378 // Moving up so reverse the direction. 379 direction = -1; 380 } 381 382 // Try to find the next 383 do { 384 index += direction; 385 if (index < 0) { 386 index = this.menuChildren.size() - 1; 387 } else if (index >= this.menuChildren.size()) { 388 // Handle wrapping. 389 index = 0; 390 } 391 next = this.menuChildren.item(index); 392 393 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. 394 checkCount++; 395 } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); 396 397 if (next) { 398 next.focus(); 399 e.preventDefault(); 400 } 401 } 402 }, 403 404 /** 405 * Hides the menu if the event happened outside the menu. 406 * 407 * @protected 408 * @method hideIfOutside 409 * @param {EventFacade} e 410 */ 411 hideIfOutside : function(e) { 412 if (!e.target.ancestor(SELECTOR.MENUCHILD, true)) { 413 this.hideMenu(); 414 } 415 }, 416 417 /** 418 * Displays the menu with the given content and alignment. 419 * 420 * @method showMenu 421 * @param {EventFacade} e 422 * @param {Node} menu 423 * @return M.core.dialogue 424 */ 425 showMenu : function(e, menu) { 426 Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME); 427 var ownerselector = menu.getData('owner'), 428 menucontent = menu.one(SELECTOR.MENUCONTENT); 429 this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; 430 this.dialogue = menu; 431 menu.addClass('show'); 432 if (this.owner) { 433 this.owner.addClass(CSS.MENUSHOWN); 434 this.menulink = this.owner.one(SELECTOR.TOGGLE); 435 } else { 436 this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); 437 } 438 this.constrain(menucontent.set('aria-hidden', false)); 439 440 this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); 441 if (this.menuChildren) { 442 this.firstMenuChild = this.menuChildren.item(0); 443 this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); 444 445 this.firstMenuChild.focus(); 446 } 447 448 // Close the menu if the user presses escape. 449 this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); 450 451 // Close the menu if the user clicks outside the menu. 452 this.events.push(BODY.on('click', this.hideIfOutside, this)); 453 454 // Close the menu if the user focuses outside the menu. 455 this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); 456 457 // Check keyboard changes. 458 this.events.push(menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this)); 459 460 // Close the menu after a button was pushed. 461 this.events.push(menu.delegate('click', function(e) { 462 if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { 463 return; 464 } 465 this.hideMenu(); 466 }, SELECTOR.MENUCHILD, this)); 467 468 return true; 469 }, 470 471 /** 472 * Constrains the node to its the page width. 473 * 474 * @method constrain 475 * @param {Node} node 476 */ 477 constrain : function(node) { 478 var selector = node.getData('constraint'), 479 nx = node.getX(), 480 ny = node.getY(), 481 nwidth = node.get('offsetWidth'), 482 nheight = node.get('offsetHeight'), 483 cx = 0, 484 cy = 0, 485 cwidth, 486 cheight, 487 coverflow = 'auto', 488 newwidth = null, 489 newheight = null, 490 newleft = null, 491 newtop = null, 492 boxshadow = null; 493 494 if (selector) { 495 selector = node.ancestor(selector); 496 } 497 if (selector) { 498 cwidth = selector.get('offsetWidth'); 499 cheight = selector.get('offsetHeight'); 500 cx = selector.getX(); 501 cy = selector.getY(); 502 coverflow = selector.getStyle('overflow') || 'auto'; 503 } else { 504 cwidth = node.get('docWidth'); 505 cheight = node.get('docHeight'); 506 } 507 508 // Constrain X. 509 // First up if the width is more than the constrain its easily full width + full height. 510 if (nwidth > cwidth) { 511 // The width of the constraint. 512 newwidth = nwidth = cwidth; 513 // The constraints xpoint. 514 newleft = nx = cx; 515 } else { 516 if (nx < cx) { 517 // If nx is less than cx we need to move it right. 518 newleft = nx = cx; 519 } else if (nx + nwidth >= cx + cwidth) { 520 // The top right of the node is outside of the constraint, move it in. 521 newleft = cx + cwidth - nwidth; 522 } 523 } 524 525 // Constrain Y. 526 if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { 527 // The node extends over the constrained area and would be clipped. 528 // Reduce the height of the node and force its overflow to scroll. 529 newheight = nheight = cheight; 530 node.setStyle('overflow', 'auto'); 531 } 532 // If the node is below the top of the constraint AND 533 // the node is longer than the constraint allows. 534 if (ny >= cy && ny + nheight > cy + cheight) { 535 // Move it up. 536 newtop = cy + cheight - nheight; 537 try { 538 boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); 539 if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { 540 newtop -= boxshadow; 541 } 542 } catch (ex) { 543 Y.log('Failed to determine box-shadow margin.', 'warn', ACTIONMENU.NAME); 544 } 545 } 546 547 if (newleft !== null) { 548 node.setX(newleft); 549 } 550 if (newtop !== null) { 551 node.setY(newtop); 552 } 553 if (newwidth !== null) { 554 node.setStyle('width', newwidth.toString() + 'px'); 555 } 556 if (newheight !== null) { 557 node.setStyle('height', newheight.toString() + 'px'); 558 } 559 } 560 }; 561 562 Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { 563 NAME : 'moodle-core-actionmenu', 564 ATTRS : { 565 align : { 566 value : [ 567 ALIGN.TR, // The dialogue. 568 ALIGN.BR // The button 569 ] 570 } 571 } 572 }); 573 574 M.core = M.core || {}; 575 M.core.actionmenu = M.core.actionmenu || {}; 576 577 /** 578 * 579 * @static 580 * @property M.core.actionmenu.instance 581 * @type {ACTIONMENU} 582 */ 583 M.core.actionmenu.instance = null; 584 585 /** 586 * Init function - will only ever create one instance of the actionmenu class. 587 * 588 * @method M.core.actionmenu.init 589 * @static 590 * @param {Object} params 591 */ 592 M.core.actionmenu.init = M.core.actionmenu.init || function(params) { 593 M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); 594 }; 595 596 /** 597 * Registers a new DOM node with the action menu causing it to be enhanced if required. 598 * 599 * @method M.core.actionmenu.newDOMNode 600 * @param node 601 * @return {boolean} 602 */ 603 M.core.actionmenu.newDOMNode = function(node) { 604 if (M.core.actionmenu.instance === null) { 605 return true; 606 } 607 node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); 608 };
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |