]> git.ipfire.org Git - people/pmueller/ipfire-2.x.git/blob - html/html/include/domMenu.js
git-svn-id: http://svn.ipfire.org/svn/ipfire/IPFire/source@16 ea5c0bd1-69bd-2848...
[people/pmueller/ipfire-2.x.git] / html / html / include / domMenu.js
1 // {{{ docs <-- this is a VIM (text editor) text fold
2
3 /**
4 * DOM Menu 0.3.2
5 *
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.
14 *
15 * Maintainer: Dan Allen <dan@mojavelinux.com>
16 *
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
21 * for and address.
22 *
23 * Homepage: http://www.mojavelinux.com/forum/viewtopic.php
24 *
25 * Freshmeat Project: http://freshmeat.net/projects/dommenu/?topic_id=92
26 *
27 * Updated: 2003/01/04
28 *
29 * Supported Browsers: Mozilla (Gecko), IE 5+, Konqueror, (not finished Opera 7), Netscape 4
30 *
31 * Usage:
32 *
33 * Menu Options: Each option is followed by the value for that option. The options avaiable are:
34 * 'contents'
35 * 'rolloverContents',
36 * 'uri' (may be javascript)
37 * 'statusText'
38 * 'target'
39 * [0-9] an index to create a submenu item
40 *
41 * API:
42 *
43 * menuElementObject {
44 * ** properties **
45 * data
46 * contents
47 * uri
48 * target
49 * statusText
50 * parentElement
51 * subMenu
52 * childElements
53 * level
54 * index (index within this level)
55 * id
56 * className
57 * style
58 * cellSpacing (Konq only)
59 *
60 * ** events **
61 * mouseover/click -> domMenu_openEvent
62 * mouseout -> domMenu_closeEvent
63 * click -> domMenu_resolveLink
64 * }
65 *
66 * If there is a non-negative click open delay, then any uri of the element will be ignored
67 *
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.
72 *
73 **/
74
75 // }}}
76 // {{{ settings (editable)
77
78 var domMenu_data = new domMenu_Hash();
79 var domMenu_settings = new domMenu_Hash();
80
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,
94 'axis', 'horizontal',
95 'verticalExpand', 'south',
96 'horizontalExpand', 'east',
97 'subMenuWidthCorrection', 0,
98 'verticalSubMenuOffsetY', 0,
99 'verticalSubMenuOffsetX', 0,
100 'horizontalSubMenuOffsetX', 0,
101 'horizontalSubMenuOffsetY', 0,
102 'screenPadding', 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,
111 'baseZIndex', 100
112 ));
113
114 // }}}
115 // {{{ global variables
116
117 /**
118 * Browser variables
119 * @var domMenu_is{Browser}
120 */
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;
129
130 /**
131 * Passport to use the menu system, checked before performing menu manipulation
132 * @var domMenu_useLibrary
133 */
134 var domMenu_useLibrary = domMenu_isIE || domMenu_isGecko || domMenu_isKonq || domMenu_isOpera ? 1 : 0;
135
136 /**
137 * The data for the menu is stored here, loaded from an external file
138 * @hash domMenu_data
139 */
140 var domMenu_data;
141
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';
146
147 var domMenu_activeElement = new domMenu_Hash();
148
149 /**
150 * Array of hashes listing the timouts currently running for opening/closing menus
151 * @array domMenu_timeouts
152 */
153 var domMenu_timeouts = new Array();
154 domMenu_timeouts['open'] = new domMenu_Hash();
155 domMenu_timeouts['close'] = new domMenu_Hash();
156
157 var domMenu_timeoutStates = new Array();
158 domMenu_timeoutStates['open'] = new domMenu_Hash();
159 domMenu_timeoutStates['close'] = new domMenu_Hash();
160
161 /**
162 * Style to use for a link pointer, which is different between Gecko and IE
163 * @var domMenu_pointerStyle
164 */
165 var domMenu_pointerStyle = domMenu_isIE ? 'hand' : 'pointer';
166
167 // }}}
168 // {{{ domMenu_Hash()
169
170 function domMenu_Hash() {
171 var argIndex = 0;
172 this.length = 0;
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++;
179 }
180
181 this.length++;
182 argIndex += 2;
183 }
184
185 this.removeItem = function(in_key)
186 {
187 var tmp_value;
188 if (typeof(this.items[in_key]) != 'undefined') {
189 this.length--;
190 if (in_key == parseInt(in_key)) {
191 this.numericLength--;
192 }
193
194 tmp_value = this.items[in_key];
195 delete this.items[in_key];
196 }
197
198 return tmp_value;
199 }
200
201 this.getItem = function(in_key)
202 {
203 return this.items[in_key];
204 }
205
206 this.setItem = function(in_key, in_value)
207 {
208 if (typeof(this.items[in_key]) == 'undefined') {
209 this.length++;
210 if (in_key == parseInt(in_key)) {
211 this.numericLength++;
212 }
213 }
214
215 this.items[in_key] = in_value;
216 }
217
218 this.hasItem = function(in_key)
219 {
220 return typeof(this.items[in_key]) != 'undefined';
221 }
222
223 this.merge = function(in_hash)
224 {
225 for (var tmp_key in in_hash.items) {
226 if (typeof(this.items[tmp_key]) == 'undefined') {
227 this.length++;
228 if (tmp_key == parseInt(tmp_key)) {
229 this.numericLength++;
230 }
231 }
232
233 this.items[tmp_key] = in_hash.items[tmp_key];
234 }
235 }
236
237 this.compare = function(in_hash)
238 {
239 if (this.length != in_hash.length) {
240 return false;
241 }
242
243 for (var tmp_key in this.items) {
244 if (this.items[tmp_key] != in_hash.items[tmp_key]) {
245 return false;
246 }
247 }
248
249 return true;
250 }
251 }
252
253 // }}}
254 // {{{ domMenu_activate()
255
256 function domMenu_activate(in_containerId)
257 {
258 var container;
259 var data;
260
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])) {
263 return;
264 }
265
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());
269 }
270
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]);
275 }
276 }
277
278 // populate the zero level element
279 container.data = new domMenu_Hash(
280 'parentElement', false,
281 'numChildren', data.numericLength,
282 'childElements', new domMenu_Hash(),
283 'level', 0,
284 'index', 1
285 );
286
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']) + '%';
289
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);
295
296 var rootMenuTable = rootMenu.appendChild(document.createElement('table'));
297 if (domMenu_isKonq) {
298 rootMenuTable.cellSpacing = 0;
299 }
300
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'));
305
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'));
311 }
312
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);
319
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,
330 'index', index
331 ));
332
333 // assign the styles
334 rootMenuTableCell.style.cursor = 'default';
335 if (settings.items['axis'] == 'horizontal') {
336 if (settings.items['distributeSpace']) {
337 rootMenuTableCell.style.width = distributeRatio;
338 }
339 }
340
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>' : '');
345
346 // attach the events
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); };
349
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; };
354 if (domMenu_isIE) {
355 rootMenuTableCell.ondblclick = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openMousedownMenuDelay']); };
356 }
357 }
358 else if (rootMenuTableCell.data.items['uri']) {
359 rootMenuTableCell.style.cursor = domMenu_pointerStyle;
360 rootMenuTableCell.onclick = function(in_event) { domMenu_resolveLink(this, in_event); };
361 }
362
363 // prevent highlighting of text
364 if (domMenu_isIE) {
365 rootMenuTableCell.onselectstart = function() { return false; };
366 }
367
368 rootMenuTableCell.oncontextmenu = function() { return false; };
369 }
370
371 // add the menu rootMenu to the zero level element
372 rootMenu = container.appendChild(rootMenu);
373
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);
377 }
378
379 // }}}
380 // {{{ domMenu_activateSubMenu()
381
382 function domMenu_activateSubMenu(in_parentElement)
383 {
384 // see if submenu already exists
385 if (in_parentElement.data.hasItem('subMenu')) {
386 domMenu_toggleSubMenu(in_parentElement, 'visible');
387 return;
388 }
389
390 var settings = domMenu_settings.items[in_parentElement.data.items['basename']];
391
392 // build the submenu
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';
400 menu.style.top = 0;
401 menu.style.left = 0;
402
403 in_parentElement.data.setItem('subMenu', menu);
404
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';
410 }
411
412 if (domMenu_isKonq) {
413 menuTable.cellSpacing = 0;
414 }
415
416 menuTable.style.border = 0;
417 menuTable.style.borderCollapse = 'collapse';
418 var menuTableBody = menuTable.appendChild(document.createElement('tbody'));
419
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 + ']';
426
427 // add element to list of parent children
428 in_parentElement.data.items['childElements'].setItem(menuTableCell.id, menuTableCell);
429
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,
439 'index', index
440 ));
441
442 // assign the styles
443 var parentStyle = in_parentElement.data.items['level'] == 1 ? in_parentElement.parentNode.style : in_parentElement.style;
444 menuTableCell.style.cursor = 'default';
445
446 var element = menuTableCell.appendChild(document.createElement('div'));
447 var outerElement = element;
448 outerElement.className = settings.items['subMenuElementClass'];
449
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';
458 }
459
460 // fill in the menu item contents
461 element.innerHTML = menuTableCell.data.items['contents'];
462
463 // attach the events
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); };
466
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; };
470 if (domMenu_isIE) {
471 menuTableCell.ondblclick = function(in_event) { domMenu_openEvent(this, in_event, settings.items['openClickSubMenuDelay']); };
472 }
473 }
474 else if (menuTableCell.data.items['uri']) {
475 menuTableCell.style.cursor = domMenu_pointerStyle;
476 menuTableCell.onclick = function(in_event) { domMenu_resolveLink(this, in_event); };
477 }
478 else if (!menuTableCell.data.items['numChildren']) {
479 outerElement.className += ' ' + settings.items['subMenuElementHeadingClass'];
480 }
481
482 // prevent highlighting of text
483 if (domMenu_isIE) {
484 menuTableCell.onselectstart = function() { return false; };
485 }
486
487 menuTableCell.oncontextmenu = function() { return false; };
488 }
489
490 menu = document.body.appendChild(menu);
491 domMenu_toggleSubMenu(in_parentElement, 'visible');
492 }
493
494 // }}}
495 // {{{ domMenu_changeActivePath()
496
497 /**
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
501 *
502 * @return mixed new active element or false if not set
503 */
504 function domMenu_changeActivePath(in_newActiveElement, in_oldActiveElement, in_closeDelay)
505 {
506 // protect against crap
507 if (!in_oldActiveElement && !in_newActiveElement) {
508 return false;
509 }
510
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');
514 }
515
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];
519
520 // build the old and new paths
521 var oldActivePath = new domMenu_Hash();
522 if (in_oldActiveElement) {
523 var tmp_oldActivePathElement = in_oldActiveElement;
524 do {
525 oldActivePath.setItem(tmp_oldActivePathElement.id, tmp_oldActivePathElement);
526 } while ((tmp_oldActivePathElement = tmp_oldActivePathElement.data.items['parentElement']) && tmp_oldActivePathElement.id != basename);
527
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);
531 }
532 }
533
534 var newActivePath = new domMenu_Hash();
535 var intersectPoint;
536 if (in_newActiveElement) {
537 var actualActiveElement = in_newActiveElement;
538 window.status = in_newActiveElement.data.items['statusText'] + ' ';
539
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);
545 return false;
546 }
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;
551 }
552
553 var tmp_newActivePathElement = in_newActiveElement;
554 do {
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;
558 }
559
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);
565 }
566 } while ((tmp_newActivePathElement = tmp_newActivePathElement.data.items['parentElement']) && tmp_newActivePathElement.id != basename);
567
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;
571 }
572 // if the new active element is in the old active path
573 else if (in_newActiveElement == in_oldActiveElement) {
574 return in_newActiveElement;
575 }
576
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];
583 break;
584 }
585 }
586 }
587
588 var isRootLevel = in_newActiveElement.data.items['level'] == 1 ? true : false;
589 var closeDelay = isRootLevel ? settings.items['closeMouseoutMenuDelay'] : settings.items['closeMouseoutSubMenuDelay'];
590 }
591 else {
592 var isRootLevel = false;
593 var closeDelay = settings.items['closeMouseoutMenuDelay'];
594 window.status = window.defaultStatus;
595 }
596
597 // override the close delay with that passed in
598 if (typeof(in_closeDelay) != 'undefined') {
599 closeDelay = in_closeDelay;
600 }
601
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
607 if (!isRootLevel) {
608 // toggle the sibling highlight (only one sibling highlighted at a time)
609 domMenu_toggleHighlight(intersectSibling, false);
610 }
611 // we are moving to another top level menu
612 // {!} clean this up {!}
613 else {
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);
620 }
621 }
622 }
623 }
624 }
625
626 // schedule the old active path to be closed
627 for (var i in oldActivePath.items) {
628 if (newActivePath.hasItem(i)) {
629 continue;
630 }
631
632 // make sure we don't double schedule here
633 domMenu_cancelTimeout(i, 'close');
634
635 if (isRootLevel) {
636 domMenu_toggleHighlight(oldActivePath.items[i], false);
637 domMenu_toggleSubMenu(oldActivePath.items[i], 'hidden');
638 }
639 else {
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);';
646 }
647
648 domMenu_callTimeout(tmp_function, closeDelay, tmp_args, i, 'close');
649 }
650 }
651
652 return in_newActiveElement;
653 }
654
655 // }}}
656 // {{{ domMenu_deactivate()
657
658 function domMenu_deactivate(in_basename, in_delay)
659 {
660 if (!in_delay) {
661 in_delay = 0;
662 }
663
664 domMenu_changeActivePath(false, domMenu_activeElement.items[in_basename], in_delay);
665 }
666
667 // }}}
668 // {{{ domMenu_openEvent()
669
670 /**
671 * Handle the mouse event to open a menu
672 *
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)
676 */
677 function domMenu_openEvent(in_this, in_event, in_openDelay)
678 {
679 if (domMenu_isGecko) {
680 try {
681 window.getSelection().removeAllRanges();
682 } catch (e) {}
683 }
684
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'];
689
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) {
692 return;
693 }
694
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']);
699 return;
700 }
701
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]));
713 }
714 else {
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');
721 }
722 }
723 else {
724 // clear the active path and initialize the new one
725 domMenu_activeElement.setItem(basename, domMenu_changeActivePath(currentTarget, domMenu_activeElement.items[basename]));
726 }
727 }
728
729 // }}}
730 // {{{ domMenu_closeEvent()
731
732 /**
733 * Handle the mouse event to close a menu
734 *
735 * When an mouseout event is received to close the menu, this function is
736 * called, sets a timeout interval for closing the menu.
737 */
738 function domMenu_closeEvent(in_this, in_event)
739 {
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);
745
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]);
751 }
752 }
753 // we are highlighting the top level, but menu is not yet 'active'
754 else {
755 if (currentTarget != relatedTarget) {
756 domMenu_cancelTimeout(currentTarget.id, 'open');
757 domMenu_toggleHighlight(currentTarget, false);
758 }
759 }
760 }
761
762 // }}}
763 // {{{ domMenu_getElement()
764
765 function domMenu_getElement(in_object, in_basename)
766 {
767 while (in_object) {
768 try {
769 if (in_object.id && in_object.id.search(new RegExp('^' + in_basename + '(\\[[0-9]\\])*\\[[0-9]\\]$')) == 0) {
770 return in_object;
771 }
772 else {
773 in_object = in_object.parentNode;
774 }
775 }
776 catch(e) {
777 return false;
778 }
779 }
780
781 return false;
782 }
783
784 // }}}
785 // {{{ domMenu_detectCollisions()
786
787 function domMenu_detectCollisions(in_menuObj, in_recover)
788 {
789 // no need to do anything for opera
790 if (domMenu_isOpera) {
791 return;
792 }
793
794 if (typeof(domMenu_selectElements) == 'undefined') {
795 domMenu_selectElements = document.getElementsByTagName('select');
796 }
797
798 // if we don't have a menu, then unhide selects
799 if (in_recover) {
800 for (var cnt = 0; cnt < domMenu_selectElements.length; cnt++) {
801 if (domMenu_isGecko && domMenu_selectElements[cnt].size <= 1 && !domMenu_selectElements[cnt].multiple) {
802 continue;
803 }
804
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';
809 }
810 }
811
812 return;
813 }
814
815 // okay, in_menu exists, let's hunt and destroy
816 var menuOffsets = domMenu_getOffsets(in_menuObj);
817
818 for (var cnt = 0; cnt < domMenu_selectElements.length; cnt++) {
819 var thisSelect = domMenu_selectElements[cnt];
820
821 // mozilla doesn't have a problem with regular selects
822 if (domMenu_isGecko && thisSelect.size <= 1 && !thisSelect.multiple) {
823 continue;
824 }
825
826 // {!} make sure this hash is congruent with domTT hash {!}
827 if (!thisSelect.hideList) {
828 thisSelect.hideList = new domMenu_Hash();
829 }
830
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));
837 }
838
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';
854 }
855 }
856 else {
857 thisSelect.hideList.setItem(in_menuObj.id, true);
858 thisSelect.style.visibility = 'hidden';
859 }
860 }
861 }
862 }
863
864 // }}}
865 // {{{ domMenu_getOffsets()
866
867 function domMenu_getOffsets(in_object)
868 {
869 var originalObject = in_object;
870 var originalWidth = in_object.offsetWidth;
871 var originalHeight = in_object.offsetHeight;
872 var offsetLeft = 0;
873 var offsetTop = 0;
874
875 while (in_object) {
876 offsetLeft += in_object.offsetLeft;
877 offsetTop += in_object.offsetTop;
878 in_object = in_object.offsetParent;
879 }
880
881 return new domMenu_Hash(
882 'left', offsetLeft,
883 'top', offsetTop,
884 'right', offsetLeft + originalWidth,
885 'bottom', offsetTop + originalHeight,
886 'leftCenter', offsetLeft + originalWidth/2,
887 'topCenter', offsetTop + originalHeight/2,
888 'radius', Math.max(originalWidth, originalHeight)
889 );
890 }
891
892 // }}}
893 // {{{ domMenu_callTimeout()
894
895 function domMenu_callTimeout(in_function, in_timeout, in_args, in_basename, in_type)
896 {
897 if (in_timeout == 0) {
898 var tmp_function = new Function('argv', in_function);
899 tmp_function(in_args);
900 }
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) + ');');
904
905 var tmp_args = new Array();
906 for (var i = 0; i < in_args.length; i++) {
907 tmp_args[i] = in_args[i];
908 }
909
910 if (!domMenu_isKonq && !domMenu_isIE50) {
911 domMenu_timeouts[in_type].setItem(in_basename, setTimeout(function() { tmp_function(tmp_args); }, in_timeout));
912 }
913 else {
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);
920
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));
922 }
923 }
924 }
925
926 // }}}
927 // {{{ domMenu_cancelTimeout()
928
929 function domMenu_cancelTimeout(in_basename, in_type)
930 {
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));
934 }
935 else {
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);
940 }
941 }
942 }
943
944 // }}}
945 // {{{ domMenu_correctEdgeBleed()
946
947 function domMenu_correctEdgeBleed(in_width, in_height, in_x, in_y, in_padding, in_axis)
948 {
949 if (domMenu_isIE && !domMenu_isIE5) {
950 var pageHeight = document.documentElement.clientHeight;
951 }
952 else if (!domMenu_isKonq) {
953 var pageHeight = document.body.clientHeight;
954 }
955 else {
956 var pageHeight = window.innerHeight;
957 }
958
959 var pageYOffset = domMenu_isIE ? document.body.scrollTop : window.pageYOffset;
960 var pageXOffset = domMenu_isIE ? document.body.scrollLeft : window.pageXOffset;
961
962
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;
966
967 // we are bleeding off the right, move menu to stay on page
968 if (bleedRight > 0) {
969 in_x -= bleedRight;
970 }
971
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
974 if (bleedLeft < 0) {
975 in_x += bleedLeft;
976 }
977 }
978 else {
979 var bleedTop = (in_y - pageYOffset) - in_padding;
980 var bleedBottom = (in_y - pageYOffset) + in_height - (pageHeight - in_padding);
981
982 // if we are bleeding off the bottom, move menu to stay on page
983 if (bleedBottom > 0) {
984 in_y -= bleedBottom;
985 }
986
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
989 if (bleedTop < 0) {
990 in_y += bleedTop;
991 }
992 }
993
994 return new Array(in_x, in_y);
995 }
996
997 // }}}
998 // {{{ domMenu_toggleSubMenu()
999
1000 function domMenu_toggleSubMenu(in_parentElement, in_style)
1001 {
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'];
1011 }
1012
1013 in_parentElement.firstChild.className = className;
1014
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'];
1023 }
1024
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;
1028
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'];
1033 }
1034 else {
1035 var yCoor = tmp_offsets.items['bottom'];
1036 }
1037 }
1038 else {
1039 var xCoor = tmp_offsets.items['right'] + settings.items['horizontalSubMenuOffsetX'];
1040 var yCoor = tmp_offsets.items['top'] + settings.items['horizontalSubMenuOffsetY'];
1041 }
1042
1043 var minWidth = settings.items['subMenuMinWidth'];
1044 var renderedWidth = subMenu.offsetWidth;
1045 if (minWidth == 'inherit') {
1046 minWidth = in_parentElement.offsetWidth + settings.items['subMenuWidthCorrection'];
1047 }
1048 else if (minWidth == 'auto') {
1049 minWidth = renderedWidth;
1050 }
1051
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';
1055 }
1056 else {
1057 // change the width of the table
1058 subMenu.firstChild.style.width = Math.max(minWidth, renderedWidth) + 'px';
1059 }
1060
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';
1064
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';
1068 }
1069 }
1070 }
1071
1072 // force konqueror to change the styles
1073 if (domMenu_isKonq) {
1074 in_parentElement.firstChild.style.display = 'none';
1075 in_parentElement.firstChild.style.display = '';
1076 }
1077
1078 subMenu.style.visibility = in_style;
1079 domMenu_detectCollisions(subMenu, (in_style == 'hidden'));
1080 }
1081 }
1082
1083 // }}}
1084 // {{{ domMenu_toggleHighlight()
1085
1086 function domMenu_toggleHighlight(in_element, in_status)
1087 {
1088 // if this is a heading, don't change the style
1089 if (!in_element.data.items['numChildren'] && !in_element.data.items['uri']) {
1090 return;
1091 }
1092
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;
1097
1098 var pseudoClass;
1099 if (in_status) {
1100 if (in_element.data.hasItem('subMenu') && in_element.data.items['subMenu'].style.visibility == 'visible') {
1101 pseudoClass = 'Active';
1102 }
1103 else if (in_element.data.items['numChildren'] || in_element.data.items['uri']) {
1104 pseudoClass = 'Hover';
1105 }
1106 }
1107
1108 if (pseudoClass) {
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 = '';
1114 }
1115 }
1116 else {
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 = '';
1121 }
1122 }
1123
1124 highlightElement.className = className;
1125
1126 // force konqueror to change the styles
1127 if (domMenu_isKonq) {
1128 highlightElement.style.display = 'none';
1129 highlightElement.style.display = '';
1130 }
1131 }
1132
1133 // }}}
1134 // {{{ domMenu_resolveLink()
1135
1136 function domMenu_resolveLink(in_this, in_event)
1137 {
1138 var eventObj = domMenu_isIE ? event : in_event;
1139 var currentTarget = domMenu_isIE ? in_this : eventObj.currentTarget;
1140 var basename = currentTarget.data.items['basename'];
1141
1142 // close the menu system immediately when we resolve the uri
1143 domMenu_changeActivePath(false, domMenu_activeElement.items[basename], 0);
1144
1145 if (currentTarget.data.items['uri']) {
1146 window.status = 'Resolving Link...';
1147
1148 // open in current window
1149 if (!currentTarget.data.items['target'] || currentTarget.data.items['target'] == '_self') {
1150 window.location = currentTarget.data.items['uri'];
1151 }
1152 // open in new window
1153 else {
1154 window.open(currentTarget.data.items['uri'], currentTarget.data.items['target']);
1155 }
1156 }
1157 }
1158
1159 // }}}
1160 // {{{ domMenu_quote()
1161
1162 function domMenu_quote(in_string)
1163 {
1164 return "'" + in_string.replace(new RegExp("'", 'g'), "\\'") + "'";
1165 }
1166
1167 // }}}