Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / helpers / dom.ts
blobc18c2ab17f8218fd6525f29054ace463e7ba726e
1 import tinycolor from 'tinycolor2';
3 interface ScriptInfo {
4     path: string;
5     integrity?: string;
8 interface Callback {
9     (event?: Event, error?: string | Event): void;
12 const loadScriptHelper = ({ path, integrity }: ScriptInfo, cb: Callback) => {
13     const script = document.createElement('script');
15     script.src = path;
16     if (integrity) {
17         script.integrity = integrity;
18     }
19     script.onload = (e) => {
20         cb(e);
21         script.remove();
22     };
23     script.onerror = (e) => cb(undefined, e);
25     document.head.appendChild(script);
28 export const loadScript = (path: string, integrity?: string) => {
29     return new Promise<Event>((resolve, reject) => {
30         loadScriptHelper({ path, integrity }, (event, error) => {
31             if (error || !event) {
32                 return reject(error);
33             }
34             return resolve(event);
35         });
36     });
39 /**
40  * Returns whether the element is a node.
41  * See {@link https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType}
42  */
43 export const isElement = (node: Node | null): node is Element => Boolean(node && node.nodeType === 1);
45 /**
46  * Returns the node if it's an element or the parent element if not
47  */
48 export const getElement = (node: Node | null) => (isElement(node) ? (node as Element) : node?.parentElement || null);
50 /**
51  * From https://stackoverflow.com/a/42543908
52  */
53 export const getScrollParent = (element: HTMLElement | null | undefined, includeHidden = false) => {
54     if (!element) {
55         return document.body;
56     }
58     const style = getComputedStyle(element);
59     const excludeStaticParent = style.position === 'absolute';
60     const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
62     if (style.position === 'fixed') {
63         return document.body;
64     }
66     for (let parent = element.parentElement; parent; parent = parent.parentElement) {
67         const style = getComputedStyle(parent);
68         if (excludeStaticParent && style.position === 'static') {
69             continue;
70         }
71         if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
72             return parent;
73         }
74     }
76     return document.body;
79 /**
80  * get computed root font size, to manage properly some elements in pixels
81  * value is dynamic
82  */
83 let rootFontSizeCache: number | undefined = undefined;
85 const getRootFontSize = () => {
86     return parseFloat(window.getComputedStyle(document.querySelector('html') as Element).getPropertyValue('font-size'));
89 export const rootFontSize = (reset?: boolean) => {
90     if (rootFontSizeCache === undefined || reset === true) {
91         rootFontSizeCache = getRootFontSize();
92     }
93     return rootFontSizeCache;
96 /**
97  * Firefox <58 does not support block: 'nearest' and just throws
98  */
99 export const scrollIntoView = (element: HTMLElement | undefined | null, extra?: boolean | ScrollIntoViewOptions) => {
100     if (!element) {
101         return;
102     }
103     try {
104         element.scrollIntoView(extra);
105         // eslint-disable-next-line no-empty
106     } catch (e: any) {}
109 export const hasChildren = (node?: ChildNode) => {
110     return node && node.childNodes && node.childNodes.length > 0;
113 export const getMaxDepth = (node: ChildNode) => {
114     let maxDepth = 0;
115     for (const child of node.childNodes) {
116         if (hasChildren(child)) {
117             const depth = getMaxDepth(child);
118             if (depth > maxDepth) {
119                 maxDepth = depth;
120             }
121         }
122     }
123     return maxDepth + 1;
126 export const checkContrast = (node: ChildNode, window: Window): boolean => {
127     if (node.nodeType === Node.ELEMENT_NODE) {
128         const style = window.getComputedStyle(node as Element);
129         const color = style.color ? tinycolor(style.color) : tinycolor('#fff');
130         const background = style.backgroundColor ? tinycolor(style.backgroundColor) : tinycolor('#000');
131         const result =
132             (color?.isDark() && (background?.isLight() || background?.getAlpha() === 0)) ||
133             (color?.isLight() && background?.isDark());
135         if (!result) {
136             return false;
137         }
138     }
139     return [...node.childNodes].every((node) => checkContrast(node, window));
142 export const getIsEventModified = (event: MouseEvent) => {
143     return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
146 export const isVisibleOnScreen = (element: HTMLElement | null) => {
147     if (!element) {
148         return false;
149     }
151     var rect = element.getBoundingClientRect();
152     var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
153     return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
156 export const isVisible = (element: HTMLElement | null) => {
157     if (!element) {
158         return false;
159     }
161     const style = getComputedStyle(element);
162     const { offsetWidth, offsetHeight } = element;
163     const { width, height } = element.getBoundingClientRect();
165     if (style.display === 'none') {
166         return false;
167     }
169     if (style.visibility !== 'visible') {
170         return false;
171     }
173     if ((style.opacity as any) === 0) {
174         return false;
175     }
177     if (offsetWidth + offsetHeight + height + width === 0) {
178         return false;
179     }
181     return true;
184 export const parseStringToDOM = (content: string, type: DOMParserSupportedType = 'text/html') => {
185     const parser = new DOMParser();
186     return parser.parseFromString(content, type);