UNPKG

@taiga-ui/cdk

Version:

Base library for creating Angular components and applications using Taiga UI principles regarding of actual visual appearance

262 lines (245 loc) 9.3 kB
import { tuiIsPresent } from '@taiga-ui/cdk/utils/miscellaneous'; import { inject, ElementRef, PLATFORM_ID, isSignal, signal, untracked, DestroyRef, effect, INJECTOR } from '@angular/core'; import { coerceElement } from '@angular/cdk/coercion'; import { isPlatformBrowser } from '@angular/common'; import { WA_WINDOW } from '@ng-web-apis/common'; import { toSignal } from '@angular/core/rxjs-interop'; import { fromEvent, startWith, map } from 'rxjs'; function tuiContainsOrAfter(current, node) { try { return (current.contains(node) || !!(node.compareDocumentPosition(current) & Node.DOCUMENT_POSITION_PRECEDING)); } catch { return false; } } function tuiIsInput(element) { return element.matches('input'); } function tuiIsTextarea(element) { return element.matches('textarea'); } function tuiIsTextfield(element) { return tuiIsInput(element) || tuiIsTextarea(element); } function tuiIsElement(node) { return !!node && 'nodeType' in node && node.nodeType === Node.ELEMENT_NODE; } function tuiIsHTMLElement(node) { const defaultView = node?.ownerDocument.defaultView; return !!node && !!defaultView && node instanceof defaultView.HTMLElement; } function tuiIsTextNode(node) { return node.nodeType === Node.TEXT_NODE; } function tuiIsInputEvent(event) { return 'data' in event && 'inputType' in event; } /** * Gets actual target from open Shadow DOM if event happened within it */ function tuiGetActualTarget(event) { return event.composedPath()[0]; } const DEFAULT_FORMAT = 'text/plain'; /** * Gets text from data of clipboardEvent, it also works in IE and Edge browsers */ function tuiGetClipboardDataText(event, format = DEFAULT_FORMAT) { return 'clipboardData' in event && event.clipboardData !== null ? event.clipboardData.getData(format) || event.clipboardData.getData(DEFAULT_FORMAT) : event.target.ownerDocument.defaultView.clipboardData.getData('text'); } function tuiGetDocumentOrShadowRoot(node) { return 'getRootNode' in node && node.isConnected ? node.getRootNode() : node.ownerDocument; } /** * Returns array of Elements covering edges of given element or null if at least one edge middle point is visible * * CAUTION: Empty array means element if offscreen i.e. covered by no elements, rather than not covered * ```ts * function tuiGetElementObscures(element: Element): readonly [Element, Element, Element, Element] | [] | null * ``` */ function tuiGetElementObscures(element) { if (!element.getBoundingClientRect) { return null; } const { left, right, top, bottom, width, height } = element.getBoundingClientRect(); if (width === 0 && height === 0) { return null; } const doc = tuiGetDocumentOrShadowRoot(element); const horizontalMiddle = Math.round(left + width / 2); const verticalMiddle = Math.round(top + height / 2); const elements = [ doc.elementFromPoint(horizontalMiddle, Math.round(top) + 2), doc.elementFromPoint(horizontalMiddle, Math.round(bottom) - 2), doc.elementFromPoint(Math.round(left) + 2, verticalMiddle), doc.elementFromPoint(Math.round(right) - 2, verticalMiddle), ].filter(tuiIsPresent); if (!elements.length) { return []; } const filtered = elements.filter((el) => !element.contains(el) && !el.contains(element)); return filtered.length === 4 ? filtered : null; } /** * Calculates offset for an element relative to it's parent several levels above * * @param host parent element * @param element * @return object with offsetTop and offsetLeft number properties */ function tuiGetElementOffset(host, element) { ngDevMode && console.assert(host.contains(element), 'Host must contain element'); let { offsetTop, offsetLeft, offsetParent } = element; while (tuiIsHTMLElement(offsetParent) && offsetParent !== host) { offsetTop += offsetParent.offsetTop; offsetLeft += offsetParent.offsetLeft; offsetParent = offsetParent.offsetParent; } return { offsetTop, offsetLeft }; } /** * @description: * cross browser way to get selected text * * History: * BUG - window.getSelection() fails when text selected in a form field * https://bugzilla.mozilla.org/show_bug.cgi?id=85686 */ function tuiGetSelectedText({ getSelection, document }) { return document.activeElement && tuiIsTextfield(document.activeElement) ? document.activeElement.value.slice(document.activeElement.selectionStart || 0, document.activeElement.selectionEnd || 0) : getSelection()?.toString() || null; } function tuiInjectElement() { return inject(ElementRef).nativeElement; } function tuiIsElementEditable(element) { return ((tuiIsTextfield(element) && !element.readOnly && element.inputMode !== 'none') || Boolean(element.isContentEditable)); } function tuiPointToClientRect(x = 0, y = 0) { const rect = { x, y, left: x, right: x, top: y, bottom: y, width: 0, height: 0, }; return { ...rect, toJSON: () => rect, }; } function tuiValue(input, injector = inject(INJECTOR)) { const win = injector.get(WA_WINDOW); if (!win.tuiInputPatched && isPlatformBrowser(injector.get(PLATFORM_ID))) { win.tuiInputPatched = true; patch(win.HTMLInputElement.prototype); patch(win.HTMLTextAreaElement.prototype); patch(win.HTMLSelectElement.prototype); } let element = isSignal(input) ? undefined : coerceElement(input); let cleanup = () => { }; const options = { injector }; const value = signal(element?.value || ''); const process = (el) => { const update = () => untracked(() => value.set(el.value)); el.addEventListener('input', update, { capture: true }); el.addEventListener('tui-input', update, { capture: true }); return () => { el.removeEventListener('input', update, { capture: true }); el.removeEventListener('tui-input', update, { capture: true }); }; }; injector.get(DestroyRef).onDestroy(() => cleanup()); if (isSignal(input)) { effect(() => { element = coerceElement(input()); cleanup(); if (element && !element.matches('select[multiple]')) { value.set(element.value); cleanup = process(element); } }, options); } else if (element && !element.matches('select[multiple]')) { cleanup = process(element); } effect(() => { const v = value(); /** * select[multiple] elements have value of first selected option, * but there could be more, setting value resets other selected options */ if (element?.matches('select[multiple]')) { return; } if (element?.matches(':focus') && 'selectionStart' in element) { const { selectionStart, selectionEnd } = element; /** * After programmatic updates of input's value, caret is automatically placed at the end – * revert to the previous position */ element.value = v; try { element.setSelectionRange(selectionStart, selectionEnd); } catch { // Only certain types of inputs support setSelectionRange, so we need to catch the error for unsupported ones } } else if (element) { element.value = v; } }, options); return value; } function patch(prototype) { const { set } = Object.getOwnPropertyDescriptor(prototype, 'value'); Object.defineProperty(prototype, 'value', { set(detail) { const value = this.value; const event = new CustomEvent('tui-input', { detail, bubbles: true }); set.call(this, detail); if (value !== detail) { this.dispatchEvent(event); } }, }); } function tuiWindowSize(window) { return toSignal(fromEvent(window, 'resize').pipe(startWith(null), map(() => { const width = Math.max(window.document.documentElement.clientWidth || 0, window.innerWidth || 0, window.visualViewport?.width || 0); const height = Math.max(window.document.documentElement.clientHeight || 0, window.innerHeight || 0, window.visualViewport?.height || 0); const rect = { width, height, top: 0, left: 0, right: width, bottom: height, x: 0, y: 0, }; return { ...rect, toJSON: () => JSON.stringify(rect), }; })), { requireSync: true }); } /** * Generated bundle index. Do not edit. */ export { tuiContainsOrAfter, tuiGetActualTarget, tuiGetClipboardDataText, tuiGetDocumentOrShadowRoot, tuiGetElementObscures, tuiGetElementOffset, tuiGetSelectedText, tuiInjectElement, tuiIsElement, tuiIsElementEditable, tuiIsHTMLElement, tuiIsInput, tuiIsInputEvent, tuiIsTextNode, tuiIsTextarea, tuiIsTextfield, tuiPointToClientRect, tuiValue, tuiWindowSize }; //# sourceMappingURL=taiga-ui-cdk-utils-dom.mjs.map