projecte_ionic/node_modules/@ionic/core/components/helpers.js
2022-02-09 18:30:03 +01:00

248 lines
8 KiB
JavaScript
Executable file

/**
* Waits for a component to be ready for
* both custom element and non-custom element builds.
* If non-custom element build, el.componentOnReady
* will be used.
* For custom element builds, we wait a frame
* so that the inner contents of the component
* have a chance to render.
*
* Use this utility rather than calling
* el.componentOnReady yourself.
*/
const componentOnReady = (el, callback) => {
if (el.componentOnReady) {
el.componentOnReady().then((resolvedEl) => callback(resolvedEl));
}
else {
raf(() => callback(el));
}
};
/**
* Elements inside of web components sometimes need to inherit global attributes
* set on the host. For example, the inner input in `ion-input` should inherit
* the `title` attribute that developers set directly on `ion-input`. This
* helper function should be called in componentWillLoad and assigned to a variable
* that is later used in the render function.
*
* This does not need to be reactive as changing attributes on the host element
* does not trigger a re-render.
*/
const inheritAttributes = (el, attributes = []) => {
const attributeObject = {};
attributes.forEach(attr => {
if (el.hasAttribute(attr)) {
const value = el.getAttribute(attr);
if (value !== null) {
attributeObject[attr] = el.getAttribute(attr);
}
el.removeAttribute(attr);
}
});
return attributeObject;
};
const addEventListener = (el, eventName, callback, opts) => {
if (typeof window !== 'undefined') {
const win = window;
const config = win && win.Ionic && win.Ionic.config;
if (config) {
const ael = config.get('_ael');
if (ael) {
return ael(el, eventName, callback, opts);
}
else if (config._ael) {
return config._ael(el, eventName, callback, opts);
}
}
}
return el.addEventListener(eventName, callback, opts);
};
const removeEventListener = (el, eventName, callback, opts) => {
if (typeof window !== 'undefined') {
const win = window;
const config = win && win.Ionic && win.Ionic.config;
if (config) {
const rel = config.get('_rel');
if (rel) {
return rel(el, eventName, callback, opts);
}
else if (config._rel) {
return config._rel(el, eventName, callback, opts);
}
}
}
return el.removeEventListener(eventName, callback, opts);
};
/**
* Gets the root context of a shadow dom element
* On newer browsers this will be the shadowRoot,
* but for older browser this may just be the
* element itself.
*
* Useful for whenever you need to explicitly
* do "myElement.shadowRoot!.querySelector(...)".
*/
const getElementRoot = (el, fallback = el) => {
return el.shadowRoot || fallback;
};
/**
* Patched version of requestAnimationFrame that avoids ngzone
* Use only when you know ngzone should not run
*/
const raf = (h) => {
if (typeof __zone_symbol__requestAnimationFrame === 'function') {
return __zone_symbol__requestAnimationFrame(h);
}
if (typeof requestAnimationFrame === 'function') {
return requestAnimationFrame(h);
}
return setTimeout(h);
};
const hasShadowDom = (el) => {
return !!el.shadowRoot && !!el.attachShadow;
};
const findItemLabel = (componentEl) => {
const itemEl = componentEl.closest('ion-item');
if (itemEl) {
return itemEl.querySelector('ion-label');
}
return null;
};
/**
* This method is used for Ionic's input components that use Shadow DOM. In
* order to properly label the inputs to work with screen readers, we need
* to get the text content of the label outside of the shadow root and pass
* it to the input inside of the shadow root.
*
* Referencing label elements by id from outside of the component is
* impossible due to the shadow boundary, read more here:
* https://developer.salesforce.com/blogs/2020/01/accessibility-for-web-components.html
*
* @param componentEl The shadow element that needs the aria label
* @param inputId The unique identifier for the input
*/
const getAriaLabel = (componentEl, inputId) => {
let labelText;
// If the user provides their own label via the aria-labelledby attr
// we should use that instead of looking for an ion-label
const labelledBy = componentEl.getAttribute('aria-labelledby');
// Grab the id off of the component in case they are using
// a custom label using the label element
const componentId = componentEl.id;
let labelId = labelledBy !== null && labelledBy.trim() !== ''
? labelledBy
: inputId + '-lbl';
let label = labelledBy !== null && labelledBy.trim() !== ''
? document.getElementById(labelledBy)
: findItemLabel(componentEl);
if (label) {
if (labelledBy === null) {
label.id = labelId;
}
labelText = label.textContent;
label.setAttribute('aria-hidden', 'true');
// if there is no label, check to see if the user has provided
// one by setting an id on the component and using the label element
}
else if (componentId.trim() !== '') {
label = document.querySelector(`label[for="${componentId}"]`);
if (label) {
if (label.id !== '') {
labelId = label.id;
}
else {
label.id = labelId = `${componentId}-lbl`;
}
labelText = label.textContent;
}
}
return { label, labelId, labelText };
};
/**
* This method is used to add a hidden input to a host element that contains
* a Shadow DOM. It does not add the input inside of the Shadow root which
* allows it to be picked up inside of forms. It should contain the same
* values as the host element.
*
* @param always Add a hidden input even if the container does not use Shadow
* @param container The element where the input will be added
* @param name The name of the input
* @param value The value of the input
* @param disabled If true, the input is disabled
*/
const renderHiddenInput = (always, container, name, value, disabled) => {
if (always || hasShadowDom(container)) {
let input = container.querySelector('input.aux-input');
if (!input) {
input = container.ownerDocument.createElement('input');
input.type = 'hidden';
input.classList.add('aux-input');
container.appendChild(input);
}
input.disabled = disabled;
input.name = name;
input.value = value || '';
}
};
const clamp = (min, n, max) => {
return Math.max(min, Math.min(n, max));
};
const assert = (actual, reason) => {
if (!actual) {
const message = 'ASSERT: ' + reason;
console.error(message);
debugger; // tslint:disable-line
throw new Error(message);
}
};
const now = (ev) => {
return ev.timeStamp || Date.now();
};
const pointerCoord = (ev) => {
// get X coordinates for either a mouse click
// or a touch depending on the given event
if (ev) {
const changedTouches = ev.changedTouches;
if (changedTouches && changedTouches.length > 0) {
const touch = changedTouches[0];
return { x: touch.clientX, y: touch.clientY };
}
if (ev.pageX !== undefined) {
return { x: ev.pageX, y: ev.pageY };
}
}
return { x: 0, y: 0 };
};
/**
* @hidden
* Given a side, return if it should be on the end
* based on the value of dir
* @param side the side
* @param isRTL whether the application dir is rtl
*/
const isEndSide = (side) => {
const isRTL = document.dir === 'rtl';
switch (side) {
case 'start': return isRTL;
case 'end': return !isRTL;
default:
throw new Error(`"${side}" is not a valid value for [side]. Use "start" or "end" instead.`);
}
};
const debounceEvent = (event, wait) => {
const original = event._original || event;
return {
_original: event,
emit: debounce(original.emit.bind(original), wait)
};
};
const debounce = (func, wait = 0) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(func, wait, ...args);
};
};
export { addEventListener as a, removeEventListener as b, componentOnReady as c, getAriaLabel as d, renderHiddenInput as e, clamp as f, getElementRoot as g, hasShadowDom as h, inheritAttributes as i, findItemLabel as j, debounceEvent as k, isEndSide as l, assert as m, debounce as n, now as o, pointerCoord as p, raf as r };