'use strict' module.exports = { /** * Setup global options * These can and will be overwriteen if a config object is passed into this.init() */ options: { html: false, placement: 'top', container: 'body', scrollContainer: window, template: '
', removalDelay: 200, tooltipOffset: 10, windowPadding: { top: 10, right: 10, bottom: 10, left: 10, }, }, /** * initializeTooltips - Initialize function to bind events and set any global data * @example * tooltips.init() * @param {object} (config) - Congifuration object that is used to overwrite the defaults in this.options * @return {void} */ init: function initializeTooltips(config) { var self = this // Check to see if we should use document.body or document.documentElement document.documentElement.scrollTop = 1 this.documentElement = document.documentElement.scrollTop === 1 ? document.documentElement : document.body // Test for browser support var div = document.createElement('div') div.innerHTML = '' + title + '
' ) tooltip.appendChild(tooltipTitle) } // If the supplied string should be interpreted as html, make an element for it... if ((this.options.html || html) && content) { if (this.isElement(content)) { tooltipContent = content } else if (this.getTagName(content)) { tooltipContent = this.createDOMElement(content) } else { tooltipContent = this.createDOMElement( '' + content + '
' ) } // ...or create a default p instead } else { tooltipContent = this.createDOMElement( '' + content + '
' ) } tooltip.appendChild(tooltipContent) // If the a container was supplied and it's not also the body element, store that element if (container && container !== this.options.container) { if (typeof container === 'string') { this.currentContainer = document.querySelector(container) } // If they initialized tooltips and set a different global container, store that element } else { this.currentContainer = document.querySelector( this.options.container ) } // If a scrollContainer was supplied and it's also not the window element, store that element if ( scrollContainer && scrollContainer !== this.options.scrollContainer ) { if (typeof scrollContainer === 'string') { this.currentScrollContainer = document.querySelector(scrollContainer) } // If they initialized tooltips and incase they set a different global container, store that element } else { this.currentScrollContainer = this.options.scrollContainer } // If autoClose is set to false, add an attribute for the event handler to look for if ( options.autoClose === false || trigger.getAttribute('data-tooltip-autoclose') === 'false' ) { tooltip.setAttribute('data-autoclose', 'false') } if (preExistingTooltip) { this.currentTooltip = preExistingTooltip.parentNode.insertBefore( tooltip, preExistingTooltip ) } else { this.currentTooltip = this.currentContainer.appendChild(tooltip) } this.currentTrigger = trigger // Position the tooltip on the page this.positionTooltip( this.currentTooltip, this.currentTrigger, placement ) // If a tooltip is open and the user scrolls, isotip needs to keep up with the trigger if (this.currentScrollContainer !== window) { this.addEventListener( this.currentScrollContainer, 'scroll', this.windowChangeHandler ) } return this.currentTooltip }, /** * closeTooltip - Main close function to close a specific tooltip * @example * tooltip.close( document.body.querySelector( '.tooltip' )) * @param {string|element} tooltip - The tooltip that needs to be closed * @return {void} */ close: function closeTooltip(tooltip) { // We need a DOM element, so make it one if it isn't already if (typeof tooltip === 'string') { tooltip = document.body.querySelector(tooltip) } if (this.currentScrollContainer !== window) { this.removeEventListener( this.currentScrollContainer, 'scroll', this.windowChangeHandler ) } this.removeClass(tooltip, 'visible') this.currentTooltip = null this.currentTrigger = null this.currentScrollContainer = null // We should assume that there will be some sort of tooltip animation with CSS or JS // So we can only remove the element after a certain period of time window.setTimeout(function removeElementFromDOM() { if ( tooltip && tooltip instanceof window.Element && tooltip.parentNode ) { tooltip.parentNode.removeChild(tooltip) } else { tooltip = document.body.querySelector('.tooltip') if (tooltip && tooltip.parentNode) { tooltip.parentNode.removeChild(tooltip) } } }, this.options.removalDelay) }, /** * positionTooltip - Logic for positioning the tooltip on the page * @example * this.positionTooltip( this.currentTooltip, this.currentTrigger, 'top' ) * @param {string|element} tooltip - The tooltip that needs to be positioned * @param {string|element} trigger - The element that triggered the tooltip * @param {string} (placement) - The position that the tooltip needs to be placed in in relation to the trigger * @return {element} - Returns the tooltip that was positioned * @api private */ positionTooltip: function positionTooltip(tooltip, trigger, placement) { if (typeof tooltip === 'string') { tooltip = document.body.querySelector(tooltip) } if (typeof trigger === 'string') { trigger = document.body.querySelector(trigger) } // Since we support this being done on scroll, we need a way to get the placement if it isn't specified if (!placement) { placement = trigger.getAttribute('data-tooltip-placement') || this.options.placement } var self = this var tooltipAccent = tooltip.querySelector('.tooltip-accent') var triggerPosition = trigger.getBoundingClientRect() var triggerWidth = Math.floor(triggerPosition.width) var triggerHeight = Math.floor(triggerPosition.height) var triggerX = triggerPosition.left var triggerY = triggerPosition.top var windowTop = this.options.windowPadding.top var windowRight = (window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth) - this.options.windowPadding.right var windowBottom = (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) - this.options.windowPadding.bottom var windowLeft = this.options.windowPadding.left var containerTop var containerRight var containerBottom var containerLeft var tooltipX var tooltipY var tooltipWidth var tooltipHeight var tooltipRight var tooltipBottom var tooltipAccentWidth var tooltipAccentHeight if (this.currentScrollContainer.getBoundingClientRect) { var scrollContainerPosition = this.currentScrollContainer.getBoundingClientRect() if (scrollContainerPosition.top >= 0) { containerTop = scrollContainerPosition.top + this.options.windowPadding.top } if (scrollContainerPosition.right <= windowRight) { containerRight = scrollContainerPosition.right - this.options.windowPadding.right } if (scrollContainerPosition.bottom <= windowBottom) { containerBottom = scrollContainerPosition.bottom - this.options.windowPadding.bottom } if (scrollContainerPosition.left >= windowLeft) { containerLeft = scrollContainerPosition.left + this.options.windowPadding.left } } /** * We sometimes need to re-position the tooltip (I.E. switch from top to bottom) * Which is why these are separate functions */ function positionTop() { tooltipX = triggerX - tooltipWidth / 2 + triggerWidth / 2 tooltipY = triggerY - tooltipHeight - self.options.tooltipOffset tooltipRight = tooltipX + tooltipWidth self.addClass(tooltip, 'tooltip-top') if (!tooltipAccentWidth) { tooltipAccentWidth = parseInt(tooltipAccent.offsetWidth, 10) } if (!tooltipAccentHeight) { tooltipAccentHeight = parseInt(tooltipAccent.offsetHeight, 10) } if (triggerY + triggerHeight <= containerTop) { if (self.hasClass(tooltip, 'visible')) { self.removeClass(tooltip, 'visible') } } // If the tooltip extends beyond the right edge of the window... if (tooltipRight > windowRight || tooltipRight > containerRight) { tooltip.style.top = 'auto' tooltip.style.bottom = windowBottom + self.options.windowPadding.bottom - triggerY + self.options.tooltipOffset + 'px' tooltip.style.right = self.options.windowPadding.right + 'px' tooltipAccent.style.left = 'auto' tooltipAccent.style.right = triggerWidth / 2 - tooltipAccentWidth / 2 + (windowRight - triggerX - triggerWidth) + 'px' // ...or if the tooltip extends beyond the top of the window... } else if (tooltipY < windowTop || tooltipY < containerTop) { self.removeClass(tooltip, 'tooltip-top') return positionBottom() // ...or if the tooltip extends beyond the left edge of the window... } else if (tooltipX < windowLeft || tooltipX < containerLeft) { tooltip.style.top = 'auto' tooltip.style.bottom = windowBottom + self.options.windowPadding.bottom - triggerY + self.options.tooltipOffset + 'px' tooltip.style.left = self.options.windowPadding.left + 'px' tooltipAccent.style.right = 'auto' tooltipAccent.style.left = triggerWidth / 2 - tooltipAccentWidth / 2 + (triggerX - windowLeft) + 'px' // ...or it fits inside the window } else { tooltip.style.top = 'auto' tooltip.style.bottom = windowBottom + self.options.windowPadding.bottom - triggerY + self.options.tooltipOffset + 'px' tooltip.style.left = tooltipX + 'px' tooltipAccent.style.top = '' tooltipAccent.style.bottom = '' tooltipAccent.style.right = '' tooltipAccent.style.left = tooltipWidth / 2 - tooltipAccentWidth / 2 + 'px' } } function positionRight() { tooltipX = triggerX + triggerWidth + self.options.tooltipOffset tooltipY = triggerY - tooltipHeight / 2 + triggerHeight / 2 tooltipRight = tooltipX + tooltipWidth self.addClass(tooltip, 'tooltip-right') if (!tooltipAccentHeight) { tooltipAccentHeight = parseInt(tooltipAccent.offsetHeight, 10) } // If the tooltip extends beyond the right edge of the screen... if (tooltipRight > windowRight || tooltipRight > containerRight) { self.removeClass(tooltip, 'tooltip-right') return positionTop() // ...or if it fits to the right of the trigger element } else { tooltip.style.top = tooltipY + 'px' tooltip.style.left = tooltipX + 'px' tooltipAccent.style.right = '' tooltipAccent.style.bottom = '' tooltipAccent.style.left = '' tooltipAccent.style.top = tooltipHeight / 2 - tooltipAccentHeight / 2 + 'px' } } function positionBottom() { tooltipX = triggerX - tooltipWidth / 2 + triggerWidth / 2 tooltipY = triggerY + triggerHeight + self.options.tooltipOffset tooltipRight = tooltipX + tooltipWidth tooltipBottom = tooltipY + tooltipHeight self.addClass(tooltip, 'tooltip-bottom') if (!tooltipAccentWidth) { tooltipAccentWidth = parseInt(tooltipAccent.offsetWidth, 10) } if (triggerY >= containerBottom) { if (self.hasClass(tooltip, 'visible')) { self.removeClass(tooltip, 'visible') } } // If the tooltip extends beyond the right edge of the window... if (tooltipRight > windowRight || tooltipRight > containerRight) { tooltip.style.top = tooltipY + 'px' tooltip.style.right = self.options.windowPadding.right + 'px' tooltipAccent.style.left = 'auto' tooltipAccent.style.right = triggerWidth / 2 - tooltipAccentWidth / 2 + (windowRight - triggerX - triggerWidth) + 'px' // ...or if the tooltip extends beyond the top of the window... } else if ( tooltipBottom > windowBottom || tooltipBottom > containerBottom ) { self.removeClass(tooltip, 'tooltip-bottom') return positionTop() // ...or if the tooltip extends beyond the left edge of the window... } else if (tooltipX < windowLeft || tooltipX < windowLeft) { tooltip.style.top = tooltipY + 'px' tooltip.style.left = self.options.windowPadding.left + 'px' tooltipAccent.style.right = 'auto' tooltipAccent.style.left = triggerWidth / 2 - tooltipAccentWidth / 2 + (triggerX - windowLeft) + 'px' // ...or it fits inside the window } else { tooltip.style.top = tooltipY + 'px' tooltip.style.bottom = 'auto' tooltip.style.left = tooltipX + 'px' tooltipAccent.style.top = '' tooltipAccent.style.bottom = '' tooltipAccent.style.right = '' tooltipAccent.style.left = tooltipWidth / 2 - tooltipAccentWidth / 2 + 'px' } } function positionLeft() { tooltipX = triggerX - tooltipWidth - self.options.tooltipOffset tooltipY = triggerY - tooltipHeight / 2 + triggerHeight / 2 self.addClass(tooltip, 'tooltip-left') if (!tooltipAccentHeight) { tooltipAccentHeight = parseInt(tooltipAccent.offsetHeight, 10) } // If the tooltip extends beyond the right edge of the screen... if (tooltipX < windowLeft || tooltipX < containerLeft) { self.removeClass(tooltip, 'tooltip-left') return positionTop() // ...or if it fits to the left of the trigger element } else { tooltip.style.top = tooltipY + 'px' tooltip.style.left = tooltipX + 'px' tooltipAccent.style.right = '' tooltipAccent.style.bottom = '' tooltipAccent.style.left = '' tooltipAccent.style.top = tooltipHeight / 2 - tooltipAccentHeight / 2 + 'px' } } tooltip.style.position = 'fixed' tooltipWidth = parseInt(tooltip.offsetWidth, 10) tooltipHeight = parseInt(tooltip.offsetHeight, 10) tooltipAccentWidth = parseInt(tooltipAccent.offsetWidth, 10) tooltipAccentHeight = parseInt(tooltipAccent.offsetHeight, 10) // clear any classes set before hand self.removeClass(tooltip, 'tooltip-top') self.removeClass(tooltip, 'tooltip-right') self.removeClass(tooltip, 'tooltip-bottom') self.removeClass(tooltip, 'tooltip-left') // position the tooltip if (placement === 'top') { positionTop() } else if (placement === 'right') { positionRight() } else if (placement === 'bottom') { positionBottom() } else if (placement === 'left') { positionLeft() } // try and give the tooltip enough time to position itself if ( !self.hasClass(tooltip, 'visible') && (containerTop === undefined || (triggerY + triggerHeight > containerTop && triggerY < containerBottom)) ) { window.setTimeout(function () { self.addClass(tooltip, 'visible') }, 50) } else if ( triggerY + triggerHeight <= containerTop || triggerY >= containerBottom ) { self.removeClass(tooltip, 'visible') } return tooltip }, /** * addEventListener - Small function to add an event listener. Should be compatible with IE8+ * @example * this.addEventListener( document.body, 'click', this.open( this.currentTooltip )) * @param {element} el - The element node that needs to have the event listener added * @param {string} eventName - The event name (sans the "on") * @param {function} handler - The function to be run when the event is triggered * @return {element} - The element that had an event bound * @api private */ addEventListener: function addEventListener( el, eventName, handler, useCapture ) { if (!useCapture) { useCapture = false } if (el.addEventListener) { el.addEventListener(eventName, handler, useCapture) return el } else { if (eventName === 'focus') { eventName = 'focusin' } el.attachEvent('on' + eventName, function () { handler.call(el) }) return el } }, /** * removeEventListener - Small function to remove and event listener. Should be compatible with IE8+ * @example * this.removeEventListener( document.body, 'click', this.open( this.currentTooltip )) * @param {element} el - The element node that needs to have the event listener removed * @param {string} eventName - The event name (sans the "on") * @param {function} handler - The function that was to be run when the event is triggered * @return {element} - The element that had an event removed * @api private */ removeEventListener: function removeEventListener( el, eventName, handler, useCapture ) { if (!useCapture) { useCapture = false } if (!el) { return } if (el.removeEventListener) { el.removeEventListener(eventName, handler, useCapture) } else { if (eventName === 'focus') { eventName = 'focusin' } el.detachEvent('on' + eventName, function () { handler.call(el) }) } return el }, /** * hasClass - Small function to see if an element has a specific class. Should be compatible with IE8+ * @example * this.hasClass( this.currentTooltip, 'visible' ) * @param {element} el - The element to check the class existence on * @param {string} className - The class to check for * @return {boolean} - True or false depending on if the element has the class * @api private */ hasClass: function hasClass(el, className) { if (el.classList) { return el.classList.contains(className) } else { return new RegExp('(^| )' + className + '( |$)', 'gi').test( el.className ) } }, /** * addClass - Small function to add a class to an element. Should be compatible with IE8+ * @example * this.addClass( this.currentTooltip, 'visible' ) * @param {element} el - The element to add the class to * @param {string} className - The class name to add to the element * @return {element} - The element that had the class added to it * @api private */ addClass: function addClass(el, className) { if (el.classList) { el.classList.add(className) } else { el.className += ' ' + className } return el }, /** * removeClass - Small function to remove a class from an element. Should be compatible with IE8+ * @example * this.removeClass( this.currentTooltip, 'visible' ) * @param {element} el - The element to remove the class from * @param {string} className - The class name to remove from the element * @return {element} - The element that had the class removed from it * @api private */ removeClass: function removeClass(el, className) { if (el) { if (el.classList) { el.classList.remove(className) } else { el.className = el.className.replace( new RegExp( '(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi' ), ' ' ) } } return el }, /** * setInnerText - Small function to set the inner text of an element. Should be compatible with IE8+ * @example * this.setInnerText( this.currentTooltip, 'Hello world' ) * @param {element} el - The element to have the text inserted into * @param {string} text - The text to insert into the element * @return {element} - The element with the new inner text * @api private */ setInnerText: function setInnerText(el, text) { if (el.textContent !== undefined) { el.textcontent = text } else { el.innerText = text } return el }, /** * getTagName - Small function to get the tag name of an html string. * @example * this.getTagName( '' ) * @param {string} html - The string of html to check for the tag * @return {object} - The object containing the tag of the root element in the html string as well as some other basic info * @api private */ getTagName: function getTagName(html) { return /<([\w:]+)/.exec(html) }, /** * isElement - Small function to determine if object is a DOM element. * @example * this.isElement( '' ); # false * @example * this.isElement( this.createElement('div') ); # true * @param {string} obj - The object to check * @return {boolean} - Whether or not the object is a DOM element. * @api private */ isElement: function getTagName(obj) { return !!(obj && obj.nodeType === 1) }, /** * createDOMElement - Small function to transform an html string into an element * @example * this.createDOMElement( '