[ Index ] |
PHP Cross Reference of Unnamed Project |
[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 next; 195 196 if (!menubar) { 197 return null; 198 } 199 200 menuitems = menubar.all(SELECTOR.MENUITEMS.join(',')); 201 202 if (!menuitems) { 203 return null; 204 } 205 206 var childCount = menuitems.size(); 207 208 if (childCount === 1) { 209 // Only one item, exit now because we should already be on it. 210 return null; 211 } 212 213 // Determine the next child. 214 var index = 0, 215 direction = 1, 216 checkCount = 0; 217 218 // Work out the index of the currently selected item. 219 for (index = 0; index < childCount; index++) { 220 if (menuitems.item(index) === currentItem) { 221 break; 222 } 223 } 224 225 // Check that the menu item was found - otherwise return null. 226 if (menuitems.item(index) !== currentItem) { 227 return null; 228 } 229 230 // Reverse the direction if we want the previous item. 231 if (previous) { 232 direction = -1; 233 } 234 235 do { 236 // Update the index in the direction of travel. 237 index += direction; 238 239 next = menuitems.item(index); 240 241 // Check that we don't loop multiple times. 242 checkCount++; 243 } while (next && next.hasAttribute('hidden')); 244 245 return next; 246 }, 247 248 /** 249 * Hides the menu if it is visible. 250 * @param {EventFacade} e 251 * @method hideMenu 252 */ 253 hideMenu: function(e) { 254 if (this.dialogue) { 255 Y.log('Hiding an action menu', 'debug', ACTIONMENU.NAME); 256 this.dialogue.removeClass('show'); 257 this.dialogue.one(SELECTOR.MENUCONTENT).set('aria-hidden', true); 258 this.dialogue = null; 259 } 260 for (var i in this.events) { 261 if (this.events[i].detach) { 262 this.events[i].detach(); 263 } 264 } 265 this.events = []; 266 if (this.owner) { 267 this.owner.removeClass(CSS.MENUSHOWN); 268 this.owner = null; 269 } 270 271 if (this.menulink) { 272 if (e.type != 'click') { 273 this.menulink.focus(); 274 } 275 this.menulink = null; 276 } 277 }, 278 279 showIfHidden: function(e) { 280 var menu = e.target.ancestor(SELECTOR.MENU), 281 menuvisible = (menu.hasClass('show')); 282 283 if (!menuvisible) { 284 e.preventDefault(); 285 this.showMenu(e, menu); 286 } 287 return this; 288 }, 289 290 /** 291 * Toggles the display of the menu. 292 * @method toggleMenu 293 * @param {EventFacade} e 294 */ 295 toggleMenu: function(e) { 296 var menu = e.target.ancestor(SELECTOR.MENU), 297 menuvisible = (menu.hasClass('show')); 298 299 // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations. 300 e.halt(true); 301 this.hideMenu(e); 302 if (menuvisible) { 303 // The menu was visible and the user has clicked to toggle it again. 304 return; 305 } 306 this.showMenu(e, menu); 307 }, 308 309 /** 310 * Handle keyboard events when the menu is open. We respond to: 311 * * escape (exit) 312 * * tab (move to next menu item) 313 * * up/down (move to previous/next menu item) 314 * 315 * @method handleKeyboardEvent 316 * @param {EventFacade} e The key event 317 */ 318 handleKeyboardEvent: function(e) { 319 var next; 320 var markEventHandled = function(e) { 321 e.preventDefault(); 322 e.stopPropagation(); 323 }; 324 325 // Handle when the menu is still selected. 326 if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) { 327 if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) { 328 this.firstMenuChild.focus(); 329 markEventHandled(e); 330 } else if (e.keyCode === 38 && this.lastMenuChild) { 331 this.lastMenuChild.focus(); 332 markEventHandled(e); 333 } else if (e.keyCode === 9 && e.shiftKey) { 334 this.hideMenu(e); 335 markEventHandled(e); 336 } 337 return this; 338 } 339 340 if (e.keyCode === 27) { 341 // The escape key was pressed so close the menu. 342 this.hideMenu(e); 343 markEventHandled(e); 344 345 } else if (e.keyCode === 32) { 346 // The space bar was pressed. Trigger a click. 347 markEventHandled(e); 348 e.currentTarget.simulate('click'); 349 } else if (e.keyCode === 9) { 350 // The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options. 351 // We only override the Shift + Tab on the first option, and Tab on the last option to change where the 352 // focus is moved to. 353 if (e.target === this.firstMenuChild && e.shiftKey) { 354 this.hideMenu(e); 355 markEventHandled(e); 356 } else if (e.target === this.lastMenuChild && !e.shiftKey) { 357 if (this.hideMenu(e)) { 358 // Determine the next selector and focus on it. 359 next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR); 360 if (next) { 361 next.focus(); 362 markEventHandled(e); 363 } 364 } 365 } 366 367 } else if (e.keyCode === 38 || e.keyCode === 40) { 368 // The up (38) or down (40) key was pushed. 369 // On cursor moves we loops through the menu rather than exiting it as in the tab behaviour. 370 var found = false, 371 index = 0, 372 direction = 1, 373 checkCount = 0; 374 375 // Determine which menu item is currently selected. 376 while (!found && index < this.menuChildren.size()) { 377 if (this.menuChildren.item(index) === e.currentTarget) { 378 found = true; 379 } else { 380 index++; 381 } 382 } 383 384 if (!found) { 385 Y.log("Unable to find this menu item in the list of menu children", 'debug', 'moodle-core-actionmenu'); 386 return; 387 } 388 389 if (e.keyCode === 38) { 390 // Moving up so reverse the direction. 391 direction = -1; 392 } 393 394 // Try to find the next 395 do { 396 index += direction; 397 if (index < 0) { 398 index = this.menuChildren.size() - 1; 399 } else if (index >= this.menuChildren.size()) { 400 // Handle wrapping. 401 index = 0; 402 } 403 next = this.menuChildren.item(index); 404 405 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. 406 checkCount++; 407 } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); 408 409 if (next) { 410 next.focus(); 411 markEventHandled(e); 412 } 413 } 414 }, 415 416 /** 417 * Hides the menu if the event happened outside the menu. 418 * 419 * @protected 420 * @method hideIfOutside 421 * @param {EventFacade} e 422 */ 423 hideIfOutside: function(e) { 424 if (!e.target.ancestor(SELECTOR.MENUCONTENT, true)) { 425 this.hideMenu(e); 426 } 427 }, 428 429 /** 430 * Displays the menu with the given content and alignment. 431 * 432 * @method showMenu 433 * @param {EventFacade} e 434 * @param {Node} menu 435 * @return M.core.dialogue 436 */ 437 showMenu: function(e, menu) { 438 Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME); 439 var ownerselector = menu.getData('owner'), 440 menucontent = menu.one(SELECTOR.MENUCONTENT); 441 this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; 442 this.dialogue = menu; 443 menu.addClass('show'); 444 if (this.owner) { 445 this.owner.addClass(CSS.MENUSHOWN); 446 this.menulink = this.owner.one(SELECTOR.TOGGLE); 447 } else { 448 this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); 449 } 450 this.constrain(menucontent.set('aria-hidden', false)); 451 452 this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); 453 if (this.menuChildren) { 454 this.firstMenuChild = this.menuChildren.item(0); 455 this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); 456 457 this.firstMenuChild.focus(); 458 } 459 460 // Close the menu if the user presses escape. 461 this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); 462 463 // Close the menu if the user clicks outside the menu. 464 this.events.push(BODY.on('click', this.hideIfOutside, this)); 465 466 // Close the menu if the user focuses outside the menu. 467 this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); 468 469 // Check keyboard changes. 470 this.events.push( 471 menu.delegate('key', this.handleKeyboardEvent, 472 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this) 473 ); 474 475 // Close the menu after a button was pushed. 476 this.events.push(menu.delegate('click', function(e) { 477 if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { 478 return; 479 } 480 this.hideMenu(e); 481 }, SELECTOR.MENUCHILD, this)); 482 483 return true; 484 }, 485 486 /** 487 * Constrains the node to its the page width. 488 * 489 * @method constrain 490 * @param {Node} node 491 */ 492 constrain: function(node) { 493 var selector = node.getData('constraint'), 494 nx = node.getX(), 495 ny = node.getY(), 496 nwidth = node.get('offsetWidth'), 497 nheight = node.get('offsetHeight'), 498 cx = 0, 499 cy = 0, 500 cwidth, 501 cheight, 502 coverflow = 'auto', 503 newwidth = null, 504 newheight = null, 505 newleft = null, 506 newtop = null, 507 boxshadow = null; 508 509 if (selector) { 510 selector = node.ancestor(selector); 511 } 512 if (selector) { 513 cwidth = selector.get('offsetWidth'); 514 cheight = selector.get('offsetHeight'); 515 cx = selector.getX(); 516 cy = selector.getY(); 517 coverflow = selector.getStyle('overflow') || 'auto'; 518 } else { 519 cwidth = node.get('docWidth'); 520 cheight = node.get('docHeight'); 521 } 522 523 // Constrain X. 524 // First up if the width is more than the constrain its easily full width + full height. 525 if (nwidth > cwidth) { 526 // The width of the constraint. 527 newwidth = nwidth = cwidth; 528 // The constraints xpoint. 529 newleft = nx = cx; 530 } else { 531 if (nx < cx) { 532 // If nx is less than cx we need to move it right. 533 newleft = nx = cx; 534 } else if (nx + nwidth >= cx + cwidth) { 535 // The top right of the node is outside of the constraint, move it in. 536 newleft = cx + cwidth - nwidth; 537 } 538 } 539 540 // Constrain Y. 541 if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { 542 // The node extends over the constrained area and would be clipped. 543 // Reduce the height of the node and force its overflow to scroll. 544 newheight = nheight = cheight; 545 node.setStyle('overflow', 'auto'); 546 } 547 // If the node is below the top of the constraint AND 548 // the node is longer than the constraint allows. 549 if (ny >= cy && ny + nheight > cy + cheight) { 550 // Move it up. 551 newtop = cy + cheight - nheight; 552 try { 553 boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); 554 if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { 555 newtop -= boxshadow; 556 } 557 } catch (ex) { 558 Y.log('Failed to determine box-shadow margin.', 'warn', ACTIONMENU.NAME); 559 } 560 } 561 562 if (newleft !== null) { 563 node.setX(newleft); 564 } 565 if (newtop !== null) { 566 node.setY(newtop); 567 } 568 if (newwidth !== null) { 569 node.setStyle('width', newwidth.toString() + 'px'); 570 } 571 if (newheight !== null) { 572 node.setStyle('height', newheight.toString() + 'px'); 573 } 574 } 575 }; 576 577 Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { 578 NAME: 'moodle-core-actionmenu', 579 ATTRS: { 580 align: { 581 value: [ 582 ALIGN.TR, // The dialogue. 583 ALIGN.BR // The button 584 ] 585 } 586 } 587 }); 588 589 M.core = M.core || {}; 590 M.core.actionmenu = M.core.actionmenu || {}; 591 592 /** 593 * 594 * @static 595 * @property M.core.actionmenu.instance 596 * @type {ACTIONMENU} 597 */ 598 M.core.actionmenu.instance = null; 599 600 /** 601 * Init function - will only ever create one instance of the actionmenu class. 602 * 603 * @method M.core.actionmenu.init 604 * @static 605 * @param {Object} params 606 */ 607 M.core.actionmenu.init = M.core.actionmenu.init || function(params) { 608 M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); 609 }; 610 611 /** 612 * Registers a new DOM node with the action menu causing it to be enhanced if required. 613 * 614 * @method M.core.actionmenu.newDOMNode 615 * @param node 616 * @return {boolean} 617 */ 618 M.core.actionmenu.newDOMNode = function(node) { 619 if (M.core.actionmenu.instance === null) { 620 return true; 621 } 622 node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); 623 }; 624 625 626 }, '@VERSION@', {"requires": ["base", "event", "node-event-simulate"]});
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 |