From aa8e994a9f75f3078ecf4b61fb593fdb88d2c187 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 3 Oct 2024 15:44:01 -0500 Subject: [PATCH] Upload Isotip --- .gitignore | 175 ++++++++ bun.lockb | Bin 0 -> 3135 bytes jsconfig.json | 27 ++ package.json | 11 + tooltip.js | 1086 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1299 insertions(+) create mode 100644 .gitignore create mode 100755 bun.lockb create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 tooltip.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e7dc3c16ab6a74691aea75f38c67d13ec611f5db GIT binary patch literal 3135 zcmd5;4Nw$y6yH0K56=W;FvlF@&eSR3_C7ey1I`0KYGRmFl+09&uf1dCxI1?D_|gc} z^b(2>!3d`e1aUM+sniCtG?VdrEFBn4m~0FQ$(lwJN5$TM**|Pe;$_XyoB7|{|L*(! z?)~3;yYD@tRJ&M4okH8yls#LK; z|J^#HbMmvjbp@8d-4o|Ki5+E?%E<@VyA$`00V5Ef0Y&)!MHdSD3~a#f21>sLqp!AQY3)?H-e&oA zo`Asy<3EDyNWi1GL3{{5Se^*R{|3gh0q+I+DOgB*A{ZYAht>lgWf)nFiK^pDFg@!Qy9M|vPkBI(D0X_*az$>Pf{Zd!`a%f19 z{QlV!S9{jB8#87F-z!?@(qFGFj@op!drMk-@H|se<=79HNM*qBqP9=kwZAJq-}{n% z|GJ!uIhH~ZuQ;BvfY%=^(3fiNHT)Qx;Ir80bpDDxWtYk$THaHA+VMm8Yp)#n@P&i3 z95u0OZ)rnpZ*lb2f=ONOF?}(O?gRSI3~QrByeQsSWKAcIUb{diMY~#aLuN}`%N8^* zYPt|Ta}$-GToPDwnXCWGaE^V$yqil`#%qpA%8#<<(rQ{i%DlxZCu>xwXkabnQ7`n>#JYQ^ID;H zulcw_dHc$jaT6>(`Sl`Rab3$g&wo>Kf8K$ZpUy7Tg-kWoXLzScO2;>Jw#1w*Ej#{Q z#j@K=IwTttld=zweR9H{&Eqp~ebKgZ=@Hl5VE5c<7qsOfUR-C$ZCSWOa&qIG7tigZ zx|Jmz-F;PejPW-kjgd`_spEDSH+1D)jNCe1XZ3rhV1fUds?g@kZI0r%`d6DRmu@6} ze`nGu;2kN!amV^`{1`#^AIII1!t4{iMPCbAZep2KlD4y4vdu=~j!Pym6lGTEGzyl^ zryQ10g}08expQmZ44 zUGM}=8hCPmC*h>dK>1~PQdzjM3k#+0mPj%6tZ*b>hxiWXVo`mB&dwGRt7!8sf@j&_o(ST4^=v8fF0GLd3&Bi7lAkg`W#lcushPIVdz9w zK!}W01L@T25u3}NMyJw`bV_UnG~%s_uYqtx_GmY9SU~5nD!$fqd`S2-4y0}v{7?>n SU}yt>ZXkIczBc}cKYsu;3`SxA literal 0 HcmV?d00001 diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ab579c1 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "tooltip.js", + "module": "tooltip.js", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/tooltip.js b/tooltip.js new file mode 100644 index 0000000..cef80df --- /dev/null +++ b/tooltip.js @@ -0,0 +1,1086 @@ +'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 = + '
a' + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + this.innerHTMLBug = !div.getElementsByTagName('link').length + div = undefined + + // touch event testing + if ( + 'ontouchstart' in window || + (window.DocumentTouch && document instanceof window.DocumentTouch) + ) { + document.body.style.cursor = 'pointer' + } + + // Wrap map from jquery. + this.map = { + legend: [1, '
', '
'], + tr: [2, '', '
'], + col: [2, '', '
'], + // for script/link/style tags to work in IE6-8, you have to wrap + // in a div with a non-whitespace character in front, ha! + defaultTag: this.innerHTMLBug + ? [1, 'X
', '
'] + : [0, '', ''], + } + + this.map.td = this.map.th = [ + 3, + '', + '
', + ] + this.map.option = this.map.optgroup = [ + 1, + '', + ] + this.map.thead = + this.map.tbody = + this.map.colgroup = + this.map.caption = + this.map.tfoot = + [1, '', '
'] + this.map.text = + this.map.circle = + this.map.ellipse = + this.map.line = + this.map.path = + this.map.polygon = + this.map.polyline = + this.map.rect = + [ + 1, + '', + '', + ] + + // Copy over ininitialization options to options object + if (config instanceof Object) { + for (var option in config) { + if ( + window.Object.hasOwnProperty.call(config, option) && + window.Object.hasOwnProperty.call(this.options, option) + ) { + // If it's a nested object, loop through that one too + if ( + typeof config[option] === 'object' && + !Array.isArray(config[option]) + ) { + for (var subkey in config[option]) { + if ( + window.Object.hasOwnProperty.call( + config[option], + subkey + ) && + window.Object.hasOwnProperty.call( + this.options[option], + subkey + ) + ) { + this.options[option][subkey] = + config[option][subkey] + } + } + } else { + this.options[option] = config[option] + } + } + } + } + + // Logic for handling a click event + function clickHandler(evt) { + if (!evt) { + evt = window.event + } + + var trigger = evt.target || evt.srcElement + + // If there's a tooltip open and it shouldn't close, don't close it _or_ open a new one + if ( + self.currentTooltip && + self.currentTooltip.getAttribute('data-autoclose') === 'false' + ) { + return + // If there's already a tooltip open, close that one... + } else if (self.currentTooltip) { + // ...unless the user is clicking on the tooltip itself... + if (trigger === self.currentTooltip) { + return + // ...or if the user if clicking on the original trigger for that tooltip + } else if (trigger === self.currentTrigger) { + self.close(self.currentTooltip) + + return + } else { + // loop through the child elements in the tooltip to see if one of them has been clicked + if (self.hasParent(trigger, self.currentTooltip)) { + return false + } + + self.close(self.currentTooltip) + } + } + + // If the element the user is clicking on isn't supposed to trigger a tooltip, bail + if (!self.hasClass(trigger, 'tooltip-click')) { + return + } + + // Open the tooltip! + self.open(trigger) + } + + // Logic for handling a mouseover event + function mouseoverHandler(evt) { + if (!evt) { + evt = window.event + } + + var trigger = evt.target || evt.srcElement + + // If the element the user is hovering over isn't supposed to trigger a tooltip, bail + if (!self.hasClass(trigger, 'tooltip-hover')) { + return + } + + // If there's a tooltip open and it shouldn't close, don't close it _or_ open a new one + if ( + self.currentTooltip && + self.currentTooltip.getAttribute('data-autoclose') === 'false' + ) { + return + // If there's already a tooltip open, close that one... + } else if (self.currentTooltip) { + // ...unless the user is hovering over the tooltip itself... + if (trigger === self.currentTooltip) { + return + } else { + // loop through the child elements in the tooltip to see if one of them has been clicked + if (self.hasParent(trigger, self.currentTooltip)) { + return false + } + + self.close(self.currentTooltip) + } + } + + // Logic for handling the mouseout event + function mouseoutHandler(moEvt) { + if (!moEvt) { + moEvt = window.event + } + + var moTrigger = evt.target || evt.srcElement + + if (self.hasClass(moTrigger)) { + return + } + + // If the tooltip shouldn't autoclose, bail + if ( + self.currentTooltip && + self.currentTooltip.getAttribute('data-autoclose') === + 'false' + ) { + return + } + + self.close(self.currentTooltip) + + // Remove self event to keep things clean + self.removeEventListener(trigger, 'mouseout', mouseoutHandler) + + return + } + + self.open(trigger) + + // Add an event to remove the tooltip when the user moves their cursor away + self.addEventListener(trigger, 'mouseout', mouseoutHandler) + + return + } + + // Logic for handling a focus event + function focusHandler(evt) { + if (!evt) { + evt = window.event + } + + var trigger = evt.target || evt.srcElement + + // If the element the user is focusing on isn't supposed to trigger a tooltip, bail + if (!self.hasClass(trigger, 'tooltip-focus')) { + return + } + + // If there's a tooltip open and it shouldn't close, don't close it _or_ open a new one + if ( + self.currentTooltip && + self.currentTooltip.getAttribute('data-autoclose') === 'false' + ) { + return + // If there's already a tooltip open, close that one... + } else if (self.currentTooltip) { + self.close(self.currentTooltip) + } + + // Logic for handling the blur event + function blurHandler() { + // If the tooltip shouldn't automatically close, bail + if ( + self.currentTooltip && + self.currentTooltip.getAttribute('data-autoclose') === + 'false' + ) { + return + } + + self.close(self.currentTooltip) + + // Remove self event to keep things clean + self.removeEventListener(trigger, 'blur', blurHandler) + + return + } + + self.open(trigger) + + // Add an event to remove the tooltip when the user blurs from the element + self.addEventListener(trigger, 'blur', blurHandler) + + return + } + + this.windowChangeHandler = function windowChangeHandler() { + if (self.currentTooltip && self.currentTrigger) { + self.positionTooltip(self.currentTooltip, self.currentTrigger) + } + + return + } + + // Add the global click handler + this.addEventListener(document.body, 'click', clickHandler) + + // Add the global mouseover handler + this.addEventListener(document.body, 'mouseover', mouseoverHandler) + + // Add the global focus handler + this.addEventListener(document.body, 'focus', focusHandler, true) + + // If a tooltip is open and the user scrolls, isotip needs to keep up with the trigger + this.addEventListener(window, 'scroll', this.windowChangeHandler) + + // If a tooltip is open and the user resizes the page, isotip needs to keep up with the trigger + this.addEventListener(window, 'resize', this.windowChangeHandler) + }, + + /** + * openTooltip - Main open function to prepare and insert the tooltip + * @example + * tooltip.open( document.body.querySelector( '#tooltip-trigger' )) + * @param {string|element} trigger - The element that serves as the trigger for the tooltip + * @param {object} (options) - An object that corresponds to the possible options using data-tooltip attributes + * @return {element} - Returns the tooltip that was inserted into the DOM + */ + open: function openTooltip(trigger, options) { + // We need a DOM element, so make it one if it isn't already + if (typeof trigger === 'string') { + trigger = document.body.querySelector(trigger) + } + + // If no options are passed in, setup a blank object to prevent errors + if (!options) { + options = {} + } + + // Setup tooltip variables, starting with the config object if there is one + var className = + options.className || trigger.getAttribute('data-tooltip-classname') + var content = + options.content || trigger.getAttribute('data-tooltip-content') + var title = options.title || trigger.getAttribute('data-tooltip-title') + var html = options.html || trigger.getAttribute('data-tooltip-html') + var placement = + options.placement || trigger.getAttribute('data-tooltip-placement') + var container = + options.container || trigger.getAttribute('data-tooltip-container') + var scrollContainer = + options.container || + trigger.getAttribute('data-tooltip-scrollContainer') + var preExistingTooltip = document.querySelector('.tooltip') + var tooltip = this.createDOMElement(this.options.template) + var tooltipTitle + var tooltipContent + + // If there isn't any content to be displayed, bail + if (!content) { + return + } + + // If there's a tooltip open with autoClose set to false, don't open a new one + if ( + this.currentTooltip && + this.currentTooltip.getAttribute('data-autoclose') === 'false' + ) { + return + } + + tooltip.appendChild( + this.createDOMElement('
') + ) + + // If there should be an added class name, add it + if (className) { + this.addClass(tooltip, className) + } + + // If there's a title to be displayed, create the title element + if (title) { + tooltipTitle = this.createDOMElement( + '

' + 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( '
Hello world
' ) + * @param {string} html - The string of html to turn into html + * @return {element} - The element created from the string of html + * @api private + */ + createDOMElement: function createDOMElement(html) { + // Remove whitespace + html = html.replace(/^\s+|\s+$/g, '') + + // Get the tag name and match it to this.map incase it needs more nodes + var templateTag = this.getTagName(html)[1] + var wrap = this.map[templateTag] || this.map.defaultTag + var depth = wrap[0] + var prefix = wrap[1] + var suffix = wrap[2] + var el = document.createElement('div') + + el.innerHTML = prefix + html + suffix + + while (depth--) { + el = el.lastChild + } + + // Extract the fresh element + return el.removeChild(el.firstChild) + }, + + /** + * hasParent - Small element to find the closest parent to an element + * @example + * element.closest('.tooltip') + * @param {Element} el - The element to start with + * @param {Element} parent - The parent element to match against/search for + * @return {bool} - Whether or not the element has the parent + */ + hasParent: function hasParent(el, parent) { + if (!el || !parent) { + return false + } + + var match = false + + while (el.parentNode && !match) { + el = el.parentNode + + if (el === parent) { + match = true + } + } + + return match + }, +}