Add types to various low-level functions (#31781)
Adds types to various low-level modules. All changes are type-only, no runtime changes. `tsc` now reports 38 less errors. One problem was that `@types/sortablejs` does not accept promise return in its functions which triggered the linter, so I disabled the rules on those line.
This commit is contained in:
parent
9633f336c8
commit
32075d2880
22
package-lock.json
generated
22
package-lock.json
generated
@ -108,6 +108,7 @@
|
|||||||
"stylelint-declaration-strict-value": "1.10.6",
|
"stylelint-declaration-strict-value": "1.10.6",
|
||||||
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
||||||
"svgo": "3.3.2",
|
"svgo": "3.3.2",
|
||||||
|
"type-fest": "4.23.0",
|
||||||
"updates": "16.3.7",
|
"updates": "16.3.7",
|
||||||
"vite-string-plugin": "1.3.4",
|
"vite-string-plugin": "1.3.4",
|
||||||
"vitest": "2.0.5"
|
"vitest": "2.0.5"
|
||||||
@ -7439,6 +7440,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/globals/node_modules/type-fest": {
|
||||||
|
"version": "0.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||||
|
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/globby": {
|
"node_modules/globby": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
|
||||||
@ -12287,13 +12301,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.20.2",
|
"version": "4.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz",
|
||||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
"integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
@ -107,6 +107,7 @@
|
|||||||
"stylelint-declaration-strict-value": "1.10.6",
|
"stylelint-declaration-strict-value": "1.10.6",
|
||||||
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
||||||
"svgo": "3.3.2",
|
"svgo": "3.3.2",
|
||||||
|
"type-fest": "4.23.0",
|
||||||
"updates": "16.3.7",
|
"updates": "16.3.7",
|
||||||
"vite-string-plugin": "1.3.4",
|
"vite-string-plugin": "1.3.4",
|
||||||
"vitest": "2.0.5"
|
"vitest": "2.0.5"
|
||||||
|
9
types.d.ts
vendored
9
types.d.ts
vendored
@ -3,6 +3,11 @@ declare module '*.svg' {
|
|||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.css' {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
|
||||||
declare let __webpack_public_path__: string;
|
declare let __webpack_public_path__: string;
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -20,3 +25,7 @@ declare module 'htmx.org/dist/htmx.esm.js' {
|
|||||||
const value = await import('htmx.org');
|
const value = await import('htmx.org');
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Element {
|
||||||
|
_tippy: import('tippy.js').Instance;
|
||||||
|
}
|
||||||
|
@ -52,7 +52,7 @@ function addCopyLink(file) {
|
|||||||
copyLinkEl.addEventListener('click', async (e) => {
|
copyLinkEl.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const success = await clippie(generateMarkdownLinkForAttachment(file));
|
const success = await clippie(generateMarkdownLinkForAttachment(file));
|
||||||
showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
|
showTemporaryTooltip(e.target as Element, success ? i18n.copy_success : i18n.copy_error);
|
||||||
});
|
});
|
||||||
file.previewTemplate.append(copyLinkEl);
|
file.previewTemplate.append(copyLinkEl);
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ async function initIssuePinSort() {
|
|||||||
|
|
||||||
createSortable(pinDiv, {
|
createSortable(pinDiv, {
|
||||||
group: 'shared',
|
group: 'shared',
|
||||||
onEnd: pinMoveEnd,
|
onEnd: pinMoveEnd, // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ async function initRepoProjectSortable() {
|
|||||||
handle: '.project-column-header',
|
handle: '.project-column-header',
|
||||||
delayOnTouchOnly: true,
|
delayOnTouchOnly: true,
|
||||||
delay: 500,
|
delay: 500,
|
||||||
onSort: async () => {
|
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||||
boardColumns = mainBoard.querySelectorAll('.project-column');
|
boardColumns = mainBoard.querySelectorAll('.project-column');
|
||||||
|
|
||||||
const columnSorting = {
|
const columnSorting = {
|
||||||
@ -84,8 +84,8 @@ async function initRepoProjectSortable() {
|
|||||||
const boardCardList = boardColumn.querySelectorAll('.cards')[0];
|
const boardCardList = boardColumn.querySelectorAll('.cards')[0];
|
||||||
createSortable(boardCardList, {
|
createSortable(boardCardList, {
|
||||||
group: 'shared',
|
group: 'shared',
|
||||||
onAdd: moveIssue,
|
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||||
onUpdate: moveIssue,
|
onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
|
||||||
delayOnTouchOnly: true,
|
delayOnTouchOnly: true,
|
||||||
delay: 500,
|
delay: 500,
|
||||||
});
|
});
|
||||||
|
@ -27,7 +27,7 @@ export function initStopwatch() {
|
|||||||
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
|
stopwatchEl.removeAttribute('href'); // intended for noscript mode only
|
||||||
|
|
||||||
createTippy(stopwatchEl, {
|
createTippy(stopwatchEl, {
|
||||||
content: stopwatchPopup.cloneNode(true),
|
content: stopwatchPopup.cloneNode(true) as Element,
|
||||||
placement: 'bottom-end',
|
placement: 'bottom-end',
|
||||||
trigger: 'click',
|
trigger: 'click',
|
||||||
maxWidth: 'none',
|
maxWidth: 'none',
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||||
|
|
||||||
|
type DirElement = HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
|
||||||
// for performance considerations, it only uses performant syntax
|
// for performance considerations, it only uses performant syntax
|
||||||
function attachDirAuto(el) {
|
function attachDirAuto(el: DirElement) {
|
||||||
if (el.type !== 'hidden' &&
|
if (el.type !== 'hidden' &&
|
||||||
el.type !== 'checkbox' &&
|
el.type !== 'checkbox' &&
|
||||||
el.type !== 'radio' &&
|
el.type !== 'radio' &&
|
||||||
@ -18,10 +20,12 @@ export function initDirAuto() {
|
|||||||
const mutation = mutationList[i];
|
const mutation = mutationList[i];
|
||||||
const len = mutation.addedNodes.length;
|
const len = mutation.addedNodes.length;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const addedNode = mutation.addedNodes[i];
|
const addedNode = mutation.addedNodes[i] as HTMLElement;
|
||||||
if (!isDocumentFragmentOrElementNode(addedNode)) continue;
|
if (!isDocumentFragmentOrElementNode(addedNode)) continue;
|
||||||
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode);
|
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') {
|
||||||
const children = addedNode.querySelectorAll('input, textarea');
|
attachDirAuto(addedNode as DirElement);
|
||||||
|
}
|
||||||
|
const children = addedNode.querySelectorAll<DirElement>('input, textarea');
|
||||||
const len = children.length;
|
const len = children.length;
|
||||||
for (let childIdx = 0; childIdx < len; childIdx++) {
|
for (let childIdx = 0; childIdx < len; childIdx++) {
|
||||||
attachDirAuto(children[childIdx]);
|
attachDirAuto(children[childIdx]);
|
||||||
@ -30,7 +34,7 @@ export function initDirAuto() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const docNodes = document.querySelectorAll('input, textarea');
|
const docNodes = document.querySelectorAll<DirElement>('input, textarea');
|
||||||
const len = docNodes.length;
|
const len = docNodes.length;
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
attachDirAuto(docNodes[i]);
|
attachDirAuto(docNodes[i]);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
export async function createSortable(el, opts = {}) {
|
import type {SortableOptions} from 'sortablejs';
|
||||||
|
|
||||||
|
export async function createSortable(el, opts: {handle?: string} & SortableOptions = {}) {
|
||||||
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
|
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
|
||||||
|
|
||||||
return new Sortable(el, {
|
return new Sortable(el, {
|
||||||
@ -15,5 +17,5 @@ export async function createSortable(el, opts = {}) {
|
|||||||
opts.onUnchoose?.(e);
|
opts.onUnchoose?.(e);
|
||||||
},
|
},
|
||||||
...opts,
|
...opts,
|
||||||
});
|
} satisfies SortableOptions);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {reactive} from 'vue';
|
import {reactive} from 'vue';
|
||||||
|
import type {Reactive} from 'vue';
|
||||||
|
|
||||||
let diffTreeStoreReactive;
|
let diffTreeStoreReactive: Reactive<Record<string, any>>;
|
||||||
export function diffTreeStore() {
|
export function diffTreeStore() {
|
||||||
if (!diffTreeStoreReactive) {
|
if (!diffTreeStoreReactive) {
|
||||||
diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo);
|
diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo);
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
import tippy, {followCursor} from 'tippy.js';
|
import tippy, {followCursor} from 'tippy.js';
|
||||||
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
|
||||||
import {formatDatetime} from '../utils/time.ts';
|
import {formatDatetime} from '../utils/time.ts';
|
||||||
|
import type {Content, Instance, Props} from 'tippy.js';
|
||||||
|
|
||||||
const visibleInstances = new Set();
|
type TippyOpts = {
|
||||||
|
role?: string,
|
||||||
|
theme?: 'default' | 'tooltip' | 'menu' | 'box-with-header' | 'bare',
|
||||||
|
} & Partial<Props>;
|
||||||
|
|
||||||
|
const visibleInstances = new Set<Instance>();
|
||||||
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>`;
|
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 = {}) {
|
export function createTippy(target: Element, opts: TippyOpts = {}) {
|
||||||
// the callback functions should be destructured from 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
|
// 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 {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||||
|
|
||||||
const instance = tippy(target, {
|
const instance: Instance = tippy(target, {
|
||||||
appendTo: document.body,
|
appendTo: document.body,
|
||||||
animation: false,
|
animation: false,
|
||||||
allowHTML: false,
|
allowHTML: false,
|
||||||
@ -18,15 +24,15 @@ export function createTippy(target, opts = {}) {
|
|||||||
interactiveBorder: 20,
|
interactiveBorder: 20,
|
||||||
ignoreAttributes: true,
|
ignoreAttributes: true,
|
||||||
maxWidth: 500, // increase over default 350px
|
maxWidth: 500, // increase over default 350px
|
||||||
onHide: (instance) => {
|
onHide: (instance: Instance) => {
|
||||||
visibleInstances.delete(instance);
|
visibleInstances.delete(instance);
|
||||||
return onHide?.(instance);
|
return onHide?.(instance);
|
||||||
},
|
},
|
||||||
onDestroy: (instance) => {
|
onDestroy: (instance: Instance) => {
|
||||||
visibleInstances.delete(instance);
|
visibleInstances.delete(instance);
|
||||||
return onDestroy?.(instance);
|
return onDestroy?.(instance);
|
||||||
},
|
},
|
||||||
onShow: (instance) => {
|
onShow: (instance: Instance) => {
|
||||||
// hide other tooltip instances so only one tooltip shows at a time
|
// hide other tooltip instances so only one tooltip shows at a time
|
||||||
for (const visibleInstance of visibleInstances) {
|
for (const visibleInstance of visibleInstances) {
|
||||||
if (visibleInstance.props.role === 'tooltip') {
|
if (visibleInstance.props.role === 'tooltip') {
|
||||||
@ -43,7 +49,7 @@ export function createTippy(target, opts = {}) {
|
|||||||
theme: theme || role || 'default',
|
theme: theme || role || 'default',
|
||||||
plugins: [followCursor],
|
plugins: [followCursor],
|
||||||
...other,
|
...other,
|
||||||
});
|
} satisfies Partial<Props>);
|
||||||
|
|
||||||
if (role === 'menu') {
|
if (role === 'menu') {
|
||||||
target.setAttribute('aria-haspopup', 'true');
|
target.setAttribute('aria-haspopup', 'true');
|
||||||
@ -58,12 +64,8 @@ export function createTippy(target, opts = {}) {
|
|||||||
* If the target element has no content, then no tooltip will be attached, and it returns null.
|
* 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.
|
* 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) {
|
function attachTooltip(target: Element, content: Content = null) {
|
||||||
switchTitleToTooltip(target);
|
switchTitleToTooltip(target);
|
||||||
|
|
||||||
content = content ?? target.getAttribute('data-tooltip-content');
|
content = content ?? target.getAttribute('data-tooltip-content');
|
||||||
@ -84,7 +86,7 @@ function attachTooltip(target, content = null) {
|
|||||||
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
|
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
|
||||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
|
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
|
||||||
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
|
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
|
||||||
};
|
} as TippyOpts;
|
||||||
|
|
||||||
if (!target._tippy) {
|
if (!target._tippy) {
|
||||||
createTippy(target, props);
|
createTippy(target, props);
|
||||||
@ -94,7 +96,7 @@ function attachTooltip(target, content = null) {
|
|||||||
return target._tippy;
|
return target._tippy;
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTitleToTooltip(target) {
|
function switchTitleToTooltip(target: Element) {
|
||||||
let title = target.getAttribute('title');
|
let title = target.getAttribute('title');
|
||||||
if (title) {
|
if (title) {
|
||||||
// apply custom formatting to relative-time's tooltips
|
// apply custom formatting to relative-time's tooltips
|
||||||
@ -118,16 +120,15 @@ function switchTitleToTooltip(target) {
|
|||||||
* According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
|
* 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)"
|
* 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
|
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
||||||
* @param e {Event}
|
|
||||||
*/
|
*/
|
||||||
function lazyTooltipOnMouseHover(e) {
|
function lazyTooltipOnMouseHover(e: MouseEvent) {
|
||||||
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
||||||
attachTooltip(this);
|
attachTooltip(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate the tooltip for current element.
|
// Activate the tooltip for current element.
|
||||||
// If the element has no aria-label, use the tooltip content as aria-label.
|
// If the element has no aria-label, use the tooltip content as aria-label.
|
||||||
function attachLazyTooltip(el) {
|
function attachLazyTooltip(el: Element) {
|
||||||
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
|
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
|
||||||
|
|
||||||
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
|
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
|
||||||
@ -140,15 +141,15 @@ function attachLazyTooltip(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Activate the tooltip for all children elements.
|
// Activate the tooltip for all children elements.
|
||||||
function attachChildrenLazyTooltip(target) {
|
function attachChildrenLazyTooltip(target: Element) {
|
||||||
for (const el of target.querySelectorAll('[data-tooltip-content]')) {
|
for (const el of target.querySelectorAll<Element>('[data-tooltip-content]')) {
|
||||||
attachLazyTooltip(el);
|
attachLazyTooltip(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initGlobalTooltips() {
|
export function initGlobalTooltips() {
|
||||||
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
|
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
|
||||||
const observerConnect = (observer) => observer.observe(document, {
|
const observerConnect = (observer: MutationObserver) => observer.observe(document, {
|
||||||
subtree: true,
|
subtree: true,
|
||||||
childList: true,
|
childList: true,
|
||||||
attributeFilter: ['data-tooltip-content', 'title'],
|
attributeFilter: ['data-tooltip-content', 'title'],
|
||||||
@ -159,7 +160,7 @@ export function initGlobalTooltips() {
|
|||||||
for (const mutation of [...mutationList, ...pending]) {
|
for (const mutation of [...mutationList, ...pending]) {
|
||||||
if (mutation.type === 'childList') {
|
if (mutation.type === 'childList') {
|
||||||
// mainly for Vue components and AJAX rendered elements
|
// mainly for Vue components and AJAX rendered elements
|
||||||
for (const el of mutation.addedNodes) {
|
for (const el of mutation.addedNodes as NodeListOf<Element>) {
|
||||||
if (!isDocumentFragmentOrElementNode(el)) continue;
|
if (!isDocumentFragmentOrElementNode(el)) continue;
|
||||||
attachChildrenLazyTooltip(el);
|
attachChildrenLazyTooltip(el);
|
||||||
if (el.hasAttribute('data-tooltip-content')) {
|
if (el.hasAttribute('data-tooltip-content')) {
|
||||||
@ -167,7 +168,7 @@ export function initGlobalTooltips() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (mutation.type === 'attributes') {
|
} else if (mutation.type === 'attributes') {
|
||||||
attachTooltip(mutation.target);
|
attachTooltip(mutation.target as Element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
observerConnect(observer);
|
observerConnect(observer);
|
||||||
@ -177,7 +178,7 @@ export function initGlobalTooltips() {
|
|||||||
attachChildrenLazyTooltip(document.documentElement);
|
attachChildrenLazyTooltip(document.documentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTemporaryTooltip(target, content) {
|
export function showTemporaryTooltip(target: Element, content: Content) {
|
||||||
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
|
// 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
|
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
|
||||||
// on the issue comment menu.
|
// on the issue comment menu.
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
import type {ColorInput} from 'tinycolor2';
|
||||||
|
|
||||||
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
|
||||||
// Keep this in sync with modules/util/color.go
|
// Keep this in sync with modules/util/color.go
|
||||||
function getRelativeLuminance(color) {
|
function getRelativeLuminance(color: ColorInput) {
|
||||||
const {r, g, b} = tinycolor(color).toRgb();
|
const {r, g, b} = tinycolor(color).toRgb();
|
||||||
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLightText(backgroundColor) {
|
function useLightText(backgroundColor: ColorInput) {
|
||||||
return getRelativeLuminance(backgroundColor) < 0.453;
|
return getRelativeLuminance(backgroundColor) < 0.453;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a background color, returns a black or white foreground color that the highest
|
// Given a background color, returns a black or white foreground color that the highest
|
||||||
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||||
export function contrastColor(backgroundColor) {
|
export function contrastColor(backgroundColor: ColorInput) {
|
||||||
return useLightText(backgroundColor) ? '#fff' : '#000';
|
return useLightText(backgroundColor) ? '#fff' : '#000';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveColors(obj) {
|
function resolveColors(obj: Record<string, string>) {
|
||||||
const styles = window.getComputedStyle(document.documentElement);
|
const styles = window.getComputedStyle(document.documentElement);
|
||||||
const getColor = (name) => styles.getPropertyValue(name).trim();
|
const getColor = (name: string) => styles.getPropertyValue(name).trim();
|
||||||
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
|
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import {debounce} from 'throttle-debounce';
|
import {debounce} from 'throttle-debounce';
|
||||||
|
import type {Promisable} from 'type-fest';
|
||||||
|
import type $ from 'jquery';
|
||||||
|
|
||||||
function elementsCall(el, func, ...args) {
|
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
||||||
|
type ElementsCallback = (el: Element) => Promisable<any>;
|
||||||
|
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
||||||
|
type IterableElements = NodeListOf<Element> | Array<Element>;
|
||||||
|
|
||||||
|
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
|
||||||
if (typeof el === 'string' || el instanceof String) {
|
if (typeof el === 'string' || el instanceof String) {
|
||||||
el = document.querySelectorAll(el);
|
el = document.querySelectorAll(el as string);
|
||||||
}
|
}
|
||||||
if (el instanceof Node) {
|
if (el instanceof Node) {
|
||||||
func(el, ...args);
|
func(el, ...args);
|
||||||
} else if (el.length !== undefined) {
|
} else if (el.length !== undefined) {
|
||||||
// this works for: NodeList, HTMLCollection, Array, jQuery
|
// this works for: NodeList, HTMLCollection, Array, jQuery
|
||||||
for (const e of el) {
|
for (const e of (el as IterableElements)) {
|
||||||
func(e, ...args);
|
func(e, ...args);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -17,10 +24,10 @@ function elementsCall(el, func, ...args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param el string (selector), Node, NodeList, HTMLCollection, Array or jQuery
|
* @param el Element
|
||||||
* @param force force=true to show or force=false to hide, undefined to toggle
|
* @param force force=true to show or force=false to hide, undefined to toggle
|
||||||
*/
|
*/
|
||||||
function toggleShown(el, force) {
|
function toggleShown(el: Element, force: boolean) {
|
||||||
if (force === true) {
|
if (force === true) {
|
||||||
el.classList.remove('tw-hidden');
|
el.classList.remove('tw-hidden');
|
||||||
} else if (force === false) {
|
} else if (force === false) {
|
||||||
@ -32,26 +39,26 @@ function toggleShown(el, force) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showElem(el) {
|
export function showElem(el: ElementArg) {
|
||||||
elementsCall(el, toggleShown, true);
|
elementsCall(el, toggleShown, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hideElem(el) {
|
export function hideElem(el: ElementArg) {
|
||||||
elementsCall(el, toggleShown, false);
|
elementsCall(el, toggleShown, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleElem(el, force) {
|
export function toggleElem(el: ElementArg, force?: boolean) {
|
||||||
elementsCall(el, toggleShown, force);
|
elementsCall(el, toggleShown, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isElemHidden(el) {
|
export function isElemHidden(el: ElementArg) {
|
||||||
const res = [];
|
const res: boolean[] = [];
|
||||||
elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
|
elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden')));
|
||||||
if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
|
if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`);
|
||||||
return res[0];
|
return res[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyElemsCallback(elems, fn) {
|
function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
for (const el of elems) {
|
for (const el of elems) {
|
||||||
fn(el);
|
fn(el);
|
||||||
@ -60,20 +67,22 @@ function applyElemsCallback(elems, fn) {
|
|||||||
return elems;
|
return elems;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryElemSiblings(el, selector = '*', fn) {
|
export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) {
|
||||||
return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn);
|
return applyElemsCallback(Array.from(el.parentNode.children).filter((child: Element) => {
|
||||||
|
return child !== el && child.matches(selector);
|
||||||
|
}), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// it works like jQuery.children: only the direct children are selected
|
// it works like jQuery.children: only the direct children are selected
|
||||||
export function queryElemChildren(parent, selector = '*', fn) {
|
export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) {
|
||||||
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryElems(selector, fn) {
|
export function queryElems(selector: string, fn?: ElementsCallback) {
|
||||||
return applyElemsCallback(document.querySelectorAll(selector), fn);
|
return applyElemsCallback(document.querySelectorAll(selector), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onDomReady(cb) {
|
export function onDomReady(cb: () => Promisable<void>) {
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', cb);
|
document.addEventListener('DOMContentLoaded', cb);
|
||||||
} else {
|
} else {
|
||||||
@ -83,7 +92,7 @@ export function onDomReady(cb) {
|
|||||||
|
|
||||||
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
|
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
|
||||||
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
|
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
|
||||||
export function isDocumentFragmentOrElementNode(el) {
|
export function isDocumentFragmentOrElementNode(el: Element) {
|
||||||
try {
|
try {
|
||||||
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
||||||
} catch {
|
} catch {
|
||||||
@ -108,12 +117,15 @@ export function isDocumentFragmentOrElementNode(el) {
|
|||||||
// The above copyright notice and this permission notice shall be
|
// The above copyright notice and this permission notice shall be
|
||||||
// included in all copies or substantial portions of the Software.
|
// included in all copies or substantial portions of the Software.
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
|
export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom = 0}: {viewportMarginBottom?: number} = {}) {
|
||||||
let isUserResized = false;
|
let isUserResized = false;
|
||||||
// lastStyleHeight and initialStyleHeight are CSS values like '100px'
|
// lastStyleHeight and initialStyleHeight are CSS values like '100px'
|
||||||
let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight;
|
let lastMouseX: number;
|
||||||
|
let lastMouseY: number;
|
||||||
|
let lastStyleHeight: string;
|
||||||
|
let initialStyleHeight: string;
|
||||||
|
|
||||||
function onUserResize(event) {
|
function onUserResize(event: MouseEvent) {
|
||||||
if (isUserResized) return;
|
if (isUserResized) return;
|
||||||
if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) {
|
if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) {
|
||||||
const newStyleHeight = textarea.style.height;
|
const newStyleHeight = textarea.style.height;
|
||||||
@ -133,7 +145,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
|
|||||||
|
|
||||||
while (el !== document.body && el !== null) {
|
while (el !== document.body && el !== null) {
|
||||||
offsetTop += el.offsetTop || 0;
|
offsetTop += el.offsetTop || 0;
|
||||||
el = el.offsetParent;
|
el = el.offsetParent as HTMLTextAreaElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const top = offsetTop - document.defaultView.scrollY;
|
const top = offsetTop - document.defaultView.scrollY;
|
||||||
@ -213,14 +225,15 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onInputDebounce(fn) {
|
export function onInputDebounce(fn: () => Promisable<any>) {
|
||||||
return debounce(300, fn);
|
return debounce(300, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement;
|
||||||
|
|
||||||
// Set the `src` attribute on an element and returns a promise that resolves once the element
|
// Set the `src` attribute on an element and returns a promise that resolves once the element
|
||||||
// has loaded or errored. Suitable for all elements mention in:
|
// has loaded or errored.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event
|
export function loadElem(el: LoadableElement, src: string) {
|
||||||
export function loadElem(el, src) {
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
el.addEventListener('load', () => resolve(true), {once: true});
|
el.addEventListener('load', () => resolve(true), {once: true});
|
||||||
el.addEventListener('error', () => resolve(false), {once: true});
|
el.addEventListener('error', () => resolve(false), {once: true});
|
||||||
@ -256,14 +269,14 @@ export function initSubmitEventPolyfill() {
|
|||||||
* @param {HTMLElement} element The element to check.
|
* @param {HTMLElement} element The element to check.
|
||||||
* @returns {boolean} True if the element is visible.
|
* @returns {boolean} True if the element is visible.
|
||||||
*/
|
*/
|
||||||
export function isElemVisible(element) {
|
export function isElemVisible(element: HTMLElement) {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
|
|
||||||
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
|
// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
|
||||||
export function replaceTextareaSelection(textarea, text) {
|
export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) {
|
||||||
const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
|
const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
|
||||||
const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
|
const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
|
||||||
let success = true;
|
let success = true;
|
||||||
@ -287,13 +300,13 @@ export function replaceTextareaSelection(textarea, text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Warning: Do not enter any unsanitized variables here
|
// Warning: Do not enter any unsanitized variables here
|
||||||
export function createElementFromHTML(htmlString) {
|
export function createElementFromHTML(htmlString: string) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerHTML = htmlString.trim();
|
div.innerHTML = htmlString.trim();
|
||||||
return div.firstChild;
|
return div.firstChild as Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createElementFromAttrs(tagName, attrs) {
|
export function createElementFromAttrs(tagName: string, attrs: Record<string, any>) {
|
||||||
const el = document.createElement(tagName);
|
const el = document.createElement(tagName);
|
||||||
for (const [key, value] of Object.entries(attrs)) {
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
if (value === undefined || value === null) continue;
|
if (value === undefined || value === null) continue;
|
||||||
@ -307,7 +320,7 @@ export function createElementFromAttrs(tagName, attrs) {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function animateOnce(el, animationClassName) {
|
export function animateOnce(el: Element, animationClassName: string): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
el.addEventListener('animationend', function onAnimationEnd() {
|
el.addEventListener('animationend', function onAnimationEnd() {
|
||||||
el.classList.remove(animationClassName);
|
el.classList.remove(animationClassName);
|
||||||
|
Loading…
Reference in New Issue
Block a user