diff options
author | silverwind <me@silverwind.io> | 2022-08-09 14:37:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-09 14:37:34 +0200 |
commit | 1b2cd4c4e19c78390be329b4a3ad50ff8857ca8d (patch) | |
tree | c3c9af67b599f92af60c9cd5bb7feee056d97734 /web_src/js | |
parent | 36f9ee5813beba0fc4b394a5db636f76afc5cc38 (diff) | |
download | gitea-1b2cd4c4e19c78390be329b4a3ad50ff8857ca8d.tar.gz gitea-1b2cd4c4e19c78390be329b4a3ad50ff8857ca8d.zip |
Replace fomantic popup module with tippy.js (#20428)
- replace fomantic popup module with tippy.js
- fix chaining and add comment
- add 100ms delay to tooltips
- stopwatch improvments, raise default maxWidth
- update web_src/js/features/common-global.js
- use type=submit instead of js
Diffstat (limited to 'web_src/js')
-rw-r--r-- | web_src/js/components/DashboardRepoList.js | 5 | ||||
-rw-r--r-- | web_src/js/features/clipboard.js | 37 | ||||
-rw-r--r-- | web_src/js/features/common-global.js | 31 | ||||
-rw-r--r-- | web_src/js/features/comp/ReactionSelector.js | 12 | ||||
-rw-r--r-- | web_src/js/features/repo-code.js | 50 | ||||
-rw-r--r-- | web_src/js/features/repo-commit.js | 15 | ||||
-rw-r--r-- | web_src/js/features/repo-diff.js | 4 | ||||
-rw-r--r-- | web_src/js/features/repo-issue.js | 13 | ||||
-rw-r--r-- | web_src/js/features/stopwatch.js | 21 | ||||
-rw-r--r-- | web_src/js/index.js | 4 | ||||
-rw-r--r-- | web_src/js/modules/tippy.js | 48 |
11 files changed, 144 insertions, 96 deletions
diff --git a/web_src/js/components/DashboardRepoList.js b/web_src/js/components/DashboardRepoList.js index 36caaf2f5b..cbbc12c2c4 100644 --- a/web_src/js/components/DashboardRepoList.js +++ b/web_src/js/components/DashboardRepoList.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import $ from 'jquery'; import {initVueSvg, vueDelimiters} from './VueComponentLoader.js'; +import {initTooltip} from '../modules/tippy.js'; const {appSubUrl, assetUrlPrefix, pageData} = window.config; @@ -138,7 +139,9 @@ function initVueComponents() { mounted() { this.changeReposFilter(this.reposFilter); - $(this.$el).find('.tooltip').popup(); + for (const el of this.$el.querySelectorAll('.tooltip')) { + initTooltip(el); + } $(this.$el).find('.dropdown').dropdown(); this.setCheckboxes(); Vue.nextTick(() => { diff --git a/web_src/js/features/clipboard.js b/web_src/js/features/clipboard.js index e4a5c4f448..85324303e3 100644 --- a/web_src/js/features/clipboard.js +++ b/web_src/js/features/clipboard.js @@ -1,24 +1,15 @@ -import $ from 'jquery'; +import {showTemporaryTooltip} from '../modules/tippy.js'; const {copy_success, copy_error} = window.config.i18n; -function onSuccess(btn) { - btn.setAttribute('data-variation', 'inverted tiny'); - $(btn).popup('destroy'); - const oldContent = btn.getAttribute('data-content'); - btn.setAttribute('data-content', copy_success); - $(btn).popup('show'); - btn.setAttribute('data-content', oldContent || ''); +export async function copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + } catch { + return fallbackCopyToClipboard(text); + } + return true; } -function onError(btn) { - btn.setAttribute('data-variation', 'inverted tiny'); - const oldContent = btn.getAttribute('data-content'); - $(btn).popup('destroy'); - btn.setAttribute('data-content', copy_error); - $(btn).popup('show'); - btn.setAttribute('data-content', oldContent || ''); -} - // Fallback to use if navigator.clipboard doesn't exist. Achieved via creating // a temporary textarea element, selecting the text, and using document.execCommand @@ -60,16 +51,8 @@ export default function initGlobalCopyToClipboardListener() { e.preventDefault(); (async() => { - try { - await navigator.clipboard.writeText(text); - onSuccess(target); - } catch { - if (fallbackCopyToClipboard(text)) { - onSuccess(target); - } else { - onError(target); - } - } + const success = await copyToClipboard(text); + showTemporaryTooltip(target, success ? copy_success : copy_error); })(); break; diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 025b44d87d..1776f6577d 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -6,6 +6,7 @@ import {initCompColorPicker} from './comp/ColorPicker.js'; import {showGlobalErrorMessage} from '../bootstrap.js'; import {attachDropdownAria} from './aria.js'; import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js'; +import {initTooltip} from '../modules/tippy.js'; const {appUrl, csrfToken} = window.config; @@ -62,18 +63,10 @@ export function initGlobalButtonClickOnEnter() { }); } -export function initPopup(target) { - const $el = $(target); - const attr = $el.attr('data-variation'); - const attrs = attr ? attr.split(' ') : []; - const variations = new Set([...attrs, 'inverted', 'tiny']); - $el.attr('data-variation', [...variations].join(' ')).popup(); -} - -export function initGlobalPopups() { - $('.tooltip').each((_, el) => { - initPopup(el); - }); +export function initGlobalTooltips() { + for (const el of document.getElementsByClassName('tooltip')) { + initTooltip(el); + } } export function initGlobalCommon() { @@ -106,7 +99,12 @@ export function initGlobalCommon() { $uiDropdowns.filter('.jump').dropdown({ action: 'hide', onShow() { - $('.tooltip').popup('hide'); + // hide associated tooltip while dropdown is open + this._tippy?.hide(); + this._tippy?.disable(); + }, + onHide() { + this._tippy?.enable(); }, fullTextSearch: 'exact' }); @@ -122,13 +120,6 @@ export function initGlobalCommon() { $('.ui.checkbox').checkbox(); - $('.top.menu .tooltip').popup({ - onShow() { - if ($('.top.menu .menu.transition').hasClass('visible')) { - return false; - } - } - }); $('.tabular.menu .item').tab(); $('.tabable.menu .item').tab(); diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js index 272ea45cdd..26c9af2ff3 100644 --- a/web_src/js/features/comp/ReactionSelector.js +++ b/web_src/js/features/comp/ReactionSelector.js @@ -1,16 +1,20 @@ import $ from 'jquery'; +import {createTippy} from '../../modules/tippy.js'; + const {csrfToken} = window.config; export function initCompReactionSelector(parent) { - let reactions = ''; + let selector = 'a.label'; if (!parent) { parent = $(document); - reactions = '.reactions > '; + selector = `.reactions ${selector}`; } - parent.find(`${reactions}a.label`).popup({position: 'bottom left', metadata: {content: 'title', title: 'none'}}); + for (const el of parent[0].querySelectorAll(selector)) { + createTippy(el, {placement: 'bottom-start', content: el.getAttribute('data-title')}); + } - parent.find(`.select-reaction > .menu > .item, ${reactions}a.label`).on('click', function (e) { + parent.find(`.select-reaction > .menu > .item, ${selector}`).on('click', function (e) { e.preventDefault(); if ($(this).hasClass('disabled')) return; diff --git a/web_src/js/features/repo-code.js b/web_src/js/features/repo-code.js index 8562ba0072..002a25f6ed 100644 --- a/web_src/js/features/repo-code.js +++ b/web_src/js/features/repo-code.js @@ -1,6 +1,8 @@ import $ from 'jquery'; import {svg} from '../svg.js'; import {invertFileFolding} from './file-fold.js'; +import {createTippy} from '../modules/tippy.js'; +import {copyToClipboard} from './clipboard.js'; function changeHash(hash) { if (window.history.pushState) { @@ -39,13 +41,13 @@ function selectRange($list, $select, $from) { $viewGitBlame.attr('href', href); }; - const updateCopyPermalinkHref = function(anchor) { + const updateCopyPermalinkUrl = function(anchor) { if ($copyPermalink.length === 0) { return; } - let link = $copyPermalink.attr('data-clipboard-text'); + let link = $copyPermalink.attr('data-url'); link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`; - $copyPermalink.attr('data-clipboard-text', link); + $copyPermalink.attr('data-url', link); }; if ($from) { @@ -67,7 +69,7 @@ function selectRange($list, $select, $from) { updateIssueHref(`L${a}-L${b}`); updateViewGitBlameFragment(`L${a}-L${b}`); - updateCopyPermalinkHref(`L${a}-L${b}`); + updateCopyPermalinkUrl(`L${a}-L${b}`); return; } } @@ -76,17 +78,36 @@ function selectRange($list, $select, $from) { updateIssueHref($select.attr('rel')); updateViewGitBlameFragment($select.attr('rel')); - updateCopyPermalinkHref($select.attr('rel')); + updateCopyPermalinkUrl($select.attr('rel')); } function showLineButton() { - if ($('.code-line-menu').length === 0) return; - $('.code-line-button').remove(); - $('.code-view td.lines-code.active').closest('tr').find('td:eq(0)').first().prepend( - $(`<button class="code-line-button">${svg('octicon-kebab-horizontal')}</button>`) - ); - $('.code-line-menu').appendTo($('.code-view')); - $('.code-line-button').popup({popup: $('.code-line-menu'), on: 'click'}); + const menu = document.querySelector('.code-line-menu'); + if (!menu) return; + + // remove all other line buttons + for (const el of document.querySelectorAll('.code-line-button')) { + el.remove(); + } + + // find active row and add button + const tr = document.querySelector('.code-view td.lines-code.active').closest('tr'); + const td = tr.querySelector('td'); + const btn = document.createElement('button'); + btn.classList.add('code-line-button'); + btn.innerHTML = svg('octicon-kebab-horizontal'); + td.prepend(btn); + + // put a copy of the menu back into DOM for the next click + btn.closest('.code-view').appendChild(menu.cloneNode(true)); + + createTippy(btn, { + trigger: 'click', + content: menu, + placement: 'right-start', + role: 'menu', + interactive: 'true', + }); } export function initRepoCodeView() { @@ -159,4 +180,9 @@ export function initRepoCodeView() { const blob = await $.get(`${url}?${query}&anchor=${anchor}`); currentTarget.closest('tr').outerHTML = blob; }); + $(document).on('click', '.copy-line-permalink', async (e) => { + const success = await copyToClipboard(e.currentTarget.getAttribute('data-url')); + if (!success) return; + document.querySelector('.code-line-button')?._tippy?.hide(); + }); } diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js index 94fca7a9c2..aac734de26 100644 --- a/web_src/js/features/repo-commit.js +++ b/web_src/js/features/repo-commit.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import {createTippy} from '../modules/tippy.js'; const {csrfToken} = window.config; @@ -58,12 +59,12 @@ export function initRepoCommitLastCommitLoader() { export function initCommitStatuses() { $('.commit-statuses-trigger').each(function () { const positionRight = $('.repository.file.list').length > 0 || $('.repository.diff').length > 0; - const popupPosition = positionRight ? 'right center' : 'left center'; - $(this) - .popup({ - on: 'click', - lastResort: popupPosition, // prevent error message "Popup does not fit within the boundaries of the viewport" - position: popupPosition, - }); + + createTippy(this, { + trigger: 'click', + content: this.nextSibling, + placement: positionRight ? 'right' : 'left', + interactive: true, + }); }); } diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js index 92d8ecfc86..59e0c147d9 100644 --- a/web_src/js/features/repo-diff.js +++ b/web_src/js/features/repo-diff.js @@ -3,7 +3,7 @@ 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 {initPopup} from './common-global.js'; +import {initTooltip} from '../modules/tippy.js'; const {csrfToken} = window.config; @@ -53,7 +53,7 @@ export function initRepoDiffConversationForm() { const newConversationHolder = $(await $.post(form.attr('action'), form.serialize())); const {path, side, idx} = newConversationHolder.data(); - initPopup(newConversationHolder.find('.tooltip')); + initTooltip(newConversationHolder.find('.tooltip')); 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 12900c2455..9dbe78edf5 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -4,6 +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} from '../modules/tippy.js'; const {appSubUrl, csrfToken} = window.config; @@ -278,7 +279,8 @@ export function initRepoPullRequestAllowMaintainerEdit() { const promptTip = $checkbox.attr('data-prompt-tip'); const promptError = $checkbox.attr('data-prompt-error'); - $checkbox.popup({content: promptTip}); + + initTooltip($checkbox[0], {content: promptTip}); $checkbox.checkbox({ 'onChange': () => { const checked = $checkbox.checkbox('is checked'); @@ -288,14 +290,7 @@ export function initRepoPullRequestAllowMaintainerEdit() { $.ajax({url, type: 'POST', data: {_csrf: csrfToken, allow_maintainer_edit: checked}, error: () => { - $checkbox.popup({ - content: promptError, - onHidden: () => { - // the error popup should be shown only once, then we restore the popup to the default message - $checkbox.popup({content: promptTip}); - }, - }); - $checkbox.popup('show'); + showTemporaryTooltip($checkbox[0], promptError); }, complete: () => { $checkbox.checkbox('set enabled'); diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js index c3aa79b767..ffa2ad4189 100644 --- a/web_src/js/features/stopwatch.js +++ b/web_src/js/features/stopwatch.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import prettyMilliseconds from 'pretty-ms'; +import {createTippy} from '../modules/tippy.js'; const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking} = window.config; @@ -8,21 +9,21 @@ export function initStopwatch() { return; } - const stopwatchEl = $('.active-stopwatch-trigger'); + const stopwatchEl = document.querySelector('.active-stopwatch-trigger'); + const stopwatchPopup = document.querySelector('.active-stopwatch-popup'); - if (!stopwatchEl.length) { + if (!stopwatchEl || !stopwatchPopup) { return; } - stopwatchEl.removeAttr('href'); // intended for noscript mode only - stopwatchEl.popup({ - position: 'bottom right', - hoverable: true, - }); + stopwatchEl.removeAttribute('href'); // intended for noscript mode only - // form handlers - $('form > button', stopwatchEl).on('click', function () { - $(this).parent().trigger('submit'); + createTippy(stopwatchEl, { + content: stopwatchPopup, + placement: 'bottom-end', + trigger: 'click', + maxWidth: 'none', + interactive: true, }); // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used. diff --git a/web_src/js/index.js b/web_src/js/index.js index 6f872b5353..b96e79c3c8 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -56,7 +56,7 @@ import { initGlobalFormDirtyLeaveConfirm, initGlobalLinkActions, initHeadNavbarContentToggle, - initGlobalPopups, + initGlobalTooltips, } from './features/common-global.js'; import {initRepoTopicBar} from './features/repo-home.js'; import {initAdminEmails} from './features/admin-emails.js'; @@ -100,7 +100,7 @@ initVueEnv(); $(document).ready(() => { initGlobalCommon(); - initGlobalPopups(); + initGlobalTooltips(); initGlobalButtonClickOnEnter(); initGlobalButtons(); initGlobalCopyToClipboardListener(); diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js index 6fd466cd92..87f9e8a4b0 100644 --- a/web_src/js/modules/tippy.js +++ b/web_src/js/modules/tippy.js @@ -1,12 +1,56 @@ import tippy from 'tippy.js'; -export function createTippy(target, opts) { - return tippy(target, { +export function createTippy(target, opts = {}) { + const instance = tippy(target, { appendTo: document.body, placement: 'top-start', animation: false, allowHTML: true, + maxWidth: 500, // increase over default 350px arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`, + ...(opts?.role && {theme: opts.role}), ...opts, }); + + // for popups where content refers to a DOM element, we use the 'hide' class to initially hide + // the content, now we can remove it as the content has been removed from the DOM by tippy + if (opts.content instanceof Element) { + opts.content.classList.remove('hide'); + } + + return instance; +} + +export function initTooltip(el, props = {}) { + const content = el.getAttribute('data-content') || props.content; + if (!content) return null; + return createTippy(el, { + content, + delay: 100, + role: 'tooltip', + ...props, + }); +} + +export function showTemporaryTooltip(target, content) { + let tippy, oldContent; + if (target._tippy) { + tippy = target._tippy; + oldContent = tippy.props.content; + } else { + tippy = initTooltip(target, {content}); + } + + tippy.setContent(content); + tippy.show(); + tippy.setProps({ + onHidden: (tippy) => { + if (oldContent) { + tippy.setContent(oldContent); + } else { + tippy.destroy(); + } + tippy.setProps({onHidden: undefined}); + }, + }); } |