]>
git.ipfire.org Git - people/ms/ipfire-2.x.git/blob - html/html/include/domMenu.js
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'), "\\'") + "'";