diff --git a/src/core/utils/container-styles.js b/src/core/utils/container-styles.ts similarity index 72% rename from src/core/utils/container-styles.js rename to src/core/utils/container-styles.ts index 3b723b3e..56bdd688 100644 --- a/src/core/utils/container-styles.js +++ b/src/core/utils/container-styles.ts @@ -1,6 +1,13 @@ -import { resolveDarkmodeValue } from '../darkmode.js'; +import { InitValue, resolveDarkmodeValue } from '../darkmode.js'; -const styles = { +type Styles = Record; +type SavedStyles = Record; +type Config = { + darkmode?: InitValue, + darkmodePersistent?: boolean; +}; + +const styles: Styles = { 'font-family': 'Tahoma, Verdana, Arial, sans-serif', 'font-size': '16px', 'line-height': '1.6', @@ -12,14 +19,14 @@ const styles = { 'transition-duration': '.25s', 'transition-timing-function': 'ease-in' }; -const darkmodeStyles = { +const darkmodeStyles: Styles = { '--discovery-background-color': '#242424', '--discovery-color': '#cccccc' }; -const knowContainer = new WeakSet(); -const containerBeforeSetStyle = new WeakMap(); +const knowContainer = new WeakSet(); +const containerBeforeSetStyle = new WeakMap(); -function saveContainerStyleProp(container, prop, styles) { +function saveContainerStyleProp(container: HTMLElement, prop: string, styles: SavedStyles) { if (prop in styles === false) { styles[prop] = [ container.style.getPropertyValue(prop), @@ -28,7 +35,7 @@ function saveContainerStyleProp(container, prop, styles) { } } -export function applyContainerStyles(container, config) { +export function applyContainerStyles(container: HTMLElement, config: Config) { config = config || {}; if (!containerBeforeSetStyle.has(container)) { @@ -36,7 +43,7 @@ export function applyContainerStyles(container, config) { } const darkmode = resolveDarkmodeValue(config.darkmode, config.darkmodePersistent); - const containerStyles = containerBeforeSetStyle.get(container); + const containerStyles = containerBeforeSetStyle.get(container) ?? {}; for (const [prop, value] of Object.entries(styles)) { if (knowContainer.has(container) || !/^transition/.test(prop)) { @@ -60,15 +67,15 @@ export function applyContainerStyles(container, config) { return darkmode; } -export function rollbackContainerStyles(container) { +export function rollbackContainerStyles(container: HTMLElement) { if (containerBeforeSetStyle.has(container)) { - const containerStyles = containerBeforeSetStyle.get(container); + const containerStyles = containerBeforeSetStyle.get(container) ?? {}; for (const [prop, value] of Object.entries(containerStyles)) { container.style.setProperty(prop, ...value); } - containerBeforeSetStyle.delete(containerBeforeSetStyle); + containerBeforeSetStyle.delete(container); knowContainer.delete(container); } } diff --git a/src/core/utils/dom.js b/src/core/utils/dom.ts similarity index 52% rename from src/core/utils/dom.js rename to src/core/utils/dom.ts index 1e175e9f..88ad5965 100644 --- a/src/core/utils/dom.js +++ b/src/core/utils/dom.ts @@ -1,6 +1,20 @@ /* eslint-env browser */ -export function createElement(tag, attrs, children) { +type EventHandler = (this: Element, evt: Event) => void; +type Attrs = { + [key in keyof HTMLElementEventMap as `on${key}`]?: EventHandler< + HTMLElementTagNameMap[TagName], + HTMLElementEventMap[key] + >; +} & { + [key: string]: any | undefined; // TODO: replace "any" with "string" +}; + +export function createElement( + tag: TagName, + attrs: Attrs | string | null, + children?: (Node | string)[] | string + ) { const el = document.createElement(tag); if (typeof attrs === 'string') { @@ -10,23 +24,23 @@ export function createElement(tag, attrs, children) { } for (let attrName in attrs) { - if (hasOwnProperty.call(attrs, attrName)) { - if (attrs[attrName] === undefined) { + if (Object.hasOwn(attrs, attrName)) { + const value = attrs[attrName]; + + if (typeof value === "undefined") { continue; } - if (attrName.startsWith('on')) { - el.addEventListener(attrName.substr(2), attrs[attrName]); + if (typeof value === "function") { + el.addEventListener(attrName.slice(2), value); } else { - el.setAttribute(attrName, attrs[attrName]); + el.setAttribute(attrName, value); } } } if (Array.isArray(children)) { - children.forEach(child => - el.appendChild(child instanceof Node ? child : createText(child)) - ); + el.append(...children); } else if (typeof children === 'string') { el.innerHTML = children; } @@ -34,11 +48,11 @@ export function createElement(tag, attrs, children) { return el; } -export function createText(text) { +export function createText(text: any) { return document.createTextNode(String(text)); } -export function createFragment(...children) { +export function createFragment(...children: (Node | string)[]) { const fragment = document.createDocumentFragment(); children.forEach(child => @@ -61,8 +75,9 @@ export const passiveSupported = (() => { } }; - window.addEventListener('test', null, options); - window.removeEventListener('test', null, options); + const cb = () => {}; + window.addEventListener('test-passive', cb, options); + window.removeEventListener('test-passive', cb); } catch (err) {} return passiveSupported; diff --git a/src/core/utils/html.js b/src/core/utils/html.ts similarity index 83% rename from src/core/utils/html.js rename to src/core/utils/html.ts index bead186d..ad264f3e 100644 --- a/src/core/utils/html.js +++ b/src/core/utils/html.ts @@ -1,4 +1,4 @@ -export function escapeHtml(str) { +export function escapeHtml(str: string) { return str .replace(/&/g, '&') .replace(/"/g, '"') @@ -6,7 +6,7 @@ export function escapeHtml(str) { .replace(/>/g, '>'); } -export function numDelim(value, escape = true) { +export function numDelim(value: any, escape = true) { const strValue = escape && typeof value !== 'number' ? escapeHtml(String(value)) : String(value); diff --git a/src/core/utils/id.js b/src/core/utils/id.js deleted file mode 100644 index f887c626..00000000 --- a/src/core/utils/id.js +++ /dev/null @@ -1,7 +0,0 @@ -export function randomId() { - return [ - parseInt(performance.timeOrigin, 10).toString(16), - parseInt(10000 * performance.now(), 10).toString(16), - String(Math.random().toString(16).slice(2)) - ].join('-'); -} diff --git a/src/core/utils/id.ts b/src/core/utils/id.ts new file mode 100644 index 00000000..3541c42d --- /dev/null +++ b/src/core/utils/id.ts @@ -0,0 +1,7 @@ +export function randomId() { + return [ + performance.timeOrigin.toString(16), + (10000 * performance.now()).toString(16), + String(Math.random().toString(16).slice(2)) + ].join('-'); +} diff --git a/src/core/utils/inject-styles.js b/src/core/utils/inject-styles.ts similarity index 67% rename from src/core/utils/inject-styles.js rename to src/core/utils/inject-styles.ts index b27a8fdd..cf13a163 100644 --- a/src/core/utils/inject-styles.js +++ b/src/core/utils/inject-styles.ts @@ -1,9 +1,20 @@ import { createElement } from './dom.js'; -export default function injectStyles(el, styles) { +export type InlineStyle = { + type: 'style' | 'inline'; + content: string; + media?: string; +}; +export type LinkStyle = { + type: 'link' | 'external'; + href: string; + media?: string; +}; +export type Style = string | InlineStyle | LinkStyle; + +export default async function injectStyles(el: HTMLElement, styles: Style[]) { const foucFix = createElement('style', null, ':host{display:none}'); - const awaitingStyles = new Set(); - let readyStyles = Promise.resolve(); + const awaitingStyles = new Set>(); if (Array.isArray(styles)) { el.append(...styles.map(style => { @@ -23,9 +34,9 @@ export default function injectStyles(el, styles) { case 'link': case 'external': { - let resolveStyle; - let rejectStyle; - let state = new Promise((resolve, reject) => { + let resolveStyle: () => void; + let rejectStyle: (err?: any) => void; + let state = new Promise((resolve, reject) => { resolveStyle = resolve; rejectStyle = reject; }); @@ -39,18 +50,10 @@ export default function injectStyles(el, styles) { onerror(err) { awaitingStyles.delete(state); rejectStyle(err); - - if (!awaitingStyles.size) { - foucFix.remove(); - } }, onload() { awaitingStyles.delete(state); resolveStyle(); - - if (!awaitingStyles.size) { - foucFix.remove(); - } } }); @@ -58,15 +61,14 @@ export default function injectStyles(el, styles) { } default: - throw new Error(`Unknown type "${style.type}" for a style descriptor`); + throw new Error(`Unknown type "${(style as any).type}" for a style descriptor`); } })); if (awaitingStyles.size) { - readyStyles = Promise.all(awaitingStyles); el.append(foucFix); + await Promise.all(awaitingStyles); + foucFix.remove(); } } - - return readyStyles; } diff --git a/src/core/utils/json.js b/src/core/utils/json.ts similarity index 81% rename from src/core/utils/json.js rename to src/core/utils/json.ts index 79649e4f..e07a8913 100644 --- a/src/core/utils/json.js +++ b/src/core/utils/json.ts @@ -1,10 +1,12 @@ import jsonExt from '@discoveryjs/json-ext'; +type Replacer = (key: string, value: any) => void; + export const { stringifyInfo: jsonStringifyInfo } = jsonExt; -function prettyFn(fn, ws, property) { +function prettyFn(fn: Function, ws: string, property: string) { const src = String(fn); const [prefix, name] = src.match(/^(?:\S+\s+)?(\S+)\(/) || []; @@ -19,7 +21,7 @@ function prettyFn(fn, ws, property) { } const lines = src.split(/\n/); - const minOffset = lines[lines.length - 1].match(/^\s*/)[0].length; + const minOffset = lines[lines.length - 1].match(/^\s*/)?.[0].length || 0; const stripOffset = new RegExp('^\\s{0,' + minOffset + '}'); return property + lines @@ -27,7 +29,7 @@ function prettyFn(fn, ws, property) { .join('\n'); } -function restoreValue(value, ws, property) { +function restoreValue(value: any, ws: string, property: string) { if (typeof value === 'function') { return prettyFn(value, ws, property); } @@ -46,9 +48,9 @@ const specialValueTypes = new Set([ '[object Date]' ]); -export function jsonStringifyAsJavaScript(value, replacer, space = 4) { - const specials = []; - const jsReplacer = function(key, value) { +export function jsonStringifyAsJavaScript(value: any, replacer: Replacer, space = 4) { + const specials: any[] = []; + const jsReplacer = function(key: string, value: any) { if (typeof value === 'string' && toString.call(this[key]) === '[object Date]') { value = this[key]; } diff --git a/src/core/utils/layout.js b/src/core/utils/layout.ts similarity index 73% rename from src/core/utils/layout.js rename to src/core/utils/layout.ts index 55f86a3b..e67435da 100644 --- a/src/core/utils/layout.js +++ b/src/core/utils/layout.ts @@ -2,39 +2,39 @@ const { documentElement } = document; const standartsMode = document.compatMode === 'CSS1Compat'; -export function getOffsetParent(node) { - let offsetParent = node.offsetParent; +export function getOffsetParent(node: HTMLElement) { + let offsetParent = node.offsetParent as HTMLElement; while ( offsetParent !== null && offsetParent !== documentElement && getComputedStyle(offsetParent).position === 'static' ) { - offsetParent = offsetParent.offsetParent; + offsetParent = offsetParent.offsetParent as HTMLElement; } return offsetParent || documentElement; } -export function getOverflowParent(node) { - let overflowParent = node.parentNode; +export function getOverflowParent(node: HTMLElement) { + let overflowParent = node.parentNode as HTMLElement; while ( overflowParent !== null && overflowParent !== documentElement && getComputedStyle(overflowParent).overflow === 'visible' ) { - overflowParent = overflowParent.parentNode; + overflowParent = overflowParent.parentNode as HTMLElement; } return overflowParent || documentElement; } -export function getPageOffset(element) { +export function getPageOffset(element: HTMLElement | null = null) { let top = 0; let left = 0; - if (element && element.getBoundingClientRect) { + if (typeof element?.getBoundingClientRect === 'function') { // offset relative to element const rect = element.getBoundingClientRect(); @@ -62,14 +62,17 @@ export function getPageOffset(element) { }; } -export function getBoundingRect(element, relElement) { +export function getBoundingRect( + element: HTMLElement | Window, + relElement: HTMLElement | null +) { const offset = getPageOffset(relElement); let top = 0; let left = 0; let right = 0; let bottom = 0; - if (element && element.getBoundingClientRect) { + if (element instanceof HTMLElement && typeof element?.getBoundingClientRect === 'function') { ({ top, left, right, bottom } = element.getBoundingClientRect()); } @@ -83,7 +86,10 @@ export function getBoundingRect(element, relElement) { }; } -export function getViewportRect(element, relElement) { +export function getViewportRect( + element: HTMLElement | Window, + relElement: HTMLElement | null = null +) { const topViewport = standartsMode ? document.documentElement : document.body; let { top, left } = element === topViewport && !relElement ? getPageOffset() @@ -91,7 +97,7 @@ export function getViewportRect(element, relElement) { let width; let height; - if (!element || element === window) { + if (!element || element instanceof Window) { width = window.innerWidth || 0; height = window.innerHeight || 0; } else { diff --git a/src/core/utils/pointer.js b/src/core/utils/pointer.ts similarity index 100% rename from src/core/utils/pointer.js rename to src/core/utils/pointer.ts diff --git a/src/core/utils/safe-filter-rx.js b/src/core/utils/safe-filter-rx.ts similarity index 51% rename from src/core/utils/safe-filter-rx.js rename to src/core/utils/safe-filter-rx.ts index 63aa1536..55f7bc06 100644 --- a/src/core/utils/safe-filter-rx.js +++ b/src/core/utils/safe-filter-rx.ts @@ -1,6 +1,6 @@ /* eslint-env browser */ -function buildRx(pattern, flags) { +function buildRx(pattern: string, flags?: string) { try { return new RegExp('((?:' + pattern + ')+)', flags); } catch (e) {} @@ -8,10 +8,8 @@ function buildRx(pattern, flags) { return new RegExp('((?:' + pattern.replace(/[\[\]\(\)\?\+\*\{\}\\]/g, '\\$&') + ')+)', flags); } -export default function safeFilterRx(pattern, flags = 'i') { - const rx = buildRx(pattern, flags); - - rx.rawSource = pattern; - - return rx; +export default function safeFilterRx(pattern: string, flags = 'i') { + return Object.assign(buildRx(pattern, flags), { + rawSource: pattern + }); } diff --git a/src/core/utils/size.js b/src/core/utils/size.js deleted file mode 100644 index 6a5a80fd..00000000 --- a/src/core/utils/size.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Observer } from '../observer.js'; - -const resizeObserverSupported = typeof ResizeObserver === 'function'; - -export class ContentRect extends Observer { - constructor() { - super(); - this.el = null; - - if (resizeObserverSupported) { - this.observer = new ResizeObserver(entries => { - for (let entry of entries) { - this.set(entry.contentRect); - } - }); - } - } - - observe(el) { - el = el || null; - - if (this.observer && this.el !== el) { - if (this.el !== null) { - this.observer.unobserve(this.el); - } - - if (el !== null) { - this.observer.observe(el); - } - } - - this.el = el; - } -} diff --git a/src/core/utils/size.ts b/src/core/utils/size.ts new file mode 100644 index 00000000..3b7b3880 --- /dev/null +++ b/src/core/utils/size.ts @@ -0,0 +1,47 @@ +import { Observer } from '../observer.js'; + +const resizeObserverSupported = typeof ResizeObserver === 'function'; + +export class ContentRect extends Observer { + private el: HTMLElement | null; + private observer: ResizeObserver | null; + + constructor() { + super(null); + + this.el = null; + this.observer = resizeObserverSupported + ? new ResizeObserver(entries => { + for (let entry of entries) { + this.set(entry.contentRect); + } + }) + : null; + } + + observe(el: HTMLElement) { + if (this.observer === null) { + this.el = null; + return; + } + + el = el || null; + + if (this.el !== el) { + if (this.el !== null) { + this.observer.unobserve(this.el); + } + + if (el !== null) { + this.observer.observe(el); + } + + this.el = el; + } + } + + dispose() { + this.el = null; + this.observer = null; + } +}