diff --git a/admin_menu.css b/admin_menu.css index f93784a..ff1719a 100644 --- a/admin_menu.css +++ b/admin_menu.css @@ -97,6 +97,9 @@ body.admin-menu { margin-top: 20px !important; } /* #210615: Mozilla on Mac fix */ html.js fieldset.collapsible div.fieldset-wrapper { overflow: visible; } +/* Keyboard navigation */ +div#admin-menu li.focused { background-color: #75757a; } + @media print { #admin-menu { display: none; } body.admin-menu { margin-top: 0 !important; } diff --git a/admin_menu.inc b/admin_menu.inc index bfe1ee2..aca0146 100644 --- a/admin_menu.inc +++ b/admin_menu.inc @@ -314,14 +314,14 @@ function admin_menu_adjust_items(&$menu_links, &$sort) { $links[] = array( 'title' => 'icon_users', 'path' => 'user', - 'weight' => -90, + 'weight' => 999, 'parent_path' => '', 'options' => array('extra class' => 'admin-menu-action admin-menu-icon admin-menu-users', 'html' => TRUE), ); $links[] = array( 'title' => 'Log out @username', 'path' => 'logout', - 'weight' => -100, + 'weight' => 1000, 'parent_path' => '', // Note: @username is dynamically replaced by default, we just invoke // replacement by setting the 't' key here. diff --git a/admin_menu.js b/admin_menu.js index 2a24251..a511bf3 100644 --- a/admin_menu.js +++ b/admin_menu.js @@ -47,4 +47,313 @@ $(document).ready(function() { uls.css({left: '-999em', display: 'none'}); }, 400); }); + + + // Keyboard navigation. + var tree, active = false, current; + + /** + * Initialize function, caches the DOM tree in an JavaScript object. + */ + + var createTree = function() { + + /** + * Cache menu's DOM as an JavaScript Object. This improves dramaticaly the + * navigation performance, cause we are not required to travers the DOM while + * each keypress instead we just travers the js object later. + * + * @param HTMLListElement element + * An HTMLListElement object containing the current LI Element + * @param integer index + * An number inidicating serial index + * @return + * This is the current structure of the return object + * { + * li: current li element + * a: current element's first A element + * children: recursively collected array of current element's children + * index: serial number of current element + * uls: immediate descendant ul + * } + */ + + var collectElements = function (element, index) { + // Cache creation of jQuery object + var _this = $(element); + + // Set default tree object + var tree = { li: _this, a: _this.find("> a"), index: index, alignment: _this.css("float") }; + + // Check whether this element has children + var children; + if ((children = _this.find("> ul > li")).length) { + // Yes, we have children, let us collect all children elements recursively + tree.children = $.map(children, collectElements); + } + return tree; + }; + + tree = $.map($("#admin-menu > ul > li"), collectElements); + + /** + * Walks through the tree object and creates parents references on the fly. + * No return is required because we are working with object reference. + * + * @param array array + * Array with elements for looping through + * @param object parent + * Refrence to the parent object + * @param interger parentIndex + * Specifies the position of parent in array + * @param interger level + * Current element depth + */ + var setParents = function (array, parent, parentIndex, level) { + // fallback level on initialization + var level = level || 1; + + + for (var i = 0; i < array.length; i++) { + // Chache object. + var object = array[i]; + + // Set current depth. + object.level = level; + + // Create reference to the parent object. + if (parent) object.parent = parent; + + // Set parent's position in one's array. + object.parentIndex = parentIndex || i; + + // Do we have children? If yes go lets apply the same on our children. + if (object.children) arguments.callee(object.children, object, i, level + 1); + } + }; + + + // Trigger the setParents function + setParents(tree); + + // Toggle the order of right floated elements + var rightFloated = [], previousElement; + + for (var i = tree.length - 1; i >= 0; i--){ + if(tree[i].alignment == "right") { + rightFloated.push(tree.pop()); + + // Toggle index and parentIndex from current to previous element + if(previousElement) { + var index = rightFloated[rightFloated.length-1].index; + var cur = rightFloated[rightFloated.length-1]; + var prev = rightFloated[rightFloated.length-2]; + cur.index = cur.parentIndex = previousElement.index; + prev.index = prev.parentIndex = index; + } + previousElement = rightFloated[rightFloated.length-1]; + } + }; + // Append rightFloated elements back to the tree + tree = tree.concat(rightFloated); + }; + + // We don't need delayed mouse out so copy and paste from above without timeout + var mouseLeave = function(element) { + element.find('> ul').css({left: '-999em', display: 'none'}); + }; + + // Allow while Keyboard navigation, mouse navigation too + $("#admin-menu a").bind("mouseenter", function(event){ + current.a.unbind("blur", blur); + }); + + var blur = function(event) { + // Find all active tabs and close them + mouseLeave($("#admin-menu li")); + + // Color mark should be erased, + $("#admin-menu .focused").removeClass("focused"); + + // Set current to undefined, so on next initialize + // the current element would be picked again. + current = undefined; + + active = false; + }; + + + // Attach the keydown trigger function on the document. + // This is on place where we could pick off the keydown event in all browsers. + var keyevent = function (event) { + // Cache keyCode + var key = event.keyCode || event.which; + + // If not left, top, right, down or shift + space is pressed. Break here. + if (!(key == 37 || key == 38 || key == 39 || key == 40 || (key == 32 && event.shiftKey))) { + return true; + } + + // Does somebody pressed shift + space? + if (key == 32 && event.shiftKey) { + + // Toggle the active state + active = !active; + + // If the keyboard navigation is not active anymore. + if (!active) { + // Blur the current A element. + current.a.blur(); + blur(); + // Cancel event's propaganation. + return false; + } + } + + // If the menu is not active, break here. + if (!active) { + return false; + } + + // If the tree is not initializated, create. + if(!tree) { + createTree(); + } + + // Reset first element on initialization. + if (!current) { + current = tree[1] || tree[0]; + } + + var next; + + switch (key) { + case 37: // Left + // Do we have first level here and can we go to the previous element? + if (current.level == 1 && tree[current.parentIndex - 1]) { + // Yes, our next selection is current's previous element. + next = tree[current.parentIndex - 1]; + } + // Handle third and deeper levels. + else if (current.level > 2) { + // Our next element is our current parent. + next = current.parent; + } + // If somebody presed left in the second level. + else if(current.parent) { + // Go to the parents previous element. + next = tree[current.parent.parentIndex - 1]; + } + break; + + case 38: // Up + // We have first level. + if (current.level == 1 && current.children) { + // Next is our last child. + next = current.children[current.children.length - 1]; + } + // We are at the second level and we are not on the top. + else if (!(current.level == 2 && current.index == 0)) { + if(current.index == 0) { + next = current.parent.children[current.parent.children.length-1]; + } else if(current.parent){ + // Select previous element. + next = current.parent.children[current.index - 1]; + } + } + // Second level an we are at the top + else if (current.level == 2 && current.index == 0) { + // Select our parent, cause parent is visualy more at the top. + next = current.parent; + } + break; + + case 39: // Right + // First level? + if (current.level == 1) { + // Next element is set? + if(tree[current.parentIndex + 1]) { + next = tree[current.parentIndex + 1]; + } + } + // Handle third level and deeper. Do we have chidlren? + else if (current.children) { + // Select our first child. + next = current.children[0]; + } + // Are we at the second level? + else if (current.level == 2) { + // Select visualy next element. + next = tree[current.parent.parentIndex + 1]; + } + break; + + case 40: // Down + // Do we are at the first level and have children? + if (current.level == 1 && tree[current.parentIndex].children) { + // Get first level element's first child. + next = tree[current.parentIndex].children[0]; + } + // Last element? + else if (current.parent && current.index == current.parent.children.length-1) { + // Are we at the second level. + if(current.level == 2) { + // Our next element is our parent. + next = tree[current.parent.index]; + } else { + next = current.parent.children[0]; + } + } else if(current.parent) { + // Just select our previous element. + next = current.parent.children[current.index + 1]; + } + break; + } + + // Fallback to current if next is not set. + if (!next) { + next = current; + } + + // Remove previous mark. + $("#admin-menu .focused").removeClass("focused"); + + // Do we go up or just have children and are at the same level? + if ((next.level < current.level) || (!next.children && next.level == current.level)) { + // Hid not required elements. + if (next.children == undefined && current.children == undefined && next.level == 1 && current.parent) { + mouseLeave(current.parent.li); + } else{ + mouseLeave(current.li); + } + } + + // Set current element mark. + next.li.parents("li").andSelf().addClass('focused'); + + // Activate the enter key + next.a.focus(); + + // Allow while Keyboard navigation, mouse navigation too + current.a.trigger("mouseenter"); + next.a.bind("blur", blur); + + // Show descendant ul + next.li.trigger('mouseenter'); + + // Save next object, so we could fall back on this next time + current = next; + + // Cancel event propagation, in all browsers. + return false; + }; + + // MSIE and Safari need to stop event propagation the keydown event + if(jQuery.browser.msie || jQuery.browser.safari) { + $(document).bind('keydown', keyevent); + } + // All other browsers are happy with the keypress event + else { + $(document).bind('keypress', keyevent); + } });