diff options
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/js/components/DashboardRepoList.vue | 4 | ||||
-rw-r--r-- | web_src/js/components/DiffFileList.vue | 12 | ||||
-rw-r--r-- | web_src/js/components/DiffFileTreeItem.vue | 2 | ||||
-rw-r--r-- | web_src/js/features/common-global.js | 7 | ||||
-rw-r--r-- | web_src/js/features/contextpopup.js | 1 | ||||
-rw-r--r-- | web_src/js/features/repo-diff.js | 5 | ||||
-rw-r--r-- | web_src/js/features/repo-issue.js | 5 | ||||
-rw-r--r-- | web_src/js/index.js | 2 | ||||
-rw-r--r-- | web_src/js/modules/tippy.js | 117 |
9 files changed, 101 insertions, 54 deletions
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 075af65af3..a97cfb02ba 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -147,7 +147,6 @@ <script> import {createApp, nextTick} from 'vue'; import $ from 'jquery'; -import {initTooltip} from '../modules/tippy.js'; import {SvgIcon} from '../svg.js'; const {appSubUrl, assetUrlPrefix, pageData} = window.config; @@ -238,9 +237,6 @@ const sfc = { mounted() { const el = document.getElementById('dashboard-repo-list'); this.changeReposFilter(this.reposFilter); - for (const elTooltip of el.querySelectorAll('.tooltip')) { - initTooltip(elTooltip); - } $(el).find('.dropdown').dropdown(); nextTick(() => { this.$refs.search.focus(); diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue index 32919156b8..86444f2b21 100644 --- a/web_src/js/components/DiffFileList.vue +++ b/web_src/js/components/DiffFileList.vue @@ -21,7 +21,6 @@ </template> <script> -import {initTooltip} from '../modules/tippy.js'; import {doLoadMoreFiles} from '../features/repo-diff.js'; const {pageData} = window.config; @@ -30,17 +29,6 @@ export default { data: () => { return pageData.diffFileInfo; }, - watch: { - fileListIsVisible(newValue) { - if (newValue === true) { - this.$nextTick(() => { - for (const el of this.$refs.root.querySelectorAll('.tooltip')) { - initTooltip(el); - } - }); - } - } - }, mounted() { document.getElementById('show-file-list-btn').addEventListener('click', this.toggleFileList); }, diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index f0a3d909b9..4084dee51d 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -1,5 +1,5 @@ <template> - <div v-show="show" class="tooltip" :title="item.name"> + <div v-show="show" :title="item.name"> <!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"--> <div class="item" :class="item.isFile ? 'filewrapper gt-p-1' : ''"> <!-- Files --> diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 113ff2e1f1..d533877c27 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -5,7 +5,6 @@ import {createDropzone} from './dropzone.js'; import {initCompColorPicker} from './comp/ColorPicker.js'; import {showGlobalErrorMessage} from '../bootstrap.js'; import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js'; -import {initTooltip} from '../modules/tippy.js'; import {svg} from '../svg.js'; import {hideElem, showElem, toggleElem} from '../utils/dom.js'; @@ -66,12 +65,6 @@ export function initGlobalButtonClickOnEnter() { }); } -export function initGlobalTooltips() { - for (const el of document.getElementsByClassName('tooltip')) { - initTooltip(el); - } -} - export function initGlobalCommon() { // Undo Safari emoji glitch fix at high enough zoom levels if (navigator.userAgent.match('Safari')) { diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js index 8e0ef92bd3..c685d93db0 100644 --- a/web_src/js/features/contextpopup.js +++ b/web_src/js/features/contextpopup.js @@ -30,6 +30,7 @@ export function initContextPopups() { createTippy(this, { content: el, + placement: 'top-start', interactive: true, interactiveBorder: 5, onShow: () => { diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index 56ebe4fc99..458f11c6f2 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -3,7 +3,6 @@ import {initCompReactionSelector} from './comp/ReactionSelector.js'; import {initRepoIssueContentHistory} from './repo-issue-content.js'; import {validateTextareaNonEmpty} from './comp/EasyMDE.js'; import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles} from './pull-view-file.js'; -import {initTooltip} from '../modules/tippy.js'; const {csrfToken} = window.config; @@ -60,10 +59,6 @@ export function initRepoDiffConversationForm() { const $newConversationHolder = $(await $.post($form.attr('action'), formDataString)); const {path, side, idx} = $newConversationHolder.data(); - $newConversationHolder.find('.tooltip').each(function () { - initTooltip(this); - }); - $form.closest('.conversation-holder').replaceWith($newConversationHolder); if ($form.closest('tr').data('line-type') === 'same') { $(`[data-path="${path}"] a.add-code-comment[data-idx="${idx}"]`).addClass('invisible'); diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index e49b1b2726..767f071151 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -4,7 +4,7 @@ import {attachTribute} from './tribute.js'; import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js'; import {initEasyMDEImagePaste} from './comp/ImagePaste.js'; import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js'; -import {initTooltip, showTemporaryTooltip, createTippy} from '../modules/tippy.js'; +import {showTemporaryTooltip, createTippy} from '../modules/tippy.js'; import {hideElem, showElem, toggleElem} from '../utils/dom.js'; import {setFileFolding} from './file-fold.js'; @@ -280,10 +280,7 @@ export function initRepoPullRequestAllowMaintainerEdit() { const $checkbox = $('#allow-edits-from-maintainers'); if (!$checkbox.length) return; - const promptTip = $checkbox.attr('data-prompt-tip'); const promptError = $checkbox.attr('data-prompt-error'); - - initTooltip($checkbox[0], {content: promptTip}); $checkbox.checkbox({ 'onChange': () => { const checked = $checkbox.checkbox('is checked'); diff --git a/web_src/js/index.js b/web_src/js/index.js index 7d74ee6b94..84ffe8e5db 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -56,7 +56,6 @@ import { initGlobalFormDirtyLeaveConfirm, initGlobalLinkActions, initHeadNavbarContentToggle, - initGlobalTooltips, } from './features/common-global.js'; import {initRepoTopicBar} from './features/repo-home.js'; import {initAdminEmails} from './features/admin/emails.js'; @@ -91,6 +90,7 @@ import {initCaptcha} from './features/captcha.js'; import {initRepositoryActionView} from './components/RepoActionView.vue'; import {initAriaCheckboxPatch} from './modules/aria/checkbox.js'; import {initAriaDropdownPatch} from './modules/aria/dropdown.js'; +import {initGlobalTooltips} from './modules/tippy.js'; // Run time-critical code as soon as possible. This is safe to do because this // script appears at the end of <body> and rendered HTML is accessible at that point. diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js index 4872608ecd..98d0ff84b6 100644 --- a/web_src/js/modules/tippy.js +++ b/web_src/js/modules/tippy.js @@ -3,7 +3,6 @@ import tippy from 'tippy.js'; export function createTippy(target, opts = {}) { const instance = tippy(target, { appendTo: document.body, - placement: target.getAttribute('data-placement') || 'top-start', animation: false, allowHTML: false, hideOnClick: false, @@ -25,38 +24,116 @@ export function createTippy(target, opts = {}) { return instance; } -export function initTooltip(el, props = {}) { - const content = el.getAttribute('data-content') || props.content; +/** + * Attach a tooltip tippy to the given target element. + * If the target element already has a tooltip tippy attached, the tooltip will be updated with the new content. + * If the target element has no content, then no tooltip will be attached, and it returns null. + * + * Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation. + * + * @param target {HTMLElement} + * @param content {null|string} + * @returns {null|tippy} + */ +function attachTooltip(target, content = null) { + content = content ?? getTooltipContent(target); if (!content) return null; - if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', content); - return createTippy(el, { + + const props = { content, delay: 100, role: 'tooltip', - ...(el.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true} : {}), - ...props, - }); -} + placement: target.getAttribute('data-tooltip-placement') || 'top-start', + ...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true} : {}), + }; -export function showTemporaryTooltip(target, content) { - let tippy, oldContent; - if (target._tippy) { - tippy = target._tippy; - oldContent = tippy.props.content; + if (!target._tippy) { + createTippy(target, props); } else { - tippy = initTooltip(target, {content}); + target._tippy.setProps(props); + } + return target._tippy; +} + +/** + * Creating tooltip tippy instance is expensive, so we only create it when the user hovers over the element + * According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event + * Some old browsers like Pale Moon doesn't support "mouseenter(capture)" + * The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy + * @param e {Event} + */ +function lazyTooltipOnMouseHover(e) { + e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true); + attachTooltip(this); +} + +function getTooltipContent(target) { + // prefer to always use the "[data-tooltip-content]" attribute + // for backward compatibility, we also support the ".tooltip[data-content]" attribute + // in next PR, refactor all the ".tooltip[data-content]" to "[data-tooltip-content]" + let content = target.getAttribute('data-tooltip-content'); + if (!content && target.classList.contains('tooltip')) { + content = target.getAttribute('data-content'); + } + return content; +} + +/** + * Activate the tooltip for all children elements + * And if the element has no aria-label, use the tooltip content as aria-label + * @param target {HTMLElement} + */ +function attachChildrenLazyTooltip(target) { + // the selector must match the logic in getTippyTooltipContent + for (const el of target.querySelectorAll('[data-tooltip-content], .tooltip[data-content]')) { + el.addEventListener('mouseover', lazyTooltipOnMouseHover, true); + + // meanwhile, if the element has no aria-label, use the tooltip content as aria-label + if (!el.hasAttribute('aria-label')) { + const content = getTooltipContent(el); + if (content) { + el.setAttribute('aria-label', content); + } + } } +} +export function initGlobalTooltips() { + // use MutationObserver to detect new elements added to the DOM, or attributes changed + const observer = new MutationObserver((mutationList) => { + for (const mutation of mutationList) { + if (mutation.type === 'childList') { + // mainly for Vue components and AJAX rendered elements + for (const el of mutation.addedNodes) { + // handle all "tooltip" elements in added nodes which have 'querySelectorAll' method, skip non-related nodes (eg: "#text") + if ('querySelectorAll' in el) { + attachChildrenLazyTooltip(el); + } + } + } else if (mutation.type === 'attributes') { + // sync the tooltip content if the attributes change + attachTooltip(mutation.target); + } + } + }); + observer.observe(document, { + subtree: true, + childList: true, + attributeFilter: ['data-tooltip-content', 'data-content'], + }); + + attachChildrenLazyTooltip(document.documentElement); +} + +export function showTemporaryTooltip(target, content) { + const tippy = target._tippy ?? attachTooltip(target, content); tippy.setContent(content); if (!tippy.state.isShown) tippy.show(); tippy.setProps({ onHidden: (tippy) => { - if (oldContent) { - tippy.setContent(oldContent); - tippy.setProps({onHidden: undefined}); - } else { + // reset the default tooltip content, if no default, then this temporary tooltip could be destroyed + if (!attachTooltip(target)) { tippy.destroy(); - // after destroy, the `_tippy` is detached, it can't do "setProps (etc...)" anymore } }, }); |