diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /app/assets/javascripts/popovers | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (diff) |
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/popovers')
-rw-r--r-- | app/assets/javascripts/popovers/components/popovers.vue | 92 | ||||
-rw-r--r-- | app/assets/javascripts/popovers/index.js | 51 |
2 files changed, 143 insertions, 0 deletions
diff --git a/app/assets/javascripts/popovers/components/popovers.vue b/app/assets/javascripts/popovers/components/popovers.vue new file mode 100644 index 00000000000..3bb6d284264 --- /dev/null +++ b/app/assets/javascripts/popovers/components/popovers.vue @@ -0,0 +1,92 @@ +<script> +// We can't use v-safe-html here as the popover's title or content might contains SVGs that would +// be stripped by the directive's sanitizer. Instead, we fallback on v-html and we use GitLab's +// dompurify config that lets SVGs be rendered properly. +// Context: https://gitlab.com/gitlab-org/gitlab/-/issues/247207 +/* eslint-disable vue/no-v-html */ +import { GlPopover } from '@gitlab/ui'; +import { sanitize } from '~/lib/dompurify'; + +const newPopover = element => { + const { content, html, placement, title, triggers = 'focus' } = element.dataset; + + return { + target: element, + content, + html, + placement, + title, + triggers, + }; +}; + +export default { + components: { + GlPopover, + }, + data() { + return { + popovers: [], + }; + }, + created() { + this.observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.removedNodes.forEach(this.dispose); + }); + }); + }, + beforeDestroy() { + this.observer.disconnect(); + }, + methods: { + addPopovers(elements) { + const newPopovers = elements.reduce((acc, element) => { + if (this.popoverExists(element)) { + return acc; + } + const popover = newPopover(element); + this.observe(popover); + return [...acc, popover]; + }, []); + + this.popovers.push(...newPopovers); + }, + observe(popover) { + this.observer.observe(popover.target.parentElement, { + childList: true, + }); + }, + dispose(target) { + if (!target) { + this.popovers = []; + } else { + const index = this.popovers.findIndex(popover => popover.target === target); + + if (index > -1) { + this.popovers.splice(index, 1); + } + } + }, + popoverExists(element) { + return this.popovers.some(popover => popover.target === element); + }, + getSafeHtml(html) { + return sanitize(html); + }, + }, +}; +</script> + +<template> + <div> + <gl-popover v-for="(popover, index) in popovers" :key="index" v-bind="popover"> + <template #title> + <span v-if="popover.html" v-html="getSafeHtml(popover.title)"></span> + <span v-else>{{ popover.title }}</span> + </template> + <span v-if="popover.html" v-html="getSafeHtml(popover.content)"></span> + <span v-else>{{ popover.content }}</span> + </gl-popover> + </div> +</template> diff --git a/app/assets/javascripts/popovers/index.js b/app/assets/javascripts/popovers/index.js new file mode 100644 index 00000000000..bfb61f02a3a --- /dev/null +++ b/app/assets/javascripts/popovers/index.js @@ -0,0 +1,51 @@ +import Vue from 'vue'; +import { toArray } from 'lodash'; +import PopoversComponent from './components/popovers.vue'; + +let app; + +const APP_ELEMENT_ID = 'gl-popovers-app'; + +const getPopoversApp = () => { + if (!app) { + const container = document.createElement('div'); + container.setAttribute('id', APP_ELEMENT_ID); + document.body.appendChild(container); + + const Popovers = Vue.extend(PopoversComponent); + app = new Popovers(); + app.$mount(`#${APP_ELEMENT_ID}`); + } + + return app; +}; + +const isPopover = (node, selector) => node.matches && node.matches(selector); + +const handlePopoverEvent = (rootTarget, e, selector) => { + for (let { target } = e; target && target !== rootTarget; target = target.parentNode) { + if (isPopover(target, selector)) { + getPopoversApp().addPopovers([target]); + break; + } + } +}; + +export const initPopovers = () => { + ['mouseenter', 'focus', 'click'].forEach(event => { + document.addEventListener( + event, + e => handlePopoverEvent(document, e, '[data-toggle="popover"]'), + true, + ); + }); + + return getPopoversApp(); +}; + +export const dispose = elements => toArray(elements).forEach(getPopoversApp().dispose); + +export const destroy = () => { + getPopoversApp().$destroy(); + app = null; +}; |