inital git commit
[phpns.git] / inc / js / tour / amberjack.js
blob20df66fe42b6ee535e8945e2a1884b52ce8562b5
1 /*\r
2  * first cut: 17. Oct 2006\r
3  * Amberjack 0.9 - Site Tour Creator - Simple. Free. Open Source.\r
4  *\r
5  * $Id: amberjack.js,v 1.17 2007/02/09 20:46:24 aya Exp $\r
6  *\r
7  * Copyright (C) 2006 Arash Yalpani <arash@yalpani.de>\r
8  *\r
9  * This library is free software; you can redistribute it and/or\r
10  * modify it under the terms of the GNU Lesser General Public\r
11  * License as published by the Free Software Foundation; either\r
12  * version 2.1 of the License, or (at your option) any later version.\r
13  *\r
14  * This library is distributed in the hope that it will be useful,\r
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
17  * Lesser General Public License for more details.\r
19  * You should have received a copy of the GNU Lesser General Public\r
20  * License along with this library; if not, write to the Free Software\r
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\r
22  */\r
25 // Try to be compatible with other browsers\r
26 // Only use firebug logging if available\r
27 if (typeof console == 'undefined') {\r
28   console = {};\r
29   console.log = function() {};\r
30 }\r
32 /**\r
33  * Capsulates some static helper functions\r
34  * @author Arash Yalpani\r
35  *\r
36  * This one is mainly for myself, but you can learn from that.\r
37  *\r
38  *\r
39  * How this library works -\r
40  *\r
41  * Hint: to change Amberjack's default behavior, set values\r
42  *       prior to the call to Amberjack.open() (in the wizard's output)\r
43  *\r
44  * 1. Amberjack.open() is called through the HTML code the wizard spit out\r
45  *    you should have includet in your site's template file\r
46  *\r
47  * 2. Amberjack.open()...:\r
48  *\r
49  *    2.1. ... checks for tourId and skinId url param...\r
50  *      2.1.1. ...and stops execution, if no tourId was passed by url\r
51  *      2.2.1. ...and sets skinId to default 'model_t' if none  was\r
52  *                passed by url\r
53  *\r
54  *    2.2. ... reads your web page's DOM structure, searches for the tour\r
55  *             definition (you should have pasted into your site's\r
56  *             template), parses it to create the array 'Amberjack.pages'\r
57  *             and to calculate the tour's params (i.e. number of tour\r
58  *             pages, closeUrl)\r
59  *\r
60  *    2.3. ... fetches control.tpl.js and style.css from\r
61  *             http://amberjack.org/src/stable/skin/<skinname>/\r
62  *             (default setting) OR from your own site, if you have set\r
63  *             Amberjack.BASE_URL's value accordingly\r
64  *\r
65  *    2.4. ... covers your web page's body with a transparent layer (DIV) if\r
66  *             Amberjack.doCoverBody is 'true' which is the default option\r
67  *\r
68  * 3. In step '2.3', I explained that control.tpl.js is fetched from\r
69  *    either amberjack.org or your own server. control.tpl.js is the\r
70  *    template file of a skin and what it does is to call the function\r
71  *    AmberjackControl.open('<div ... </div>') like this. The HTML\r
72  *    inside is the control's template.\r
73  *\r
74  *    3.1. AmberjackControl.open() ...\r
75  *      3.1.1. ... fills the template's placeholders with values\r
76  *      3.1.2. ... creates a DIV for the control\r
77  *      3.1.3. ... fills the DIV's content with the assembled skin\r
78  *                 template (see 2.3)\r
79  *      3.1.4. ... hides the control's close button if no closeUrl\r
80  *                 was specified through wizard's output and\r
81  *                 option 'onCloseClickStay' was not set to true\r
82  *      3.1.4. ... checks for optional Amberjack.ADD_STYLE and\r
83  *                 Amberjack.ADD_SCRIPT and post fetches them if set.\r
84  *                 You can use this to manipulate tour's behaviour\r
85  *                 right after it gets visible. Maximum flexibility!\r
86  *\r
87  * That's it, basically!\r
88  */\r
91 AmberjackBase = {\r
93   /**\r
94    * Proxy alerter\r
95    * @author Arash Yalpani\r
96    *\r
97    * @param str Text for alert\r
98    *\r
99    * @example alert('An error occurred')\r
100    */\r
102   alert: function(str) {\r
103     alert('Amberjack alert: ' + str);\r
104   },\r
106   /**\r
107    * Returns FIRST matching element by tagname\r
108    * @author Arash Yalpani\r
109    *\r
110    * @param tagName name of tags to filter\r
111    * @return first matching dom node or false if none exists\r
112    *\r
113    * @example getByTagName('div') => domNode\r
114    * @example getByTagName('notexistent') => false\r
115    */\r
117   getByTagName: function(tagName) {\r
118     var els = document.getElementsByTagName(tagName);\r
119     if (els.length > 0) {\r
120       return els[0];\r
121     }\r
123     return false;\r
124   },\r
126   /**\r
127    * Returns an array of matching DOM nodes\r
128    * @author Arash Yalpani\r
129    *\r
130    * @param tagName name of tags to filter\r
131    * @param attrName name of attribute, matching tags must contain\r
132    * @param attrValue value of attribute, matching tags must contain\r
133    * @param domNode optional: dom node to start filtering from\r
134    * @return Array of matching dom nodes\r
135    *\r
136    * @example getElementsByTagNameAndAttr('div', 'class', 'highlight') => [domNode1, domNode2, ...]\r
137    */\r
138    getElementsByTagNameAndAttr: function(tagName, attrName, attrValue, domNode) {\r
139     if (domNode) {\r
140       els = domNode.getElementsByTagName(tagName);\r
141     }\r
142     else {\r
143       els = document.getElementsByTagName(tagName);\r
144     }\r
146     if (els.length === 0) {\r
147       return [];\r
148     }\r
150     var _els = [];\r
151     for (var i = 0; i < els.length; i++) {\r
152       if (attrName == 'class') {\r
153         classNames = '';\r
154         if (els[i].getAttribute('class')) {\r
155           classNames = els[i].getAttribute('class');\r
156         }\r
157         else {\r
158           if (els[i].getAttribute('className')) {\r
159             classNames = els[i].getAttribute('className');\r
160           }\r
161         }\r
163         var reg = new RegExp('(^| )'+ attrValue +'($| )');\r
164         if (reg.test(classNames)) {\r
165           _els.push(els[i]);\r
166         }\r
167       }\r
168       else {\r
169         if (els[i].getAttribute(attrName) == attrValue) {\r
170           _els.push(els[i]);\r
171         }\r
172       }\r
173     }\r
175     return _els;\r
176   },\r
178   /**\r
179    * Returns url param value\r
180    * @author Arash Yalpani\r
181    *\r
182    * @param url The url to be queried\r
183    * @param paramName The params name\r
184    * @return paramName's value or false if param does not exist or is empty\r
185    *\r
186    * @example getUrlParam('http://localhost/?a=123', 'a') => 123\r
187    * @example getUrlParam('http://localhost/?a=123', 'b') => false\r
188    * @example getUrlParam('http://localhost/?a=',    'a') => false\r
189    */\r
191   getUrlParam: function(url, paramName) {\r
192     var urlSplit = url.split('?');\r
193     if (!urlSplit[1]) { // no query\r
194       return false;\r
195     }\r
197     var urlQuery = urlSplit[1];\r
198     var paramsSplit = urlSplit[1].split('&');\r
199     for (var i = 0; i < paramsSplit.length; i++) {\r
200       paramSplit = paramsSplit[i].split('=');\r
201       if (paramSplit[0] == paramName) {\r
202         return paramSplit[1] ? paramSplit[1] : false;\r
203       }\r
204     }\r
206     return false;\r
207   },\r
209   /**\r
210    * Injects javascript or css file into document\r
211    *\r
212    * @author Arash Yalpani\r
213    *\r
214    * @param url The JavaScript/CSS file's url\r
215    * @param type Either 'script' OR 'style'\r
216    * @param onerror Optional: callback handler if loading did not work\r
217    *\r
218    * @example loadScript('http://localhost/js/dummy.js', function(){alert('could not load')})\r
219    * Note that a HEAD tag needs to be existent in the current document\r
220    */\r
222   postFetch: function(url, type, onerror) {\r
223     if (type === 'script') {\r
224       scriptOrStyle = document.createElement('script');\r
225       scriptOrStyle.type = 'text/javascript';\r
226       scriptOrStyle.src  = url;\r
227     }\r
228     else {\r
229       scriptOrStyle = document.createElement('link');\r
230       scriptOrStyle.type = 'text/css';\r
231       scriptOrStyle.rel  = 'stylesheet';\r
232       scriptOrStyle.href = url;\r
233     }\r
235     if (onerror) { scriptOrStyle.onerror = onerror; }\r
237     var head = AmberjackBase.getByTagName('head');\r
238     if (head) {\r
239       head.appendChild(scriptOrStyle);\r
240       return ;\r
241     }\r
243     AmberjackBase.alert('head tag is missing');\r
244   }\r
245 };\r
248 /**\r
249  * Amberjack Control class\r
250  * @author Arash Yalpani\r
251  */\r
253 AmberjackControl = {\r
255   /**\r
256    * Callback handler for template files. Takes template HTML and fills placeholders\r
257    * @author Arash Yalpani\r
258    *\r
259    * @param tplHtml HTML code including Amberjack placeholders\r
260    *\r
261    * @example AmberjackControl.open('<div>{body}</div>')\r
262    * Note that this method should be called directly through control.tpl.js files\r
263    */\r
265   open: function(tplHtml) {\r
266     var urlSplit = false;\r
267     var urlQuery = false;\r
268     tplHtml = tplHtml.replace(/{skinId}/, Amberjack.skinId);\r
269     if (Amberjack.pages[Amberjack.pageId].prevUrl) {\r
270       var prevUrl = Amberjack.pages[Amberjack.pageId].prevUrl;\r
271       urlSplit = prevUrl.split('?');\r
272       urlQuery = urlSplit[1] ? urlSplit[1] : false;\r
273       if (Amberjack.urlPassTourParams) {\r
274         prevUrl+= (urlQuery ? '&' : '?') + 'tourId=' + Amberjack.tourId + (Amberjack.skinId ? '&skinId=' + Amberjack.skinId : '');\r
275       }\r
277       tplHtml = tplHtml.replace(/{prevClick}/,   "location.href='" + prevUrl + "';return false;");\r
278       tplHtml = tplHtml.replace(/{prevClass}/,   '');\r
279     }\r
280     else {\r
281       tplHtml = tplHtml.replace(/{prevClick}/,   'return false;');\r
282       tplHtml = tplHtml.replace(/{prevClass}/,   'disabled');\r
283     }\r
285     if (Amberjack.pages[Amberjack.pageId].nextUrl) {\r
286       var nextUrl = Amberjack.pages[Amberjack.pageId].nextUrl;\r
287       urlSplit = nextUrl.split('?');\r
288       urlQuery = urlSplit[1] ? urlSplit[1] : false;\r
289       if (Amberjack.urlPassTourParams && (!Amberjack.hasExitPage || Amberjack.pages[nextUrl].nextUrl)) { // do not append params for exit page (if exit page exists)\r
290         nextUrl+= (urlQuery ? '&' : '?') + 'tourId=' + Amberjack.tourId + (Amberjack.skinId ? '&skinId=' + Amberjack.skinId : '');\r
291       }\r
293       tplHtml = tplHtml.replace(/{nextClick}/,       "location.href='" + nextUrl + "';return false;");\r
294       tplHtml = tplHtml.replace(/{nextClass}/,       '');\r
295     }\r
296     else {\r
297       tplHtml = tplHtml.replace(/{nextClick}/,       'return false;');\r
298       tplHtml = tplHtml.replace(/{nextClass}/,       'disabled');\r
299     }\r
301     tplHtml = tplHtml.replace(/{textOf}/,          Amberjack.textOf);\r
302     tplHtml = tplHtml.replace(/{textClose}/,       Amberjack.textClose);\r
303     tplHtml = tplHtml.replace(/{textPrev}/,        Amberjack.textPrev);\r
304     tplHtml = tplHtml.replace(/{textNext}/,        Amberjack.textNext);\r
305     tplHtml = tplHtml.replace(/{currPage}/,        Amberjack.pageCurrent);\r
306     tplHtml = tplHtml.replace(/{pageCount}/,       Amberjack.pageCount);\r
308     tplHtml = tplHtml.replace(/{body}/,            AmberjackBase.getElementsByTagNameAndAttr('div', 'title', Amberjack.pageId, document.getElementById(Amberjack.tourId))[0].innerHTML);\r
310     var div = document.createElement('div');\r
311     div.id = 'AmberjackControl';\r
312     div.innerHTML = tplHtml;\r
314     document.body.appendChild(div);\r
316     // Amberjack.doHighlight();\r
318     // No URL was set AND no click-close-action was configured:\r
319     if (!Amberjack.closeUrl && !Amberjack.onCloseClickStay) {\r
320       document.getElementById('ajClose').style.display = 'none';\r
321     }\r
323     // post fetch a CSS file you can define by setting Amberjack.ADD_STYLE\r
324     // right before the call to Amberjack.open();\r
325     if (Amberjack.ADD_STYLE) {\r
326       AmberjackBase.postFetch(Amberjack.ADD_STYLE, 'style');\r
327     }\r
329     // post fetch a script you can define by setting Amberjack.ADD_SCRIPT\r
330     // right before the call to Amberjack.open();\r
331     if (Amberjack.ADD_SCRIPT) {\r
332       AmberjackBase.postFetch(Amberjack.ADD_SCRIPT, 'script');\r
333     }\r
334   },\r
336   /**\r
337    * Removes AmberjackControl div from DOM\r
338    * @author Arash Yalpani\r
339    *\r
340    * @example AmberjackControl.close()\r
341    */\r
343   close: function() {\r
344     e = document.getElementById('AmberjackControl');\r
345     e.parentNode.removeChild(e);\r
346   }\r
347 };\r
350 /**\r
351  * Amberjack's main class\r
352  * @author Arash Yalpani\r
353  */\r
355 Amberjack = {\r
357   // constants\r
359   BASE_URL: 'inc/js/tour/', // do not forget trailing slash!\r
361   // explicit attributes\r
363   // - set these through url (...&tourId=MyTour&skinId=Safari...)\r
364   // - OR in your tour template right above the call to Amberjack.open()\r
366   tourId:    false,     // mandatory: if not set, tour will not open\r
367   skinId:    false,     // optional: if not set, skin "model_t" will be used\r
369   // - set these in your tour template right above the call to Amberjack.open()\r
371   textOf:    'of',      // text of splitter between "2 of 3"\r
372   textClose: 'x',       // text of close button\r
373   textPrev:  '&laquo;', // text of previous button\r
374   textNext:  '&raquo;', // text of next button\r
376   // - set set these in your tour template right above the call to Amberjack.open()\r
378   onCloseClickStay     : false, // set this to 'true', if you want the close button to close tour but remain on current page\r
379   doCoverBody          : true,  // set this to 'false' if you don't want your site's page to be covered\r
380   bodyCoverCloseOnClick: false, // set this to 'true', if a click on the body cover should force it to close\r
381   urlPassTourParams    : true,  // set this to false, if you have hard coded the tourId and skinId in your tour\r
382                                 //     template. the tourId and skindId params will not get passed on prev/next button click\r
387   // private attributes - don't touch\r
389   pageId:    false,\r
390   pages:     {},\r
391   pageCount: 0,\r
392   hasExitPage: false,\r
393   interval: false,\r
396   /**\r
397    * Initializes tour, creates transparent layer and causes AmberjackControl\r
398    * to open the skin's template (control.tpl.js) into document. Call this\r
399    * manually right after inclusion of this library. Don't forget to pass\r
400    * tourId param through URL to show tour!\r
401    *\r
402    * Iterates child DIVs of DIV.ajTourDef, extracts tour pages\r
403    *\r
404    * @author Arash Yalpani\r
405    *\r
406    * @example Amberjack.open()\r
407    * Note that a HEAD tag needs to be existent in the current document\r
408    */\r
410   open: function() {\r
411     Amberjack.tourId = Amberjack.tourId ? Amberjack.tourId : AmberjackBase.getUrlParam(location.href, 'tourId');\r
412     Amberjack.skinId = Amberjack.skinId ? Amberjack.skinId : AmberjackBase.getUrlParam(location.href, 'skinId');\r
414     if (!Amberjack.tourId) { // do nothing if tourId is not passed through url\r
415       return;\r
416     }\r
418     if (!Amberjack.skinId) { // set default skinId\r
419       Amberjack.skinId = 'model_t';\r
420     }\r
422     var tourDef = false;\r
423     var tourDefElements = AmberjackBase.getElementsByTagNameAndAttr('div', 'class', 'ajTourDef');\r
424     for (i = 0; i < tourDefElements.length; i++) {\r
425       if (tourDefElements[i].getAttribute('id') == Amberjack.tourId) {\r
426         tourDef = tourDefElements[i];\r
427       }\r
428     }\r
430     if (!tourDef) {\r
431       AmberjackBase.alert('DIV with CLASS "ajTourDef" and ID "' + Amberjack.tourId + '" is not defined');\r
432     }\r
434     // Is there a specified closeUrl (title attribute of DIV.ajTourDef)?\r
435     // Don't show close button if not set\r
436     Amberjack.closeUrl = tourDef.getAttribute('title') ? tourDef.getAttribute('title') : false;\r
438     var children = tourDef.childNodes;\r
439     var _children = []; // cleaned up version...\r
440     for (i = 0; i < children.length; i++) {\r
441       if (!children[i].tagName || children[i].tagName.toLowerCase() != 'div') { continue ; }\r
442       _children.push(children[i]);\r
443     }\r
445     // init tour pages\r
446     for (i = 0; i < _children.length; i++) {\r
447       Amberjack.pages[_children[i].getAttribute('title')] = {};\r
448     }\r
450     for (i = 0; i < _children.length; i++) {\r
451       if (!_children[i].tagName || _children[i].tagName.toLowerCase() != 'div') { continue ; }\r
453       if (!_children[i].getAttribute('title')) {\r
454         AmberjackBase.alert('attribute "title" is missing');\r
455         return ;\r
456       }\r
458       // -- start: check for matching page in divs --\r
459       if (Amberjack.urlMatch(_children[i].getAttribute('title')) && _children[i].innerHTML !== '') {\r
460         Amberjack.pageCurrent = i + 1;\r
461         Amberjack.pageId = _children[i].getAttribute('title');\r
462       }\r
463       // -- end: check for matching page in divs --\r
465       Amberjack.pageCount++;\r
466       if (i >= 1 && i < _children.length) {\r
467         Amberjack.pages[_children[i].getAttribute('title')].prevUrl = _children[i - 1].getAttribute('title');\r
468       }\r
469       if (i < _children.length - 1) {\r
470         Amberjack.pages[_children[i].getAttribute('title')].nextUrl = _children[i + 1].getAttribute('title');\r
471       }\r
472     }\r
474     if (_children[i-1].innerHTML === '') { // empty page div reduces pageCount by 1\r
475       Amberjack.pageCount = Amberjack.pageCount - 1;\r
476       Amberjack.hasExitPage = true;\r
477     }\r
479     if (!Amberjack.pageId) {\r
480       AmberjackBase.alert('no matching page in ajTourDef found');\r
481     }\r
483     AmberjackBase.postFetch(Amberjack.BASE_URL + 'skin/' + Amberjack.skinId.toLowerCase() + '/control.tpl.js', 'script');\r
484     AmberjackBase.postFetch(Amberjack.BASE_URL + 'skin/' + Amberjack.skinId.toLowerCase() + '/style.css', 'style');\r
486     if (Amberjack.doCoverBody) {\r
487       Amberjack.coverBody();\r
488     }\r
489   },\r
491   /**\r
492    * Checks if passed href is *included* in current location's href\r
493    * @author Arash Yalpani\r
494    *\r
495    * @param href URL to be matched against\r
496    *\r
497    * @example Amberjack.urlMatch('http://mysite.com/domains/')\r
498    */\r
499   urlMatch: function(href) {\r
500     return (location.href.indexOf(href) != -1);\r
501   },\r
504   /**\r
505    * Return height of inner window\r
506    * Copied and modified:\r
507    * http://www.dynamicdrive.com/forums/archive/index.php/t-10373.html\r
508    *\r
509    * @author Arash Yalpani\r
510    * @example Amberjack.getWindowInnerHeight()\r
511    */\r
512   getWindowInnerHeight: function() {\r
513     var yInner;\r
515     if (window.innerHeight && window.scrollMaxY) {\r
516       yInner = window.innerHeight + window.scrollMaxY;\r
517     }\r
518     else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac\r
519       yInner = document.body.scrollHeight;\r
520     }\r
521     else if (document.documentElement && document.documentElement.scrollHeight > document.documentElement.offsetHeight){ // Explorer 6 strict mode\r
522       yInner = document.documentElement.scrollHeight;\r
523     }\r
524     else { // Explorer Mac...would also work in Mozilla and Safari\r
525       yInner = document.body.offsetHeight;\r
526     }\r
528     var windowWidth, windowHeight;\r
529     if (self.innerHeight) { // all except Explorer\r
530       windowHeight = self.innerHeight;\r
531     }\r
532     else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode\r
533       windowHeight = document.documentElement.clientHeight;\r
534     }\r
535     else if (document.body) { // other Explorers\r
536       windowHeight = document.body.clientHeight;\r
537     }\r
539     // for small pages with total height less then height of the viewport\r
540     return (yInner < windowHeight) ? windowHeight : yInner;\r
541   },\r
543   /**\r
544    * Creates transparent layer and places it in the document, in front of\r
545    * all other layers (through CSS z-index)\r
546    * @author Arash Yalpani\r
547    *\r
548    * @example Amberjack.coverBody()\r
549    */\r
550   coverBody: function() {\r
551     var div = document.createElement('div');\r
552     div.id = 'ajBodyCover';\r
554     div.style.height = Amberjack.getWindowInnerHeight() + 'px';\r
556     if (Amberjack.bodyCoverCloseOnClick) {\r
557       div.onclick = function() {\r
558         Amberjack.uncoverBody();\r
559       };\r
560     }\r
562     document.body.appendChild(div);\r
563     Amberjack.interval = window.setInterval(Amberjack.refreshCover, 2000);\r
564   },\r
566   /**\r
567    * refreshes transparent layer's height\r
568    * @author Arash Yalpani\r
569    *\r
570    * @example Amberjack.refreshCover()\r
571    */\r
572   refreshCover: function() {\r
573     document.getElementById('ajBodyCover').style.height = Amberjack.getWindowInnerHeight() + 'px';\r
574   },\r
576   /**\r
577    * Removes transparent layer from document\r
578    * @author Arash Yalpani\r
579    *\r
580    * @example Amberjack.uncoverBody()\r
581    */\r
582   uncoverBody: function() {\r
583     window.clearInterval(Amberjack.interval);\r
584     document.body.removeChild(document.getElementById('ajBodyCover'));\r
585   },\r
587   /*\r
588   doHighlight: function() {\r
589     var body = document.body;\r
590     var highlightElements = AmberjackBase.getElementsByTagNameAndAttr('div', 'class', 'ajHighlight', body);\r
591     for (i = 0; i < highlightElements.length; i++) {\r
592       highlightElements[i].style.border = '3px solid red';\r
593       highlightElements[i].style.backgroundColor = '#fee';\r
594     }\r
595   },\r
596   */\r
599   /**\r
600    * Gets called, whenever the user clicks on the close button of Amberjack control\r
601    * @author Arash Yalpani\r
602    *\r
603    * @example Amberjack.close()\r
604    */\r
605   close: function() {\r
606     if (Amberjack.onCloseClickStay) {\r
607       AmberjackControl.close();\r
608       if (Amberjack.doCoverBody) {\r
609         Amberjack.uncoverBody();\r
610       }\r
611       return null;\r
612     }\r
614     if (Amberjack.closeUrl) {\r
615       window.location.href = Amberjack.closeUrl;\r
616     }\r
617     return null;\r
618   }\r