UNPKG

10.9 kBJavaScriptView Raw
1/*!
2 * (C) Ionic http://ionicframework.com - MIT License
3 */
4const transitionEndAsync = (el, expectedDuration = 0) => {
5 return new Promise(resolve => {
6 transitionEnd(el, expectedDuration, resolve);
7 });
8};
9/**
10 * Allows developer to wait for a transition
11 * to finish and fallback to a timer if the
12 * transition is cancelled or otherwise
13 * never finishes. Also see transitionEndAsync
14 * which is an await-able version of this.
15 */
16const transitionEnd = (el, expectedDuration = 0, callback) => {
17 let unRegTrans;
18 let animationTimeout;
19 const opts = { passive: true };
20 const ANIMATION_FALLBACK_TIMEOUT = 500;
21 const unregister = () => {
22 if (unRegTrans) {
23 unRegTrans();
24 }
25 };
26 const onTransitionEnd = (ev) => {
27 if (ev === undefined || el === ev.target) {
28 unregister();
29 callback(ev);
30 }
31 };
32 if (el) {
33 el.addEventListener('webkitTransitionEnd', onTransitionEnd, opts);
34 el.addEventListener('transitionend', onTransitionEnd, opts);
35 animationTimeout = setTimeout(onTransitionEnd, expectedDuration + ANIMATION_FALLBACK_TIMEOUT);
36 unRegTrans = () => {
37 if (animationTimeout) {
38 clearTimeout(animationTimeout);
39 animationTimeout = undefined;
40 }
41 el.removeEventListener('webkitTransitionEnd', onTransitionEnd, opts);
42 el.removeEventListener('transitionend', onTransitionEnd, opts);
43 };
44 }
45 return unregister;
46};
47/**
48 * Waits for a component to be ready for
49 * both custom element and non-custom element builds.
50 * If non-custom element build, el.componentOnReady
51 * will be used.
52 * For custom element builds, we wait a frame
53 * so that the inner contents of the component
54 * have a chance to render.
55 *
56 * Use this utility rather than calling
57 * el.componentOnReady yourself.
58 */
59const componentOnReady = (el, callback) => {
60 if (el.componentOnReady) {
61 el.componentOnReady().then((resolvedEl) => callback(resolvedEl));
62 }
63 else {
64 raf(() => callback(el));
65 }
66};
67/**
68 * Elements inside of web components sometimes need to inherit global attributes
69 * set on the host. For example, the inner input in `ion-input` should inherit
70 * the `title` attribute that developers set directly on `ion-input`. This
71 * helper function should be called in componentWillLoad and assigned to a variable
72 * that is later used in the render function.
73 *
74 * This does not need to be reactive as changing attributes on the host element
75 * does not trigger a re-render.
76 */
77const inheritAttributes = (el, attributes = []) => {
78 const attributeObject = {};
79 attributes.forEach(attr => {
80 if (el.hasAttribute(attr)) {
81 const value = el.getAttribute(attr);
82 if (value !== null) {
83 attributeObject[attr] = el.getAttribute(attr);
84 }
85 el.removeAttribute(attr);
86 }
87 });
88 return attributeObject;
89};
90const addEventListener = (el, eventName, callback, opts) => {
91 if (typeof window !== 'undefined') {
92 const win = window;
93 const config = win && win.Ionic && win.Ionic.config;
94 if (config) {
95 const ael = config.get('_ael');
96 if (ael) {
97 return ael(el, eventName, callback, opts);
98 }
99 else if (config._ael) {
100 return config._ael(el, eventName, callback, opts);
101 }
102 }
103 }
104 return el.addEventListener(eventName, callback, opts);
105};
106const removeEventListener = (el, eventName, callback, opts) => {
107 if (typeof window !== 'undefined') {
108 const win = window;
109 const config = win && win.Ionic && win.Ionic.config;
110 if (config) {
111 const rel = config.get('_rel');
112 if (rel) {
113 return rel(el, eventName, callback, opts);
114 }
115 else if (config._rel) {
116 return config._rel(el, eventName, callback, opts);
117 }
118 }
119 }
120 return el.removeEventListener(eventName, callback, opts);
121};
122/**
123 * Gets the root context of a shadow dom element
124 * On newer browsers this will be the shadowRoot,
125 * but for older browser this may just be the
126 * element itself.
127 *
128 * Useful for whenever you need to explicitly
129 * do "myElement.shadowRoot!.querySelector(...)".
130 */
131const getElementRoot = (el, fallback = el) => {
132 return el.shadowRoot || fallback;
133};
134/**
135 * Patched version of requestAnimationFrame that avoids ngzone
136 * Use only when you know ngzone should not run
137 */
138const raf = (h) => {
139 if (typeof __zone_symbol__requestAnimationFrame === 'function') {
140 return __zone_symbol__requestAnimationFrame(h);
141 }
142 if (typeof requestAnimationFrame === 'function') {
143 return requestAnimationFrame(h);
144 }
145 return setTimeout(h);
146};
147const hasShadowDom = (el) => {
148 return !!el.shadowRoot && !!el.attachShadow;
149};
150const findItemLabel = (componentEl) => {
151 const itemEl = componentEl.closest('ion-item');
152 if (itemEl) {
153 return itemEl.querySelector('ion-label');
154 }
155 return null;
156};
157const focusElement = (el) => {
158 el.focus();
159 /**
160 * When programmatically focusing an element,
161 * the focus-visible utility will not run because
162 * it is expecting a keyboard event to have triggered this;
163 * however, there are times when we need to manually control
164 * this behavior so we call the `setFocus` method on ion-app
165 * which will let us explicitly set the elements to focus.
166 */
167 if (el.classList.contains('ion-focusable')) {
168 const app = el.closest('ion-app');
169 if (app) {
170 app.setFocus([el]);
171 }
172 }
173};
174/**
175 * This method is used for Ionic's input components that use Shadow DOM. In
176 * order to properly label the inputs to work with screen readers, we need
177 * to get the text content of the label outside of the shadow root and pass
178 * it to the input inside of the shadow root.
179 *
180 * Referencing label elements by id from outside of the component is
181 * impossible due to the shadow boundary, read more here:
182 * https://developer.salesforce.com/blogs/2020/01/accessibility-for-web-components.html
183 *
184 * @param componentEl The shadow element that needs the aria label
185 * @param inputId The unique identifier for the input
186 */
187const getAriaLabel = (componentEl, inputId) => {
188 let labelText;
189 // If the user provides their own label via the aria-labelledby attr
190 // we should use that instead of looking for an ion-label
191 const labelledBy = componentEl.getAttribute('aria-labelledby');
192 // Grab the id off of the component in case they are using
193 // a custom label using the label element
194 const componentId = componentEl.id;
195 let labelId = labelledBy !== null && labelledBy.trim() !== ''
196 ? labelledBy
197 : inputId + '-lbl';
198 let label = labelledBy !== null && labelledBy.trim() !== ''
199 ? document.getElementById(labelledBy)
200 : findItemLabel(componentEl);
201 if (label) {
202 if (labelledBy === null) {
203 label.id = labelId;
204 }
205 labelText = label.textContent;
206 label.setAttribute('aria-hidden', 'true');
207 // if there is no label, check to see if the user has provided
208 // one by setting an id on the component and using the label element
209 }
210 else if (componentId.trim() !== '') {
211 label = document.querySelector(`label[for="${componentId}"]`);
212 if (label) {
213 if (label.id !== '') {
214 labelId = label.id;
215 }
216 else {
217 label.id = labelId = `${componentId}-lbl`;
218 }
219 labelText = label.textContent;
220 }
221 }
222 return { label, labelId, labelText };
223};
224/**
225 * This method is used to add a hidden input to a host element that contains
226 * a Shadow DOM. It does not add the input inside of the Shadow root which
227 * allows it to be picked up inside of forms. It should contain the same
228 * values as the host element.
229 *
230 * @param always Add a hidden input even if the container does not use Shadow
231 * @param container The element where the input will be added
232 * @param name The name of the input
233 * @param value The value of the input
234 * @param disabled If true, the input is disabled
235 */
236const renderHiddenInput = (always, container, name, value, disabled) => {
237 if (always || hasShadowDom(container)) {
238 let input = container.querySelector('input.aux-input');
239 if (!input) {
240 input = container.ownerDocument.createElement('input');
241 input.type = 'hidden';
242 input.classList.add('aux-input');
243 container.appendChild(input);
244 }
245 input.disabled = disabled;
246 input.name = name;
247 input.value = value || '';
248 }
249};
250const clamp = (min, n, max) => {
251 return Math.max(min, Math.min(n, max));
252};
253const assert = (actual, reason) => {
254 if (!actual) {
255 const message = 'ASSERT: ' + reason;
256 console.error(message);
257 debugger; // tslint:disable-line
258 throw new Error(message);
259 }
260};
261const now = (ev) => {
262 return ev.timeStamp || Date.now();
263};
264const pointerCoord = (ev) => {
265 // get X coordinates for either a mouse click
266 // or a touch depending on the given event
267 if (ev) {
268 const changedTouches = ev.changedTouches;
269 if (changedTouches && changedTouches.length > 0) {
270 const touch = changedTouches[0];
271 return { x: touch.clientX, y: touch.clientY };
272 }
273 if (ev.pageX !== undefined) {
274 return { x: ev.pageX, y: ev.pageY };
275 }
276 }
277 return { x: 0, y: 0 };
278};
279/**
280 * @hidden
281 * Given a side, return if it should be on the end
282 * based on the value of dir
283 * @param side the side
284 * @param isRTL whether the application dir is rtl
285 */
286const isEndSide = (side) => {
287 const isRTL = document.dir === 'rtl';
288 switch (side) {
289 case 'start': return isRTL;
290 case 'end': return !isRTL;
291 default:
292 throw new Error(`"${side}" is not a valid value for [side]. Use "start" or "end" instead.`);
293 }
294};
295const debounceEvent = (event, wait) => {
296 const original = event._original || event;
297 return {
298 _original: event,
299 emit: debounce(original.emit.bind(original), wait)
300 };
301};
302const debounce = (func, wait = 0) => {
303 let timer;
304 return (...args) => {
305 clearTimeout(timer);
306 timer = setTimeout(func, wait, ...args);
307 };
308};
309/**
310 * Check whether the two string maps are shallow equal.
311 *
312 * undefined is treated as an empty map.
313 *
314 * @returns whether the keys are the same and the values are shallow equal.
315 */
316const shallowEqualStringMap = (map1, map2) => {
317 map1 !== null && map1 !== void 0 ? map1 : (map1 = {});
318 map2 !== null && map2 !== void 0 ? map2 : (map2 = {});
319 if (map1 === map2) {
320 return true;
321 }
322 const keys1 = Object.keys(map1);
323 if (keys1.length !== Object.keys(map2).length) {
324 return false;
325 }
326 for (const k1 of keys1) {
327 if (!(k1 in map2)) {
328 return false;
329 }
330 if (map1[k1] !== map2[k1]) {
331 return false;
332 }
333 }
334 return true;
335};
336
337export { addEventListener as a, removeEventListener as b, componentOnReady as c, getAriaLabel as d, renderHiddenInput as e, focusElement as f, getElementRoot as g, hasShadowDom as h, inheritAttributes as i, clamp as j, debounceEvent as k, findItemLabel as l, isEndSide as m, assert as n, debounce as o, pointerCoord as p, now as q, raf as r, shallowEqualStringMap as s, transitionEndAsync as t };