@taiga-ui/cdk
Version:
Base library for creating Angular components and applications using Taiga UI principles regarding of actual visual appearance
318 lines (296 loc) • 11.2 kB
JavaScript
import { tuiClamp } from '@taiga-ui/cdk/utils/math';
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 { TUI_ALLOW_SIGNAL_WRITES } from '@taiga-ui/cdk/constants';
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) {
const { ownerDocument } = element;
if (!ownerDocument?.defaultView || !element.getBoundingClientRect) {
return null;
}
const { innerWidth, innerHeight } = ownerDocument.defaultView;
const doc = tuiGetDocumentOrShadowRoot(element);
const rect = element.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) {
return null;
}
const left = tuiClamp(Math.round(rect.left) + 2, 0, innerWidth);
const top = tuiClamp(Math.round(rect.top) + 2, 0, innerHeight);
const right = tuiClamp(Math.round(rect.right) - 2, 0, innerWidth);
const bottom = tuiClamp(Math.round(rect.bottom) - 2, 0, innerHeight);
const horizontalMiddle = tuiClamp(Math.round(rect.left + rect.width / 2), 0, innerWidth);
const verticalMiddle = tuiClamp(Math.round(rect.top + rect.height / 2), 0, innerHeight);
const elements = [
doc.elementFromPoint(horizontalMiddle, top),
doc.elementFromPoint(horizontalMiddle, bottom),
doc.elementFromPoint(left, verticalMiddle),
doc.elementFromPoint(right, verticalMiddle),
];
const nonNull = elements.filter(tuiIsPresent);
if (!nonNull.length) {
return [];
}
const filtered = nonNull.filter((el) => !element.contains(el) && !el.contains(element));
return filtered.length === 4
? filtered
: null;
}
/// <reference types="@taiga-ui/tsconfig/ng-dev-mode" />
/**
* 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 };
}
function tuiGetElementPoint(x, y, element) {
const { left, top, width, height } = element.getBoundingClientRect();
return [tuiClamp(x - left, 0, width) / width, tuiClamp(y - top, 0, height) / height];
}
/**
* @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 tuiIsCurrentTarget({ target, currentTarget }) {
return target === currentTarget;
}
function tuiIsElementEditable(element) {
return ((tuiIsTextfield(element) && !element.readOnly && element.inputMode !== 'none') ||
Boolean(element.isContentEditable));
}
/**
* Checks if an app is running inside <iframe /> tag
*/
function tuiIsInsideIframe(win) {
return win.parent !== win;
}
/**
* Checks if node is inside a specific selector
*
* @param node
* @param selector
* @return true if node is inside a particular selector
*/
function tuiIsNodeIn(node, selector) {
return tuiIsTextNode(node)
? !!node.parentElement?.closest(selector)
: tuiIsElement(node) && !!node.closest(selector);
}
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 tuiRetargetedBoundaryCrossing(event) {
// firefox
if ('explicitOriginalTarget' in event) {
return event?.explicitOriginalTarget !== event.target;
}
// chrome
if ('pointerId' in event) {
return event.pointerId === -1;
}
// safari
if ('detail' in event && 'webkitForce' in event) {
return event?.detail === 0;
}
return false;
}
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, ...TUI_ALLOW_SIGNAL_WRITES };
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;
element.setSelectionRange(selectionStart, selectionEnd);
}
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);
}
},
});
}
/**
* Host binding `host: {'[value]': 'yourSignal()'}` is not an option for our textfields –
* they use {@link TuiTextfieldDirective} as a host directive.
* `TuiTextfieldDirective` has host binding which depends on native input's value.
* Host bindings of the host directives are re-calculated BEFORE component's ones –
* native input's value should be updated SYNCHRONOUSLY before next change detection iteration.
* @deprecated
*/
function tuiValueBinding(value = signal(tuiInjectElement().value || '')) {
const el = tuiInjectElement();
effect(() => {
if (el.value === value()) {
return;
}
const { selectionStart, selectionEnd } = el;
el.value = value();
if (el.matches(':focus')) {
/**
* After programmatic updates of input's value, caret is automatically placed at the end –
* revert to the previous position
*/
el.setSelectionRange(selectionStart, selectionEnd);
}
});
return value;
}
/**
* Generated bundle index. Do not edit.
*/
export { tuiContainsOrAfter, tuiGetActualTarget, tuiGetClipboardDataText, tuiGetDocumentOrShadowRoot, tuiGetElementObscures, tuiGetElementOffset, tuiGetElementPoint, tuiGetSelectedText, tuiInjectElement, tuiIsCurrentTarget, tuiIsElement, tuiIsElementEditable, tuiIsHTMLElement, tuiIsInput, tuiIsInputEvent, tuiIsInsideIframe, tuiIsNodeIn, tuiIsTextNode, tuiIsTextarea, tuiIsTextfield, tuiPointToClientRect, tuiRetargetedBoundaryCrossing, tuiValue, tuiValueBinding };
//# sourceMappingURL=taiga-ui-cdk-utils-dom.mjs.map