Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-27 18:08:39 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-27 18:08:39 +0300
commit2b1e7f7dac0fa5d7bb3bdf415cec1b3c67ed77b0 (patch)
tree725ae8200573957bff6fa03aee237f738dadf1d7 /app/assets/javascripts/popovers
parenteb004dc626d3a1c9497e8b9dc0f3f578afd05fd9 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/popovers')
-rw-r--r--app/assets/javascripts/popovers/components/popovers.vue92
-rw-r--r--app/assets/javascripts/popovers/index.js51
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;
+};