aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorsilverwind <me@silverwind.io>2022-08-09 14:37:34 +0200
committerGitHub <noreply@github.com>2022-08-09 14:37:34 +0200
commit1b2cd4c4e19c78390be329b4a3ad50ff8857ca8d (patch)
treec3c9af67b599f92af60c9cd5bb7feee056d97734 /web_src/js
parent36f9ee5813beba0fc4b394a5db636f76afc5cc38 (diff)
downloadgitea-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.js5
-rw-r--r--web_src/js/features/clipboard.js37
-rw-r--r--web_src/js/features/common-global.js31
-rw-r--r--web_src/js/features/comp/ReactionSelector.js12
-rw-r--r--web_src/js/features/repo-code.js50
-rw-r--r--web_src/js/features/repo-commit.js15
-rw-r--r--web_src/js/features/repo-diff.js4
-rw-r--r--web_src/js/features/repo-issue.js13
-rw-r--r--web_src/js/features/stopwatch.js21
-rw-r--r--web_src/js/index.js4
-rw-r--r--web_src/js/modules/tippy.js48
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});
+ },
+ });
}