Convert frontend code to typescript (#31559)
None of the frontend js/ts files was touched besides these two commands
(edit: no longer true, I touched one file in
61105d0618
because of a deprecation that was not showing before the rename).
`tsc` currently reports 778 errors, so I have disabled it in CI as
planned.
Everything appears to work fine.
This commit is contained in:
parent
5115c278ff
commit
5791a73e75
168 changed files with 562 additions and 386 deletions
197
web_src/js/modules/tippy.ts
Normal file
197
web_src/js/modules/tippy.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
import tippy, {followCursor} from 'tippy.js';
|
||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||
import {formatDatetime} from '../utils/time.ts';
|
||||
|
||||
const visibleInstances = new Set();
|
||||
const arrowSvg = `<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>`;
|
||||
|
||||
export function createTippy(target, opts = {}) {
|
||||
// the callback functions should be destructured from opts,
|
||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||
|
||||
const instance = tippy(target, {
|
||||
appendTo: document.body,
|
||||
animation: false,
|
||||
allowHTML: false,
|
||||
hideOnClick: false,
|
||||
interactiveBorder: 20,
|
||||
ignoreAttributes: true,
|
||||
maxWidth: 500, // increase over default 350px
|
||||
onHide: (instance) => {
|
||||
visibleInstances.delete(instance);
|
||||
return onHide?.(instance);
|
||||
},
|
||||
onDestroy: (instance) => {
|
||||
visibleInstances.delete(instance);
|
||||
return onDestroy?.(instance);
|
||||
},
|
||||
onShow: (instance) => {
|
||||
// hide other tooltip instances so only one tooltip shows at a time
|
||||
for (const visibleInstance of visibleInstances) {
|
||||
if (visibleInstance.props.role === 'tooltip') {
|
||||
visibleInstance.hide();
|
||||
}
|
||||
}
|
||||
visibleInstances.add(instance);
|
||||
return onShow?.(instance);
|
||||
},
|
||||
arrow: arrow || (theme === 'bare' ? false : arrowSvg),
|
||||
// HTML role attribute, ideally the default role would be "popover" but it does not exist
|
||||
role: role || 'menu',
|
||||
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
|
||||
theme: theme || role || 'default',
|
||||
plugins: [followCursor],
|
||||
...other,
|
||||
});
|
||||
|
||||
if (role === 'menu') {
|
||||
target.setAttribute('aria-haspopup', 'true');
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
switchTitleToTooltip(target);
|
||||
|
||||
content = content ?? target.getAttribute('data-tooltip-content');
|
||||
if (!content) return null;
|
||||
|
||||
// when element has a clipboard target, we update the tooltip after copy
|
||||
// in which case it is undesirable to automatically hide it on click as
|
||||
// it would momentarily flash the tooltip out and in.
|
||||
const hasClipboardTarget = target.hasAttribute('data-clipboard-target');
|
||||
const hideOnClick = !hasClipboardTarget;
|
||||
|
||||
const props = {
|
||||
content,
|
||||
delay: 100,
|
||||
role: 'tooltip',
|
||||
theme: 'tooltip',
|
||||
hideOnClick,
|
||||
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
|
||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
|
||||
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
|
||||
};
|
||||
|
||||
if (!target._tippy) {
|
||||
createTippy(target, props);
|
||||
} else {
|
||||
target._tippy.setProps(props);
|
||||
}
|
||||
return target._tippy;
|
||||
}
|
||||
|
||||
function switchTitleToTooltip(target) {
|
||||
let title = target.getAttribute('title');
|
||||
if (title) {
|
||||
// apply custom formatting to relative-time's tooltips
|
||||
if (target.tagName.toLowerCase() === 'relative-time') {
|
||||
const datetime = target.getAttribute('datetime');
|
||||
if (datetime) {
|
||||
title = formatDatetime(new Date(datetime));
|
||||
}
|
||||
}
|
||||
target.setAttribute('data-tooltip-content', title);
|
||||
target.setAttribute('aria-label', title);
|
||||
// keep the attribute, in case there are some other "[title]" selectors
|
||||
// and to prevent infinite loop with <relative-time> which will re-add
|
||||
// title if it is absent
|
||||
target.setAttribute('title', '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 browsers like PaleMoon don't support "addEventListener('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);
|
||||
}
|
||||
|
||||
// Activate the tooltip for current element.
|
||||
// If the element has no aria-label, use the tooltip content as aria-label.
|
||||
function attachLazyTooltip(el) {
|
||||
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
|
||||
|
||||
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
|
||||
if (!el.hasAttribute('aria-label')) {
|
||||
const content = el.getAttribute('data-tooltip-content');
|
||||
if (content) {
|
||||
el.setAttribute('aria-label', content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Activate the tooltip for all children elements.
|
||||
function attachChildrenLazyTooltip(target) {
|
||||
for (const el of target.querySelectorAll('[data-tooltip-content]')) {
|
||||
attachLazyTooltip(el);
|
||||
}
|
||||
}
|
||||
|
||||
export function initGlobalTooltips() {
|
||||
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
|
||||
const observerConnect = (observer) => observer.observe(document, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributeFilter: ['data-tooltip-content', 'title'],
|
||||
});
|
||||
const observer = new MutationObserver((mutationList, observer) => {
|
||||
const pending = observer.takeRecords();
|
||||
observer.disconnect();
|
||||
for (const mutation of [...mutationList, ...pending]) {
|
||||
if (mutation.type === 'childList') {
|
||||
// mainly for Vue components and AJAX rendered elements
|
||||
for (const el of mutation.addedNodes) {
|
||||
if (!isDocumentFragmentOrElementNode(el)) continue;
|
||||
attachChildrenLazyTooltip(el);
|
||||
if (el.hasAttribute('data-tooltip-content')) {
|
||||
attachLazyTooltip(el);
|
||||
}
|
||||
}
|
||||
} else if (mutation.type === 'attributes') {
|
||||
attachTooltip(mutation.target);
|
||||
}
|
||||
}
|
||||
observerConnect(observer);
|
||||
});
|
||||
observerConnect(observer);
|
||||
|
||||
attachChildrenLazyTooltip(document.documentElement);
|
||||
}
|
||||
|
||||
export function showTemporaryTooltip(target, content) {
|
||||
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
|
||||
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
|
||||
// on the issue comment menu.
|
||||
if (target.closest('.ui.dropdown > .menu')) return;
|
||||
|
||||
const tippy = target._tippy ?? attachTooltip(target, content);
|
||||
tippy.setContent(content);
|
||||
if (!tippy.state.isShown) tippy.show();
|
||||
tippy.setProps({
|
||||
onHidden: (tippy) => {
|
||||
// reset the default tooltip content, if no default, then this temporary tooltip could be destroyed
|
||||
if (!attachTooltip(target)) {
|
||||
tippy.destroy();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue