1 /** 2 * The Nornix.TreeMenu class creates an explorer-like interface from nested lists. 3 * The menu can use either server-side facilities to create classes for 4 * the styling with CSS to hook on to. 5 * 6 * Only set inAllowWhitespace to true when necessary, as it slows down the script. 7 * 8 * Usage instructions at http://treemenu.nornix.com/info/usage 9 * 10 * @author Anders Nawroth <http://www.anders.nawroth.com/> 11 * @license LGPL http://treemenu.nornix.com/license 12 * @copyright 2006--2008 13 * @version @version@ 14 * @constructor 15 * @param {String} inMenuId id of the element surrounding the menu 16 * @param {boolean} inAllowWhitespace if true, whitespace is allowed between menu elements in the HTML code 17 */ 18 Nornix.TreeMenu = function (inMenuId, inAllowWhitespace) 19 { 20 /** 21 * Variable to work around ECMAScript problems with the "this" keyword inside event handlers. 22 * @type Object 23 */ 24 var thiz = this; 25 /** 26 * Id of current menu wrapper. 27 * @type String 28 */ 29 var menuId = inMenuId ? inMenuId : "menu"; 30 /** 31 * Allow whitespace in menu (true) or not (false). 32 * @type boolean 33 */ 34 var allowWhitespace = (inAllowWhitespace === false) ? false : true; 35 /** 36 * Name of cookie, to avoid namespace-clashes between multiple menus. 37 * @type String 38 */ 39 var cookieName = "tree" + menuId; 40 /** 41 * Cookie with saved node status information. 42 * @type String 43 */ 44 var oldTree = Nornix.cookies.read(cookieName); 45 /** 46 * Clean tree state on save when set to true. 47 * @type String 48 */ 49 var cleanMemory = false; 50 /** 51 * Empty location for links, to compare with. 52 * @type String 53 */ 54 var emptyHref = window.location+"#"; 55 /** 56 * Unordered list to clone from. 57 * @type HTMLULElement 58 */ 59 var folderUl = document.createElement("ul"); 60 /** 61 * List item to clone from. 62 * @type HTMLLIElement 63 */ 64 var folderLi = document.createElement("li"); 65 /** 66 * Span to clone and use for folder open/close icons. 67 * @type HTMLSpanElement 68 */ 69 var folderSpan = document.createElement("span"); 70 /** 71 * Anchor element to clone from. 72 * @type HTMLAnchorElement 73 */ 74 var commonAnchor = document.createElement("a"); 75 /** 76 * Anchor element to clone from for action "buttons". 77 * @type HTMLAnchorElement 78 */ 79 var actionAnchor = commonAnchor.cloneNode(false); 80 /** 81 * Object to keep menu buttons. 82 * @type HTMLAnchorElement[] 83 */ 84 var menuButtons = []; 85 /** 86 * Object to store navigation items. 87 * @property {HTMLAnchorElement} first 88 * @property {HTMLAnchorElement} previous 89 * @property {HTMLAnchorElement} next 90 * @property {HTMLAnchorElement} last 91 */ 92 var navigation = {}; 93 /** 94 * Object to keep menu button actions. 95 * @type Function[] 96 */ 97 var menuButtonActions = []; 98 /** 99 * The current marked item in the menu. 100 * This has to be remebered for dynamic usage of the menu. 101 * @type HTMLAnchorElement 102 */ 103 var currentItem = null; 104 /** 105 * The href attribute that was removed from the current marked item in the menu. 106 * This has to be remebered for dynamic usage of the menu. 107 * @type String 108 */ 109 var currentItemHref = null; 110 /** 111 * The selected item in the menu. 112 * This has to be remebered for dynamic usage of the menu. 113 * @type HTMLAnchorElement 114 */ 115 var selectedItem = null; 116 /** 117 * RegExp to find the class "open" in strings. 118 * @type RegExp 119 * @final 120 */ 121 var openPattern = /(^| )open( |$)/; 122 /** 123 * Open all folders in the menu. 124 * @function 125 */ 126 this.openAll = openAll; 127 /** 128 * Close all folders in the menu. 129 * @function 130 */ 131 this.closeAll = closeAll; 132 /** 133 * Open tree to the specified node. 134 * The node can be given as a DOM node or as a string. 135 * The string will be interpreted as a href value 136 * or node text value. 137 * When used in event handlers, the return value can be useful. 138 * @function 139 * @param {HTMLAnchorElement|HTMLLIElement|string} node node to open to, either A och LI element or string 140 * @param {boolean} [setFocus] set to true to move focus to node 141 * @param {boolean} [makeCurrent] make node the current node 142 * @param {boolean} [doClick] set to true to "click" the link 143 * @param {Event} [e] event object 144 * @return {boolean} true if the event action should continue. 145 */ 146 this.openToNode = openToNode; 147 148 /** 149 * Start the tree menu. 150 * Initialization will be called as soon as the menu object is available in the DOM. 151 * Images will be preloaded while the DOM is built. 152 * @function 153 */ 154 this.start = function() 155 { 156 if (!document.getElementById || !document.createElement) return; 157 Nornix.events.delayedInit(menuId, init); 158 actionAnchor.href = "javascript:;"; 159 // no preload for IE < 7 160 if (thiz.config.preloadImages && !Nornix.util.isIeLt7) 161 { 162 Nornix.dom.imagePreload(thiz.config.preloadImages, thiz.config.imagePath, thiz.config.imageExtension); 163 } 164 }; 165 166 /** 167 * Prevent the tree state from being saved when leaving the page. 168 * @function 169 */ 170 this.preventStatusSave = function() 171 { 172 cleanMemory = true; 173 }; 174 175 /** 176 * Register a menu button an its action. 177 * @param {HTMLAnchorElement} menu button element 178 * @param {Function} function to call when button is pressed 179 */ 180 this.registerMenuButton = function(a, func) 181 { 182 menuButtons[menuButtons.length] = a; 183 menuButtonActions[menuButtonActions.length] = func; 184 } 185 186 /** 187 * Create a new menu (document) node. 188 * If child nodes are added the node will automatically 189 * become a folder node, once it's inside of the menu. 190 * To add more attributes or whatever, use the returned LI 191 * element or the A element at LI.firstChild. 192 * @function 193 * @param {string} text link text for node 194 * @param {string} [href] link address to use for A element (or null) 195 * @param {string} [title] text to show on hovering the A element 196 * @param {HTMLLIElement} the LI element that was created 197 */ 198 this.createNode = function(text, href, title) 199 { 200 var li = folderLi.cloneNode(false); 201 li.className = "document"; 202 var a = commonAnchor.cloneNode(false); 203 if (href) 204 { 205 a.href = href; 206 } 207 if (title) 208 { 209 a.title = title; 210 } 211 a.appendChild(document.createTextNode(text)); 212 li.appendChild(a); 213 return li; 214 }; 215 216 /** 217 * Add child node to a menu item. 218 * @function 219 * @param {HTMLAnchorElement|string} node menu item 220 * @param {HTMLLIElement|string} newNode child node to add 221 * @param {boolean} [showNode] make new node visible 222 * @param {boolean} [setFocus] set focus to the child node 223 * @param {boolean} [move] set to true when moving a node 224 */ 225 this.appendChild = function(node, newNode, showNode, setFocus, move) 226 { 227 insertNode(function(node, newNode, showNode) 228 { 229 var ul; 230 Nornix.css.add(newNode, "last"); 231 if (node.nextSibling && Nornix.dom.eqNodeName(node.nextSibling, "ul")) 232 { 233 ul = node.nextSibling; 234 Nornix.css.add(newNode, "last"); 235 makeFolder(node.parentNode, node, showNode); 236 } 237 else 238 { 239 ul = folderUl.cloneNode(false); 240 var p = node.parentNode; 241 p.appendChild(ul); 242 makeFolder(p, node, showNode); 243 thiz.menuFolders[thiz.menuFolders.length] = p; // not at correct position 244 } 245 ul.appendChild(newNode); 246 if (newNode.previousSibling) 247 { 248 Nornix.css.remove(newNode.previousSibling, "last"); 249 } 250 }, node, newNode, showNode, setFocus, move); 251 }; 252 253 /** 254 * Add menu item before another menu item. 255 * @function 256 * @param {HTMLAnchorElement|string} node menu item 257 * @param {HTMLLIElement|string} newNode child node to add 258 * @param {boolean} [showNode] make new node visible 259 * @param {boolean} [setFocus] set focus to the child node 260 * @param {boolean} [move] set to true when moving a node 261 */ 262 this.insertBefore = function(node, newNode, showNode, setFocus, move) 263 { 264 insertNode(function(node, newNode) 265 { 266 node.parentNode.parentNode.insertBefore(newNode, node.parentNode); 267 }, node, newNode, showNode, setFocus, move); 268 }; 269 270 /** 271 * Add menu item after another menu item. 272 * @function 273 * @param {HTMLAnchorElement|string} node menu item 274 * @param {HTMLLIElement|string} newNode child node to add 275 * @param {boolean} [showNode] make new node visible 276 * @param {boolean} [setFocus] set focus to the child node 277 * @param {boolean} [move] set to true when moving a node 278 */ 279 this.insertAfter = function(node, newNode, showNode, setFocus, move) 280 { 281 insertNode(function(node, newNode) 282 { 283 if (node.parentNode.nextSibling) 284 { 285 node.parentNode.parentNode.insertBefore(newNode, node.parentNode.nextSibling); 286 } 287 else 288 { 289 node.parentNode.parentNode.appendChild(newNode); 290 } 291 }, node, newNode, showNode, setFocus, move); 292 }; 293 294 /** 295 * Remove item from menu. 296 * @function 297 * @param {HTMLAnchorElement|string} node menu item 298 */ 299 this.remove = function(node) 300 { 301 node = resolveNode(node); 302 if (!node || !node.parentNode || !node.parentNode.parentNode) return; 303 // check if current node! TODO? 304 cleanUpForReMove(node.parentNode); 305 node.parentNode.parentNode.removeChild(node.parentNode); 306 refreshItemLists(); 307 }; 308 309 /** 310 * Go to and click first link in menu. 311 * @function 312 */ 313 this.goToFirstNode = function() {goToNavigationItem("first")}; 314 315 /** 316 * Go to and click last link in menu. 317 * @function 318 */ 319 this.goToLastNode = function() {goToNavigationItem("last")}; 320 321 /** 322 * Go to and click next link in menu. 323 * @function 324 */ 325 this.goToNextNode = function() {goToNavigationItem("next")}; 326 327 /** 328 * Go to and click previous link in menu. 329 * @function 330 */ 331 this.goToPrevNode = function() {goToNavigationItem("previous")}; 332 333 /** 334 * Initialize the menu. 335 * @param {HTMLElement} menu the menu object 336 */ 337 function init (menu) 338 { 339 thiz.menu = menu; 340 thiz.menuElements = Nornix.dom.live2copy(thiz.menu.getElementsByTagName("li")); 341 342 // set classes, if not set in the HTML, and prepare folders 343 setClasses(); 344 345 // re-render menu now if IE 346 ieFix(); 347 348 // add extra icons 349 if (thiz.config.openCloseAll) 350 { 351 createOpenCloseAllIcons(); 352 } 353 if (thiz.config.navigationIcons) 354 { 355 createNavigationIcons(); 356 } 357 358 // set up event handling now 359 EventHandlers(); 360 361 // initialize navigation, if enabled 362 if (thiz.config.navigationIcons) 363 { 364 initNavigation(); 365 } 366 } 367 368 /** 369 * Initialize the event handlers for click and keyboard events. 370 * @constructor 371 */ 372 function EventHandlers() 373 { 374 init(); 375 376 /** 377 * Initialize event handlers. 378 */ 379 function init () 380 { 381 // add click handler to menu link. 382 if (thiz.config.menuLinkElement) 383 { 384 Nornix.events.add(document.getElementById(thiz.config.menuLinkElement), 'click', menuJump); 385 if (Nornix.util.isIe) 386 { 387 Nornix.events.add(document.getElementById(thiz.config.menuLinkElement), 'focus', menuJumpIe); 388 } 389 } 390 391 // if whitespace was allowed, remove it and change the setting to reflect the new state 392 if (allowWhitespace) 393 { 394 removeWhitespace(thiz.menu); 395 allowWhitespace = false; 396 } 397 398 // add event handlers to menu 399 Nornix.events.add(thiz.menu, 'click', checkClickDynamic, true); 400 Nornix.events.add(thiz.menu, 'keydown', checkKeyDynamic, true); 401 402 // add unload handler to save cookie 403 Nornix.events.add(window, "unload", save); 404 } 405 406 /** 407 * Move focus to the root element of the menu. 408 * Used as an event handler. 409 * @member EventHandlers 410 * @param {Event} e event object 411 * @return {boolean} always returns false 412 */ 413 function menuJump (e) 414 { 415 focusNode(thiz.menu); 416 Nornix.events.cancel(e); 417 return false; 418 } 419 420 /** 421 * Move focus from the menu link to the menu itself in Internet Exlorer. 422 * Used as an event handler. 423 * @param {Event} e event object 424 */ 425 function menuJumpIe (e) 426 { 427 if (e.altKey) 428 { 429 focusNode(thiz.menu); 430 } 431 } 432 433 /** 434 * Handle all click events. 435 * @param {Event} e event object 436 * @return {boolean} false when handling the event 437 */ 438 function checkClickDynamic (e) 439 { 440 var t = e.target; 441 if (Nornix.css.contains(t, "menuAction")) 442 { 443 var i = menuButtons.length; 444 while (i--) 445 { 446 if (t === menuButtons[i]) 447 { 448 menuButtonActions[i](); 449 break; 450 } 451 } 452 Nornix.events.cancel(e); 453 return false; 454 } 455 var p = t.parentNode; 456 if (!p) return true; // we can't handle this 457 if (Nornix.dom.eqNodeName(t, "span")) 458 { 459 toggle(p); 460 return; 461 } 462 return performClick(p, t, e); 463 } 464 465 /** 466 * Handle all keydown events. 467 * Looks for lots of different key strokes. 468 * @param {Event} e event object 469 * @return {boolean} true if the event action should continue. 470 */ 471 function checkKeyDynamic(e) 472 { 473 var isRoot, isFolder, isDocument, isCloser, isOpener, isDocOrFolder, isCloserOpener, 474 o = e.target, p = o.parentNode, node; 475 switch (true) 476 { 477 case Nornix.css.contains(o, "root"): isRoot = true; break; 478 case Nornix.css.contains(o, "closeTree"): isCloser = true; isCloserOpener = true; break; 479 case Nornix.css.contains(o, "openTree"): isOpener = true; isCloserOpener = true; break; 480 case Nornix.css.contains(p, "folder"): isFolder = true; isDocOrFolder = true; break; 481 case Nornix.css.contains(p, "document"): isDocument = true; isDocOrFolder = true; break; 482 default: return true; // not found; 483 } 484 485 var keyCode = e.keyCode !== null ? e.keyCode : e.which; 486 if (keyCode === 56) // ( 8 487 { 488 openAll(); 489 } 490 else if (keyCode === 57) // 9 ) 491 { 492 closeAll(o); 493 if (!o.offsetParent) 494 { 495 Nornix.dom.findChildOfType( 496 thiz.menu, "a", function (a) 497 { 498 focusAnchor(a); 499 } 500 ); 501 } 502 } 503 else if (isFolder && (keyCode === 32 || (keyCode === 13 && isHrefEmpty(o)))) 504 { 505 toggle(p); 506 } 507 else if (isRoot && keyCode === 40) // arrow down 508 { 509 Nornix.dom.findChildOfType(p, "ul", function (ul) { 510 focusNode(ul.firstChild); 511 }); 512 } 513 else if (isRoot && keyCode === 39 && o.nextSibling) // arrow right 514 { 515 focusAnchor(o.nextSibling); 516 } 517 else if (isDocOrFolder && (keyCode === 40)) // arrow down 518 { 519 if (isDocument || !isOpen(p)) 520 { 521 if (node = p.nextSibling) focusNode(node); 522 else if (isFolder && (node = o.nextSibling.firstChild)) 523 { 524 // open folder when moving down from it 525 toggle(p); 526 focusNode(node); 527 } 528 else 529 { 530 // find next available node 531 node = p; 532 while (node && node !== thiz.menu) 533 { 534 node = node.parentNode.parentNode; 535 if (node.nextSibling) 536 { 537 focusNode(node.nextSibling); 538 break; 539 } 540 } 541 } 542 } 543 else 544 { 545 if (node = p.nextSibling) focusNode(node); 546 else if (node = o.nextSibling.firstChild) focusNode(node); 547 } 548 } 549 else if (isDocOrFolder && (keyCode === 38)) // arrow up 550 { 551 if (node = p.previousSibling) focusNode(node); 552 else if (node = p.parentNode.parentNode) focusNode(node); 553 } 554 else if (isDocOrFolder && (keyCode === 39)) // arrow right 555 { 556 if (isDocument && (node = p.nextSibling)) focusNode(node); 557 else if (o.nextSibling && (node = o.nextSibling.firstChild)) 558 { 559 if (isOpen(p)) focusNode(node); 560 else 561 { 562 toggle(p); 563 focusNode(node); 564 } 565 } 566 } 567 else if (isDocOrFolder && (keyCode === 37)) // arrow left 568 { 569 if (node = p.parentNode.parentNode) focusNode(node); 570 if (isFolder && isOpen(p)) toggle(p); 571 } 572 else if (isDocument && (keyCode === 32)) {} // SPACE 573 else if (keyCode === 27 && thiz.config.contentElement) // ESC 574 { 575 window.location.hash = thiz.config.contentElement; 576 } 577 else if (isCloserOpener && (keyCode === 37 || keyCode === 38)) // arrow left or up 578 { 579 focusAnchor(o.previousSibling); 580 } 581 else if (isCloser && (keyCode === 39 || keyCode === 40)) // arrow right or down 582 { 583 focusAnchor(o.nextSibling); 584 } 585 else if (isOpener && (keyCode === 39 || keyCode === 40)) // arrow right or down 586 { 587 focusNode(o.nextSibling.firstChild); 588 } 589 else 590 { 591 return true; 592 } 593 Nornix.events.cancel(e); 594 return false; 595 } 596 } 597 598 /** 599 * Cleanup node before (re)moval of it 600 * @param {HTMLLIElement} li list item to clean up 601 */ 602 function cleanUpForReMove(li) 603 { 604 if (Nornix.css.contains(li, "last")) 605 { 606 if (li.previousSibling) 607 { 608 Nornix.css.add(li.previousSibling, "last"); 609 } 610 else 611 { 612 makeDocument(li.parentNode.parentNode); 613 } 614 Nornix.css.remove(li, "last"); 615 } 616 } 617 618 /** 619 * Refresh list of items, folders and the current item. 620 */ 621 function refreshItemLists() 622 { 623 thiz.menuElements = Nornix.dom.live2copy(thiz.menu.getElementsByTagName("li")); 624 var li, i = 0, folders = [], currentFound = false, selectedFound = false, 625 current = (currentItem && currentItem.parentNode) ? currentItem.parentNode : null; 626 while (li = thiz.menuElements[i++]) 627 { 628 if (isFolder(li)) 629 { 630 folders[folders.length] = li; 631 } 632 if (li === current) 633 { 634 currentFound = true; 635 } 636 if (li === selectedItem) 637 { 638 selectedFound = true; 639 } 640 } 641 thiz.menuFolders = folders; 642 if (!currentFound) 643 { 644 currentItem = currentItemHref = null; 645 } 646 if (!selectedFound) 647 { 648 selectedItem = null; 649 } 650 getMenuNodes(true); 651 } 652 653 /** 654 * Insert node in menu. 655 * @param {Function} insertFunc insert implementation (node, newNode) 656 * @param {HTMLAnchorElement|string} node menu item 657 * @param {HTMLLIElement|string} newNode child node to add 658 * @param {boolean} [showNode] make new node visible 659 * @param {boolean} [setFocus] set focus to the child node 660 * @param {boolean} [move] set to true when moving a node 661 */ 662 function insertNode(insertFunc, node, newNode, showNode, setFocus, move) 663 { 664 node = resolveNode(node); 665 if (!node) return; 666 if (typeof(newNode) === "string") 667 { 668 newNode = resolveNode(newNode); 669 if (!newNode || !newNode.parentNode) return; 670 newNode = newNode.parentNode; 671 move = true; 672 } 673 if (!node.parentNode || !node.parentNode.parentNode) return; 674 if (move) 675 { 676 cleanUpForReMove(newNode); 677 } 678 insertFunc(node, newNode, showNode); 679 if (showNode) 680 { 681 openToNode(newNode.firstChild, setFocus); 682 } 683 ieFix(); 684 refreshItemLists(); 685 } 686 687 /** 688 * Perform link click. 689 * @param {HTMLLIElement} li HTML li element 690 * @param {HTMLAnchorElement} a anchor element to click 691 * @param {Event} [e] event object 692 * @return {boolean} false when handling the event 693 */ 694 function performClick(li, a, e) 695 { 696 // change current marker? 697 if (!isHrefEmpty(a)) 698 // :TODO: isHrefEmpty should be replaceable by some user function in this case! 699 { 700 changeCurrentItem(a, false, false); 701 } 702 // run click handlers and toggle folders if empty href 703 if (isFolder(li)) 704 { 705 // p is a folder 706 if (thiz.hooks.dynamicFolderLinks && !thiz.hooks.dynamicFolderLinks(a)) 707 { 708 if (thiz.config.navigationIcons) 709 { 710 initNavigation(); 711 } 712 if (e) 713 { 714 Nornix.events.cancel(e); 715 } 716 return false; 717 } 718 if (isHrefEmpty(a)) 719 { 720 toggle(li); 721 if (e) 722 { 723 Nornix.events.cancel(e); 724 } 725 return false; 726 } 727 } 728 else 729 { 730 // p is not a folder, then it iss a document 731 if (thiz.hooks.dynamicDocumentLinks && !thiz.hooks.dynamicDocumentLinks(a)) 732 { 733 if (thiz.config.navigationIcons) 734 { 735 initNavigation(); 736 } 737 if (e) 738 { 739 Nornix.events.cancel(e); 740 } 741 return false; 742 } 743 } 744 return true; // not found 745 } 746 747 /** 748 * If node is of type string, resolve it to the corresponding A element. 749 * @param {HTMLAnchorElement|string} 750 * @return {HTMLAnchorElement} the node 751 */ 752 function resolveNode(node) 753 { 754 if (typeof(node) === "string") 755 { 756 return searchNode(node); 757 } 758 return node; 759 } 760 761 /** 762 * Get node from text. 763 * Will match te first node that contains the value of 764 * the text parameter. Make sure it has enough uniqueness. 765 * If no found in the href attributes, it will search the 766 * link texts too, if not configured otherwise. 767 * @param {string} text text to search for 768 * @return {HTMLAnchorELement} node or null 769 */ 770 function searchNode(text) 771 { 772 var a, collection = getMenuNodes(); 773 switch (thiz.config.searchNodeMode) 774 { 775 case 0: 776 return searchInText(collection, text) || searchInHref(collection, text); 777 case 1: 778 return searchInHref(collection, text) || searchInText(collection, text); 779 case 2: 780 return searchInText(collection, text); 781 case 3: 782 return searchInHref(collection, text); 783 } 784 return null; 785 } 786 787 788 /** 789 * Get node from text, compare to text contents. 790 * @param {string} text text to search for 791 * @return {HTMLAnchorELement} node or null 792 */ 793 function searchInText(collection, text) 794 { 795 var i = 0, a; 796 while (a = collection[i++]) 797 { 798 if ((currentItem && a === currentItem && Nornix.dom.getTextContent(currentItem).indexOf(text) !== -1) 799 || (Nornix.dom.getTextContent(a).indexOf(text) !== -1)) 800 { 801 return a; 802 } 803 } 804 return null; 805 } 806 807 /** 808 * Get node from text, compare to href attributet contents. 809 * @param {string} text text to search for 810 * @return {HTMLAnchorELement} node or null 811 */ 812 function searchInHref(collection, text) 813 { 814 var i = 0, a; 815 while (a = collection[i++]) 816 { 817 if ((currentItemHref && a === currentItem && currentItemHref.indexOf(text) !== -1) 818 || (a.href.indexOf(text) !== -1)) 819 { 820 return a; 821 } 822 } 823 return null; 824 } 825 826 /** 827 * Get all Anchor elements from the menu as a static copy. 828 * @param {boolean} reset clean the list if it already exists 829 * @return {HTMLAnchorElement[]} 830 */ 831 function getMenuNodes(reset) 832 { 833 if (!thiz.menuNodes || reset) 834 { 835 thiz.menuNodes = Nornix.dom.live2copy(thiz.menu.getElementsByTagName('a')); 836 } 837 return thiz.menuNodes; 838 } 839 840 /** 841 * Open tree to the specified node. 842 * See the public interace of the function. 843 */ 844 function openToNode(node, setFocus, makeCurrent, doClick, e) 845 { 846 var li, a; 847 node = resolveNode(node); 848 if (!node) return true; 849 if (Nornix.dom.eqNodeName(node, "a")) 850 { 851 a = node; 852 li = a.parentNode; 853 } 854 else 855 { 856 li = node; 857 // - find an <a> element that is a child of the menu element 858 Nornix.dom.findChildOfType( 859 li, "a", function (theA) 860 { 861 a = theA; 862 } 863 ); 864 } 865 if (!a) return true; // :TODO: error handling 866 if (makeCurrent && !doClick) setEmptyHrefAsCurrent(a); 867 if (isFolder(li)) // only open if li is folder 868 { 869 Nornix.css.add(li, "folder"); // why this?? 870 makeOpen(li); 871 } 872 // trace upwards in the tree to open the "path" to the current page 873 node = li.parentNode.parentNode; 874 while (node && node != thiz.menu) 875 { 876 makeOpen(node); 877 node = node.parentNode.parentNode; 878 } 879 ieFix(); 880 if (setFocus) 881 { 882 focusAnchor(a); 883 } 884 if (doClick) 885 { 886 if (a.click) 887 { 888 a.click(); 889 return false; 890 } 891 if (a.onclick && !a.onclick()) return false; 892 if (!performClick(li, a, e)) return false; 893 // fake a link click 894 if (a.target) 895 { 896 if (window.top.frames && window.top.frames[a.target]) 897 { 898 window.top.frames[a.target].location.href = a.href; 899 return false; 900 } 901 // :TODO: handle special cases _blank _top _self _parent 902 return true; 903 } 904 window.location.href = a.href; 905 return false; 906 } 907 return true; 908 } 909 910 /** 911 * Set classes to display tree menu. 912 * Dynamically set classes on menu items, when not set in the HTML code. 913 * Uses the classes folder/document/open/closed/last. 914 * Make folders from the current element/page and "upwards" open. 915 * Prepare tree from stored state in cookie. 916 * Adds span elements inside all li elements. 917 * Adds event handlers on menu items. 918 * Creates the thiz.menuFolders array of all menu folders. 919 * @requires Nornix.cookies Uses the Nornix.cookies.read() function. 920 */ 921 function setClasses () 922 { 923 var folders = []; 924 var i, li, a, itemIsFolder, chr; 925 var menuElements = thiz.menuElements; 926 // setup 927 var span; 928 // setup list of folder elements 929 var iFolder = 0; 930 931 if (thiz.config.dynamicClasses) 932 { 933 var page = window.location.href; 934 // set up root node 935 // - find an <a> element that is a child of the menu element 936 Nornix.dom.findChildOfType( 937 thiz.menu, "a", function (a) 938 { 939 Nornix.css.add(a, "root"); 940 if (a.href && a.href === page) 941 { 942 setEmptyHrefAsCurrent(a); 943 } 944 } 945 ); 946 947 // loop list items 948 i = 0; 949 while (li = menuElements[i++]) 950 { 951 a = li.firstChild; 952 itemIsFolder = isFolder(li); 953 if (itemIsFolder) 954 { 955 folders[folders.length] = li; 956 chr = oldTree.charAt(iFolder++); 957 makeFolder(li, a, (chr && chr === "-")); 958 } 959 else 960 { 961 Nornix.css.add(li, "document"); 962 } 963 if (allowWhitespace) 964 { 965 Nornix.dom.findChildOfType( 966 li.parentNode, "li", 967 function (last) 968 { 969 if (li === last) 970 { 971 Nornix.css.add(li, "last"); 972 } 973 }, 974 true // search backwards 975 ); 976 } 977 else 978 { 979 if (li === li.parentNode.lastChild) 980 { 981 Nornix.css.add(li, "last"); 982 } 983 } 984 if (a && a.href && a.href === page) 985 { 986 openToNode(a, thiz.config.focusCurrentItem, true); 987 } 988 } 989 } 990 else 991 { 992 i = 0; 993 while (li = menuElements[i++]) 994 { 995 a = li.firstChild; 996 itemIsFolder = isFolder(li); 997 // current item? 998 if (isHrefEmpty(a)) 999 { 1000 setEmptyHrefAsCurrent(a); 1001 } 1002 if (itemIsFolder) 1003 { 1004 folders[folders.length] = li; 1005 // open/close folders with the space bar or enter key or mouse click 1006 chr = oldTree.charAt(iFolder++); 1007 makeFolder(li, a, (chr && chr === "-")); 1008 } 1009 } 1010 // is root node also current? 1011 i = 0; 1012 while (a = thiz.menu.childNodes[i++]) 1013 { 1014 if (Nornix.dom.eqNodeName(a, "a")) 1015 { 1016 setEmptyHrefAsCurrent(a); 1017 break; 1018 } 1019 } 1020 } 1021 thiz.menuFolders = folders; 1022 } 1023 1024 /** 1025 * Make LI element a folder. 1026 * @param {HTMLLIElement} li menu item to make a folder 1027 * @param {HTMLAnchorElement} a anchor element in menu item 1028 * @param {boolean} isOpen true to set folder to an open state 1029 */ 1030 function makeFolder(li, a, isOpen) 1031 { 1032 span = folderSpan.cloneNode(false); 1033 li.insertBefore(span, a); 1034 Nornix.css.swap(li, "document", "folder"); 1035 if (isOpen) 1036 { 1037 makeOpen(li); 1038 } 1039 else 1040 { 1041 makeClosed(li); 1042 } 1043 } 1044 1045 /** 1046 * Make LI element a document (from being a folder). 1047 * @param {HTMLLIElement} li menu item to make a folder 1048 */ 1049 function makeDocument(li) 1050 { 1051 li.removeChild(li.firstChild); // remove span on folder 1052 if (li.firstChild.nextSibling) // make sure to remove UL if it exists 1053 { 1054 li.removeChild(li.firstChild.nextSibling); 1055 } 1056 Nornix.css.swap(li, "folder", "document"); 1057 Nornix.css.remove(li, "open"); 1058 Nornix.css.remove(li, "closed"); 1059 } 1060 1061 /** 1062 * Check if Anchor element has no href attribute, in that case, set it to "javascript:;". 1063 * @param {HTMLAnchorElement} a anchor element to test/change 1064 */ 1065 function setEmptyHrefAsCurrent (a) 1066 { 1067 if (thiz.config.dynamicClasses) 1068 { 1069 changeCurrentItem(a, true, false); 1070 } 1071 if (!a.href) 1072 { 1073 changeCurrentItem(a, false, true); 1074 } 1075 } 1076 1077 /** 1078 * Move "current" status to another document node. 1079 * Also remove status on old current node. 1080 * @param {HTMLAnchorElement} a anchor element to change 1081 * @param {boolean} [remove] set to true to remove href attribute 1082 * @param {boolean} [setDummy] set to true to make a dummy link (javascript:;) 1083 */ 1084 function changeCurrentItem (a, remove, setDummy) 1085 { 1086 // remove current status on old current item 1087 if (currentItem) 1088 { 1089 if (currentItemHref) 1090 { 1091 currentItem.href = currentItemHref; // restore href 1092 } 1093 Nornix.css.remove(currentItem, "current"); 1094 if (thiz.config.markCurrentItem) 1095 { 1096 currentItem.removeChild(currentItem.firstChild); 1097 } 1098 } 1099 currentItemHref = a.href; 1100 currentItem = a; 1101 Nornix.css.add(a, "current"); 1102 if (thiz.config.markCurrentItem) 1103 { 1104 a.insertBefore(folderSpan.cloneNode(false), a.firstChild); 1105 } 1106 if (remove) 1107 { 1108 a.removeAttribute("href"); 1109 } else if (setDummy) 1110 { 1111 a.href = "javascript:;"; 1112 } 1113 } 1114 1115 /** 1116 * Method that saves the current tree state in a cookie. 1117 * @requires Nornix Uses the Nornix.cookies.create() function. 1118 */ 1119 function save() 1120 { 1121 var s = ""; 1122 if (!cleanMemory) 1123 { 1124 var i = 0, li, menuFolders = thiz.menuFolders; 1125 while (li = menuFolders[i++]) 1126 { 1127 if (isOpen(li)) 1128 { 1129 s += "-"; 1130 } 1131 else 1132 { 1133 s += "+"; 1134 } 1135 } 1136 } 1137 Nornix.cookies.create(cookieName, s); 1138 } 1139 1140 /** 1141 * Check if a link is emtpy or "fake-empty". 1142 * "#" or "javascript:;" are regarded as "fake-empty". 1143 * @param {HTMLAnchorElement} node HTML a element 1144 * @return {boolean} true if the link is empty or "fake-empty" 1145 */ 1146 function isHrefEmpty(node) 1147 { 1148 if (node.href && (node.href == emptyHref || node.href === "javascript:;")) 1149 { 1150 return true; 1151 } 1152 return !node.href; 1153 } 1154 1155 /** 1156 * IE bugfix, forces IE to "re-render" the menu 1157 * and sets focus on elements that have fired an event 1158 */ 1159 function ieFix() {} 1160 if (Nornix.util.isIe && Nornix.util.isIeLt7) 1161 { 1162 ieFix = function(li) 1163 { 1164 thiz.menu.style.position = "absolute"; 1165 thiz.menu.style.position = "relative"; 1166 try 1167 { 1168 window.event.srcElement.focus(); 1169 } 1170 catch (err) {} 1171 }; 1172 } 1173 else 1174 { 1175 ieFix = function(){}; 1176 } 1177 1178 /** 1179 * Remove whitespace from node and children. 1180 * One level of unrolling the recursive call seems 1181 * to be the optimal choice in IE6 (which is slow in this case). 1182 * @param {HTMLElement} n HTML element 1183 */ 1184 function removeWhitespace(n) 1185 { 1186 var i = 0, c; 1187 while (c = n.childNodes[i]) 1188 { 1189 switch (c.nodeType) 1190 { 1191 case 1: 1192 var j = 0, c2; 1193 while (c2 = c.childNodes[j]) 1194 { 1195 switch (c2.nodeType) 1196 { 1197 case 1: // element node 1198 removeWhitespace(c2); 1199 break; 1200 case 3: // text node 1201 if (!/\S/.test(c2.nodeValue)) 1202 { 1203 c.removeChild(c2); 1204 continue; 1205 } 1206 break; 1207 case 8: // comments and empty textnodes 1208 c.removeChild(c2); 1209 continue; 1210 } 1211 j++; // don't move! 1212 } 1213 break; 1214 case 3: 1215 if (!/\S/.test(c.nodeValue)) 1216 { 1217 n.removeChild(c); 1218 continue; 1219 } 1220 break; 1221 case 8: 1222 n.removeChild(c); 1223 continue; 1224 } 1225 i++; // don't move this, the deleting of nodes depends on the ++ not being run! 1226 } 1227 } 1228 1229 /* 1230 * Get node li element from a or span child element. 1231 * Traces parent nodes until a li element is found, or the 1232 * menu element is found and null returned. 1233 * @param {HTMLElement, String} el HTML element or id of element 1234 * @return {HTMLLIElement, undefined} node corresponding to element 1235 function getTargetNode (el) 1236 { 1237 if (typeof(el) === 'string') 1238 { 1239 el = document.getElementById(el); 1240 } 1241 while (el && el !== thiz.menu) 1242 { 1243 if (Nornix.dom.eqNodeName(el, 'li')) 1244 { 1245 return el; 1246 } 1247 el = el.parentNode; 1248 } 1249 return null; 1250 } 1251 */ 1252 1253 /** 1254 * Check if a li element is representng a folder in the menu. 1255 * Works different when allowing or not allowing whitespace in the menu. 1256 * @param {HTMLLIElement} li HTML li element 1257 * @return {boolean} true if the li element is a folder in the menu 1258 */ 1259 function isFolder (li) 1260 { 1261 if (allowWhitespace) 1262 { 1263 return Nornix.dom.findChildOfType(li, "ul", function (x) {return true;}); 1264 } 1265 else 1266 { 1267 return li.childNodes.length > 1; 1268 } 1269 } 1270 1271 /** 1272 * Toggle a folder in the menu. 1273 * @param {HTMLLIElement} node HTML li element 1274 */ 1275 function toggle(node) 1276 { 1277 if (!isOpen(node)) 1278 { 1279 makeOpen(node); 1280 } 1281 else 1282 { 1283 makeClosed(node); 1284 } 1285 ieFix(); 1286 } 1287 1288 /** 1289 * Make a folder in the menu open. 1290 * @param {HTMLLIElement} li HTML li element 1291 */ 1292 function makeOpen(li) 1293 { 1294 Nornix.css.swap(li, "closed", "open"); 1295 li.firstChild.title = thiz.texts.closeFolderTitle; 1296 } 1297 1298 /** 1299 * Make a folder in the menu closed. 1300 * @param {HTMLLIElement} li HTML li element 1301 */ 1302 function makeClosed(li) 1303 { 1304 Nornix.css.swap(li, "open", "closed"); 1305 li.firstChild.title = thiz.texts.openFolderTitle; 1306 } 1307 1308 /** 1309 * Close all folders. 1310 */ 1311 function closeAll() 1312 { 1313 var i = 0, menuFolders = thiz.menuFolders; 1314 while (li = menuFolders[i++]) 1315 { 1316 makeClosed(li); 1317 } 1318 ieFix(); 1319 } 1320 1321 /** 1322 * Open all folders. 1323 */ 1324 function openAll() 1325 { 1326 var i = 0, menuFolders = thiz.menuFolders; 1327 while (li = menuFolders[i++]) 1328 { 1329 makeOpen(li); 1330 } 1331 ieFix(); 1332 } 1333 1334 /** 1335 * Move focus to the a element in this LI or DIV element. 1336 * @param {HTMLLIElement} node HTML li element (or other element) 1337 * @return {boolean} true if the move was successful. 1338 */ 1339 function focusNode (node) 1340 { 1341 if (!node || !node.firstChild) return false; 1342 var n = node.firstChild; 1343 if (focusAnchor(n)) return true; 1344 if (!n.nextSibling || node === thiz.menu) return false; 1345 n = n.nextSibling; 1346 return focusAnchor(n); 1347 } 1348 1349 /** 1350 * Move focus to the a element sent. 1351 * @param {HTMLAnchorElement} a HTML A element 1352 * @return {boolean} true if the move of focus was successful. 1353 */ 1354 function focusAnchor (a) 1355 { 1356 if (Nornix.dom.eqNodeName(a, "a")) 1357 { 1358 a.focus(); 1359 selectedItem = a; 1360 return true; 1361 } 1362 return false; 1363 } 1364 1365 /** 1366 * Get the current best guess on the selected menu item. 1367 * @return {HTMLAnchorElement} the selected item 1368 */ 1369 function getSelectedItem() 1370 { 1371 return selectedItem ? selectedItem : currentItem; 1372 } 1373 1374 /** 1375 * Initialize the navigation object. 1376 * Calculate first, previous, next and last menu items. 1377 */ 1378 function initNavigation() 1379 { 1380 var node = selectedItem ? selectedItem : currentItem, 1381 nodes = getMenuNodes(), i = 0, a; 1382 if (!node || Nornix.css.contains(node, "menuAction")) 1383 { 1384 node = navigation.first = searchRelatedNode(nodes, 0, 1); 1385 } 1386 if (!node) return null; 1387 while (a = nodes[i++]) 1388 { 1389 if (a === node) 1390 { 1391 i--; 1392 break; 1393 } 1394 } 1395 if (i >= nodes.length) return; 1396 if (!navigation.first) 1397 { 1398 navigation.first = searchRelatedNode(nodes, 0, 1); 1399 } 1400 navigation.last = searchRelatedNode(nodes, nodes.length - 1, -1); 1401 navigation.next = searchRelatedNode(nodes, i + 1, 1); 1402 if (!navigation.next) 1403 { 1404 navigation.next = navigation.first; 1405 } 1406 if (i < 1) 1407 { 1408 navigation.previous = navigation.last; 1409 } 1410 else 1411 { 1412 navigation.previous = searchRelatedNode(nodes, i - 1, -1); 1413 } 1414 } 1415 1416 /** 1417 * Open page from navigation item. 1418 * @param {string} one of "first", "previous", "next", "last" 1419 */ 1420 function goToNavigationItem(name) 1421 { 1422 if (!navigation[name]) initNavigation(); 1423 if (!navigation[name]) return; 1424 var node = navigation[name]; 1425 if (!node) return; 1426 openToNode(node, true, true, true); 1427 } 1428 1429 /** 1430 * Find the next real link in any direction. 1431 * @param {HTMLAnchorELement[]} nodes nodes to search in 1432 * @param {integer} i node position to start from (index in the nodes collection) 1433 * @param {integer} direction +1 is forward, -1 is backward 1434 * @return {HTMLAnchorElement|null} next "real" node in the selected direction 1435 */ 1436 function searchRelatedNode(nodes, i, direction) 1437 { 1438 var a; 1439 while ((a = nodes[i]) && (a !== currentItem || currentItemHref === "javascript:;") && isHrefEmpty(a)) {i += direction} 1440 if (i < nodes.length && i >= 0) 1441 { 1442 return nodes[i]; 1443 } 1444 return null; 1445 } 1446 1447 /** 1448 * Check if folder is open. 1449 * @param {HTMLLIElement} node HTML li element 1450 * @return {boolean} true if the folder is open 1451 */ 1452 function isOpen(li) 1453 { 1454 return li.className.search(openPattern) !== -1; 1455 } 1456 1457 /** 1458 * Create an Anchor element for use as "action button". 1459 * @param {string} className class name of anchor 1460 * @param {string} title title of anchor 1461 * @return {HTMLAnchorElement} anchor "button" 1462 */ 1463 function createMenuActionAnchor(className, title, func) 1464 { 1465 var a = actionAnchor.cloneNode(false); // new <a> tag 1466 a.className = className + " menuAction"; 1467 a.title = title; 1468 thiz.registerMenuButton(a, func); 1469 return a; 1470 } 1471 1472 /** 1473 * Add icons as HTML anchor elements for opening and closing all icons. 1474 */ 1475 function createOpenCloseAllIcons() 1476 { 1477 thiz.menu.insertBefore(createMenuActionAnchor("closeTree", thiz.texts.closeTreeTitle, closeAll), 1478 thiz.menu.firstChild.nextSibling); 1479 thiz.menu.insertBefore(createMenuActionAnchor("openTree", thiz.texts.openTreeTitle, openAll), 1480 thiz.menu.firstChild.nextSibling.nextSibling); 1481 } 1482 1483 /** 1484 * Add icons as HTML anchor elements for navigating. 1485 */ 1486 function createNavigationIcons() 1487 { 1488 /* var relLink = document.createElement("link"); 1489 var link = 1490 document.documentElement.firstChild.appendChild(link.cloneNode(false));*/ 1491 thiz.menu.appendChild(createMenuActionAnchor("treeNavigation goFirstTree", thiz.texts.goFirstTitle, thiz.goToFirstNode)); 1492 thiz.menu.appendChild(createMenuActionAnchor("treeNavigation goPrevTree", thiz.texts.goPrevTitle, thiz.goToPrevNode)); 1493 thiz.menu.appendChild(createMenuActionAnchor("treeNavigation goNextTree", thiz.texts.goNextTitle, thiz.goToNextNode)); 1494 thiz.menu.appendChild(createMenuActionAnchor("treeNavigation goLastTree", thiz.texts.goLastTitle, thiz.goToLastNode)); 1495 } 1496 } 1497 1498 /** 1499 * Configuration of the menu. 1500 * 1501 * To change all instances, use: Nornix.TreeMenu.prototype.config.parameter = value; 1502 * To change a particular instance, use: treemenu.config.parameter = value; 1503 * 1504 * @memberOf Nornix.TreeMenu 1505 * @namespace Nornix.TreeMenu.config 1506 * @param {boolean} dynamicClasses Turn dynamic class population on/off. 1507 * @param {boolean} openCloseAll Add icons for open/close all folders. 1508 * @param {boolean} navigationIcons Add icons for navigation. 1509 * @param {boolean} markCurrentItem Add an empty span to mark the current element. 1510 * @param {boolean} focusCurrentItem Move focus to the current menu item. 1511 * @param {string|boolean} contentElement ID of the page content containing element. Set to false to disable. 1512 * @param {string|boolean} menuLinkElement ID of a link to the menu. Set to false to disable. 1513 * @param {string[]} preloadImages Names of images to preload. Be careful with the order of the images! 1514 * @param {string} imagePath Path to the images used in the menu. Used for preloading. 1515 * @param {string} imageExtension Extension for image files. A dot is not automatically prepended! 1516 * @param {integer} searchNodeMode How to resolve nodes when searching for them: 1517 * 0: try text search first, then try href attributes 1518 * 1: try href attributes first, then try text 1519 * 2: try only text search 1520 * 3: try only href attribute search 1521 */ 1522 Nornix.TreeMenu.prototype.config = 1523 { 1524 "dynamicClasses" : true, 1525 "openCloseAll" : true, 1526 "navigationIcons" : true, 1527 "markCurrentItem" : false, 1528 "focusCurrentItem" : false, 1529 "contentElement" : false, 1530 "menuLinkElement" : false, 1531 "preloadImages" : 1532 [ 1533 "treemenu-sprites", 1534 "treemenu-line", 1535 "treemenu-corner" 1536 /* "home-icon", 1537 "close-icon", 1538 "open-icon", 1539 "plus-node", 1540 "minus-node", 1541 "folder-closed-icon", 1542 "doc-node-icon", 1543 "folder-open-icon", 1544 "treemenu-line", 1545 "treemenu-current", 1546 "first", 1547 "previous", 1548 "next", 1549 "last"*/ 1550 ], 1551 "imagePath" : "style/nornix-", 1552 "imageExtension" : ".png", 1553 "searchNodeMode" : 0 1554 }; 1555 1556 /** 1557 * Extension points to add your own hooks inside the tree menu. 1558 * 1559 * The functions used will receive the clicked HTMLAnchorElement as 1560 * a parameter. To prevent any further event handling, let the function 1561 * return false, otherwise let it return true. 1562 * 1563 * To change all instances, use: Nornix.TreeMenu.prototype.hooks.parameter = value; 1564 * To change a particular instance, use: treemenu.hooks.parameter = value; 1565 * 1566 * @memberOf Nornix.TreeMenu 1567 * @namespace Nornix.TreeMenu.hooks 1568 * @param {Function|boolean} dynamicDocumentLinks Function to handle document node clicks (or set to false). 1569 * @param {Function|boolean} dynamicFolderLinks Function to handle folder node clicks (or set to false). 1570 */ 1571 Nornix.TreeMenu.prototype.hooks = 1572 { 1573 "dynamicDocumentLinks" : false, 1574 "dynamicFolderLinks" : false 1575 }; 1576 1577 /** 1578 * Texts used by the tree menu. 1579 * 1580 * To change all instances, use: Nornix.TreeMenu.prototype.texts.parameter = value; 1581 * To change a particular instance, use: treemenu.texts.parameter = value; 1582 * 1583 * To change all texts use: Nornix.TreeMenu.prototype.texts = {...}; or 1584 * treemenu.prototype.texts = {...}; (see source code!) 1585 * 1586 * @memberOf Nornix.TreeMenu 1587 * @namespace Nornix.TreeMenu.texts 1588 * @param {string} closeTreeTitle Text for "close all" title attribute 1589 * @param {string} openTreeTitle Text for "open all" title attribute. 1590 * @param {string} closeFolderTitle Text for "close folder" title attribute. 1591 * @param {string} openFolderTitle Text for "open folder" title attribute. 1592 * @param {string} goFirstTitle Text for "go to first" title attribute. 1593 * @param {string} goLastTitle Text for "go to last" title attribute. 1594 * @param {string} goNextTitle Text for "go to next" title attribute. 1595 * @param {string} goPrevTitle Text for "go to previous" title attribute. 1596 */ 1597 Nornix.TreeMenu.prototype.texts = 1598 { 1599 "closeTreeTitle" : "close all folders", 1600 "openTreeTitle" : "open all folders", 1601 "closeFolderTitle" : "close folder", 1602 "openFolderTitle" : "open folder", 1603 "goFirstTitle" : "first page", 1604 "goLastTitle" : "last page", 1605 "goNextTitle" : "next page", 1606 "goPrevTitle" : "previous page" 1607 }; 1608 1609 1610 // start the script 1611 var treemenu = new Nornix.TreeMenu(); 1612 treemenu.start(); 1613