[ 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.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 194 if (!menubar) { 195 return null; 196 } 197 198 menuitems = menubar.all(SELECTOR.MENUITEMS.join(',')); 199 200 if (!menuitems) { 201 return null; 202 } 203 204 var childCount = menuitems.size(); 205 206 if (childCount === 1) { 207 // Only one item, exit now because we should already be on it. 208 return null; 209 } 210 211 // Determine the next child. 212 var index = 0, 213 direction = 1, 214 checkCount = 0; 215 216 // Work out the index of the currently selected item. 217 for (index = 0; index < childCount; index++) { 218 if (menuitems.item(index) === currentItem) { 219 break; 220 } 221 } 222 223 // Check that the menu item was found - otherwise return null. 224 if (menuitems.item(index) !== currentItem) { 225 return null; 226 } 227 228 // Reverse the direction if we want the previous item. 229 if (previous) { 230 direction = -1; 231 } 232 233 do { 234 // Update the index in the direction of travel. 235 index += direction; 236 237 next = menuitems.item(index); 238 239 // Check that we don't loop multiple times. 240 checkCount++; 241 } while (next && next.hasAttribute('hidden')); 242 243 return next; 244 }, 245 246 /** 247 * Hides the menu if it is visible. 248 * @method hideMenu 249 */ 250 hideMenu : function() { 251 if (this.dialogue) { 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 return; 374 } 375 376 if (e.keyCode === 38) { 377 // Moving up so reverse the direction. 378 direction = -1; 379 } 380 381 // Try to find the next 382 do { 383 index += direction; 384 if (index < 0) { 385 index = this.menuChildren.size() - 1; 386 } else if (index >= this.menuChildren.size()) { 387 // Handle wrapping. 388 index = 0; 389 } 390 next = this.menuChildren.item(index); 391 392 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. 393 checkCount++; 394 } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); 395 396 if (next) { 397 next.focus(); 398 e.preventDefault(); 399 } 400 } 401 }, 402 403 /** 404 * Hides the menu if the event happened outside the menu. 405 * 406 * @protected 407 * @method hideIfOutside 408 * @param {EventFacade} e 409 */ 410 hideIfOutside : function(e) { 411 if (!e.target.ancestor(SELECTOR.MENUCHILD, true)) { 412 this.hideMenu(); 413 } 414 }, 415 416 /** 417 * Displays the menu with the given content and alignment. 418 * 419 * @method showMenu 420 * @param {EventFacade} e 421 * @param {Node} menu 422 * @return M.core.dialogue 423 */ 424 showMenu : function(e, menu) { 425 var ownerselector = menu.getData('owner'), 426 menucontent = menu.one(SELECTOR.MENUCONTENT); 427 this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; 428 this.dialogue = menu; 429 menu.addClass('show'); 430 if (this.owner) { 431 this.owner.addClass(CSS.MENUSHOWN); 432 this.menulink = this.owner.one(SELECTOR.TOGGLE); 433 } else { 434 this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); 435 } 436 this.constrain(menucontent.set('aria-hidden', false)); 437 438 this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); 439 if (this.menuChildren) { 440 this.firstMenuChild = this.menuChildren.item(0); 441 this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); 442 443 this.firstMenuChild.focus(); 444 } 445 446 // Close the menu if the user presses escape. 447 this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); 448 449 // Close the menu if the user clicks outside the menu. 450 this.events.push(BODY.on('click', this.hideIfOutside, this)); 451 452 // Close the menu if the user focuses outside the menu. 453 this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); 454 455 // Check keyboard changes. 456 this.events.push(menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this)); 457 458 // Close the menu after a button was pushed. 459 this.events.push(menu.delegate('click', function(e) { 460 if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { 461 return; 462 } 463 this.hideMenu(); 464 }, SELECTOR.MENUCHILD, this)); 465 466 return true; 467 }, 468 469 /** 470 * Constrains the node to its the page width. 471 * 472 * @method constrain 473 * @param {Node} node 474 */ 475 constrain : function(node) { 476 var selector = node.getData('constraint'), 477 nx = node.getX(), 478 ny = node.getY(), 479 nwidth = node.get('offsetWidth'), 480 nheight = node.get('offsetHeight'), 481 cx = 0, 482 cy = 0, 483 cwidth, 484 cheight, 485 coverflow = 'auto', 486 newwidth = null, 487 newheight = null, 488 newleft = null, 489 newtop = null, 490 boxshadow = null; 491 492 if (selector) { 493 selector = node.ancestor(selector); 494 } 495 if (selector) { 496 cwidth = selector.get('offsetWidth'); 497 cheight = selector.get('offsetHeight'); 498 cx = selector.getX(); 499 cy = selector.getY(); 500 coverflow = selector.getStyle('overflow') || 'auto'; 501 } else { 502 cwidth = node.get('docWidth'); 503 cheight = node.get('docHeight'); 504 } 505 506 // Constrain X. 507 // First up if the width is more than the constrain its easily full width + full height. 508 if (nwidth > cwidth) { 509 // The width of the constraint. 510 newwidth = nwidth = cwidth; 511 // The constraints xpoint. 512 newleft = nx = cx; 513 } else { 514 if (nx < cx) { 515 // If nx is less than cx we need to move it right. 516 newleft = nx = cx; 517 } else if (nx + nwidth >= cx + cwidth) { 518 // The top right of the node is outside of the constraint, move it in. 519 newleft = cx + cwidth - nwidth; 520 } 521 } 522 523 // Constrain Y. 524 if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { 525 // The node extends over the constrained area and would be clipped. 526 // Reduce the height of the node and force its overflow to scroll. 527 newheight = nheight = cheight; 528 node.setStyle('overflow', 'auto'); 529 } 530 // If the node is below the top of the constraint AND 531 // the node is longer than the constraint allows. 532 if (ny >= cy && ny + nheight > cy + cheight) { 533 // Move it up. 534 newtop = cy + cheight - nheight; 535 try { 536 boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); 537 if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { 538 newtop -= boxshadow; 539 } 540 } catch (ex) { 541 } 542 } 543 544 if (newleft !== null) { 545 node.setX(newleft); 546 } 547 if (newtop !== null) { 548 node.setY(newtop); 549 } 550 if (newwidth !== null) { 551 node.setStyle('width', newwidth.toString() + 'px'); 552 } 553 if (newheight !== null) { 554 node.setStyle('height', newheight.toString() + 'px'); 555 } 556 } 557 }; 558 559 Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { 560 NAME : 'moodle-core-actionmenu', 561 ATTRS : { 562 align : { 563 value : [ 564 ALIGN.TR, // The dialogue. 565 ALIGN.BR // The button 566 ] 567 } 568 } 569 }); 570 571 M.core = M.core || {}; 572 M.core.actionmenu = M.core.actionmenu || {}; 573 574 /** 575 * 576 * @static 577 * @property M.core.actionmenu.instance 578 * @type {ACTIONMENU} 579 */ 580 M.core.actionmenu.instance = null; 581 582 /** 583 * Init function - will only ever create one instance of the actionmenu class. 584 * 585 * @method M.core.actionmenu.init 586 * @static 587 * @param {Object} params 588 */ 589 M.core.actionmenu.init = M.core.actionmenu.init || function(params) { 590 M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); 591 }; 592 593 /** 594 * Registers a new DOM node with the action menu causing it to be enhanced if required. 595 * 596 * @method M.core.actionmenu.newDOMNode 597 * @param node 598 * @return {boolean} 599 */ 600 M.core.actionmenu.newDOMNode = function(node) { 601 if (M.core.actionmenu.instance === null) { 602 return true; 603 } 604 node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); 605 }; 606 607 608 }, '@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 |