Build custom v-tooltip (#290)
Remove tooltips when their elements are unbound Add support for .bottom modifier Remove v-tooltip from dependencies Add comments Fix usage with bigger tooltips Add very basic vanilla js tooltip Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/290 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
70508202c0
commit
092e5165dc
6 changed files with 115 additions and 37 deletions
|
@ -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",
|
||||
|
|
|
@ -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'">
|
||||
<span class="icon">
|
||||
<icon icon="cog"/>
|
||||
</span>
|
||||
|
|
83
src/directives/tooltip.js
Normal file
83
src/directives/tooltip.js
Normal file
|
@ -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()
|
||||
}
|
||||
},
|
||||
}
|
|
@ -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 => {
|
||||
|
|
|
@ -3,22 +3,38 @@
|
|||
z-index: 10000;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
|
||||
.tooltip-inner {
|
||||
background: $dark;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px 5px;
|
||||
}
|
||||
opacity: 0;
|
||||
transition: opacity $transition;
|
||||
|
||||
// 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"] {
|
||||
|
|
19
yarn.lock
19
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"
|
||||
|
|
Loading…
Reference in a new issue