helpers.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { p as printIonError } from './index4.js';
  5. const transitionEndAsync = (el, expectedDuration = 0) => {
  6. return new Promise((resolve) => {
  7. transitionEnd(el, expectedDuration, resolve);
  8. });
  9. };
  10. /**
  11. * Allows developer to wait for a transition
  12. * to finish and fallback to a timer if the
  13. * transition is cancelled or otherwise
  14. * never finishes. Also see transitionEndAsync
  15. * which is an await-able version of this.
  16. */
  17. const transitionEnd = (el, expectedDuration = 0, callback) => {
  18. let unRegTrans;
  19. let animationTimeout;
  20. const opts = { passive: true };
  21. const ANIMATION_FALLBACK_TIMEOUT = 500;
  22. const unregister = () => {
  23. if (unRegTrans) {
  24. unRegTrans();
  25. }
  26. };
  27. const onTransitionEnd = (ev) => {
  28. if (ev === undefined || el === ev.target) {
  29. unregister();
  30. callback(ev);
  31. }
  32. };
  33. if (el) {
  34. el.addEventListener('webkitTransitionEnd', onTransitionEnd, opts);
  35. el.addEventListener('transitionend', onTransitionEnd, opts);
  36. animationTimeout = setTimeout(onTransitionEnd, expectedDuration + ANIMATION_FALLBACK_TIMEOUT);
  37. unRegTrans = () => {
  38. if (animationTimeout !== undefined) {
  39. clearTimeout(animationTimeout);
  40. animationTimeout = undefined;
  41. }
  42. el.removeEventListener('webkitTransitionEnd', onTransitionEnd, opts);
  43. el.removeEventListener('transitionend', onTransitionEnd, opts);
  44. };
  45. }
  46. return unregister;
  47. };
  48. /**
  49. * Waits for a component to be ready for
  50. * both custom element and non-custom element builds.
  51. * If non-custom element build, el.componentOnReady
  52. * will be used.
  53. * For custom element builds, we wait a frame
  54. * so that the inner contents of the component
  55. * have a chance to render.
  56. *
  57. * Use this utility rather than calling
  58. * el.componentOnReady yourself.
  59. */
  60. const componentOnReady = (el, callback) => {
  61. if (el.componentOnReady) {
  62. // eslint-disable-next-line custom-rules/no-component-on-ready-method
  63. el.componentOnReady().then((resolvedEl) => callback(resolvedEl));
  64. }
  65. else {
  66. raf(() => callback(el));
  67. }
  68. };
  69. /**
  70. * This functions checks if a Stencil component is using
  71. * the lazy loaded build of Stencil. Returns `true` if
  72. * the component is lazy loaded. Returns `false` otherwise.
  73. */
  74. const hasLazyBuild = (stencilEl) => {
  75. return stencilEl.componentOnReady !== undefined;
  76. };
  77. /**
  78. * Elements inside of web components sometimes need to inherit global attributes
  79. * set on the host. For example, the inner input in `ion-input` should inherit
  80. * the `title` attribute that developers set directly on `ion-input`. This
  81. * helper function should be called in componentWillLoad and assigned to a variable
  82. * that is later used in the render function.
  83. *
  84. * This does not need to be reactive as changing attributes on the host element
  85. * does not trigger a re-render.
  86. */
  87. const inheritAttributes = (el, attributes = []) => {
  88. const attributeObject = {};
  89. attributes.forEach((attr) => {
  90. if (el.hasAttribute(attr)) {
  91. const value = el.getAttribute(attr);
  92. if (value !== null) {
  93. attributeObject[attr] = el.getAttribute(attr);
  94. }
  95. el.removeAttribute(attr);
  96. }
  97. });
  98. return attributeObject;
  99. };
  100. /**
  101. * List of available ARIA attributes + `role`.
  102. * Removed deprecated attributes.
  103. * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes
  104. */
  105. const ariaAttributes = [
  106. 'role',
  107. 'aria-activedescendant',
  108. 'aria-atomic',
  109. 'aria-autocomplete',
  110. 'aria-braillelabel',
  111. 'aria-brailleroledescription',
  112. 'aria-busy',
  113. 'aria-checked',
  114. 'aria-colcount',
  115. 'aria-colindex',
  116. 'aria-colindextext',
  117. 'aria-colspan',
  118. 'aria-controls',
  119. 'aria-current',
  120. 'aria-describedby',
  121. 'aria-description',
  122. 'aria-details',
  123. 'aria-disabled',
  124. 'aria-errormessage',
  125. 'aria-expanded',
  126. 'aria-flowto',
  127. 'aria-haspopup',
  128. 'aria-hidden',
  129. 'aria-invalid',
  130. 'aria-keyshortcuts',
  131. 'aria-label',
  132. 'aria-labelledby',
  133. 'aria-level',
  134. 'aria-live',
  135. 'aria-multiline',
  136. 'aria-multiselectable',
  137. 'aria-orientation',
  138. 'aria-owns',
  139. 'aria-placeholder',
  140. 'aria-posinset',
  141. 'aria-pressed',
  142. 'aria-readonly',
  143. 'aria-relevant',
  144. 'aria-required',
  145. 'aria-roledescription',
  146. 'aria-rowcount',
  147. 'aria-rowindex',
  148. 'aria-rowindextext',
  149. 'aria-rowspan',
  150. 'aria-selected',
  151. 'aria-setsize',
  152. 'aria-sort',
  153. 'aria-valuemax',
  154. 'aria-valuemin',
  155. 'aria-valuenow',
  156. 'aria-valuetext',
  157. ];
  158. /**
  159. * Returns an array of aria attributes that should be copied from
  160. * the shadow host element to a target within the light DOM.
  161. * @param el The element that the attributes should be copied from.
  162. * @param ignoreList The list of aria-attributes to ignore reflecting and removing from the host.
  163. * Use this in instances where we manually specify aria attributes on the `<Host>` element.
  164. */
  165. const inheritAriaAttributes = (el, ignoreList) => {
  166. let attributesToInherit = ariaAttributes;
  167. if (ignoreList && ignoreList.length > 0) {
  168. attributesToInherit = attributesToInherit.filter((attr) => !ignoreList.includes(attr));
  169. }
  170. return inheritAttributes(el, attributesToInherit);
  171. };
  172. const addEventListener = (el, eventName, callback, opts) => {
  173. return el.addEventListener(eventName, callback, opts);
  174. };
  175. const removeEventListener = (el, eventName, callback, opts) => {
  176. return el.removeEventListener(eventName, callback, opts);
  177. };
  178. /**
  179. * Gets the root context of a shadow dom element
  180. * On newer browsers this will be the shadowRoot,
  181. * but for older browser this may just be the
  182. * element itself.
  183. *
  184. * Useful for whenever you need to explicitly
  185. * do "myElement.shadowRoot!.querySelector(...)".
  186. */
  187. const getElementRoot = (el, fallback = el) => {
  188. return el.shadowRoot || fallback;
  189. };
  190. /**
  191. * Patched version of requestAnimationFrame that avoids ngzone
  192. * Use only when you know ngzone should not run
  193. */
  194. const raf = (h) => {
  195. if (typeof __zone_symbol__requestAnimationFrame === 'function') {
  196. return __zone_symbol__requestAnimationFrame(h);
  197. }
  198. if (typeof requestAnimationFrame === 'function') {
  199. return requestAnimationFrame(h);
  200. }
  201. return setTimeout(h);
  202. };
  203. const hasShadowDom = (el) => {
  204. return !!el.shadowRoot && !!el.attachShadow;
  205. };
  206. const focusVisibleElement = (el) => {
  207. el.focus();
  208. /**
  209. * When programmatically focusing an element,
  210. * the focus-visible utility will not run because
  211. * it is expecting a keyboard event to have triggered this;
  212. * however, there are times when we need to manually control
  213. * this behavior so we call the `setFocus` method on ion-app
  214. * which will let us explicitly set the elements to focus.
  215. */
  216. if (el.classList.contains('ion-focusable')) {
  217. const app = el.closest('ion-app');
  218. if (app) {
  219. app.setFocus([el]);
  220. }
  221. }
  222. };
  223. /**
  224. * This method is used to add a hidden input to a host element that contains
  225. * a Shadow DOM. It does not add the input inside of the Shadow root which
  226. * allows it to be picked up inside of forms. It should contain the same
  227. * values as the host element.
  228. *
  229. * @param always Add a hidden input even if the container does not use Shadow
  230. * @param container The element where the input will be added
  231. * @param name The name of the input
  232. * @param value The value of the input
  233. * @param disabled If true, the input is disabled
  234. */
  235. const renderHiddenInput = (always, container, name, value, disabled) => {
  236. if (always || hasShadowDom(container)) {
  237. let input = container.querySelector('input.aux-input');
  238. if (!input) {
  239. input = container.ownerDocument.createElement('input');
  240. input.type = 'hidden';
  241. input.classList.add('aux-input');
  242. container.appendChild(input);
  243. }
  244. input.disabled = disabled;
  245. input.name = name;
  246. input.value = value || '';
  247. }
  248. };
  249. const clamp = (min, n, max) => {
  250. return Math.max(min, Math.min(n, max));
  251. };
  252. const assert = (actual, reason) => {
  253. if (!actual) {
  254. const message = 'ASSERT: ' + reason;
  255. printIonError(message);
  256. debugger; // eslint-disable-line
  257. throw new Error(message);
  258. }
  259. };
  260. const pointerCoord = (ev) => {
  261. // get X coordinates for either a mouse click
  262. // or a touch depending on the given event
  263. if (ev) {
  264. const changedTouches = ev.changedTouches;
  265. if (changedTouches && changedTouches.length > 0) {
  266. const touch = changedTouches[0];
  267. return { x: touch.clientX, y: touch.clientY };
  268. }
  269. if (ev.pageX !== undefined) {
  270. return { x: ev.pageX, y: ev.pageY };
  271. }
  272. }
  273. return { x: 0, y: 0 };
  274. };
  275. /**
  276. * @hidden
  277. * Given a side, return if it should be on the end
  278. * based on the value of dir
  279. * @param side the side
  280. * @param isRTL whether the application dir is rtl
  281. */
  282. const isEndSide = (side) => {
  283. const isRTL = document.dir === 'rtl';
  284. switch (side) {
  285. case 'start':
  286. return isRTL;
  287. case 'end':
  288. return !isRTL;
  289. default:
  290. throw new Error(`"${side}" is not a valid value for [side]. Use "start" or "end" instead.`);
  291. }
  292. };
  293. const debounceEvent = (event, wait) => {
  294. const original = event._original || event;
  295. return {
  296. _original: event,
  297. emit: debounce(original.emit.bind(original), wait),
  298. };
  299. };
  300. const debounce = (func, wait = 0) => {
  301. let timer;
  302. return (...args) => {
  303. clearTimeout(timer);
  304. timer = setTimeout(func, wait, ...args);
  305. };
  306. };
  307. /**
  308. * Check whether the two string maps are shallow equal.
  309. *
  310. * undefined is treated as an empty map.
  311. *
  312. * @returns whether the keys are the same and the values are shallow equal.
  313. */
  314. const shallowEqualStringMap = (map1, map2) => {
  315. map1 !== null && map1 !== void 0 ? map1 : (map1 = {});
  316. map2 !== null && map2 !== void 0 ? map2 : (map2 = {});
  317. if (map1 === map2) {
  318. return true;
  319. }
  320. const keys1 = Object.keys(map1);
  321. if (keys1.length !== Object.keys(map2).length) {
  322. return false;
  323. }
  324. for (const k1 of keys1) {
  325. if (!(k1 in map2)) {
  326. return false;
  327. }
  328. if (map1[k1] !== map2[k1]) {
  329. return false;
  330. }
  331. }
  332. return true;
  333. };
  334. /**
  335. * Checks input for usable number. Not NaN and not Infinite.
  336. */
  337. const isSafeNumber = (input) => {
  338. return typeof input === 'number' && !isNaN(input) && isFinite(input);
  339. };
  340. export { addEventListener as a, removeEventListener as b, componentOnReady as c, inheritAttributes as d, renderHiddenInput as e, focusVisibleElement as f, getElementRoot as g, hasShadowDom as h, inheritAriaAttributes as i, hasLazyBuild as j, clamp as k, debounceEvent as l, isEndSide as m, assert as n, isSafeNumber as o, debounce as p, pointerCoord as q, raf as r, shallowEqualStringMap as s, transitionEndAsync as t };