diff --git a/package.json b/package.json index ad5db514..ccfc9124 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "marked": "1.2.3", "register-service-worker": "1.7.1", "snake-case": "3.0.3", - "v-tooltip": "2.0.3", "verte": "0.0.12", "vue": "2.6.12", "vue-advanced-cropper": "0.17.8", diff --git a/src/components/home/navigation.vue b/src/components/home/navigation.vue index 7bb1ff07..de0540d5 100644 --- a/src/components/home/navigation.vue +++ b/src/components/home/navigation.vue @@ -63,7 +63,7 @@ :to="{name: 'namespace.edit', params: {id: n.id} }" class="nsettings" v-if="n.id > 0" - v-tooltip.right="'Settings'"> + v-tooltip="'Settings'"> diff --git a/src/directives/tooltip.js b/src/directives/tooltip.js new file mode 100644 index 00000000..ebcdd03d --- /dev/null +++ b/src/directives/tooltip.js @@ -0,0 +1,83 @@ +const calculateTop = (coords, tooltip) => { + // Bottom tooltip use the exact inverse calculation compared to the default. + if (tooltip.classList.contains('bottom')) { + return coords.top + tooltip.offsetHeight + 5 + } + + // The top position of the tooltip is the coordinates of the bound element - the height of the tooltip - + // 5px spacing for the arrow (which is exactly 5px high) + return coords.top - tooltip.offsetHeight - 5 +} + +const calculateArrowTop = (top, tooltip) => { + if (tooltip.classList.contains('bottom')) { + return `${top - 5}px` // 5px arrow height + } + return `${top + tooltip.offsetHeight}px` +} + +// This global object holds all created tooltip elements (and their arrows) using the element they were created for as +// key. This allows us to find the tooltip elements if the element the tooltip was created for is unbound so that +// we can remove the tooltip element. +const createdTooltips = {} + +export default { + inserted: (el, {value, modifiers}) => { + // First, we create the tooltip and arrow elements + const tooltip = document.createElement('div') + tooltip.style.position = 'fixed' + tooltip.innerText = value + tooltip.classList.add('tooltip') + const arrow = document.createElement('div') + arrow.classList.add('tooltip-arrow') + arrow.style.position = 'fixed' + + if (typeof modifiers.bottom !== 'undefined') { + tooltip.classList.add('bottom') + arrow.classList.add('bottom') + } + + // We don't append the element until hovering over it because that's the most reliable way to determine + // where the parent elemtent is located at the time the user hovers over it. + el.addEventListener('mouseover', () => { + // Appending the element right away because we can only calculate the height of the element if it is + // already in the DOM. + document.body.appendChild(tooltip) + document.body.appendChild(arrow) + + const coords = el.getBoundingClientRect() + const top = calculateTop(coords, tooltip) + // The left position of the tooltip is calculated so that the middle point of the tooltip + // (where the arrow will be) is the middle of the bound element + const left = coords.left - (tooltip.offsetWidth / 2) + (el.offsetWidth / 2) + // Now setting all the values + tooltip.style.top = `${top}px` + tooltip.style.left = `${coords.left}px` + tooltip.style.left = `${left}px` + + arrow.style.left = `${left + (tooltip.offsetWidth / 2) - (arrow.offsetWidth / 2)}px` + arrow.style.top = calculateArrowTop(top, tooltip) + + // And finally make it visible to the user. This will also trigger a nice fade-in animation through + // css transitions + tooltip.classList.add('visible') + arrow.classList.add('visible') + }) + + el.addEventListener('mouseout', () => { + tooltip.classList.remove('visible') + arrow.classList.remove('visible') + }) + + createdTooltips[el] = { + tooltip: tooltip, + arrow: arrow, + } + }, + unbind: el => { + if (typeof createdTooltips[el] !== 'undefined') { + createdTooltips[el].tooltip.remove() + createdTooltips[el].arrow.remove() + } + }, +} diff --git a/src/main.js b/src/main.js index a34dd7ce..27f9e402 100644 --- a/src/main.js +++ b/src/main.js @@ -56,8 +56,6 @@ import { } from '@fortawesome/free-solid-svg-icons' import {faCalendarAlt, faClock, faComments, faSave, faStar, faTimesCircle} from '@fortawesome/free-regular-svg-icons' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' -// Tooltip -import VTooltip from 'v-tooltip' // PWA import './registerServiceWorker' @@ -140,13 +138,14 @@ library.add(faStarSolid) Vue.component('icon', FontAwesomeIcon) -Vue.use(VTooltip, {defaultHtml: false}) - Vue.use(vueShortkey) import focus from '@/directives/focus' Vue.directive('focus', focus) +import tooltip from '@/directives/tooltip' +Vue.directive('tooltip', tooltip) + Vue.mixin({ methods: { formatDateSince: date => { diff --git a/src/styles/components/base/tooltip.scss b/src/styles/components/base/tooltip.scss index 76f5b3c4..46e876a9 100644 --- a/src/styles/components/base/tooltip.scss +++ b/src/styles/components/base/tooltip.scss @@ -3,22 +3,38 @@ z-index: 10000; font-size: 0.8em; text-align: center; + background: $dark; + color: white; + border-radius: 5px; + padding: 5px 10px 5px; + opacity: 0; + transition: opacity $transition; - .tooltip-inner { - background: $dark; - color: white; - border-radius: 5px; - padding: 5px 10px 5px; - } + // If the tooltip is multiline, it would make the height calculations needed to properly position it a lot harder. + white-space: nowrap; + overflow: hidden; + + &-arrow { + opacity: 0; + content: ''; + display: block; + position: absolute; + transition: opacity $transition; + z-index: 10000; - .tooltip-arrow { width: 0; height: 0; - border-style: solid; - position: absolute; - margin: 5px; - border-color: $dark; - z-index: 1; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid $dark; + + &.bottom { + transform: rotate(180deg); + } + } + + &.visible, &-arrow.visible { + opacity: 1; } &[x-placement^="top"] { diff --git a/yarn.lock b/yarn.lock index 373a3dec..5b4e0a03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10295,11 +10295,6 @@ pnp-webpack-plugin@^1.6.4: dependencies: ts-pnp "^1.1.6" -popper.js@^1.16.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" - integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== - portfinder@^1.0.26: version "1.0.26" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" @@ -13106,15 +13101,6 @@ uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v-tooltip@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.3.tgz#34fd64096656f032b1616567bf62f6165c57d529" - integrity sha512-KZZY3s+dcijzZmV2qoDH4rYmjMZ9YKGBVoUznZKQX0e3c2GjpJm3Sldzz8HHH2Ud87JqhZPB4+4gyKZ6m98cKQ== - dependencies: - lodash "^4.17.15" - popper.js "^1.16.0" - vue-resize "^0.4.5" - v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" @@ -13330,11 +13316,6 @@ vue-notification@1.3.20: resolved "https://registry.yarnpkg.com/vue-notification/-/vue-notification-1.3.20.tgz#d85618127763b46f3e25b8962b857947d5a97cbe" integrity sha512-vPj67Ah72p8xvtyVE8emfadqVWguOScAjt6OJDEUdcW5hW189NsqvfkOrctxHUUO9UYl9cTbIkzAEcPnHu+zBQ== -vue-resize@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea" - integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg== - vue-router@3.4.9: version "3.4.9" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.9.tgz#c016f42030ae2932f14e4748b39a1d9a0e250e66"