/* NAME: MDPopover.js AUTHOR: Ben Delaney DESCRIPTION: Creates and displays 'popover' UI elements — diffent from modals in that they do not completely obscure the rest of the interface, and float above it. REQUIREMENTS: Imagery and CSS. See /assets/images/popoverShadows/ PROVIDES: MD.Popover TODO: THIS IS ALL OLD!... OPTIONS: referenceElement: '' // the element that the popover is being triggered by or relevant to. ,position: '' // [string] will be determined automatically unless you override it here. see http://mootools.net/docs/more/Element/Element.Position for options ,edge: '' // [string] will be determined automatically unless you override it here. see http://mootools.net/docs/more/Element/Element.Position for options ,content: '' // if content is being passed into the popover (as in the case of a modal message or warning.) ,className: '' ,height: '' // 250? ,width: '' // 280? ,zIndex: '' ,offset: {} ,inject: { target: '' // [string] or [element] A target element to put the popover into (or relative to). Defaults to document.body ,where: '' // [string] 'inside', 'after', 'before'. Where to put the popover relative to the target element. } ,exclusive: false // [boolean] Whether to force any other open MDPopovers to close upon the .show() of this one. ,centerOnPage: false // [boolean] Whether to ignore the above 'target' and 'where' options and put the popover in the center of the overall page. ,maskColor: '#000' // [string] The color to make the background mask. maskBackground must be true for this to apply. ,maskOpacity: 0.7 // [integer] maskBackground must be true for this to apply. ,maskZindex: '' ,clickToClose: false // [boolean] Whether a click ON the popover itself triggers the .hide method. (useful for messages, warnings, etc.) ,clickOutsideToClose: true // [boolean] Whether clicking anywhere outside the popover triggers the hide() method ,destroyOnHide: true // [boolean] Whether to destroy the popover HTML elements when hidden. ,closeButton: false // [boolean] Whether to include a 'close' button in the upper left of the popover. // ,onShow: function(popoverContents, referenceElement, popoverObject){} // ,onHide: function(popoverContents, referenceElement, popoverObject){} // ,onFetchSuccess: function(popoverContents, responseTree, responseElements, responseHTML, responseJavaScript){} RETURNS: [object] The HTML is built and positioned, it just needs to be activated with the .show() method. HOW TO USE EXAMPLE */ /*TODO: - Take into account position: 'center', edge: 'center' -- remove nipple - Take into account "interior" positioning, i.e, position: 'centerBottom', edge: 'centerBottom' */ MD.Popover = new Class({ Implements: [Events, Options] ,options: { className: '' ,pointTo: null ,content: null ,height: null ,width: null ,offset: {} ,position: null ,edge: null ,closeButton: true ,clickToClose: false // Mask ,maskTarget: null ,maskOptions: { color: '#000' ,opacity: 0.7 } ,zIndex: 5000 ,exclusive: true ,exclusiveImmune: false ,clickOutsideToClose: true ,destroyOnHide: true ,timeout: null ,injectInto:null // ,onShow: function(thisObj){} // ,onHide: function(thisObj){} } ,initialize: function(options){ this.setOptions(options); // this.windowCoordinates = window.getCoordinates(); // this.zIndex = this.options.zIndex; this.height = this.options.height; this.width = this.options.width; this.maskOptions = this.options.maskOptions; this.pointTo = this.options.pointTo; this.showing = false; this.uniqueId = String.uniqueID(); this.offset = this.options.offset; this.exclusiveImmune = this.options.exclusiveImmune; // Set up the MD.PopoverStack MD.PopoverStack = MD.PopoverStack || []; this.build(); } ,build: function() { // Create the popoverContentsWrapper and popoverContents divs and store in the popoverParts array this.html = new Element('div', { 'class':'MDPopover '+this.options.className ,id: String.uniqueID() ,styles:{ position:'absolute' ,opacity:0 ,display: 'none' } ,morph: {duration:200} ,tween: {duration:200} }).adopt( this.contentContainer = new Element('div.MDPopover-Content') ,(function(){ if (this.options.closeButton) { this.closeButton = new MD.Button({ text: 'close' ,className: 'MDPopover-CloseButton' ,onClick: function(){ this.hide(); }.bind(this) }); return this.closeButton.toElement().hide() } else { return ''; } }.bind(this))() ); // If Content has been passed in, inject it into the contentContainer... if (this.options.content != '') { switch (typeOf(this.options.content)){ case 'string': this.contentContainer.grab( this.content = new Element('p.MDPopover-DefaultContent', {html:this.options.content}) ); break; case 'element': this.content = this.options.content; this.contentContainer.grab(this.content); break; case 'elements': console.log(this.options.content); this.options.content.each(function(el, index, array) { this.contentContainer.grab(el); }, this); this.content = this.contentContainer.getChildren(); break; case 'array': this.options.content.each(function(el, index, array) { if (typeOf(el) == 'element') { this.contentContainer.grab(el); } else if (typeOf(el) == 'string') { this.contentContainer.grab(new Element('p.MDPopover-DefaultContent', {html:el})); } else if (typeOf(el) == 'object'){ if (this.options.content.toElement == null) { console.error('This content cannot be inserted into this MDPopover. Not an accepted content type.', this.options.content); } else { this.contentContainer.grab(this.options.content.toElement()); } } else { console.error('This content cannot be inserted into this MDPopover. Not an accepted content type.', this.options.content); } }, this); this.content = this.contentContainer.getChildren(); break; case 'object': if (this.options.content.toElement == null) { console.error('This content cannot be inserted into this MDPopover. Not an accepted content type.', this.options.content); return ''; } else { this.contentContainer.grab(this.options.content.toElement()); this.content = this.contentContainer.getChildren(); } break; }; } // Make sure this one has the highest zIndex this.zIndex = this.options.zIndex if (MD.PopoverStack.length > 0 && this.options.zIndex != 5000) { var highestZindex = 0; MD.PopoverStack.each(function(i, index, array) { if (i.zIndex.toInt() > highestZindex) { highestZindex = i.zIndex.toInt(); } }); this.zIndex = highestZindex + 50; } // console.log('this.zIndex', this.zIndex); // console.log('this.maskZindex', this.maskZindex); // Set the zIndex on the main element... this.html.setStyle('z-index', this.zIndex+10); if(this.options.injectInto == null){ this.targetElement = (this.options.maskTarget ? this.options.maskTarget : $$('body')[0]); if (this.targetElement == $$('body')[0]) { this.html.inject(this.targetElement); } else { this.html.inject(this.targetElement, 'after'); } } else { this.targetElement = this.options.maskTarget;//(this.options.injectInto ? this.options.injectInto.getParent() : $$('body')[0]); this.options.injectInto.grab(this.html); } // Store the object in the main HTML element this.html.store('object', this); return this; } ,show: function(){ if (this.options.exclusive) { MD.PopoverStack.each(function(popover) { if (!popover.exclusiveImmune) popover.hide(); }); } if (this.showing) return; if (!this.html) this.initialize(); if (this.options.maskTarget != null) { this.mask = new Mask(this.targetElement, Object.merge({ destroyOnHide: true ,style:{ opacity:this.maskOptions.opacity ,'background-color':this.maskOptions.color ,'z-index': this.zIndex } }, this.options.maskOptions)); } if (this.mask) this.mask.show(); // POSITION IT this.positionAndDisplay(); if (this.options.clickToClose) { this.html.setStyle('cursor', 'pointer').addEvent('click', function(){ this.hide(); }.bind(this)); } this.fireEvent('show', this, 250); // Add the event(s) to close if (this.options.clickOutsideToClose) { this.html.addEvent('clickout', function(e){ e.stopPropagation(); try { this.hide(); } catch(error){ console.error(error); } }.bind(this)); } // Show the close button if we need to... if (this.options.closeButton) this.closeButton.toElement().show(); // console.log('clientHeight', document.body.clientHeight); // console.log('scrollHeight', document.body.scrollHeight ); // console.log(document.body.clientHeight < document.body.scrollHeight ); this.showing = true; MD.PopoverStack.push(this); if (this.options.timeout) (function(){ this.hide(); }.bind(this)).delay(this.options.timeout); return this; } ,positionAndDisplay: function(){ this.windowCoordinates = window.getCoordinates(); // Determine this.contentSize ... by recreating the popover and it's contents, injecting it invisibly into the body, measuring it, and then deleting it. if (this.options.content != '') { var contentTempInner, contentTemp; contentTemp = new Element('div', { 'class': this.html.get('class') ,styles: { position: 'static' ,width: (this.options.width ? this.options.width : 'auto') } }).grab( contentTempInner = new Element('div', { 'class':this.contentContainer.get('class') ,styles: { position: 'static' ,float: 'left' } }).adopt( this.contentContainer.clone().getChildren() ) ); $$('body')[0].grab(contentTemp); this.renderedSize = contentTemp.getDimensions(true); this.contentSize = contentTempInner.getDimensions(true); this.renderedSize.height = this.contentSize.height; contentTemp.destroy(); } // Set basic dimentions... this.height = (this.height || this.contentSize.height); this.width = (this.width || this.contentSize.width); this.html.setStyles({ height: this.height ,width: this.width ,'max-height': this.windowCoordinates.height-25 ,'max-width': this.windowCoordinates.width-25 ,opacity: 0 ,visibility: 'hidden' ,display: 'block' }); // If we are doing the default, which is to center the popover on the page... if (!this.pointTo) { // get the center coordinates relative to the document.body this.centerPoint = this.html.position({relativeTo: this.targetElement, position:'center', offset: this.offset}); this.html.fade('in'); } else if (this.pointTo) /////////////////////////////////////////////////////////////////////////////////// // If we are positioning relative to an element { // if (this.nipple && this.nipple.isDisplayed()) { // this.nipple.destroy(); // delete this.nipple; // } this.html.grab( this.nipple = new Element('div', { 'class':'MDPopoverNipple' ,styles: { position:'absolute' ,'z-index':this.zIndex+100 } }) ); this.nippleSize = this.nipple.getDimensions(true); //console.log('this.renderedSize', this.renderedSize); //console.log('this.height', this.height); //console.log('this.contentSize', this.contentSize); //console.log('this.nippleSize', this.nippleSize); // Position the nipple vertically this.nipple.setStyle('top', ((this.renderedSize.height/2) - (this.nippleSize.x/2))); // return; // Set up morph FX instance... this.morphToPosition = new Fx.Morph(this.html, { duration: 200 ,transition: Fx.Transitions.Sine.easeOut ,link: 'chain' ,onChainComplete: function(){ // if(this.options.centerOnPage) this.html.tween('opacity', [0,1]); }.bind(this) }); // Get coordinates of the element we are pointing to... this.pointToPosition = this.pointTo.getCoordinates(); // return; // If we DO NOT have position and edge values... if (this.options.position == null || this.options.edge == null) { // Predict the direction the popover should go based on proximity to the edge of the viewport. // Start by setting the .popDirection property, which will determine whether this popover appears to the left or right of its referenceElement... if ((this.windowCoordinates.right - this.pointToPosition.right) <= this.width+100) { this.popDirection = 'Left'; } else { this.popDirection = 'Right'; } this.html.removeClass('Left').removeClass('Right').addClass(this.popDirection); // now, put it where it belongs... if (this.popDirection == 'Right') { // get the coordinates of the center of the referenceElement... this.centerPoint = this.html.position({ relativeTo: this.pointTo ,position:(this.options.position ? this.options.position : 'centerRight') ,edge: (this.options.edge ? this.options.edge : 'centerLeft') ,offset: this.offset ,returnPos:true }); this.nipple.setStyle('left',-this.nippleSize.x); this.html.setStyles({ visibility: 'visible' ,left: this.centerPoint.left+35 ,top: this.centerPoint.top }); this.morphToPosition.start({ opacity: [0,1] ,left: [this.centerPoint.left+35 , this.centerPoint.left+15] // ,top: this.pointToPosition.bottom-((this.pointToPosition.bottom-this.pointToPosition.top)/2) ,top: this.centerPoint.top }); } else if (this.popDirection == 'Left') { this.centerPoint = this.html.position({ relativeTo: this.pointTo ,position: (this.options.position ? this.options.position : 'centerLeft') ,edge: (this.options.edge ? this.options.edge : 'centerRight') ,offset: this.offset ,returnPos: true }); this.nipple.setStyle('right',-this.nippleSize.x); this.html.setStyles({ visibility: 'visible' ,left: this.centerPoint.left+35 ,top: this.centerPoint.top }); this.morphToPosition.start({ opacity: [0,1] ,left: [this.centerPoint.left-35 , this.centerPoint.left-15] ,top: this.centerPoint.top }); } this.nipple.setStyle('top', ((this.height/2) - (this.nippleSize.x/2))); } else { // Someday you should probably make it work so that it can't accidentally go "off the page"... but not right now... -BD 7/8/11 this.centerPoint = this.html.position({ relativeTo: this.pointTo ,position: this.options.position ,edge: this.options.edge ,offset: this.offset ,returnPos: true }); // topLeft || upperLeft var edge = this.options.edge.toLowerCase(); if ((edge.test('top') || edge.test('upper')) && edge.test('left')) { this.html.addClass('Bottom'); this.nipple.setStyle('top', -this.nippleSize.y); this.nipple.setStyle('left', 5); } else // bottomLeft || lowerLeft if ((edge.test('bottom') || edge.test('lower')) && edge.test('left')) { this.html.addClass('Top'); this.nipple.setStyle('bottom', -this.nippleSize.y); this.nipple.setStyle('left', 5); } else // topRight || upperRight if ((edge.test('top') || edge.test('upper')) && edge.test('right')) { this.html.addClass('Bottom'); this.nipple.setStyle('top', -this.nippleSize.y); this.nipple.setStyle('right', 5); } else // bottomRight || lowerRight if ((edge.test('bottom') || edge.test('lower')) && edge.test('right')) { this.html.addClass('Top'); this.nipple.setStyle('bottom', -this.nippleSize.y); this.nipple.setStyle('right', 5); } // if the 'edge' is centered if (edge.test('center')) { if (this.options.position.toLowerCase() == 'center') { this.nipple.hide(); } if (edge.test('top') || edge.test('upper')) { this.html.addClass('Bottom'); this.nipple.setStyle('top', -this.nippleSize.y); this.nipple.setStyle('left', (this.width / 2).round(0) - (this.nipple.getStyle('width').toInt() / 2).round(0)); } else if (edge.test('bottom')) { this.html.addClass('Top'); this.nipple.setStyle('bottom', -this.nippleSize.y); this.nipple.setStyle('left', (this.width / 2).round(0) - (this.nipple.getStyle('width').toInt() / 2).round(0)); } else if (edge.test('left')) { this.html.addClass('Right'); this.nipple.setStyle('left',-this.nippleSize.x); this.nipple.setStyle('top', ((this.height/2) - (this.nippleSize.x/2))); } else if (edge.test('right')) { this.html.addClass('Left'); this.nipple.setStyle('right',-this.nippleSize.x); this.nipple.setStyle('top', ((this.height/2) - (this.nippleSize.x/2))); } } // return; this.html.setStyles({ display: 'block' ,left: this.centerPoint.left ,top: this.centerPoint.top+((this.options.edge.test('top') || this.options.edge.test('upper')) ? -15 : 15) }); if (edge == 'center' && this.options.position.toLowerCase() == 'center') { this.html.fade('in'); } else { this.morphToPosition.start({ opacity: [0,1] ,visibility: 'visible' ,left: this.centerPoint.left ,top: [this.centerPoint.top+((this.options.edge.test('top') || this.options.edge.test('upper')) ? -15 : 15), this.centerPoint.top] }); } } } } ,complete: function(){ // Fire the onComplete event this.fireEvent('complete', [this.html, this.referenceElement], 150); } ,hide: function(suppressOnHideEvent){ try { if (suppressOnHideEvent != false) // so that we can suppress the onHide event if we need to. this.fireEvent('hide', [this.contentContainer, this]); if (this.mask) this.mask.hide(); if (this.html) { this.html.setStyles({ visibility: 'hidden' ,opacity: 0 ,display: 'none' }); } if (this.options.destroyOnHide) { try { (function(){ if (this.html) this.html.destroy(); delete this.html; }.bind(this)).delay(240); } catch(error){ console.error(error); } } if (!this.options.destroyOnHide && this.options.closeButton) this.closeButton.toElement().hide(); // Remove this Popover from the stack if (MD.PopoverStack.length > 1) { MD.PopoverStack = MD.PopoverStack.splice(MD.PopoverStack.indexOf(this), 1); } else if (MD.PopoverStack.length == 1) { MD.PopoverStack.empty(); } } catch(error){ console.error(error); } this.showing = false; return this; } ,destroy: function(){ this.html.getElements('*').destroy(); this.html.destroy(); } ,changeSize: function(dimensions){ this.html.morph({ height:dimensions.height || this.options.height ,width:dimensions.width || this.options.width }); } ,setContent: function(content){ if (typeOf(content) == 'element') { this.contentContainer.grab(content); } else if (typeOf(content) == 'elements') { content.each(function(el, index, array) { this.contentContainer.grab(el); }, this); } else if (typeOf(content) == 'array') { content.each(function(el, index, array) { if (typeOf(el) == 'element') { this.contentContainer.grab(el); } else if (typeOf(el) == 'string') { this.contentContainer.grab(new Element('p.MDPopover-DefaultContent', {html:el})); } }, this); } else if (typeOf(content) == 'string') { this.contentContainer.grab(new Element('p.MDPopover-DefaultContent', {html:content})); } else { throw new Error('Cannot set content because supplied content is of type: \''+typeOf(content)+'\' --> '+content); } return this; } ,toElement: function(){ return this.html; } ,isActive: function() { return this.showing; } });