diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /app/assets/javascripts/tooltips | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) | |
download | gitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'app/assets/javascripts/tooltips')
-rw-r--r-- | app/assets/javascripts/tooltips/components/tooltips.vue | 116 | ||||
-rw-r--r-- | app/assets/javascripts/tooltips/index.js | 120 |
2 files changed, 236 insertions, 0 deletions
diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue new file mode 100644 index 00000000000..8307f878def --- /dev/null +++ b/app/assets/javascripts/tooltips/components/tooltips.vue @@ -0,0 +1,116 @@ +<script> +import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; + +const getTooltipTitle = element => { + return element.getAttribute('title') || element.dataset.title; +}; + +const newTooltip = (element, config = {}) => { + const { placement, container, boundary, html, triggers } = element.dataset; + const title = getTooltipTitle(element); + + return { + id: uniqueId('gl-tooltip'), + target: element, + title, + html, + placement, + container, + boundary, + triggers, + disabled: !title, + ...config, + }; +}; + +export default { + components: { + GlTooltip, + }, + directives: { + SafeHtml, + }, + data() { + return { + tooltips: [], + }; + }, + created() { + this.observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.removedNodes.forEach(this.dispose); + }); + }); + }, + beforeDestroy() { + this.observer.disconnect(); + }, + methods: { + addTooltips(elements, config) { + const newTooltips = elements + .filter(element => !this.tooltipExists(element)) + .map(element => newTooltip(element, config)); + + newTooltips.forEach(tooltip => this.observe(tooltip)); + + this.tooltips.push(...newTooltips); + }, + observe(tooltip) { + this.observer.observe(tooltip.target.parentElement, { + childList: true, + }); + }, + dispose(target) { + if (!target) { + this.tooltips = []; + } else { + const index = this.tooltips.indexOf(this.findTooltipByTarget(target)); + + if (index > -1) { + this.tooltips.splice(index, 1); + } + } + }, + fixTitle(target) { + const tooltip = this.findTooltipByTarget(target); + + if (tooltip) { + tooltip.title = target.getAttribute('title'); + } + }, + triggerEvent(target, event) { + const tooltip = this.findTooltipByTarget(target); + + if (tooltip) { + this.$refs[tooltip.id][0].$emit(event); + } + }, + tooltipExists(element) { + return Boolean(this.findTooltipByTarget(element)); + }, + findTooltipByTarget(element) { + return this.tooltips.find(tooltip => tooltip.target === element); + }, + }, +}; +</script> +<template> + <div> + <gl-tooltip + v-for="(tooltip, index) in tooltips" + :id="tooltip.id" + :ref="tooltip.id" + :key="index" + :target="tooltip.target" + :triggers="tooltip.triggers" + :placement="tooltip.placement" + :container="tooltip.container" + :boundary="tooltip.boundary" + :disabled="tooltip.disabled" + > + <span v-if="tooltip.html" v-safe-html="tooltip.title"></span> + <span v-else>{{ tooltip.title }}</span> + </gl-tooltip> + </div> +</template> diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js new file mode 100644 index 00000000000..cfbd88d6c40 --- /dev/null +++ b/app/assets/javascripts/tooltips/index.js @@ -0,0 +1,120 @@ +import Vue from 'vue'; +import jQuery from 'jquery'; +import { toArray, isFunction } from 'lodash'; +import Tooltips from './components/tooltips.vue'; + +let app; + +const EVENTS_MAP = { + hover: 'mouseenter', + click: 'click', + focus: 'focus', +}; + +const DEFAULT_TRIGGER = 'hover focus'; +const APP_ELEMENT_ID = 'gl-tooltips-app'; + +const tooltipsApp = () => { + if (!app) { + const container = document.createElement('div'); + + container.setAttribute('id', APP_ELEMENT_ID); + document.body.appendChild(container); + + app = new Vue({ + render(h) { + return h(Tooltips, { + props: { + elements: this.elements, + }, + ref: 'tooltips', + }); + }, + }).$mount(container); + } + + return app.$refs.tooltips; +}; + +const isTooltip = (node, selector) => node.matches && node.matches(selector); + +const addTooltips = (elements, config) => { + tooltipsApp().addTooltips(toArray(elements), config); +}; + +const handleTooltipEvent = (rootTarget, e, selector, config = {}) => { + for (let { target } = e; target && target !== rootTarget; target = target.parentNode) { + if (isTooltip(target, selector)) { + addTooltips([target], { + show: true, + ...config, + }); + break; + } + } +}; + +const applyToElements = (elements, handler) => toArray(elements).forEach(handler); + +const invokeBootstrapApi = (elements, method) => { + if (isFunction(elements.tooltip)) { + jQuery(elements).tooltip(method); + } +}; + +const isGlTooltipsEnabled = () => Boolean(window.gon.glTooltipsEnabled); + +const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => { + if (isGlTooltipsEnabled()) { + applyToElements(elements, glHandler); + } else { + bsHandler(elements, ...params); + } +}; + +export const initTooltips = (config = {}) => { + if (isGlTooltipsEnabled()) { + const triggers = config?.triggers || DEFAULT_TRIGGER; + const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]); + + events.forEach(event => { + document.addEventListener( + event, + e => handleTooltipEvent(document, e, config.selector, config), + true, + ); + }); + + return tooltipsApp(); + } + + return invokeBootstrapApi(document.body, config); +}; +export const dispose = tooltipApiInvoker({ + glHandler: element => tooltipsApp().dispose(element), + bsHandler: elements => invokeBootstrapApi(elements, 'dispose'), +}); +export const fixTitle = tooltipApiInvoker({ + glHandler: element => tooltipsApp().fixTitle(element), + bsHandler: elements => invokeBootstrapApi(elements, '_fixTitle'), +}); +export const enable = tooltipApiInvoker({ + glHandler: element => tooltipsApp().triggerEvent(element, 'enable'), + bsHandler: elements => invokeBootstrapApi(elements, 'enable'), +}); +export const disable = tooltipApiInvoker({ + glHandler: element => tooltipsApp().triggerEvent(element, 'disable'), + bsHandler: elements => invokeBootstrapApi(elements, 'disable'), +}); +export const hide = tooltipApiInvoker({ + glHandler: element => tooltipsApp().triggerEvent(element, 'close'), + bsHandler: elements => invokeBootstrapApi(elements, 'hide'), +}); +export const show = tooltipApiInvoker({ + glHandler: element => tooltipsApp().triggerEvent(element, 'open'), + bsHandler: elements => invokeBootstrapApi(elements, 'show'), +}); +export const destroy = () => { + tooltipsApp().$destroy(); + app = null; +}; |