/* Menu class */ var MENU_IDX_ID = 0; // ID: A number or string; null makes the menu item a separator var MENU_IDX_NAME = 1; // Name: A string var MENU_IDX_URL = 2; // URL: A string for the URL, or a function to call when the menu item is clicked var MENU_IDX_SUB = 3; // Submenu: Child menu var MENU_IDX_OPT = 4; // Options: JSON array with additional options var Menu = new function() { var self = this; /***********/ /* PUBLIC */ /**********/ self.add = function(node, menu, opt) // Add a menu to a DOM element { if(!opt) opt = $.noop; var $node = $(node); $node.data('menu', menu); if(opt.showAtCursor) { $node.click(rootNodeClick); } else { $node .mouseover(rootNodeOver) .mouseout(rootNodeOut); } }; self.remove = function(node) // Remove menu from DOM element { $(node) .data('menu', null) .unbind('click', rootNodeClick) .unbind('mouseover', rootNodeOver) .unbind('mouseout', rootNodeOut); }; self.show = function(menu, node) { var $node = $(node); show(menu, $node); } self.showAtCursor = function(menu, event) // {event} must have been normalized by jQuery { showAtXY(menu, event.pageX, event.pageY); } self.showAtXY = function(menu, x, y) { showAtXY(menu, x, y); } self.hide = function() { rootNodeOut(); } self.addButtons = function(dest, menu) // Create a set of menu buttons (as seen on the homepage or in the top bar) { var $dest = $(dest); if(!$dest.length) return; var $buttons = $(''); $.each(menu, function(idx, menuItem) { if(isSeparator(menuItem)) return; var $a = $('') var $span = $('', { text: menuItem[MENU_IDX_NAME] }).appendTo($a); self.linkifyItem(menuItem, $a); if(hasSubmenu(menuItem)) { $span.addClass('hassubmenu'); self.add($a, menuItem[MENU_IDX_SUB]); } $buttons.append($a); }); $dest.append($buttons); }; self.linkifyItem = function(menuItem, $a) // Also used by PageTemplate when generating the top tabs { var opt = self.getItemOpt(menuItem); if(!menuItem[MENU_IDX_URL]) // Unlinked { $a.attr('href', 'javascript:;'); $a.addClass('unlinked'); return; } if(typeof menuItem[MENU_IDX_URL] == 'function') // Function { $a.attr('href', 'javascript:;'); $a.click(hide); $a.click(menuItem[MENU_IDX_URL]); } else // URL { var url = self.getItemUrl(menuItem); $a.attr('href', url); if(opt.newWindow || g_isExternalUrl(url)) $a.attr('target', '_blank'); if(opt.rel) $a.attr('rel', opt.rel); } if(typeof menuItem[MENU_IDX_OPT] == 'object' && menuItem[MENU_IDX_OPT].className) $a.addClass(menuItem[MENU_IDX_OPT].className); }; self.updateItem = function(menuItem) // Also used by PageTemplate when the breadcrumb changes { var $a = menuItem.$a; if(!$a) return; var opt = self.getItemOpt(menuItem); $a.removeClass('checked tinyicon icon'); $a.css('background-image', ''); if(menuItem.checked) { $a.addClass('checked'); } else if(opt.tinyIcon) { $a.addClass('tinyicon'); $a.css('background-image', 'url(' + (opt.tinyIcon.indexOf('/') != -1 ? opt.tinyIcon : g_staticUrl + '/images/wow/icons/tiny/' + opt.tinyIcon.toLowerCase() + '.gif') + ')'); } else if(opt.icon) { $a.addClass('icon'); $a.css('background-image', 'url(' + opt.icon + ')'); } else if(opt.socketColor && g_file_gems[opt.socketColor]) { $a.addClass('socket-' + g_file_gems[opt.socketColor]); } var className = (opt['class'] || opt.className); if(className) $a.addClass(className); }; // MENU UTILITY FUNCTIONS self.hasMenu = function(node) { var $node = $(node); return $node.data('menu') != null; } self.modifyUrl = function(menuItem, params, opt) { var json = { params: params, opt: opt }; traverseMenuItem(menuItem, function(x) { x.modifyUrl = json; }); PageTemplate.updateBreadcrumb(); }; self.fixUrls = function(menu, url, opt) { opt = opt || {}; opt.hash = (opt.hash ? '#' + opt.hash : ''); fixUrls(menu, url, opt, 0); }; self.sort = function(menu) // Sort a specific menu { if(hasSeparators(menu)) sortSeparatedMenu(menu); else sortMenu(menu); }; self.sortSubmenus = function(menu, paths) // Sort the submenus of {menu} specified by {paths} { $.each(paths, function(idx, path) { var submenu = self.findItem(menu, path); if(submenu && submenu[MENU_IDX_SUB]) self.sort(submenu[MENU_IDX_SUB]); }); }; self.implode = function(menu, opt) // Return a new menu where items preceded by a heading are merged into a single item w/ a submenu { if(!opt) opt = $.noop; var result = []; var groupedMenu; if(opt.createHeadinglessGroup) { groupedMenu = []; result.push([0, '', null, groupedMenu]); } $.each(menu, function(idx, menuItem) { if(isSeparator(menuItem)) { groupedMenu = []; result.push([0, menuItem[MENU_IDX_NAME], null, groupedMenu]); } else { if(groupedMenu) groupedMenu.push(menuItem); else result.push(menuItem); } }); return result; }; self.findItem = function(menu, path) // Return the menu item specified by {path} in {menu} { return self.getFullPath(menu, path).pop(); }; self.getFullPath = function(menu, path) // Return an array with all the menu items specified by {path} in {menu} { var result = []; for(var i = 0; i < path.length; ++i) { var pos = findItemPosById(menu, path[i]); if(pos != -1) { var menuItem = menu[pos]; menuItem.parentMenu = menu; menu = menuItem[MENU_IDX_SUB]; result.push(menuItem); } } return result; }; self.getItemUrl = function(menuItem) { var url = menuItem[MENU_IDX_URL]; if(!url) return null; var opt = self.getItemOpt(menuItem); if(menuItem.modifyUrl) url = g_modifyUrl(url, menuItem.modifyUrl.params, menuItem.modifyUrl.opt); return url; }; self.getItemOpt = function(menuItem) { if(!menuItem[MENU_IDX_OPT]) menuItem[MENU_IDX_OPT] = {}; return menuItem[MENU_IDX_OPT]; }; self.removeItemById = function(menu, id) { var pos = findItemPosById(menu, id); if(pos != -1) menu.splice(pos, 1); }; self.setSubmenu = function (menuItem, submenu) { menuItem[MENU_IDX_SUB] = submenu; }; /***********/ /* PRIVATE */ /***********/ var DELAY_BEFORESHOW = 25; var DELAY_BEFOREHIDE = 333; var MAX_COLUMNS = 4; // Should match the styles used in Menu.css var SIZE_SHADOW_WIDTH = 6; var SIZE_SHADOW_HEIGHT = 6; var SIZE_BORDER_HEIGHT = 3; var SIZE_MENUITEM_HEIGHT = 26; var inited = false; var timer; var $rootNode; var visibleMenus = {}; var openLinks = {}; var divCache = {}; var menuItemsCache = {}; var nextUniqueId = 0; function init() { if(inited) return; inited = true; // Run a test to get the height of a menu item (in case it's different in some browser/OS combos) var $div = $('
').css({left: '-1000px', top: '-1000px'}).appendTo(document.body); var height = $div.children('a').outerHeight(); $div.remove(); if(height > 15) // Safety SIZE_MENUITEM_HEIGHT = height; } function show(menu, $node) { if($rootNode) $rootNode.removeClass('open'); $rootNode = $node; $rootNode.addClass('open'); displayFirstMenu(menu); } function hide() { if($rootNode) { $rootNode.removeClass('open'); $rootNode = null; } hideMenus(0); } function showAtXY(menu, x, y) { clearTimeout(timer); displayFirstMenu(menu, x, y); } function displayFirstMenu(menu, x, y) { closeLinks(0); displayMenu(menu, 0, x, y); hideMenus(1); } function displayMenu(menu, depth, x, y) { init(); beforeShowMenu(menu); var $div = createDiv(depth); var $menuItems = createMenuItems(menu); var $menu = createMenu($menuItems, depth); $div.append($menu); var animated = !isMenuVisible(depth); visibleMenus[depth] = $div; var pos = getMenuPosition($div, depth, x, y); $div.css({ left: pos.x + 'px', top: pos.y + 'px' }); revealDiv($div, animated); } function createDiv(depth) { if(divCache[depth]) { var $div = divCache[depth]; $div.children().detach(); // Similar to $div.empty(), except custom data is preserved. return $div; } var $div = $('') .mouseover(menuDivOver) .mouseleave(menuDivOut) .delegate('a', 'mouseenter', { depth: depth }, menuItemOver) .delegate('a', 'click', menuItemClick); $div.appendTo(document.body); divCache[depth] = $div; return $div; } function createMenuItems(menu) { var uid = getUniqueId(menu); if(menuItemsCache[uid]) return menuItemsCache[uid]; var nextSeparator; // Used to make sure a separator is always followed by a visible item var menuItems = []; $.each(menu, function(idx, menuItem) { if(!isItemVisible(menuItem)) return; $a = createMenuItem(menuItem); if(isSeparator(menuItem)) { nextSeparator = $a; return; } if(nextSeparator) { menuItems.push(nextSeparator); nextSeparator = null; } menuItems.push($a); }); var $menuItems = $(menuItems); menuItemsCache[menu] = $menuItems; return $menuItems; } function createMenuItem(menuItem) { beforeCreateMenuItem(menuItem); var $a = $(''); menuItem.$a = $a; $a.data('menuItem', menuItem); self.linkifyItem(menuItem, $a); self.updateItem(menuItem); // Separator if(isSeparator(menuItem)) { $a.addClass('separator'); $a.text(menuItem[MENU_IDX_NAME]); return $a; } var $span = $(''); $span.text(menuItem[MENU_IDX_NAME]); $span.appendTo($a); // Submenu if(hasSubmenu(menuItem)) $span.addClass('hassubmenu'); return $a; } function createMenu($menuItems, depth) { var $a = $rootNode; var $w = $(window); var nItems = $menuItems.length; var availableHeight = $w.height() - (SIZE_BORDER_HEIGHT * 2) - SIZE_SHADOW_HEIGHT; var nItemsThatCanFit = Math.floor(Math.max(0, availableHeight) / SIZE_MENUITEM_HEIGHT); // 1 column if(nItemsThatCanFit >= nItems) { var $outerDiv = $(''); var $innerDiv = $(''); $menuItems.each(function () { $innerDiv.append(this) }); $outerDiv.append($innerDiv); $outerDiv.contextmenu($WH.rf); // aowow custom: prevent browser context menu when right clicking to get our context menu as it placed under the mouse cursor return $outerDiv; } // Multiple columns var nColumns = Math.min(MAX_COLUMNS, Math.ceil(nItems / nItemsThatCanFit)); var nItemsPerColumn = Math.ceil(nItems / nColumns); var nItemsAdded = 0; var nItemsRemaining = nItems; var $holder = $(''); while(nItemsRemaining > 0) { var $outerDiv = $(''); var $innerDiv = $(''); var nItemsToAdd = Math.min(nItemsRemaining, nItemsPerColumn); var start = nItemsAdded; var end = start + nItemsToAdd; $menuItems.slice(start, end).each(function() { $innerDiv.append(this) }); $outerDiv.append($innerDiv); $holder.append($outerDiv); nItemsAdded += nItemsToAdd; nItemsRemaining -= nItemsToAdd; } return $holder; } function getMenuPosition($div, depth, x, y) { if(depth == 0) return getFirstMenuPosition($div, depth, x, y); return getSubmenuPosition($div, depth); } function getFirstMenuPosition($div, depth, x, y) { var viewport = g_getViewport(); var menuWidth = $div.width(); var menuHeight = $div.height(); var menuWidthShadow = menuWidth + SIZE_SHADOW_WIDTH; var menuHeightShadow = menuHeight + SIZE_SHADOW_HEIGHT; var showingAtCursor = (x != null && y != null); if(showingAtCursor) { if(y + menuHeightShadow > viewport.b) // Make sure menu doesn't overflow vertically y = Math.max(viewport.t, viewport.b - menuHeightShadow); // Place along the bottom edge if it does } else { var $a = $rootNode; var offset = $a.offset(); // Place underneath the root node by default x = offset.left y = offset.top + $a.outerHeight(); // Show menu upwards if it's going to be outside the viewport otherwise (only if there's room) if(y + menuHeightShadow > viewport.b && offset.top >= menuHeightShadow) y = offset.top - menuHeightShadow; } if(x + menuWidthShadow > viewport.r) // Make sure menu doesn't overflow horizontally x = Math.max(viewport.l, viewport.r - menuWidthShadow); // Place along the right edge if it does return {x: x, y: y}; } function getSubmenuPosition($div, depth) { var viewport = g_getViewport(); var menuWidth = $div.width(); var menuHeight = $div.height(); var menuWidthShadow = menuWidth + SIZE_SHADOW_WIDTH; var menuHeightShadow = menuHeight + SIZE_SHADOW_HEIGHT; var $a = openLinks[depth - 1]; var offset = $a.offset(); var openToTheLeft = false; // Show to the right of the menu item x = offset.left + $a.outerWidth() - 5; y = offset.top - 2; if(x + menuWidthShadow > viewport.r) // Make sure menu doesn't overflow horizontally openToTheLeft = true; if(openToTheLeft) x = Math.max(viewport.l, offset.left - menuWidth); if(y + menuHeightShadow > viewport.b) // Make sure menu doesn't overflow vertically y = Math.max(viewport.t, viewport.b - menuHeightShadow); // Place along the bottom edge if it does return {x: x, y: y}; } function revealDiv($div, animated) { if(animated) { $div.css({ opacity: '0' }) .show() .animate({ opacity: '1' }, 'fast', null, doneRevealing); } else $div.show(); } function doneRevealing(a) { $(this).css('opacity', ''); // Remove opacity once animation is over to prevent a bug in IE8 } function hideMenus(depth) { while(visibleMenus[depth]) { visibleMenus[depth].stop().hide(); visibleMenus[depth] = null; ++depth; } } function closeLinks(depth) { while(openLinks[depth]) { openLinks[depth].removeClass('open'); openLinks[depth] = null; ++depth; } } function isMenuVisible(depth) { return visibleMenus[depth || 0] != null; } // MENU UTILITY FUNCTIONS function getItemId(menuItem) { return menuItem[MENU_IDX_ID]; } function isSeparator(menuItem) { return menuItem[MENU_IDX_ID] == null; // Separators don't have an ID } function hasSeparators(menu) { return $WH.in_array(menu, true, isSeparator) != -1; } function hasSubmenu(menuItem) { return menuItem[MENU_IDX_SUB] != null; } function findItemPosById(menu, id) { return $WH.in_array(menu, id, getItemId); } function hasPermissions(menuItem) { var opt = self.getItemOpt(menuItem); if(opt.requiredAccess && !User.hasPermissions(opt.requiredAccess)) return false; return true; } function isItemVisible(menuItem) { if(!hasPermissions(menuItem)) return false; if(hasSubmenu(menuItem)) { if(!hasVisibleItems(menuItem[MENU_IDX_SUB])) return false; } return true; } function hasVisibleItems(menu) { return $WH.in_array(menu, true, isSubitemVisible) != -1; } function isSubitemVisible(menuItem) { return !isSeparator(menuItem) && hasPermissions(menuItem); } function getUniqueId(menu) { if(menu.uniqueId == null) menu.uniqueId = nextUniqueId++; return menu.uniqueId; } function traverseMenu(menu, func) { $.each(menu, function(idx, menuItem) { traverseMenuItem(menuItem, func); }); } function traverseMenuItem(menuItem, func) { func(menuItem); if(hasSubmenu(menuItem)) traverseMenu(menuItem[MENU_IDX_SUB], func); } function fixUrls(menu, url, opt, depth) { $.each(menu, function(idx, menuItem) { if(menuItem === undefined) return; if(isSeparator(menuItem)) return; if(menuItem[MENU_IDX_URL] == null) // Don't override if already set menuItem[MENU_IDX_URL] = url + menuItem[MENU_IDX_ID] + opt.hash; if(hasSubmenu(menuItem)) { var accumulate = true; if(opt.useSimpleIds) accumulate = false; else if(opt.useSimpleIdsAfter != null && depth >= opt.useSimpleIdsAfter) accumulate = false; var nextUrl = url; if(accumulate) nextUrl += menuItem[MENU_IDX_ID] + '.'; fixUrls(menuItem[MENU_IDX_SUB], nextUrl, opt, depth + 1); } }); } function sortMenu(menu) { menu.sort(function(a, b) { return $WH.strcmp(a[MENU_IDX_NAME], b[MENU_IDX_NAME]); }); } function sortSeparatedMenu(menu) { // Sort each group separately so headings stay where they are. var implodedMenu = self.implode(menu, { createHeadinglessGroup: true }); $.each(implodedMenu, function(idx, submenu) { sortMenu(submenu[MENU_IDX_SUB]); }); explodeInto(menu, implodedMenu); }; function explodeInto(menu, implodedMenu) // Reverse of implode { menu.splice(0, menu.length); // Clear menu $.each(implodedMenu, function(idx, menuItem) { if(menuItem[MENU_IDX_NAME]) menu.push([, menuItem[MENU_IDX_NAME]]); // Heading $.each(menuItem[MENU_IDX_SUB], function(idx, menuItem) { menu.push(menuItem); }); }); } // EVENTS function beforeCreateMenuItem(menuItem) { var opt = self.getItemOpt(menuItem); if(opt.checkedUrl && location.href.match(opt.checkedUrl)) menuItem.checked = true; } function beforeShowMenu(menu) { if(menu.onBeforeShow) menu.onBeforeShow(menu); $.each(menu, function(idx, menuItem) { var opt = self.getItemOpt(menuItem); if(opt.onBeforeShow) opt.onBeforeShow(menuItem); }); } function rootNodeOver(event) { clearTimeout(timer); var $node = $(this); if(!isMenuVisible()) { // Use a small delay the 1st time to prevent undesired appearances timer = setTimeout(show.bind(null, $node.data('menu'), $node), DELAY_BEFORESHOW); return; } show($node.data('menu'), $node); } function rootNodeOut(event) { clearTimeout(timer); if(isMenuVisible()) timer = setTimeout(hide, DELAY_BEFOREHIDE); } function rootNodeClick(event) { clearTimeout(timer); self.showAtCursor($(this).data('menu'), event); } function menuDivOver(event) { clearTimeout(timer); } function menuDivOut(event) { clearTimeout(timer); timer = setTimeout(hide, DELAY_BEFOREHIDE); } function menuItemOver(event) { clearTimeout(timer); var $a = $(this); var depth = event.data.depth; closeLinks(depth); var menuItem = $a.data('menuItem'); var deepestDepth = depth; if(menuItem && hasSubmenu(menuItem)) { $a.addClass('open'); openLinks[depth] = $a; displayMenu(menuItem[MENU_IDX_SUB], depth + 1); ++deepestDepth; } hideMenus(deepestDepth + 1); } function menuItemClick(event) { var $a = $(this); var menuItem = $a.data('menuItem'); if(!menuItem) return; var opt = self.getItemOpt(menuItem); if(opt.onClick) opt.onClick(); } }; Menu.fixUrls(mn_achievements, '?achievements='); Menu.fixUrls(mn_classes, '?class='); Menu.fixUrls(mn_currencies, '?currencies='); Menu.fixUrls(mn_factions, '?factions='); Menu.fixUrls(mn_items, '?items='); Menu.fixUrls(mn_itemSets, '?itemsets&filter=cl=', { hash: '0-2+1' }); Menu.fixUrls(mn_npcs, '?npcs='); Menu.fixUrls(mn_objects, '?objects='); Menu.fixUrls(mn_petCalc, '?petcalc='); Menu.fixUrls(mn_pets, '?pets='); Menu.fixUrls(mn_quests, '?quests='); Menu.fixUrls(mn_races, '?race='); Menu.fixUrls(mn_spells, '?spells='); Menu.fixUrls(mn_titles, '?titles='); Menu.fixUrls(mn_zones, '?zones='); $(document).ready(function() // Locale is only known later { // if(Locale.getId() == LOCALE_ENUS) // return; Menu.sort(mn_classes); Menu.sort(mn_database); Menu.sortSubmenus(mn_items, [ [4, 1], // Armor > Cloth [4, 2], // Armor > Leather [4, 3], // Armor > Mail [4, 4], // Armor > Plate [1], // Containers [0], // Consumables [16], // Glyphs [7], // Trade Goods [6], // Projectiles [9] // Recipes ]); Menu.sort(mn_itemSets); Menu.sort(mn_npcs); Menu.sort(mn_objects); Menu.sort(mn_talentCalc); Menu.sort(mn_petCalc); Menu.sort(mn_pets); Menu.sort(mn_races); Menu.sort(mn_skills); Menu.sortSubmenus(mn_spells, [ [7], // Abilities [-2], // Talents [-3], // Pet Abilities [11], // Professions [9] // Secondary Skills ]); // aowow - why wasn't this already sorted..? Menu.sortSubmenus(mn_quests, [ [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10] ]); });