Files
aowow/setup/tools/filegen/templates/global.js/menu.js
Sarjuuk 6557e70d5c Template/Update (Part 47)
* split global.js into its components, so it can be reasonably processed by setup
 * make reputation requirements configurable
 * move Markup and Locale back into global.js (removed associated build scripts)
 * extend Icon to display iconId in lightbox popup
2025-09-25 16:01:14 +02:00

988 lines
24 KiB
JavaScript

/*
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 = $('<span class="menu-buttons"></span>');
$.each(menu, function(idx, menuItem)
{
if(isSeparator(menuItem)) return;
var $a = $('<a></a>')
var $span = $('<span></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 = $('<div class="menu"><a href="#"><span>ohai</span></a></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 = $('<div class="menu"></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 = $('<a></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></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 = $('<div class="menu-outer"></div>');
var $innerDiv = $('<div class="menu-inner"></div>');
$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 = $('<div></div>');
while(nItemsRemaining > 0)
{
var $outerDiv = $('<div class="menu-outer"></div>');
var $innerDiv = $('<div class="menu-inner"></div>');
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]
]);
});