1
// {{{ docs <-- this is a VIM (text editor) text fold
6
* Summary: Allows developers to add dynamic drop down menus on webpages. The
7
* menu can either be horizontal or vertical, and can open in either
8
* direction. It has both edge detection and <select> tag detection
9
* (for browsers that cannot hide these form elements). The styles
10
* for the menu items are controlled almost entirely through CSS and
11
* the menus are created and destroyed using the DOM. Menu configuration
12
* is done using a custom Hash() class and is very portable from a PHP
13
* type array structure.
15
* Maintainer: Dan Allen <dan@mojavelinux.com>
17
* License: LGPL - however, if you use this library, please post to my forum where you
18
* use it so that I get a chance to see my baby in action. If you are doing
19
* this for commercial work perhaps you could send me a few Starbucks Coffee
20
* gift dollars to encourage future developement (NOT REQUIRED). E-mail me
23
* Homepage: http://www.mojavelinux.com/forum/viewtopic.php
25
* Freshmeat Project: http://freshmeat.net/projects/dommenu/?topic_id=92
29
* Supported Browsers: Mozilla (Gecko), IE 5+, Konqueror, (not finished Opera 7), Netscape 4
33
* Menu Options: Each option is followed by the value for that option. The options avaiable are:
36
* 'uri' (may be javascript)
39
* [0-9] an index to create a submenu item
54
* index (index within this level)
58
* cellSpacing (Konq only)
61
* mouseover/click -> domMenu_openEvent
62
* mouseout -> domMenu_closeEvent
63
* click -> domMenu_resolveLink
66
* If there is a non-negative click open delay, then any uri of the element will be ignored
68
* The alternate contents for a hover element are treated by creating to <span> wrapper elements
69
* and then alternating the display of them. This avoids the need for innerHTML, which can
70
* do nasty things to the browsers. If <span> turns out to be a bad choice for tags, then a
71
* non-HTML element can be used instead.
76
// {{{ settings (editable)
78
var domMenu_data = new domMenu_Hash();
79
var domMenu_settings = new domMenu_Hash();
81
domMenu_settings.setItem('global', new domMenu_Hash(
82
'menuBarClass', 'domMenu_menuBar',
83
'menuElementClass', 'domMenu_menuElement',
84
'menuElementHoverClass', 'domMenu_menuElementHover',
85
'menuElementActiveClass', 'domMenu_menuElementHover',
86
'subMenuBarClass', 'domMenu_subMenuBar',
87
'subMenuElementClass', 'domMenu_subMenuElement',
88
'subMenuElementHoverClass', 'domMenu_subMenuElementHover',
89
'subMenuElementActiveClass', 'domMenu_subMenuElementHover',
90
'subMenuElementHeadingClass', 'domMenu_subMenuElementHeading',
91
'menuBarWidth', '100%',
92
'subMenuMinWidth', 'inherit',
93
'distributeSpace', true,
95
'verticalExpand', 'south',
96
'horizontalExpand', 'east',
97
'subMenuWidthCorrection', 0,
98
'verticalSubMenuOffsetY', 0,
99
'verticalSubMenuOffsetX', 0,
100
'horizontalSubMenuOffsetX', 0,
101
'horizontalSubMenuOffsetY', 0,
103
'openMouseoverMenuDelay', 300,
104
'openMousedownMenuDelay', -1,
105
'closeMouseoutMenuDelay', 800,
106
'closeClickMenuDelay', -1,
107
'openMouseoverSubMenuDelay', 300,
108
'openClickSubMenuDelay', -1,
109
'closeMouseoutSubMenuDelay', 300,
110
'closeClickSubMenuDelay', -1,
115
// {{{ global variables
119
* @var domMenu_is{Browser}
121
var domMenu_userAgent = navigator.userAgent.toLowerCase();
122
var domMenu_isOpera = domMenu_userAgent.indexOf('opera 7') != -1 ? 1 : 0;
123
var domMenu_isKonq = domMenu_userAgent.indexOf('konq') != -1 ? 1 : 0;
124
var domMenu_isIE = !domMenu_isKonq && !domMenu_isOpera && document.all ? 1 : 0;
125
var domMenu_isIE50 = domMenu_isIE && domMenu_userAgent.indexOf('msie 5.0') != -1;
126
var domMenu_isIE55 = domMenu_isIE && domMenu_userAgent.indexOf('msie 5.5') != -1;
127
var domMenu_isIE5 = domMenu_isIE50 || domMenu_isIE55;
128
var domMenu_isGecko = !domMenu_isKonq && domMenu_userAgent.indexOf('gecko') != -1 ? 1 : 0;
131
* Passport to use the menu system, checked before performing menu manipulation
132
* @var domMenu_useLibrary
134
var domMenu_useLibrary = domMenu_isIE || domMenu_isGecko || domMenu_isKonq || domMenu_isOpera ? 1 : 0;
137
* The data for the menu is stored here, loaded from an external file
142
var domMenu_selectElements;
143
var domMenu_scrollbarWidth = 14;
144
var domMenu_eventTo = domMenu_isIE ? 'toElement' : 'relatedTarget';
145
var domMenu_eventFrom = domMenu_isIE ? 'fromElement' : 'relatedTarget';
147
var domMenu_activeElement = new domMenu_Hash();
150
* Array of hashes listing the timouts currently running for opening/closing menus
151
* @array domMenu_timeouts
153
var domMenu_timeouts = new Array();
154
domMenu_timeouts['open'] = new domMenu_Hash();
155
domMenu_timeouts['close'] = new domMenu_Hash();
157
var domMenu_timeoutStates = new Array();
158
domMenu_timeoutStates['open'] = new domMenu_Hash();
159
domMenu_timeoutStates['close'] = new domMenu_Hash();
162
* Style to use for a link pointer, which is different between Gecko and IE
163
* @var domMenu_pointerStyle
165
var domMenu_pointerStyle = domMenu_isIE ? 'hand' : 'pointer';
168
// {{{ domMenu_Hash()
170
function domMenu_Hash() {
173
this.numericLength = 0;
174
this.items = new Array();
175
while (arguments.length > argIndex) {
176
this.items[arguments[argIndex]] = arguments[argIndex + 1];
177
if (arguments[argIndex] == parseInt(arguments[argIndex])) {
178
this.numericLength++;
185
this.removeItem = function(in_key)
188
if (typeof(this.items[in_key]) != 'undefined') {
190
if (in_key == parseInt(in_key)) {
191
this.numericLength--;
194
tmp_value = this.items[in_key];
195
delete this.items[in_key];
201
this.getItem = function(in_key)
203
return this.items[in_key];
206
this.setItem = function(in_key, in_value)
208
if (typeof(this.items[in_key]) == 'undefined') {
210
if (in_key == parseInt(in_key)) {
211
this.numericLength++;
215
this.items[in_key] = in_value;
218
this.hasItem = function(in_key)
220
return typeof(this.items[in_key]) != 'undefined';
223
this.merge = function(in_hash)
225
for (var tmp_key in in_hash.items) {
226
if (typeof(this.items[tmp_key]) == 'undefined') {
228
if (tmp_key == parseInt(tmp_key)) {
229
this.numericLength++;
233
this.items[tmp_key] = in_hash.items[tmp_key];
237
this.compare = function(in_hash)
239
if (this.length != in_hash.length) {
243
for (var tmp_key in this.items) {
244
if (this.items[tmp_key] != in_hash.items[tmp_key]) {
254
// {{{ domMenu_activate()
256
function domMenu_activate(in_containerId)
261
// make sure we can use the menu system and this is a valid menu
262
if (!domMenu_useLibrary || !(container = document.getElementById(in_containerId)) || !(data = domMenu_data.items[in_containerId])) {
266
// start with the global settings and merge in the local changes
267
if (!domMenu_settings.hasItem(in_containerId)) {
268
domMenu_settings.setItem(in_containerId, new domMenu_Hash());
271
var settings = domMenu_settings.items[in_containerId];
272
for (var i in domMenu_settings.items['global'].items) {
273
if (!settings.hasItem(i)) {
274
settings.setItem(i, domMenu_settings.items['global'].items[i]);
278
// populate the zero level element
279
container.data = new domMenu_Hash(
280
'parentElement', false,
281
'numChildren', data.numericLength,
282
'childElements', new domMenu_Hash(),
287
// if we choose to distribute either height or width, determine ratio of each cell
288
var distributeRatio = Math.round(100/container.data.items['numChildren']) + '%';
290
// the first menu is the rootMenu, which is a child of the zero level element
291
var rootMenu = document.createElement('div');
292
rootMenu.id = in_containerId + '[0]';
293
rootMenu.className = settings.items['menuBarClass'];
294
container.data.setItem('subMenu', rootMenu);
296
var rootMenuTable = rootMenu.appendChild(document.createElement('table'));
297
if (domMenu_isKonq) {
298
rootMenuTable.cellSpacing = 0;
301
rootMenuTable.style.border = 0;
302
rootMenuTable.style.borderCollapse = 'collapse';
303
rootMenuTable.style.width = settings.items['menuBarWidth'];
304
var rootMenuTableBody = rootMenuTable.appendChild(document.createElement('tbody'));
306
var numSiblings = container.data.items['numChildren'];
307
for (var index = 1; index <= numSiblings; index++) {
308
// create a row the first time if horizontal or each time if vertical
309
if (index == 1 || settings.items['axis'] == 'vertical') {
310
var rootMenuTableRow = rootMenuTableBody.appendChild(document.createElement('tr'));
313
// create an instance of the root level menu element
314
var rootMenuTableCell = rootMenuTableRow.appendChild(document.createElement('td'));
315
rootMenuTableCell.style.padding = 0;
316
rootMenuTableCell.id = in_containerId + '[' + index + ']';
317
// add element to list of parent children
318
container.data.items['childElements'].setItem(rootMenuTableCell.id, rootMenuTableCell);
320
// assign the settings to the root level element
321
// {!} this is a problem if two menus are using the same data {!}
322
rootMenuTableCell.data = data.items[index];
323
rootMenuTableCell.data.merge(new domMenu_Hash(
324
'basename', in_containerId,
325
'parentElement', container,
326
'numChildren', rootMenuTableCell.data.numericLength,
327
'childElements', new domMenu_Hash(),
328
'offsets', new domMenu_Hash(),
329
'level', container.data.items['level'] + 1,
334
rootMenuTableCell.style.cursor = 'default';
335
if (settings.items['axis'] == 'horizontal') {
336
if (settings.items['distributeSpace']) {
337
rootMenuTableCell.style.width = distributeRatio;
341
var rootElement = rootMenuTableCell.appendChild(document.createElement('div'));
342
rootElement.className = settings.items['menuElementClass'];
343
// fill in the menu element contents
344
rootElement.innerHTML = '<span>' + rootMenuTableCell.data.items['contents'] + '</span>' + (rootMenuTableCell.data.hasItem('contentsHover') ? '<span style="display: none;">' + rootMenuTableCell.data.items['contentsHover'] + '</span>' : '');
347
rootMenuTableCell.onmouseover = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openMouseoverMenuDelay']); };
348
rootMenuTableCell.onmouseout = function(in_event) { domMenu_closeEvent(this, in_event); };
350
if (settings.items['openMousedownMenuDelay'] >= 0 && rootMenuTableCell.data.items['numChildren']) {
351
rootMenuTableCell.onmousedown = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openMousedownMenuDelay']); };
352
// cancel mouseup so that it doesn't propogate to global mouseup event
353
rootMenuTableCell.onmouseup = function(in_event) { var eventObj = domMenu_isIE ? event : in_event; eventObj.cancelBubble = true; };
355
rootMenuTableCell.ondblclick = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openMousedownMenuDelay']); };
358
else if (rootMenuTableCell.data.items['uri']) {
359
rootMenuTableCell.style.cursor = domMenu_pointerStyle;
360
rootMenuTableCell.onclick = function(in_event) { domMenu_resolveLink(this, in_event); };
363
// prevent highlighting of text
365
rootMenuTableCell.onselectstart = function() { return false; };
368
rootMenuTableCell.oncontextmenu = function() { return false; };
371
// add the menu rootMenu to the zero level element
372
rootMenu = container.appendChild(rootMenu);
374
// even though most cases the top level menu does not go away, it could
375
// if this menu system is used by another process
376
domMenu_detectCollisions(rootMenu);
380
// {{{ domMenu_activateSubMenu()
382
function domMenu_activateSubMenu(in_parentElement)
384
// see if submenu already exists
385
if (in_parentElement.data.hasItem('subMenu')) {
386
domMenu_toggleSubMenu(in_parentElement, 'visible');
390
var settings = domMenu_settings.items[in_parentElement.data.items['basename']];
393
var menu = document.createElement('div');
394
menu.id = in_parentElement.id + '[0]';
395
menu.className = settings.items['subMenuBarClass'];
396
menu.style.zIndex = settings.items['baseZIndex'];
397
menu.style.position = 'absolute';
398
// position the menu in the upper left corner hidden so that we can work on it
399
menu.style.visibility = 'hidden';
403
in_parentElement.data.setItem('subMenu', menu);
405
var menuTable = menu.appendChild(document.createElement('table'));
406
// ** opera wants to make absolute tables width 100% **
407
if (domMenu_isOpera) {
408
menuTable.style.width = '1px';
409
menuTable.style.whiteSpace = 'nowrap';
412
if (domMenu_isKonq) {
413
menuTable.cellSpacing = 0;
416
menuTable.style.border = 0;
417
menuTable.style.borderCollapse = 'collapse';
418
var menuTableBody = menuTable.appendChild(document.createElement('tbody'));
420
var numSiblings = in_parentElement.data.items['numChildren'];
421
for (var index = 1; index <= numSiblings; index++) {
422
var dataIndex = in_parentElement.data.items['level'] == 1 && settings.items['verticalExpand'] == 'north' && settings.items['axis'] == 'horizontal' ? numSiblings + 1 - index : index;
423
var menuTableCell = menuTableBody.appendChild(document.createElement('tr')).appendChild(document.createElement('td'));
424
menuTableCell.style.padding = 0;
425
menuTableCell.id = in_parentElement.id + '[' + dataIndex + ']';
427
// add element to list of parent children
428
in_parentElement.data.items['childElements'].setItem(menuTableCell.id, menuTableCell);
430
// assign the settings to nth level element
431
menuTableCell.data = in_parentElement.data.items[dataIndex];
432
menuTableCell.data.merge(new domMenu_Hash(
433
'basename', in_parentElement.data.items['basename'],
434
'parentElement', in_parentElement,
435
'numChildren', menuTableCell.data.numericLength,
436
'childElements', new domMenu_Hash(),
437
'offsets', new domMenu_Hash(),
438
'level', in_parentElement.data.items['level'] + 1,
443
var parentStyle = in_parentElement.data.items['level'] == 1 ? in_parentElement.parentNode.style : in_parentElement.style;
444
menuTableCell.style.cursor = 'default';
446
var element = menuTableCell.appendChild(document.createElement('div'));
447
var outerElement = element;
448
outerElement.className = settings.items['subMenuElementClass'];
450
if (menuTableCell.data.items['numChildren']) {
451
element = outerElement.appendChild(document.createElement('div'));
452
// {!} depends on which way we are opening {!}
453
element.style.backgroundImage = 'url(arrow.gif)';
454
element.style.backgroundRepeat = 'no-repeat';
455
element.style.backgroundPosition = 'right center';
456
// add appropriate padding to fit the arrow
457
element.style.paddingRight = '12px';
460
// fill in the menu item contents
461
element.innerHTML = menuTableCell.data.items['contents'];
464
menuTableCell.onmouseover = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openMouseoverSubMenuDelay']); };
465
menuTableCell.onmouseout = function(in_event) { domMenu_closeEvent(this, in_event); };
467
if (settings.items['openClickSubMenuDelay'] >= 0 && menuTableCell.data.items['numChildren']) {
468
menuTableCell.onmousedown = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openClickSubMenuDelay']); };
469
menuTableCell.onmouseup = function(in_event) { var eventObj = domMenu_isIE ? event : in_event; eventObj.cancelBubble = true; };
471
menuTableCell.ondblclick = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openClickSubMenuDelay']); };
474
else if (menuTableCell.data.items['uri']) {
475
menuTableCell.style.cursor = domMenu_pointerStyle;
476
menuTableCell.onclick = function(in_event) { domMenu_resolveLink(this, in_event); };
478
else if (!menuTableCell.data.items['numChildren']) {
479
outerElement.className += ' ' + settings.items['subMenuElementHeadingClass'];
482
// prevent highlighting of text
484
menuTableCell.onselectstart = function() { return false; };
487
menuTableCell.oncontextmenu = function() { return false; };
490
menu = document.body.appendChild(menu);
491
domMenu_toggleSubMenu(in_parentElement, 'visible');
495
// {{{ domMenu_changeActivePath()
498
* Close the old active path up to the new active element
499
* and return the value of the new active element (or the same if unchanged)
500
* If the new active element is not set, the top level is assumed
502
* @return mixed new active element or false if not set
504
function domMenu_changeActivePath(in_newActiveElement, in_oldActiveElement, in_closeDelay)
506
// protect against crap
507
if (!in_oldActiveElement && !in_newActiveElement) {
511
// cancel open timeouts since we know we are opening something different now
512
for (var i in domMenu_timeouts['open'].items) {
513
domMenu_cancelTimeout(i, 'open');
516
// grab some info about this menu system
517
var basename = in_oldActiveElement ? in_oldActiveElement.data.items['basename'] : in_newActiveElement.data.items['basename'];
518
var settings = domMenu_settings.items[basename];
520
// build the old and new paths
521
var oldActivePath = new domMenu_Hash();
522
if (in_oldActiveElement) {
523
var tmp_oldActivePathElement = in_oldActiveElement;
525
oldActivePath.setItem(tmp_oldActivePathElement.id, tmp_oldActivePathElement);
526
} while ((tmp_oldActivePathElement = tmp_oldActivePathElement.data.items['parentElement']) && tmp_oldActivePathElement.id != basename);
528
// unhighlight the old active element if it doesn't have children open
529
if (!in_oldActiveElement.data.items['subMenu'] || in_oldActiveElement.data.items['subMenu'].style.visibility == 'hidden') {
530
domMenu_toggleHighlight(in_oldActiveElement, false);
534
var newActivePath = new domMenu_Hash();
536
if (in_newActiveElement) {
537
var actualActiveElement = in_newActiveElement;
538
window.status = in_newActiveElement.data.items['statusText'] + ' ';
540
// in the event we have no old active element, just highlight new one and return
541
// without setting the new active element (handled later)
542
if (!in_oldActiveElement) {
543
domMenu_cancelTimeout(in_newActiveElement.id, 'close');
544
domMenu_toggleHighlight(in_newActiveElement, true);
547
// if the new element is in the path of the old element, then pretend event is
548
// on the old active element
549
else if (oldActivePath.hasItem(in_newActiveElement.id)) {
550
in_newActiveElement = in_oldActiveElement;
553
var tmp_newActivePathElement = in_newActiveElement;
555
// if we have met up with the old active path, then record merge point
556
if (!intersectPoint && oldActivePath.hasItem(tmp_newActivePathElement.id)) {
557
intersectPoint = tmp_newActivePathElement;
560
newActivePath.setItem(tmp_newActivePathElement.id, tmp_newActivePathElement);
561
domMenu_cancelTimeout(tmp_newActivePathElement.id, 'close');
562
// {!} this is ugly {!}
563
if (tmp_newActivePathElement != in_oldActiveElement || actualActiveElement == in_oldActiveElement) {
564
domMenu_toggleHighlight(tmp_newActivePathElement, true);
566
} while ((tmp_newActivePathElement = tmp_newActivePathElement.data.items['parentElement']) && tmp_newActivePathElement.id != basename);
568
// if we move to the child of the old active element
569
if (in_newActiveElement.data.items['parentElement'] == in_oldActiveElement) {
570
return in_newActiveElement;
572
// if the new active element is in the old active path
573
else if (in_newActiveElement == in_oldActiveElement) {
574
return in_newActiveElement;
577
// find the sibling element
578
var intersectSibling;
579
if (intersectPoint) {
580
for (var i in oldActivePath.items) {
581
if (oldActivePath.items[i].data.items['parentElement'] == intersectPoint) {
582
intersectSibling = oldActivePath.items[i];
588
var isRootLevel = in_newActiveElement.data.items['level'] == 1 ? true : false;
589
var closeDelay = isRootLevel ? settings.items['closeMouseoutMenuDelay'] : settings.items['closeMouseoutSubMenuDelay'];
592
var isRootLevel = false;
593
var closeDelay = settings.items['closeMouseoutMenuDelay'];
594
window.status = window.defaultStatus;
597
// override the close delay with that passed in
598
if (typeof(in_closeDelay) != 'undefined') {
599
closeDelay = in_closeDelay;
602
// if there is an intersect sibling, then we need to work from there up to
603
// preserve the active path
604
if (intersectSibling) {
605
// only if this is not the root level to we allow the scheduled close
606
// events to persist...otherwise we close immediately
608
// toggle the sibling highlight (only one sibling highlighted at a time)
609
domMenu_toggleHighlight(intersectSibling, false);
611
// we are moving to another top level menu
612
// {!} clean this up {!}
614
// add lingering menus outside of old active path to active path
615
for (var i in domMenu_timeouts['close'].items) {
616
if (!oldActivePath.hasItem(i)) {
617
var tmp_element = document.getElementById(i);
618
if (tmp_element.data.items['basename'] == basename) {
619
oldActivePath.setItem(i, tmp_element);
626
// schedule the old active path to be closed
627
for (var i in oldActivePath.items) {
628
if (newActivePath.hasItem(i)) {
632
// make sure we don't double schedule here
633
domMenu_cancelTimeout(i, 'close');
636
domMenu_toggleHighlight(oldActivePath.items[i], false);
637
domMenu_toggleSubMenu(oldActivePath.items[i], 'hidden');
640
var tmp_args = new Array();
641
tmp_args[0] = oldActivePath.items[i];
642
var tmp_function = 'domMenu_toggleHighlight(argv[0], false); domMenu_toggleSubMenu(argv[0], ' + domMenu_quote('hidden') + ');';
643
// if this is the top level, then the menu is being deactivated
644
if (oldActivePath.items[i].data.items['level'] == 1) {
645
tmp_function += ' domMenu_activeElement.setItem(' + domMenu_quote(basename) + ', false);';
648
domMenu_callTimeout(tmp_function, closeDelay, tmp_args, i, 'close');
652
return in_newActiveElement;
656
// {{{ domMenu_deactivate()
658
function domMenu_deactivate(in_basename, in_delay)
664
domMenu_changeActivePath(false, domMenu_activeElement.items[in_basename], in_delay);
668
// {{{ domMenu_openEvent()
671
* Handle the mouse event to open a menu
673
* When an event is received to open the menu, this function is
674
* called, handles reinitialization of the menu state and sets
675
* a timeout interval for opening the submenu (if one exists)
677
function domMenu_openEvent(in_this, in_event, in_openDelay)
679
if (domMenu_isGecko) {
681
window.getSelection().removeAllRanges();
685
// setup the cross-browser event object and target
686
var eventObj = domMenu_isIE ? event : in_event;
687
var currentTarget = domMenu_isIE ? in_this : eventObj.currentTarget;
688
var basename = currentTarget.data.items['basename'];
690
// if we are moving amoungst children of the same element, just ignore event
691
if (eventObj.type != 'mousedown' && domMenu_getElement(eventObj[domMenu_eventFrom], basename) == currentTarget) {
695
// if we click on an open menu, close it
696
if (eventObj.type == 'mousedown' && domMenu_activeElement.items[basename]) {
697
var settings = domMenu_settings.items[basename];
698
domMenu_changeActivePath(false, domMenu_activeElement.items[basename], currentTarget.data.items['level'] == 1 ? settings.items['closeClickMenuDelay'] : settings.items['closeClickSubMenuDelay']);
702
// if this element has children, popup the child menu
703
if (currentTarget.data.items['numChildren']) {
704
// the top level menus have no delay when moving between them
705
// so activate submenu immediately
706
if (currentTarget.data.items['level'] == 1 && domMenu_activeElement.items[basename]) {
707
// ** I place changeActivePath() call here so the hiding of selects does not flicker **
708
// {!} instead I could tell changeActivePath to clear select ownership but not
709
// toggle visibility....hmmm....{!}
710
domMenu_activateSubMenu(currentTarget);
711
// clear the active path and initialize the new one
712
domMenu_activeElement.setItem(basename, domMenu_changeActivePath(currentTarget, domMenu_activeElement.items[basename]));
715
// clear the active path and initialize the new one
716
domMenu_activeElement.setItem(basename, domMenu_changeActivePath(currentTarget, domMenu_activeElement.items[basename]));
717
var tmp_args = new Array();
718
tmp_args[0] = currentTarget;
719
var tmp_function = 'if (!domMenu_activeElement.items[' + domMenu_quote(basename) + ']) { domMenu_activeElement.setItem(' + domMenu_quote(basename) + ', argv[0]); } domMenu_activateSubMenu(argv[0]);';
720
domMenu_callTimeout(tmp_function, in_openDelay, tmp_args, currentTarget.id, 'open');
724
// clear the active path and initialize the new one
725
domMenu_activeElement.setItem(basename, domMenu_changeActivePath(currentTarget, domMenu_activeElement.items[basename]));
730
// {{{ domMenu_closeEvent()
733
* Handle the mouse event to close a menu
735
* When an mouseout event is received to close the menu, this function is
736
* called, sets a timeout interval for closing the menu.
738
function domMenu_closeEvent(in_this, in_event)
740
// setup the cross-browser event object and target
741
var eventObj = domMenu_isIE ? event : in_event;
742
var currentTarget = domMenu_isIE ? in_this : eventObj.currentTarget;
743
var basename = currentTarget.data.items['basename'];
744
var relatedTarget = domMenu_getElement(eventObj[domMenu_eventTo], basename);
746
// if the related target is not a menu element then we left the menu system
747
// at this point (or cannot discern where we are in the menu)
748
if (domMenu_activeElement.items[basename]) {
749
if (!relatedTarget) {
750
domMenu_changeActivePath(false, domMenu_activeElement.items[basename]);
753
// we are highlighting the top level, but menu is not yet 'active'
755
if (currentTarget != relatedTarget) {
756
domMenu_cancelTimeout(currentTarget.id, 'open');
757
domMenu_toggleHighlight(currentTarget, false);
763
// {{{ domMenu_getElement()
765
function domMenu_getElement(in_object, in_basename)
769
if (in_object.id && in_object.id.search(new RegExp('^' + in_basename + '(\\[[0-9]\\])*\\[[0-9]\\]$')) == 0) {
773
in_object = in_object.parentNode;
785
// {{{ domMenu_detectCollisions()
787
function domMenu_detectCollisions(in_menuObj, in_recover)
789
// no need to do anything for opera
790
if (domMenu_isOpera) {
794
if (typeof(domMenu_selectElements) == 'undefined') {
795
domMenu_selectElements = document.getElementsByTagName('select');
798
// if we don't have a menu, then unhide selects
800
for (var cnt = 0; cnt < domMenu_selectElements.length; cnt++) {
801
if (domMenu_isGecko && domMenu_selectElements[cnt].size <= 1 && !domMenu_selectElements[cnt].multiple) {
805
var thisSelect = domMenu_selectElements[cnt];
806
thisSelect.hideList.removeItem(in_menuObj.id);
807
if (!thisSelect.hideList.length) {
808
domMenu_selectElements[cnt].style.visibility = 'visible';
815
// okay, in_menu exists, let's hunt and destroy
816
var menuOffsets = domMenu_getOffsets(in_menuObj);
818
for (var cnt = 0; cnt < domMenu_selectElements.length; cnt++) {
819
var thisSelect = domMenu_selectElements[cnt];
821
// mozilla doesn't have a problem with regular selects
822
if (domMenu_isGecko && thisSelect.size <= 1 && !thisSelect.multiple) {
826
// {!} make sure this hash is congruent with domTT hash {!}
827
if (!thisSelect.hideList) {
828
thisSelect.hideList = new domMenu_Hash();
831
var selectOffsets = domMenu_getOffsets(thisSelect);
832
// for mozilla we only have to worry about the scrollbar itself
833
if (domMenu_isGecko) {
834
selectOffsets.setItem('left', selectOffsets.items['left'] + thisSelect.offsetWidth - domMenu_scrollbarWidth);
835
selectOffsets.setItem('leftCenter', selectOffsets.items['left'] + domMenu_scrollbarWidth/2);
836
selectOffsets.setItem('radius', Math.max(thisSelect.offsetHeight, domMenu_scrollbarWidth/2));
839
var center2centerDistance = Math.sqrt(Math.pow(selectOffsets.items['leftCenter'] - menuOffsets.items['leftCenter'], 2) + Math.pow(selectOffsets.items['topCenter'] - menuOffsets.items['topCenter'], 2));
840
var radiusSum = selectOffsets.items['radius'] + menuOffsets.items['radius'];
841
// the encompassing circles are overlapping, get in for a closer look
842
if (center2centerDistance < radiusSum) {
843
// tip is left of select
844
if ((menuOffsets.items['leftCenter'] <= selectOffsets.items['leftCenter'] && menuOffsets.items['right'] < selectOffsets.items['left']) ||
845
// tip is right of select
846
(menuOffsets.items['leftCenter'] > selectOffsets.items['leftCenter'] && menuOffsets.items['left'] > selectOffsets.items['right']) ||
847
// tip is above select
848
(menuOffsets.items['topCenter'] <= selectOffsets.items['topCenter'] && menuOffsets.items['bottom'] < selectOffsets.items['top']) ||
849
// tip is below select
850
(menuOffsets.items['topCenter'] > selectOffsets.items['topCenter'] && menuOffsets.items['top'] > selectOffsets.items['bottom'])) {
851
thisSelect.hideList.removeItem(in_menuObj.id);
852
if (!thisSelect.hideList.length) {
853
thisSelect.style.visibility = 'visible';
857
thisSelect.hideList.setItem(in_menuObj.id, true);
858
thisSelect.style.visibility = 'hidden';
865
// {{{ domMenu_getOffsets()
867
function domMenu_getOffsets(in_object)
869
var originalObject = in_object;
870
var originalWidth = in_object.offsetWidth;
871
var originalHeight = in_object.offsetHeight;
876
offsetLeft += in_object.offsetLeft;
877
offsetTop += in_object.offsetTop;
878
in_object = in_object.offsetParent;
881
return new domMenu_Hash(
884
'right', offsetLeft + originalWidth,
885
'bottom', offsetTop + originalHeight,
886
'leftCenter', offsetLeft + originalWidth/2,
887
'topCenter', offsetTop + originalHeight/2,
888
'radius', Math.max(originalWidth, originalHeight)
893
// {{{ domMenu_callTimeout()
895
function domMenu_callTimeout(in_function, in_timeout, in_args, in_basename, in_type)
897
if (in_timeout == 0) {
898
var tmp_function = new Function('argv', in_function);
899
tmp_function(in_args);
901
else if (in_timeout > 0) {
902
// after we complete the timeout call, we want to remove the reference, so always add that
903
var tmp_function = new Function('argv', in_function + ' domMenu_timeouts[' + domMenu_quote(in_type) + '].removeItem(' + domMenu_quote(in_basename) + ');');
905
var tmp_args = new Array();
906
for (var i = 0; i < in_args.length; i++) {
907
tmp_args[i] = in_args[i];
910
if (!domMenu_isKonq && !domMenu_isIE50) {
911
domMenu_timeouts[in_type].setItem(in_basename, setTimeout(function() { tmp_function(tmp_args); }, in_timeout));
914
var tmp_data = new Array();
915
tmp_data['function'] = tmp_function;
916
tmp_data['args'] = tmp_args;
917
domMenu_timeoutStates[in_type].setItem(in_basename, tmp_data);
918
var tmp_type = domMenu_quote(in_type);
919
var tmp_basename = domMenu_quote(in_basename);
921
domMenu_timeouts[in_type].setItem(in_basename, setTimeout('domMenu_timeoutStates[' + tmp_type + '].items[' + tmp_basename + '][' + domMenu_quote('function') + '](domMenu_timeoutStates[' + tmp_type + '].items[' + tmp_basename + '][' + domMenu_quote('args') + ']); domMenu_timeoutStates[' + tmp_type + '].removeItem(' + tmp_basename + ');', in_timeout));
927
// {{{ domMenu_cancelTimeout()
929
function domMenu_cancelTimeout(in_basename, in_type)
931
// take advantage of browsers which use the anonymous function
932
if (!domMenu_isKonq && !domMenu_isIE50) {
933
clearTimeout(domMenu_timeouts[in_type].removeItem(in_basename));
936
// if konqueror, we only want to clearTimeout if it is still running
937
if (domMenu_timeoutStates[in_type].hasItem(in_basename)) {
938
clearTimeout(domMenu_timeouts[in_type].removeItem(in_basename));
939
domMenu_timeoutStates[in_type].removeItem(in_basename);
945
// {{{ domMenu_correctEdgeBleed()
947
function domMenu_correctEdgeBleed(in_width, in_height, in_x, in_y, in_padding, in_axis)
949
if (domMenu_isIE && !domMenu_isIE5) {
950
var pageHeight = document.documentElement.clientHeight;
952
else if (!domMenu_isKonq) {
953
var pageHeight = document.body.clientHeight;
956
var pageHeight = window.innerHeight;
959
var pageYOffset = domMenu_isIE ? document.body.scrollTop : window.pageYOffset;
960
var pageXOffset = domMenu_isIE ? document.body.scrollLeft : window.pageXOffset;
963
if (in_axis == 'horizontal') {
964
var bleedRight = (in_x - pageXOffset) + in_width - (document.body.clientWidth - in_padding);
965
var bleedLeft = (in_x - pageXOffset) - in_padding;
967
// we are bleeding off the right, move menu to stay on page
968
if (bleedRight > 0) {
972
// we are bleeding to the left, move menu over to stay on page
973
// we don't want an 'else if' here, because if it doesn't fit we will bleed off the right
979
var bleedTop = (in_y - pageYOffset) - in_padding;
980
var bleedBottom = (in_y - pageYOffset) + in_height - (pageHeight - in_padding);
982
// if we are bleeding off the bottom, move menu to stay on page
983
if (bleedBottom > 0) {
987
// if we are bleeding off the top, move menu down
988
// we don't want an 'else if' here, because if we just can't fit it, bleed off the bottom
994
return new Array(in_x, in_y);
998
// {{{ domMenu_toggleSubMenu()
1000
function domMenu_toggleSubMenu(in_parentElement, in_style)
1002
var subMenu = in_parentElement.data.items['subMenu'];
1003
if (subMenu && subMenu.style.visibility != in_style) {
1004
var settings = domMenu_settings.items[in_parentElement.data.items['basename']];
1005
var prefix = in_parentElement.data.items['level'] == 1 ? 'menu' : 'subMenu';
1006
var className = settings.items[prefix + 'ElementClass'];
1007
// :BUG: this is a problem if submenus click to open, then it won't
1008
// have the right class when you click to close
1009
if (in_style == 'visible') {
1010
className += ' ' + settings.items[prefix + 'Element' + (in_style == 'visible' ? 'Active' : 'Hover') + 'Class'];
1013
in_parentElement.firstChild.className = className;
1015
// position our submenu
1016
if (in_style == 'visible') {
1017
var tmp_offsets = domMenu_getOffsets(in_parentElement);
1018
if (in_parentElement.data.items['level'] == 1) {
1019
tmp_offsets.items['top'] += settings.items['verticalSubMenuOffsetY'];
1020
tmp_offsets.items['bottom'] += settings.items['verticalSubMenuOffsetY'];
1021
tmp_offsets.items['left'] += settings.items['verticalSubMenuOffsetX'];
1022
tmp_offsets.items['right'] += settings.items['verticalSubMenuOffsetX'];
1025
// reposition if there was a change in the parent position/size
1026
if (!in_parentElement.data.items['offsets'].compare(tmp_offsets)) {
1027
in_parentElement.data.items['offsets'] = tmp_offsets;
1029
if (settings.items['axis'] == 'horizontal' && in_parentElement.data.items['level'] == 1) {
1030
var xCoor = tmp_offsets.items['left'];
1031
if (settings.items['verticalExpand'] == 'north') {
1032
var yCoor = tmp_offsets.items['top'] - subMenu.offsetHeight - settings.items['verticalSubMenuOffsetY'];
1035
var yCoor = tmp_offsets.items['bottom'];
1039
var xCoor = tmp_offsets.items['right'] + settings.items['horizontalSubMenuOffsetX'];
1040
var yCoor = tmp_offsets.items['top'] + settings.items['horizontalSubMenuOffsetY'];
1043
var minWidth = settings.items['subMenuMinWidth'];
1044
var renderedWidth = subMenu.offsetWidth;
1045
if (minWidth == 'inherit') {
1046
minWidth = in_parentElement.offsetWidth + settings.items['subMenuWidthCorrection'];
1048
else if (minWidth == 'auto') {
1049
minWidth = renderedWidth;
1052
if (domMenu_isKonq) {
1053
// change with width of the first cell
1054
subMenu.firstChild.firstChild.firstChild.firstChild.style.width = Math.max(minWidth, renderedWidth) + 'px';
1057
// change the width of the table
1058
subMenu.firstChild.style.width = Math.max(minWidth, renderedWidth) + 'px';
1061
var coordinates = domMenu_correctEdgeBleed(subMenu.offsetWidth, subMenu.offsetHeight, xCoor, yCoor, settings.items['screenPadding'], settings.items['axis']);
1062
subMenu.style.left = coordinates[0] + 'px';
1063
subMenu.style.top = coordinates[1] + 'px';
1065
// ** if we inherit, it is necessary to check the parent element width again **
1066
if (settings.items['axis'] == 'horizontal' && settings.items['subMenuMinWidth'] == 'inherit') {
1067
subMenu.firstChild.style.width = Math.max(in_parentElement.offsetWidth + settings.items['subMenuWidthCorrection'], renderedWidth) + 'px';
1072
// force konqueror to change the styles
1073
if (domMenu_isKonq) {
1074
in_parentElement.firstChild.style.display = 'none';
1075
in_parentElement.firstChild.style.display = '';
1078
subMenu.style.visibility = in_style;
1079
domMenu_detectCollisions(subMenu, (in_style == 'hidden'));
1084
// {{{ domMenu_toggleHighlight()
1086
function domMenu_toggleHighlight(in_element, in_status)
1088
// if this is a heading, don't change the style
1089
if (!in_element.data.items['numChildren'] && !in_element.data.items['uri']) {
1093
var settings = domMenu_settings.items[in_element.data.items['basename']];
1094
var prefix = in_element.data.items['level'] == 1 ? 'menu' : 'subMenu';
1095
var className = settings.items[prefix + 'ElementClass'];
1096
var highlightElement = in_element.firstChild;
1100
if (in_element.data.hasItem('subMenu') && in_element.data.items['subMenu'].style.visibility == 'visible') {
1101
pseudoClass = 'Active';
1103
else if (in_element.data.items['numChildren'] || in_element.data.items['uri']) {
1104
pseudoClass = 'Hover';
1109
className += ' ' + settings.items[prefix + 'Element' + pseudoClass + 'Class'];
1110
// if we are changing to hover, change the alt contents (only change if needs it)
1111
if (highlightElement.childNodes.length == 2 && highlightElement.lastChild.style.display == 'none') {
1112
highlightElement.firstChild.style.display = 'none';
1113
highlightElement.lastChild.style.display = '';
1117
// if we are changing to non-hover, change the alt contents (only change if needs it)
1118
if (highlightElement.childNodes.length == 2 && highlightElement.firstChild.style.display == 'none') {
1119
highlightElement.lastChild.style.display = 'none';
1120
highlightElement.firstChild.style.display = '';
1124
highlightElement.className = className;
1126
// force konqueror to change the styles
1127
if (domMenu_isKonq) {
1128
highlightElement.style.display = 'none';
1129
highlightElement.style.display = '';
1134
// {{{ domMenu_resolveLink()
1136
function domMenu_resolveLink(in_this, in_event)
1138
var eventObj = domMenu_isIE ? event : in_event;
1139
var currentTarget = domMenu_isIE ? in_this : eventObj.currentTarget;
1140
var basename = currentTarget.data.items['basename'];
1142
// close the menu system immediately when we resolve the uri
1143
domMenu_changeActivePath(false, domMenu_activeElement.items[basename], 0);
1145
if (currentTarget.data.items['uri']) {
1146
window.status = 'Resolving Link...';
1148
// open in current window
1149
if (!currentTarget.data.items['target'] || currentTarget.data.items['target'] == '_self') {
1150
window.location = currentTarget.data.items['uri'];
1152
// open in new window
1154
window.open(currentTarget.data.items['uri'], currentTarget.data.items['target']);
1160
// {{{ domMenu_quote()
1162
function domMenu_quote(in_string)
1164
return "'" + in_string.replace(new RegExp("'", 'g'), "\\'") + "'";