[ 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.all(SELECTOR.MENU).each(this.enhance, this); 123 BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this); 124 125 BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); 126 BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); 127 128 // Ensure that we toggle on menuitems when the spacebar is pressed. 129 BODY.delegate('key', function(e) { 130 e.currentTarget.simulate('click'); 131 e.preventDefault(); 132 }, 'down:32', SELECTOR.MENUBARITEMS.join(',')); 133 }, 134 135 /** 136 * Enhances a menu adding aria attributes and flagging it as functional. 137 * 138 * @method enhance 139 * @param {Node} menu 140 * @return boolean 141 */ 142 enhance: function(menu) { 143 var menucontent = menu.one(SELECTOR.MENUCONTENT), 144 align; 145 if (!menucontent) { 146 return false; 147 } 148 align = menucontent.getData('align') || this.get('align').join('-'); 149 menu.one(SELECTOR.TOGGLE).set('aria-haspopup', true); 150 menucontent.set('aria-hidden', true); 151 if (!menucontent.hasClass('align-' + align)) { 152 menucontent.addClass('align-' + align); 153 } 154 if (menucontent.hasChildNodes()) { 155 menu.setAttribute('data-enhanced', '1'); 156 } 157 }, 158 159 /** 160 * Handle movement between menu items in a menubar. 161 * 162 * @method moveMenuItem 163 * @param {EventFacade} e The event generating the move request 164 * @chainable 165 */ 166 moveMenuItem: function(e) { 167 var nextFocus, 168 menuitem = e.target.ancestor(SELECTOR.MENUITEM, true); 169 170 if (e.keyCode === 37) { 171 nextFocus = this.getMenuItem(menuitem, true); 172 } else if (e.keyCode === 39) { 173 nextFocus = this.getMenuItem(menuitem); 174 } 175 176 if (nextFocus) { 177 nextFocus.focus(); 178 } 179 return this; 180 }, 181 182 /** 183 * Get the next menuitem in a menubar. 184 * 185 * @method getMenuItem 186 * @param {Node} currentItem The currently focused item in the menubar 187 * @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards 188 * @return {Node|null} The next item, or null if none was found 189 */ 190 getMenuItem: function(currentItem, previous) { 191 var menubar = currentItem.ancestor(SELECTOR.MENUBAR), 192 menuitems, 193 next; 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 * @param {EventFacade} e 250 * @method hideMenu 251 */ 252 hideMenu: function(e) { 253 if (this.dialogue) { 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 if (e.type != 'click') { 271 this.menulink.focus(); 272 } 273 this.menulink = null; 274 } 275 }, 276 277 showIfHidden: function(e) { 278 var menu = e.target.ancestor(SELECTOR.MENU), 279 menuvisible = (menu.hasClass('show')); 280 281 if (!menuvisible) { 282 e.preventDefault(); 283 this.showMenu(e, menu); 284 } 285 return this; 286 }, 287 288 /** 289 * Toggles the display of the menu. 290 * @method toggleMenu 291 * @param {EventFacade} e 292 */ 293 toggleMenu: function(e) { 294 var menu = e.target.ancestor(SELECTOR.MENU), 295 menuvisible = (menu.hasClass('show')); 296 297 // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations. 298 e.halt(true); 299 this.hideMenu(e); 300 if (menuvisible) { 301 // The menu was visible and the user has clicked to toggle it again. 302 return; 303 } 304 this.showMenu(e, menu); 305 }, 306 307 /** 308 * Handle keyboard events when the menu is open. We respond to: 309 * * escape (exit) 310 * * tab (move to next menu item) 311 * * up/down (move to previous/next menu item) 312 * 313 * @method handleKeyboardEvent 314 * @param {EventFacade} e The key event 315 */ 316 handleKeyboardEvent: function(e) { 317 var next; 318 var markEventHandled = function(e) { 319 e.preventDefault(); 320 e.stopPropagation(); 321 }; 322 323 // Handle when the menu is still selected. 324 if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) { 325 if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) { 326 this.firstMenuChild.focus(); 327 markEventHandled(e); 328 } else if (e.keyCode === 38 && this.lastMenuChild) { 329 this.lastMenuChild.focus(); 330 markEventHandled(e); 331 } else if (e.keyCode === 9 && e.shiftKey) { 332 this.hideMenu(e); 333 markEventHandled(e); 334 } 335 return this; 336 } 337 338 if (e.keyCode === 27) { 339 // The escape key was pressed so close the menu. 340 this.hideMenu(e); 341 markEventHandled(e); 342 343 } else if (e.keyCode === 32) { 344 // The space bar was pressed. Trigger a click. 345 markEventHandled(e); 346 e.currentTarget.simulate('click'); 347 } else if (e.keyCode === 9) { 348 // The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options. 349 // We only override the Shift + Tab on the first option, and Tab on the last option to change where the 350 // focus is moved to. 351 if (e.target === this.firstMenuChild && e.shiftKey) { 352 this.hideMenu(e); 353 markEventHandled(e); 354 } else if (e.target === this.lastMenuChild && !e.shiftKey) { 355 if (this.hideMenu(e)) { 356 // Determine the next selector and focus on it. 357 next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR); 358 if (next) { 359 next.focus(); 360 markEventHandled(e); 361 } 362 } 363 } 364 365 } else if (e.keyCode === 38 || e.keyCode === 40) { 366 // The up (38) or down (40) key was pushed. 367 // On cursor moves we loops through the menu rather than exiting it as in the tab behaviour. 368 var found = false, 369 index = 0, 370 direction = 1, 371 checkCount = 0; 372 373 // Determine which menu item is currently selected. 374 while (!found && index < this.menuChildren.size()) { 375 if (this.menuChildren.item(index) === e.currentTarget) { 376 found = true; 377 } else { 378 index++; 379 } 380 } 381 382 if (!found) { 383 return; 384 } 385 386 if (e.keyCode === 38) { 387 // Moving up so reverse the direction. 388 direction = -1; 389 } 390 391 // Try to find the next 392 do { 393 index += direction; 394 if (index < 0) { 395 index = this.menuChildren.size() - 1; 396 } else if (index >= this.menuChildren.size()) { 397 // Handle wrapping. 398 index = 0; 399 } 400 next = this.menuChildren.item(index); 401 402 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. 403 checkCount++; 404 } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); 405 406 if (next) { 407 next.focus(); 408 markEventHandled(e); 409 } 410 } 411 }, 412 413 /** 414 * Hides the menu if the event happened outside the menu. 415 * 416 * @protected 417 * @method hideIfOutside 418 * @param {EventFacade} e 419 */ 420 hideIfOutside: function(e) { 421 if (!e.target.ancestor(SELECTOR.MENUCONTENT, true)) { 422 this.hideMenu(e); 423 } 424 }, 425 426 /** 427 * Displays the menu with the given content and alignment. 428 * 429 * @method showMenu 430 * @param {EventFacade} e 431 * @param {Node} menu 432 * @return M.core.dialogue 433 */ 434 showMenu: function(e, menu) { 435 var ownerselector = menu.getData('owner'), 436 menucontent = menu.one(SELECTOR.MENUCONTENT); 437 this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; 438 this.dialogue = menu; 439 menu.addClass('show'); 440 if (this.owner) { 441 this.owner.addClass(CSS.MENUSHOWN); 442 this.menulink = this.owner.one(SELECTOR.TOGGLE); 443 } else { 444 this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); 445 } 446 this.constrain(menucontent.set('aria-hidden', false)); 447 448 this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); 449 if (this.menuChildren) { 450 this.firstMenuChild = this.menuChildren.item(0); 451 this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); 452 453 this.firstMenuChild.focus(); 454 } 455 456 // Close the menu if the user presses escape. 457 this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); 458 459 // Close the menu if the user clicks outside the menu. 460 this.events.push(BODY.on('click', this.hideIfOutside, this)); 461 462 // Close the menu if the user focuses outside the menu. 463 this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); 464 465 // Check keyboard changes. 466 this.events.push( 467 menu.delegate('key', this.handleKeyboardEvent, 468 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this) 469 ); 470 471 // Close the menu after a button was pushed. 472 this.events.push(menu.delegate('click', function(e) { 473 if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { 474 return; 475 } 476 this.hideMenu(e); 477 }, SELECTOR.MENUCHILD, this)); 478 479 return true; 480 }, 481 482 /** 483 * Constrains the node to its the page width. 484 * 485 * @method constrain 486 * @param {Node} node 487 */ 488 constrain: function(node) { 489 var selector = node.getData('constraint'), 490 nx = node.getX(), 491 ny = node.getY(), 492 nwidth = node.get('offsetWidth'), 493 nheight = node.get('offsetHeight'), 494 cx = 0, 495 cy = 0, 496 cwidth, 497 cheight, 498 coverflow = 'auto', 499 newwidth = null, 500 newheight = null, 501 newleft = null, 502 newtop = null, 503 boxshadow = null; 504 505 if (selector) { 506 selector = node.ancestor(selector); 507 } 508 if (selector) { 509 cwidth = selector.get('offsetWidth'); 510 cheight = selector.get('offsetHeight'); 511 cx = selector.getX(); 512 cy = selector.getY(); 513 coverflow = selector.getStyle('overflow') || 'auto'; 514 } else { 515 cwidth = node.get('docWidth'); 516 cheight = node.get('docHeight'); 517 } 518 519 // Constrain X. 520 // First up if the width is more than the constrain its easily full width + full height. 521 if (nwidth > cwidth) { 522 // The width of the constraint. 523 newwidth = nwidth = cwidth; 524 // The constraints xpoint. 525 newleft = nx = cx; 526 } else { 527 if (nx < cx) { 528 // If nx is less than cx we need to move it right. 529 newleft = nx = cx; 530 } else if (nx + nwidth >= cx + cwidth) { 531 // The top right of the node is outside of the constraint, move it in. 532 newleft = cx + cwidth - nwidth; 533 } 534 } 535 536 // Constrain Y. 537 if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { 538 // The node extends over the constrained area and would be clipped. 539 // Reduce the height of the node and force its overflow to scroll. 540 newheight = nheight = cheight; 541 node.setStyle('overflow', 'auto'); 542 } 543 // If the node is below the top of the constraint AND 544 // the node is longer than the constraint allows. 545 if (ny >= cy && ny + nheight > cy + cheight) { 546 // Move it up. 547 newtop = cy + cheight - nheight; 548 try { 549 boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); 550 if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { 551 newtop -= boxshadow; 552 } 553 } catch (ex) { 554 } 555 } 556 557 if (newleft !== null) { 558 node.setX(newleft); 559 } 560 if (newtop !== null) { 561 node.setY(newtop); 562 } 563 if (newwidth !== null) { 564 node.setStyle('width', newwidth.toString() + 'px'); 565 } 566 if (newheight !== null) { 567 node.setStyle('height', newheight.toString() + 'px'); 568 } 569 } 570 }; 571 572 Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { 573 NAME: 'moodle-core-actionmenu', 574 ATTRS: { 575 align: { 576 value: [ 577 ALIGN.TR, // The dialogue. 578 ALIGN.BR // The button 579 ] 580 } 581 } 582 }); 583 584 M.core = M.core || {}; 585 M.core.actionmenu = M.core.actionmenu || {}; 586 587 /** 588 * 589 * @static 590 * @property M.core.actionmenu.instance 591 * @type {ACTIONMENU} 592 */ 593 M.core.actionmenu.instance = null; 594 595 /** 596 * Init function - will only ever create one instance of the actionmenu class. 597 * 598 * @method M.core.actionmenu.init 599 * @static 600 * @param {Object} params 601 */ 602 M.core.actionmenu.init = M.core.actionmenu.init || function(params) { 603 M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); 604 }; 605 606 /** 607 * Registers a new DOM node with the action menu causing it to be enhanced if required. 608 * 609 * @method M.core.actionmenu.newDOMNode 610 * @param node 611 * @return {boolean} 612 */ 613 M.core.actionmenu.newDOMNode = function(node) { 614 if (M.core.actionmenu.instance === null) { 615 return true; 616 } 617 node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); 618 }; 619 620 621 }, '@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 |