mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	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:
		
							
								
								
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -108,6 +108,7 @@ | ||||
|         "stylelint-declaration-strict-value": "1.10.6", | ||||
|         "stylelint-value-no-unknown-custom-properties": "6.0.1", | ||||
|         "svgo": "3.3.2", | ||||
|         "type-fest": "4.23.0", | ||||
|         "updates": "16.3.7", | ||||
|         "vite-string-plugin": "1.3.4", | ||||
|         "vitest": "2.0.5" | ||||
| @@ -7439,6 +7440,19 @@ | ||||
|         "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": { | ||||
|       "version": "11.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", | ||||
| @@ -12287,13 +12301,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "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==", | ||||
|       "version": "4.23.0", | ||||
|       "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", | ||||
|       "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", | ||||
|       "dev": true, | ||||
|       "license": "(MIT OR CC0-1.0)", | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|         "node": ">=16" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/sindresorhus" | ||||
|   | ||||
| @@ -107,6 +107,7 @@ | ||||
|     "stylelint-declaration-strict-value": "1.10.6", | ||||
|     "stylelint-value-no-unknown-custom-properties": "6.0.1", | ||||
|     "svgo": "3.3.2", | ||||
|     "type-fest": "4.23.0", | ||||
|     "updates": "16.3.7", | ||||
|     "vite-string-plugin": "1.3.4", | ||||
|     "vitest": "2.0.5" | ||||
|   | ||||
							
								
								
									
										9
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,11 @@ declare module '*.svg' { | ||||
|   export default value; | ||||
| } | ||||
|  | ||||
| declare module '*.css' { | ||||
|   const value: string; | ||||
|   export default value; | ||||
| } | ||||
|  | ||||
| declare let __webpack_public_path__: string; | ||||
|  | ||||
| interface Window { | ||||
| @@ -20,3 +25,7 @@ declare module 'htmx.org/dist/htmx.esm.js' { | ||||
|   const value = await import('htmx.org'); | ||||
|   export default value; | ||||
| } | ||||
|  | ||||
| interface Element { | ||||
|   _tippy: import('tippy.js').Instance; | ||||
| } | ||||
|   | ||||
| @@ -52,7 +52,7 @@ function addCopyLink(file) { | ||||
|   copyLinkEl.addEventListener('click', async (e) => { | ||||
|     e.preventDefault(); | ||||
|     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); | ||||
| } | ||||
|   | ||||
| @@ -196,7 +196,7 @@ async function initIssuePinSort() { | ||||
|  | ||||
|   createSortable(pinDiv, { | ||||
|     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', | ||||
|     delayOnTouchOnly: true, | ||||
|     delay: 500, | ||||
|     onSort: async () => { | ||||
|     onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises | ||||
|       boardColumns = mainBoard.querySelectorAll('.project-column'); | ||||
|  | ||||
|       const columnSorting = { | ||||
| @@ -84,8 +84,8 @@ async function initRepoProjectSortable() { | ||||
|     const boardCardList = boardColumn.querySelectorAll('.cards')[0]; | ||||
|     createSortable(boardCardList, { | ||||
|       group: 'shared', | ||||
|       onAdd: moveIssue, | ||||
|       onUpdate: moveIssue, | ||||
|       onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises | ||||
|       onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises | ||||
|       delayOnTouchOnly: true, | ||||
|       delay: 500, | ||||
|     }); | ||||
|   | ||||
| @@ -27,7 +27,7 @@ export function initStopwatch() { | ||||
|     stopwatchEl.removeAttribute('href'); // intended for noscript mode only | ||||
|  | ||||
|     createTippy(stopwatchEl, { | ||||
|       content: stopwatchPopup.cloneNode(true), | ||||
|       content: stopwatchPopup.cloneNode(true) as Element, | ||||
|       placement: 'bottom-end', | ||||
|       trigger: 'click', | ||||
|       maxWidth: 'none', | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; | ||||
|  | ||||
| type DirElement = HTMLInputElement | HTMLTextAreaElement; | ||||
|  | ||||
| // for performance considerations, it only uses performant syntax | ||||
| function attachDirAuto(el) { | ||||
| function attachDirAuto(el: DirElement) { | ||||
|   if (el.type !== 'hidden' && | ||||
|       el.type !== 'checkbox' && | ||||
|       el.type !== 'radio' && | ||||
| @@ -18,10 +20,12 @@ export function initDirAuto() { | ||||
|       const mutation = mutationList[i]; | ||||
|       const len = mutation.addedNodes.length; | ||||
|       for (let i = 0; i < len; i++) { | ||||
|         const addedNode = mutation.addedNodes[i]; | ||||
|         const addedNode = mutation.addedNodes[i] as HTMLElement; | ||||
|         if (!isDocumentFragmentOrElementNode(addedNode)) continue; | ||||
|         if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode); | ||||
|         const children = addedNode.querySelectorAll('input, textarea'); | ||||
|         if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') { | ||||
|           attachDirAuto(addedNode as DirElement); | ||||
|         } | ||||
|         const children = addedNode.querySelectorAll<DirElement>('input, textarea'); | ||||
|         const len = children.length; | ||||
|         for (let childIdx = 0; childIdx < len; 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; | ||||
|   for (let i = 0; i < len; 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'); | ||||
|  | ||||
|   return new Sortable(el, { | ||||
| @@ -15,5 +17,5 @@ export async function createSortable(el, opts = {}) { | ||||
|       opts.onUnchoose?.(e); | ||||
|     }, | ||||
|     ...opts, | ||||
|   }); | ||||
|   } satisfies SortableOptions); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import {reactive} from 'vue'; | ||||
| import type {Reactive} from 'vue'; | ||||
|  | ||||
| let diffTreeStoreReactive; | ||||
| let diffTreeStoreReactive: Reactive<Record<string, any>>; | ||||
| export function diffTreeStore() { | ||||
|   if (!diffTreeStoreReactive) { | ||||
|     diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo); | ||||
|   | ||||
| @@ -1,16 +1,22 @@ | ||||
| import tippy, {followCursor} from 'tippy.js'; | ||||
| import {isDocumentFragmentOrElementNode} from '../utils/dom.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>`; | ||||
|  | ||||
| export function createTippy(target, opts = {}) { | ||||
| export function createTippy(target: Element, opts: TippyOpts = {}) { | ||||
|   // 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, { | ||||
|   const instance: Instance = tippy(target, { | ||||
|     appendTo: document.body, | ||||
|     animation: false, | ||||
|     allowHTML: false, | ||||
| @@ -18,15 +24,15 @@ export function createTippy(target, opts = {}) { | ||||
|     interactiveBorder: 20, | ||||
|     ignoreAttributes: true, | ||||
|     maxWidth: 500, // increase over default 350px | ||||
|     onHide: (instance) => { | ||||
|     onHide: (instance: Instance) => { | ||||
|       visibleInstances.delete(instance); | ||||
|       return onHide?.(instance); | ||||
|     }, | ||||
|     onDestroy: (instance) => { | ||||
|     onDestroy: (instance: Instance) => { | ||||
|       visibleInstances.delete(instance); | ||||
|       return onDestroy?.(instance); | ||||
|     }, | ||||
|     onShow: (instance) => { | ||||
|     onShow: (instance: Instance) => { | ||||
|       // hide other tooltip instances so only one tooltip shows at a time | ||||
|       for (const visibleInstance of visibleInstances) { | ||||
|         if (visibleInstance.props.role === 'tooltip') { | ||||
| @@ -43,7 +49,7 @@ export function createTippy(target, opts = {}) { | ||||
|     theme: theme || role || 'default', | ||||
|     plugins: [followCursor], | ||||
|     ...other, | ||||
|   }); | ||||
|   } satisfies Partial<Props>); | ||||
|  | ||||
|   if (role === 'menu') { | ||||
|     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. | ||||
|  * | ||||
|  * 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); | ||||
|  | ||||
|   content = content ?? target.getAttribute('data-tooltip-content'); | ||||
| @@ -84,7 +86,7 @@ function attachTooltip(target, content = null) { | ||||
|     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}} : {}), | ||||
|   }; | ||||
|   } as TippyOpts; | ||||
|  | ||||
|   if (!target._tippy) { | ||||
|     createTippy(target, props); | ||||
| @@ -94,7 +96,7 @@ function attachTooltip(target, content = null) { | ||||
|   return target._tippy; | ||||
| } | ||||
|  | ||||
| function switchTitleToTooltip(target) { | ||||
| function switchTitleToTooltip(target: Element) { | ||||
|   let title = target.getAttribute('title'); | ||||
|   if (title) { | ||||
|     // 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 | ||||
|  * 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) { | ||||
| function lazyTooltipOnMouseHover(e: MouseEvent) { | ||||
|   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) { | ||||
| function attachLazyTooltip(el: Element) { | ||||
|   el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true}); | ||||
|  | ||||
|   // 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. | ||||
| function attachChildrenLazyTooltip(target) { | ||||
|   for (const el of target.querySelectorAll('[data-tooltip-content]')) { | ||||
| function attachChildrenLazyTooltip(target: Element) { | ||||
|   for (const el of target.querySelectorAll<Element>('[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, { | ||||
|   const observerConnect = (observer: MutationObserver) => observer.observe(document, { | ||||
|     subtree: true, | ||||
|     childList: true, | ||||
|     attributeFilter: ['data-tooltip-content', 'title'], | ||||
| @@ -159,7 +160,7 @@ export function initGlobalTooltips() { | ||||
|     for (const mutation of [...mutationList, ...pending]) { | ||||
|       if (mutation.type === 'childList') { | ||||
|         // 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; | ||||
|           attachChildrenLazyTooltip(el); | ||||
|           if (el.hasAttribute('data-tooltip-content')) { | ||||
| @@ -167,7 +168,7 @@ export function initGlobalTooltips() { | ||||
|           } | ||||
|         } | ||||
|       } else if (mutation.type === 'attributes') { | ||||
|         attachTooltip(mutation.target); | ||||
|         attachTooltip(mutation.target as Element); | ||||
|       } | ||||
|     } | ||||
|     observerConnect(observer); | ||||
| @@ -177,7 +178,7 @@ export function initGlobalTooltips() { | ||||
|   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 | ||||
|   // closes, the tippy would be pushed unsightly to the top-left of the screen like seen | ||||
|   // on the issue comment menu. | ||||
|   | ||||
| @@ -1,26 +1,27 @@ | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import type {ColorInput} from 'tinycolor2'; | ||||
|  | ||||
| // Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance | ||||
| // Keep this in sync with modules/util/color.go | ||||
| function getRelativeLuminance(color) { | ||||
| function getRelativeLuminance(color: ColorInput) { | ||||
|   const {r, g, b} = tinycolor(color).toRgb(); | ||||
|   return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; | ||||
| } | ||||
|  | ||||
| function useLightText(backgroundColor) { | ||||
| function useLightText(backgroundColor: ColorInput) { | ||||
|   return getRelativeLuminance(backgroundColor) < 0.453; | ||||
| } | ||||
|  | ||||
| // 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. | ||||
| // 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'; | ||||
| } | ||||
|  | ||||
| function resolveColors(obj) { | ||||
| function resolveColors(obj: Record<string, string>) { | ||||
|   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)])); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,21 @@ | ||||
| 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) { | ||||
|     el = document.querySelectorAll(el); | ||||
|     el = document.querySelectorAll(el as string); | ||||
|   } | ||||
|   if (el instanceof Node) { | ||||
|     func(el, ...args); | ||||
|   } else if (el.length !== undefined) { | ||||
|     // this works for: NodeList, HTMLCollection, Array, jQuery | ||||
|     for (const e of el) { | ||||
|     for (const e of (el as IterableElements)) { | ||||
|       func(e, ...args); | ||||
|     } | ||||
|   } 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 | ||||
|  */ | ||||
| function toggleShown(el, force) { | ||||
| function toggleShown(el: Element, force: boolean) { | ||||
|   if (force === true) { | ||||
|     el.classList.remove('tw-hidden'); | ||||
|   } 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); | ||||
| } | ||||
|  | ||||
| export function hideElem(el) { | ||||
| export function hideElem(el: ElementArg) { | ||||
|   elementsCall(el, toggleShown, false); | ||||
| } | ||||
|  | ||||
| export function toggleElem(el, force) { | ||||
| export function toggleElem(el: ElementArg, force?: boolean) { | ||||
|   elementsCall(el, toggleShown, force); | ||||
| } | ||||
|  | ||||
| export function isElemHidden(el) { | ||||
|   const res = []; | ||||
| export function isElemHidden(el: ElementArg) { | ||||
|   const res: boolean[] = []; | ||||
|   elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden'))); | ||||
|   if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`); | ||||
|   return res[0]; | ||||
| } | ||||
|  | ||||
| function applyElemsCallback(elems, fn) { | ||||
| function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) { | ||||
|   if (fn) { | ||||
|     for (const el of elems) { | ||||
|       fn(el); | ||||
| @@ -60,20 +67,22 @@ function applyElemsCallback(elems, fn) { | ||||
|   return elems; | ||||
| } | ||||
|  | ||||
| export function queryElemSiblings(el, selector = '*', fn) { | ||||
|   return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn); | ||||
| export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) { | ||||
|   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 | ||||
| export function queryElemChildren(parent, selector = '*', fn) { | ||||
| export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) { | ||||
|   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); | ||||
| } | ||||
|  | ||||
| export function onDomReady(cb) { | ||||
| export function onDomReady(cb: () => Promisable<void>) { | ||||
|   if (document.readyState === 'loading') { | ||||
|     document.addEventListener('DOMContentLoaded', cb); | ||||
|   } 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 | ||||
| // 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 { | ||||
|     return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE; | ||||
|   } catch { | ||||
| @@ -108,12 +117,15 @@ export function isDocumentFragmentOrElementNode(el) { | ||||
| // The above copyright notice and this permission notice shall be | ||||
| // 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; | ||||
|   // 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 (lastMouseX !== event.clientX || lastMouseY !== event.clientY) { | ||||
|       const newStyleHeight = textarea.style.height; | ||||
| @@ -133,7 +145,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) { | ||||
|  | ||||
|     while (el !== document.body && el !== null) { | ||||
|       offsetTop += el.offsetTop || 0; | ||||
|       el = el.offsetParent; | ||||
|       el = el.offsetParent as HTMLTextAreaElement; | ||||
|     } | ||||
|  | ||||
|     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); | ||||
| } | ||||
|  | ||||
| type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement; | ||||
|  | ||||
| // 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: | ||||
| // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event | ||||
| export function loadElem(el, src) { | ||||
| // has loaded or errored. | ||||
| export function loadElem(el: LoadableElement, src: string) { | ||||
|   return new Promise((resolve) => { | ||||
|     el.addEventListener('load', () => resolve(true), {once: true}); | ||||
|     el.addEventListener('error', () => resolve(false), {once: true}); | ||||
| @@ -256,14 +269,14 @@ export function initSubmitEventPolyfill() { | ||||
|  * @param {HTMLElement} element The element to check. | ||||
|  * @returns {boolean} True if the element is visible. | ||||
|  */ | ||||
| export function isElemVisible(element) { | ||||
| export function isElemVisible(element: HTMLElement) { | ||||
|   if (!element) return false; | ||||
|  | ||||
|   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 | ||||
| export function replaceTextareaSelection(textarea, text) { | ||||
| export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) { | ||||
|   const before = textarea.value.slice(0, textarea.selectionStart ?? undefined); | ||||
|   const after = textarea.value.slice(textarea.selectionEnd ?? undefined); | ||||
|   let success = true; | ||||
| @@ -287,13 +300,13 @@ export function replaceTextareaSelection(textarea, text) { | ||||
| } | ||||
|  | ||||
| // Warning: Do not enter any unsanitized variables here | ||||
| export function createElementFromHTML(htmlString) { | ||||
| export function createElementFromHTML(htmlString: string) { | ||||
|   const div = document.createElement('div'); | ||||
|   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); | ||||
|   for (const [key, value] of Object.entries(attrs)) { | ||||
|     if (value === undefined || value === null) continue; | ||||
| @@ -307,7 +320,7 @@ export function createElementFromAttrs(tagName, attrs) { | ||||
|   return el; | ||||
| } | ||||
|  | ||||
| export function animateOnce(el, animationClassName) { | ||||
| export function animateOnce(el: Element, animationClassName: string): Promise<void> { | ||||
|   return new Promise((resolve) => { | ||||
|     el.addEventListener('animationend', function onAnimationEnd() { | ||||
|       el.classList.remove(animationClassName); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user