// Prototip 1.1.0 // by Nick Stakenburg - http://www.nickstakenburg.com // 08-11-2007 // // More information on this project: // http://www.nickstakenburg.com/projects/prototip/ // // Licensed under the Creative Commons Attribution 3.0 License // http://creativecommons.org/licenses/by/3.0/ // var Prototip = { Version: '1.1.0', REQUIRED_Prototype: '1.6.0', REQUIRED_Scriptaculous: '1.8.0', start: function() { this.require('Prototype'); }, require: function(library) { if ((typeof window[library] == 'undefined') || (this.convertVersionString(window[library].Version) < this.convertVersionString(this['REQUIRED_' + library]))) throw('Prototip requires ' + library + ' >= ' + this['REQUIRED_' + library]); }, // based on Scriptaculous' implementation convertVersionString: function(versionString) { var r = versionString.split('.'); return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]); }, // fixed viewport.getDimensions. Also excludes scrollbars in firefox. Valid doctype required. viewport : { getDimensions: function() { var dimensions = { }; var B = Prototype.Browser; $w('width height').each(function(d) { var D = d.capitalize(); if (B.Opera) dimensions[d] = document.body['client' + D]; else if (B.WebKit) dimensions[d] = self['inner' + D]; else dimensions[d] = document.documentElement['client' + D]; }); return dimensions; } } }; Prototip.start(); var Tips = { // Configuration closeButtons: false, zIndex: 1200, fixIE: (function(agent){ var version = new RegExp('MSIE ([\\d.]+)').exec(agent); return version ? (parseFloat(version[1]) <= 6) : false; })(navigator.userAgent), tips : [], visible : [], add: function(tip) { this.tips.push(tip); }, remove: function(element) { var tip = this.tips.find(function(t){ return t.element == $(element); }); if (tip) { tip.deactivate(); if (tip.tooltip) { tip.wrapper.remove(); if (Tips.fixIE) tip.iframeShim.remove(); } this.tips = this.tips.without(tip); } }, zIndexRestore : 1200, raise: function(tip) { var highestZ = this.zIndexHighest(); if (!highestZ) { tip.style.zIndex = this.zIndexRestore; return; } var newZ = (tip.style.zIndex != highestZ) ? highestZ + 1 : highestZ; this.tips.pluck('wrapper').invoke('removeClassName', 'highest'); tip.setStyle({ zIndex : newZ }).addClassName('highest'); }, zIndexHighest: function() { var highestZ = this.visible.max(function(v) { return parseInt(v.style.zIndex); }); return highestZ; }, addVisibile: function(tip) { this.removeVisible(tip); this.visible.push(tip); }, removeVisible: function(tip) { this.visible = this.visible.without(tip); } }; var Tip = Class.create({ initialize: function(element, content) { this.element = $(element); Tips.remove(this.element); this.content = content; var isHooking = (arguments[2] && arguments[2].hook); var isShowOnClick = (arguments[2] && arguments[2].showOn == 'click'); this.options = Object.extend({ className: 'default', // see css, this will lead to .prototip .default closeButton: Tips.closeButtons, // true, false delay: !isShowOnClick ? 0.2 : false, // seconds before tooltip appears duration: 0.3, // duration of the effect effect: false, // false, 'appear' or 'blind' hideOn: 'mouseout', hook: false, // { element: topLeft|topRight|bottomLeft|bottomRight, tip: see element } offset: isHooking ? {x:0, y:0} : {x:16, y:16}, fixed: isHooking ? true : false, // follow the mouse if false showOn: 'mousemove', target: this.element, // or another element title: false, viewport: isHooking ? false : true // keep within viewport if mouse is followed }, arguments[2] || {}); this.target = $(this.options.target); this.setup(); if (this.options.effect) { Prototip.require('Scriptaculous'); this.queue = { position: 'end', limit: 1, scope: this.wrapper.identify() } } Tips.add(this); this.activate(); }, setup: function() { // Everything that needs to be build for observing is done here this.wrapper = new Element('div', { 'class' : 'prototip' }).setStyle({ display: 'none', zIndex: Tips.zIndex++ }); this.wrapper.identify(); if (Tips.fixIE) { this.iframeShim = new Element('iframe', { 'class' : 'iframeShim', src: 'javascript:false;' }).setStyle({ display: 'none', zIndex: Tips.zIndexRestore - 1 }); } this.tip = new Element('div', { 'class' : 'content' }).update(this.content); this.tip.insert(new Element('div').setStyle({ clear: 'both' })); if (this.options.closeButton || (this.options.hideOn.element && this.options.hideOn.element == 'closeButton')) this.closeButton = new Element('a', { href: 'javascript:;', 'class' : 'close' }); }, build: function() { if (Tips.fixIE) document.body.appendChild(this.iframeShim).setOpacity(0); // effects go smooth with extra wrapper var wrapper = 'wrapper'; if (this.options.effect) { this.effectWrapper = this.wrapper.appendChild(new Element('div', { 'class' : 'effectWrapper' })); wrapper = 'effectWrapper'; } this.tooltip = this[wrapper].appendChild(new Element('div', { 'class' : 'tooltip ' + this.options.className })); if (this.options.title || this.options.closeButton) { this.toolbar = this.tooltip.appendChild(new Element('div', { 'class' : 'toolbar' })); this.title = this.toolbar.appendChild(new Element('div', { 'class' : 'title' }).update(this.options.title || ' ')); } this.tooltip.insert(this.tip); document.body.appendChild(this.wrapper); // fixate elements for better positioning and effects var fixate = (this.options.effect) ? [this.wrapper, this.effectWrapper]: [this.wrapper]; if (Tips.fixIE) fixate.push(this.iframeShim); // fix width var fixedWidth = this.wrapper.getWidth(); fixate.invoke('setStyle', { width: fixedWidth + 'px' }); // make toolbar width fixed if(this.toolbar) { this.wrapper.setStyle({ visibility : 'hidden' }).show(); this.toolbar.setStyle({ width: this.toolbar.getWidth() + 'px'}); this.wrapper.hide().setStyle({ visibility : 'visible' }); } // add close button if (this.closeButton) this.title.insert({ top: this.closeButton }).insert(new Element('div').setStyle({ clear: 'both' })); var fixedHeight = this.wrapper.getHeight(); fixate.invoke('setStyle', { width: fixedWidth + 'px', height: fixedHeight + 'px' }); this[this.options.effect ? wrapper : 'tooltip'].hide(); }, activate: function() { this.eventShow = this.showDelayed.bindAsEventListener(this); this.eventHide = this.hide.bindAsEventListener(this); // if fixed use mouseover instead of mousemove for less event calls if (this.options.fixed && this.options.showOn == 'mousemove') this.options.showOn = 'mouseover'; if(this.options.showOn == this.options.hideOn) { this.eventToggle = this.toggle.bindAsEventListener(this); this.element.observe(this.options.showOn, this.eventToggle); } this.hideElement = Object.isUndefined(this.options.hideOn.element) ? 'element' : this.options.hideOn.element; var hideOptions = { 'element': this.eventToggle ? [] : [this.element], 'target': this.eventToggle ? [] : [this.target], 'tip': this.eventToggle ? [] : [this.wrapper], 'closeButton': [], '.close' : this.tip.select('.close') } this.hideTargets = hideOptions[this.hideElement]; // add show and hide observers if (this.element && !this.eventToggle) this.element.observe(this.options.showOn, this.eventShow); this.hideAction = (this.options.hideOn.event || this.options.hideOn); if (this.hideTargets) this.hideTargets.invoke('observe', this.hideAction, this.eventHide); // add position observer if not fixed if (!this.options.fixed && this.options.showOn == 'click') { this.eventPosition = this.position.bindAsEventListener(this); this.element.observe('mousemove', this.eventPosition); } // add hide observers to close button and non click elements when they are not the close (delay needs this) if (this.closeButton) this.closeButton.observe('click', this.eventHide); if (this.options.showOn != 'click' && this.hideElement != 'element') { this.eventCheckDelay = this.checkDelay.bindAsEventListener(this); this.element.observe('mouseout', this.eventCheckDelay); } // observe wrapper to raise zIndex this.wrapper.observe('mouseover', function(){ Tips.raise(this.wrapper); }.bind(this)); }, deactivate: function() { if(this.options.showOn == this.options.hideOn) this.element.stopObserving(this.options.showOn, this.eventToggle); else { this.element.stopObserving(this.options.showOn, this.eventShow); this.hideTargets.invoke('stopObserving', this.hideAction, this.eventHide); } if (this.eventPosition) this.element.stopObserving('mousemove', this.eventPosition); if (this.closeButton) this.closeButton.stopObserving(); if (this.eventCheckDelay) this.element.stopObserving('mouseout', this.eventCheckDelay); this.wrapper.stopObserving(); }, showDelayed: function(event){ if (!this.tooltip) this.build(); this.position(event); // follow mouse if (this.wrapper.visible()) return; this.checkDelay(); this.timer = this.show.bind(this).delay(this.options.delay); }, checkDelay: function(){ if (this.timer) { clearTimeout(this.timer); this.timer = null; } }, show: function(){ if (this.wrapper.visible() && this.options.effect != 'appear') return; if (Tips.fixIE) this.iframeShim.show(); Tips.addVisibile(this.wrapper); this.wrapper.show(); if (!this.options.effect) this.tooltip.show(); else { if (this.activeEffect) Effect.Queues.get(this.queue.scope).remove(this.activeEffect); this.activeEffect = Effect[Effect.PAIRS[this.options.effect][0]](this.effectWrapper, { duration: this.options.duration, queue: this.queue}); } }, hide: function(){ this.checkDelay(); if(!this.wrapper.visible()) return; if (!this.options.effect) { if (Tips.fixIE) this.iframeShim.hide(); this.tooltip.hide(); this.wrapper.hide(); Tips.removeVisible(this.wrapper); } else { if (this.activeEffect) Effect.Queues.get(this.queue.scope).remove(this.activeEffect); this.activeEffect = Effect[Effect.PAIRS[this.options.effect][1]](this.effectWrapper, { duration: this.options.duration, queue: this.queue, afterFinish: function(){ if (Tips.fixIE) this.iframeShim.hide(); this.wrapper.hide(); Tips.removeVisible(this.wrapper); }.bind(this)}); } }, toggle: function(event){ if (this.wrapper && this.wrapper.visible()) this.hide(event); else this.showDelayed(event); }, position: function(event){ if (!this.wrapper.hasClassName('highest')) Tips.raise(this.wrapper); var offset = {left: this.options.offset.x, top: this.options.offset.y}; var targetPosition = Position.cumulativeOffset(this.target); var tipd = this.wrapper.getDimensions(); var pos = { left: (this.options.fixed) ? targetPosition[0] : Event.pointerX(event), top: (this.options.fixed) ? targetPosition[1] : Event.pointerY(event) }; // add offsets pos.left += offset.left; pos.top += offset.top; if (this.options.hook) { var dims = {target: this.target.getDimensions(), tip: tipd} var hooks = {target: Position.cumulativeOffset(this.target), tip: Position.cumulativeOffset(this.target)} for(var z in hooks) { switch(this.options.hook[z]){ case 'topRight': hooks[z][0] += dims[z].width; break; case 'topMiddle': hooks[z][0] += (dims[z].width / 2); break; case 'rightMiddle': hooks[z][0] += dims[z].width; hooks[z][1] += (dims[z].height / 2); break; case 'bottomLeft': hooks[z][1] += dims[z].height; break; case 'bottomRight': hooks[z][0] += dims[z].width; hooks[z][1] += dims[z].height; break; case 'bottomMiddle': hooks[z][0] += (dims[z].width / 2); hooks[z][1] += dims[z].height; break; case 'leftMiddle': hooks[z][1] += (dims[z].height / 2); break; } } // move based on hooks pos.left += -1*(hooks.tip[0] - hooks.target[0]); pos.top += -1*(hooks.tip[1] - hooks.target[1]); } // move tooltip when there is a different target if (!this.options.fixed && this.element !== this.target) { var elementPosition = Position.cumulativeOffset(this.element); pos.left += -1*(elementPosition[0] - targetPosition[0]); pos.top += -1*(elementPosition[1] - targetPosition[1]); } if (!this.options.fixed && this.options.viewport) { var scroll = document.viewport.getScrollOffsets(); var viewport = Prototip.viewport.getDimensions(); var pair = {left: 'width', top: 'height'}; for(var z in pair) { if ((pos[z] + tipd[pair[z]] - scroll[z]) > viewport[pair[z]]) pos[z] = pos[z] - tipd[pair[z]] - 2*offset[z]; } } var finalPosition = { left: pos.left + 'px', top: pos.top + 'px' }; this.wrapper.setStyle(finalPosition); if (Tips.fixIE) this.iframeShim.setStyle(finalPosition); } });