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