diff --git a/package.json b/package.json index 2351da6..3440c4e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "mini-css-extract-plugin": "^0.4.2", "portal-vue": "^2.1.7", "vue-dragscroll": "^1.10.2", + "vue-fragment": "^1.5.1", "vue-removed-hook-mixin": "^0.1.1", "vue2-leaflet": "^1.0.2", "vuex": "^3.0.1", diff --git a/resources/components/tooltip.html b/resources/components/tooltip.html index e5ebf55..65805b0 100644 --- a/resources/components/tooltip.html +++ b/resources/components/tooltip.html @@ -1,7 +1,10 @@ - - - - - - - + + + + + + + + diff --git a/resources/styles/_animations.scss b/resources/styles/_animations.scss index 8dbe350..592f3b1 100644 --- a/resources/styles/_animations.scss +++ b/resources/styles/_animations.scss @@ -14,7 +14,16 @@ } } -@include vue-animation(fade, 150ms ease-in-out) { +@include vue-animation(fade, 250ms ease-in-out) { + 0% { + opacity: 0 + } + 100% { + opacity: 1 + } +} + +@include vue-animation(tooltip, 100ms ease-in-out) { 0% { opacity: 0 } diff --git a/resources/ts/app.ts b/resources/ts/app.ts index 2385e16..24d2987 100644 --- a/resources/ts/app.ts +++ b/resources/ts/app.ts @@ -15,16 +15,27 @@ import Vue from "vue"; import Vuex from 'vuex'; import PortalVue from 'portal-vue'; import VueDragscroll from 'vue-dragscroll'; +import { Plugin as VueFragment } from 'vue-fragment'; import { Workbox } from "workbox-window"; import { migrate } from "./store/migrations"; +import { Component } from "vue-property-decorator"; Vue.use(Vuex); Vue.use(PortalVue); Vue.use(VueDragscroll); +Vue.use(VueFragment); + +declare module 'vue/types/vue' { + interface Vue { + $isTouch: boolean; + } +} Vue.prototype.$isTouch = 'ontouchstart' in window || navigator.msMaxTouchPoints > 0; +Component.registerHooks(['removed']); + // async dependencies (async function () { await migrate("vuex"); diff --git a/resources/ts/components/tooltip.ts b/resources/ts/components/tooltip.ts index 49755be..01a4326 100644 --- a/resources/ts/components/tooltip.ts +++ b/resources/ts/components/tooltip.ts @@ -1,34 +1,88 @@ import Vue from 'vue'; import Component from "vue-class-component"; -import { Prop } from "vue-property-decorator"; +import { Prop, Watch } from "vue-property-decorator"; + +type Events = { + [evnet: string]: (...any) => void, +} + +type Trigger = "hover" | "focus"; + +const longPressTimeout = 1000; @Component({ template: require('../../components/tooltip.html') }) export class TooltipComponent extends Vue { @Prop({ type: String, default: "auto" }) public placement: string; - @Prop({ type: Number, default: 100 }) public delay: number; + @Prop({ type: Number, default: 200 }) public delay: number; + @Prop({ type: Array, default: ["hover", "focus"]}) public triggers: Trigger[]; public show: boolean = false; - public element: Element = null; + public root: Element = null; - private _events: { [event: string]: any }; - private _timeout: number; + private _events: Events; mounted() { - this.$el.parentElement.addEventListener('mouseenter', this._events['mouseenter'] = () => { - this._timeout = window.setTimeout(() => { this.show = true }, this.delay); - }); + this.root = (this.$refs['root'] as HTMLSpanElement).parentElement; - this.$el.parentElement.addEventListener('mouseleave', this._events['mouseleave'] = () => { - window.clearTimeout(this._timeout); - this.show = false - }); - - this.element = this.$el.parentElement; + this._registerEventListeners(); } beforeDestroy() { - this.$el.parentElement.removeEventListener('mouseenter', this._events['mouseenter']); - this.$el.parentElement.removeEventListener('mouseleave', this._events['mouseleave']); + this._removeEventListeners(); + } + + @Watch('triggers', { immediate: true }) + updateTriggers() { + this._removeEventListeners(); + + this._events = {}; + + let blocked: boolean = false; + + if (this.triggers.includes("hover")) { + let timeout; + + this._events['mouseenter'] = () => { + timeout = window.setTimeout(() => { this.show = !blocked }, this.delay); + }; + + this._events['mouseleave'] = () => { + window.clearTimeout(timeout); + this.show = false + }; + } + + if (this.triggers.includes("focus") || (this.triggers.includes("hover") && this.$isTouch)) { + if (this.$isTouch) { + this._events['touchstart'] = () => { + // this is to prevent showing tooltips after tap + blocked = true; + setTimeout(() => blocked = false, longPressTimeout); + } + } + + this._events['focus'] = () => { + this.show = !blocked; + }; + + this._events['blur'] = () => { + this.show = false + }; + } + + this._registerEventListeners(); + } + + private _registerEventListeners() { + for (const [event, handler] of Object.entries(this._events)) { + this.root.addEventListener(event, handler); + } + } + + private _removeEventListeners() { + for (const [event, handler] of Object.entries(this._events)) { + this.root.removeEventListener(event, handler); + } } } diff --git a/resources/ts/components/utils.ts b/resources/ts/components/utils.ts index c81743e..200e083 100644 --- a/resources/ts/components/utils.ts +++ b/resources/ts/components/utils.ts @@ -104,7 +104,7 @@ export class PopperComponent extends Vue { } removed() { - this._popper.destroy(); + this._popper.destroy() } } diff --git a/yarn.lock b/yarn.lock index 2f3da73..016780d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6453,6 +6453,11 @@ vue-dragscroll@^1.10.2: resolved "https://registry.yarnpkg.com/vue-dragscroll/-/vue-dragscroll-1.10.2.tgz#34ace3c6aa7a39edc157cac5e9ea1d5b31b1119c" integrity sha512-fGVw8KP3ggbp49csa1Tbj2my0YuNmZ1zxYYge4QWIypGNHVwd9hResy/v6QF5HxY0a+qd2EBteeBpxtJxFMp5A== +vue-fragment@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vue-fragment/-/vue-fragment-1.5.1.tgz#44c070d55ed1e9a6c698ef57a5c83f64bb06feeb" + integrity sha512-ig6eES6TcMBbANW71ylB+AJgRN+Zksb3f50AxjGpAk6hMzqmeuD80qeh4LJP0jVw2dMBMjgRUfIkrvxygoRgtQ== + vue-property-decorator@^7.0.0: version "7.3.0" resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-7.3.0.tgz#d50d67f0b0f1c814f9f2fba36d6eeccbcc62dbb6"