2 * first cut: 17. Oct 2006
\r
3 * Amberjack 0.9 - Site Tour Creator - Simple. Free. Open Source.
\r
5 * $Id: amberjack.js,v 1.17 2007/02/09 20:46:24 aya Exp $
\r
7 * Copyright (C) 2006 Arash Yalpani <arash@yalpani.de>
\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
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
25 // Try to be compatible with other browsers
\r
26 // Only use firebug logging if available
\r
27 if (typeof console == 'undefined') {
\r
29 console.log = function() {};
\r
33 * Capsulates some static helper functions
\r
34 * @author Arash Yalpani
\r
36 * This one is mainly for myself, but you can learn from that.
\r
39 * How this library works -
\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
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
47 * 2. Amberjack.open()...:
\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
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
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
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
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
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
87 * That's it, basically!
\r
95 * @author Arash Yalpani
\r
97 * @param str Text for alert
\r
99 * @example alert('An error occurred')
\r
102 alert: function(str) {
\r
103 alert('Amberjack alert: ' + str);
\r
107 * Returns FIRST matching element by tagname
\r
108 * @author Arash Yalpani
\r
110 * @param tagName name of tags to filter
\r
111 * @return first matching dom node or false if none exists
\r
113 * @example getByTagName('div') => domNode
\r
114 * @example getByTagName('notexistent') => false
\r
117 getByTagName: function(tagName) {
\r
118 var els = document.getElementsByTagName(tagName);
\r
119 if (els.length > 0) {
\r
127 * Returns an array of matching DOM nodes
\r
128 * @author Arash Yalpani
\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
136 * @example getElementsByTagNameAndAttr('div', 'class', 'highlight') => [domNode1, domNode2, ...]
\r
138 getElementsByTagNameAndAttr: function(tagName, attrName, attrValue, domNode) {
\r
140 els = domNode.getElementsByTagName(tagName);
\r
143 els = document.getElementsByTagName(tagName);
\r
146 if (els.length === 0) {
\r
151 for (var i = 0; i < els.length; i++) {
\r
152 if (attrName == 'class') {
\r
154 if (els[i].getAttribute('class')) {
\r
155 classNames = els[i].getAttribute('class');
\r
158 if (els[i].getAttribute('className')) {
\r
159 classNames = els[i].getAttribute('className');
\r
163 var reg = new RegExp('(^| )'+ attrValue +'($| )');
\r
164 if (reg.test(classNames)) {
\r
169 if (els[i].getAttribute(attrName) == attrValue) {
\r
179 * Returns url param value
\r
180 * @author Arash Yalpani
\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
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
191 getUrlParam: function(url, paramName) {
\r
192 var urlSplit = url.split('?');
\r
193 if (!urlSplit[1]) { // no query
\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
210 * Injects javascript or css file into document
\r
212 * @author Arash Yalpani
\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
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
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
229 scriptOrStyle = document.createElement('link');
\r
230 scriptOrStyle.type = 'text/css';
\r
231 scriptOrStyle.rel = 'stylesheet';
\r
232 scriptOrStyle.href = url;
\r
235 if (onerror) { scriptOrStyle.onerror = onerror; }
\r
237 var head = AmberjackBase.getByTagName('head');
\r
239 head.appendChild(scriptOrStyle);
\r
243 AmberjackBase.alert('head tag is missing');
\r
249 * Amberjack Control class
\r
250 * @author Arash Yalpani
\r
253 AmberjackControl = {
\r
256 * Callback handler for template files. Takes template HTML and fills placeholders
\r
257 * @author Arash Yalpani
\r
259 * @param tplHtml HTML code including Amberjack placeholders
\r
261 * @example AmberjackControl.open('<div>{body}</div>')
\r
262 * Note that this method should be called directly through control.tpl.js files
\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
277 tplHtml = tplHtml.replace(/{prevClick}/, "location.href='" + prevUrl + "';return false;");
\r
278 tplHtml = tplHtml.replace(/{prevClass}/, '');
\r
281 tplHtml = tplHtml.replace(/{prevClick}/, 'return false;');
\r
282 tplHtml = tplHtml.replace(/{prevClass}/, 'disabled');
\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
293 tplHtml = tplHtml.replace(/{nextClick}/, "location.href='" + nextUrl + "';return false;");
\r
294 tplHtml = tplHtml.replace(/{nextClass}/, '');
\r
297 tplHtml = tplHtml.replace(/{nextClick}/, 'return false;');
\r
298 tplHtml = tplHtml.replace(/{nextClass}/, 'disabled');
\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
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
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
337 * Removes AmberjackControl div from DOM
\r
338 * @author Arash Yalpani
\r
340 * @example AmberjackControl.close()
\r
343 close: function() {
\r
344 e = document.getElementById('AmberjackControl');
\r
345 e.parentNode.removeChild(e);
\r
351 * Amberjack's main class
\r
352 * @author Arash Yalpani
\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: '«', // text of previous button
\r
374 textNext: '»', // 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
392 hasExitPage: false,
\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
402 * Iterates child DIVs of DIV.ajTourDef, extracts tour pages
\r
404 * @author Arash Yalpani
\r
406 * @example Amberjack.open()
\r
407 * Note that a HEAD tag needs to be existent in the current document
\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
418 if (!Amberjack.skinId) { // set default skinId
\r
419 Amberjack.skinId = 'model_t';
\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
431 AmberjackBase.alert('DIV with CLASS "ajTourDef" and ID "' + Amberjack.tourId + '" is not defined');
\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
446 for (i = 0; i < _children.length; i++) {
\r
447 Amberjack.pages[_children[i].getAttribute('title')] = {};
\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
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
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
469 if (i < _children.length - 1) {
\r
470 Amberjack.pages[_children[i].getAttribute('title')].nextUrl = _children[i + 1].getAttribute('title');
\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
479 if (!Amberjack.pageId) {
\r
480 AmberjackBase.alert('no matching page in ajTourDef found');
\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
492 * Checks if passed href is *included* in current location's href
\r
493 * @author Arash Yalpani
\r
495 * @param href URL to be matched against
\r
497 * @example Amberjack.urlMatch('http://mysite.com/domains/')
\r
499 urlMatch: function(href) {
\r
500 return (location.href.indexOf(href) != -1);
\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
509 * @author Arash Yalpani
\r
510 * @example Amberjack.getWindowInnerHeight()
\r
512 getWindowInnerHeight: function() {
\r
515 if (window.innerHeight && window.scrollMaxY) {
\r
516 yInner = window.innerHeight + window.scrollMaxY;
\r
518 else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
\r
519 yInner = document.body.scrollHeight;
\r
521 else if (document.documentElement && document.documentElement.scrollHeight > document.documentElement.offsetHeight){ // Explorer 6 strict mode
\r
522 yInner = document.documentElement.scrollHeight;
\r
524 else { // Explorer Mac...would also work in Mozilla and Safari
\r
525 yInner = document.body.offsetHeight;
\r
528 var windowWidth, windowHeight;
\r
529 if (self.innerHeight) { // all except Explorer
\r
530 windowHeight = self.innerHeight;
\r
532 else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
\r
533 windowHeight = document.documentElement.clientHeight;
\r
535 else if (document.body) { // other Explorers
\r
536 windowHeight = document.body.clientHeight;
\r
539 // for small pages with total height less then height of the viewport
\r
540 return (yInner < windowHeight) ? windowHeight : yInner;
\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
548 * @example Amberjack.coverBody()
\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
562 document.body.appendChild(div);
\r
563 Amberjack.interval = window.setInterval(Amberjack.refreshCover, 2000);
\r
567 * refreshes transparent layer's height
\r
568 * @author Arash Yalpani
\r
570 * @example Amberjack.refreshCover()
\r
572 refreshCover: function() {
\r
573 document.getElementById('ajBodyCover').style.height = Amberjack.getWindowInnerHeight() + 'px';
\r
577 * Removes transparent layer from document
\r
578 * @author Arash Yalpani
\r
580 * @example Amberjack.uncoverBody()
\r
582 uncoverBody: function() {
\r
583 window.clearInterval(Amberjack.interval);
\r
584 document.body.removeChild(document.getElementById('ajBodyCover'));
\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
600 * Gets called, whenever the user clicks on the close button of Amberjack control
\r
601 * @author Arash Yalpani
\r
603 * @example Amberjack.close()
\r
605 close: function() {
\r
606 if (Amberjack.onCloseClickStay) {
\r
607 AmberjackControl.close();
\r
608 if (Amberjack.doCoverBody) {
\r
609 Amberjack.uncoverBody();
\r
614 if (Amberjack.closeUrl) {
\r
615 window.location.href = Amberjack.closeUrl;
\r