UNPKG

react-klasha

Version:

This is an reactJS library for implementing klasha payment gateway

388 lines (387 loc) 13.7 kB
import { config } from '../global/config'; import { getIonMode } from '../global/ionic-global'; import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button'; import { addEventListener, componentOnReady, getElementRoot, removeEventListener } from './helpers'; let lastId = 0; export const activeAnimations = new WeakMap(); const createController = (tagName) => { return { create(options) { return createOverlay(tagName, options); }, dismiss(data, role, id) { return dismissOverlay(document, data, role, tagName, id); }, async getTop() { return getOverlay(document, tagName); } }; }; export const alertController = /*@__PURE__*/ createController('ion-alert'); export const actionSheetController = /*@__PURE__*/ createController('ion-action-sheet'); export const loadingController = /*@__PURE__*/ createController('ion-loading'); export const modalController = /*@__PURE__*/ createController('ion-modal'); export const pickerController = /*@__PURE__*/ createController('ion-picker'); export const popoverController = /*@__PURE__*/ createController('ion-popover'); export const toastController = /*@__PURE__*/ createController('ion-toast'); export const prepareOverlay = (el) => { /* tslint:disable-next-line */ if (typeof document !== 'undefined') { connectListeners(document); } const overlayIndex = lastId++; el.overlayIndex = overlayIndex; if (!el.hasAttribute('id')) { el.id = `ion-overlay-${overlayIndex}`; } }; export const createOverlay = (tagName, opts) => { /* tslint:disable-next-line */ if (typeof customElements !== 'undefined') { return customElements.whenDefined(tagName).then(() => { const element = document.createElement(tagName); element.classList.add('overlay-hidden'); // convert the passed in overlay options into props // that get passed down into the new overlay Object.assign(element, opts); // append the overlay element to the document body getAppRoot(document).appendChild(element); return new Promise(resolve => componentOnReady(element, resolve)); }); } return Promise.resolve(); }; const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input:not([type=hidden]):not([tabindex^="-"]), textarea:not([tabindex^="-"]), button:not([tabindex^="-"]), select:not([tabindex^="-"]), .ion-focusable:not([tabindex^="-"])'; const innerFocusableQueryString = 'input:not([type=hidden]), textarea, button, select'; const focusFirstDescendant = (ref, overlay) => { let firstInput = ref.querySelector(focusableQueryString); const shadowRoot = firstInput && firstInput.shadowRoot; if (shadowRoot) { // If there are no inner focusable elements, just focus the host element. firstInput = shadowRoot.querySelector(innerFocusableQueryString) || firstInput; } if (firstInput) { firstInput.focus(); } else { // Focus overlay instead of letting focus escape overlay.focus(); } }; const focusLastDescendant = (ref, overlay) => { const inputs = Array.from(ref.querySelectorAll(focusableQueryString)); let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null; const shadowRoot = lastInput && lastInput.shadowRoot; if (shadowRoot) { // If there are no inner focusable elements, just focus the host element. lastInput = shadowRoot.querySelector(innerFocusableQueryString) || lastInput; } if (lastInput) { lastInput.focus(); } else { // Focus overlay instead of letting focus escape overlay.focus(); } }; /** * Traps keyboard focus inside of overlay components. * Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html * This includes the following components: Action Sheet, Alert, Loading, Modal, * Picker, and Popover. * Should NOT include: Toast */ const trapKeyboardFocus = (ev, doc) => { const lastOverlay = getOverlay(doc); const target = ev.target; // If no active overlay, ignore this event if (!lastOverlay || !target) { return; } /** * If we are focusing the overlay, clear * the last focused element so that hitting * tab activates the first focusable element * in the overlay wrapper. */ if (lastOverlay === target) { lastOverlay.lastFocus = undefined; /** * Otherwise, we must be focusing an element * inside of the overlay. The two possible options * here are an input/button/etc or the ion-focus-trap * element. The focus trap element is used to prevent * the keyboard focus from leaving the overlay when * using Tab or screen assistants. */ } else { /** * We do not want to focus the traps, so get the overlay * wrapper element as the traps live outside of the wrapper. */ const overlayRoot = getElementRoot(lastOverlay); if (!overlayRoot.contains(target)) { return; } const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper'); if (!overlayWrapper) { return; } /** * If the target is inside the wrapper, let the browser * focus as normal and keep a log of the last focused element. */ if (overlayWrapper.contains(target)) { lastOverlay.lastFocus = target; } else { /** * Otherwise, we must have focused one of the focus traps. * We need to wrap the focus to either the first element * or the last element. */ /** * Once we call `focusFirstDescendant` and focus the first * descendant, another focus event will fire which will * cause `lastOverlay.lastFocus` to be updated before * we can run the code after that. We will cache the value * here to avoid that. */ const lastFocus = lastOverlay.lastFocus; // Focus the first element in the overlay wrapper focusFirstDescendant(overlayWrapper, lastOverlay); /** * If the cached last focused element is the * same as the active element, then we need * to wrap focus to the last descendant. This happens * when the first descendant is focused, and the user * presses Shift + Tab. The previous line will focus * the same descendant again (the first one), causing * last focus to equal the active element. */ if (lastFocus === doc.activeElement) { focusLastDescendant(overlayWrapper, lastOverlay); } lastOverlay.lastFocus = doc.activeElement; } } }; export const connectListeners = (doc) => { if (lastId === 0) { lastId = 1; doc.addEventListener('focus', ev => trapKeyboardFocus(ev, doc), true); // handle back-button click doc.addEventListener('ionBackButton', ev => { const lastOverlay = getOverlay(doc); if (lastOverlay && lastOverlay.backdropDismiss) { ev.detail.register(OVERLAY_BACK_BUTTON_PRIORITY, () => { return lastOverlay.dismiss(undefined, BACKDROP); }); } }); // handle ESC to close overlay doc.addEventListener('keyup', ev => { if (ev.key === 'Escape') { const lastOverlay = getOverlay(doc); if (lastOverlay && lastOverlay.backdropDismiss) { lastOverlay.dismiss(undefined, BACKDROP); } } }); } }; export const dismissOverlay = (doc, data, role, overlayTag, id) => { const overlay = getOverlay(doc, overlayTag, id); if (!overlay) { return Promise.reject('overlay does not exist'); } return overlay.dismiss(data, role); }; export const getOverlays = (doc, selector) => { if (selector === undefined) { selector = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker,ion-popover,ion-toast'; } return Array.from(doc.querySelectorAll(selector)) .filter(c => c.overlayIndex > 0); }; export const getOverlay = (doc, overlayTag, id) => { const overlays = getOverlays(doc, overlayTag); return (id === undefined) ? overlays[overlays.length - 1] : overlays.find(o => o.id === id); }; /** * When an overlay is presented, the main * focus is the overlay not the page content. * We need to remove the page content from the * accessibility tree otherwise when * users use "read screen from top" gestures with * TalkBack and VoiceOver, the screen reader will begin * to read the content underneath the overlay. * * We need a container where all page components * exist that is separate from where the overlays * are added in the DOM. For most apps, this element * is the top most ion-router-outlet. In the event * that devs are not using a router, * they will need to add the "ion-view-container-root" * id to the element that contains all of their views. * * TODO: If Framework supports having multiple top * level router outlets we would need to update this. * Example: One outlet for side menu and one outlet * for main content. */ export const setRootAriaHidden = (hidden = false) => { const root = getAppRoot(document); const viewContainer = root.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root'); if (!viewContainer) { return; } if (hidden) { viewContainer.setAttribute('aria-hidden', 'true'); } else { viewContainer.removeAttribute('aria-hidden'); } }; export const present = async (overlay, name, iosEnterAnimation, mdEnterAnimation, opts) => { if (overlay.presented) { return; } setRootAriaHidden(true); overlay.presented = true; overlay.willPresent.emit(); const mode = getIonMode(overlay); // get the user's animation fn if one was provided const animationBuilder = (overlay.enterAnimation) ? overlay.enterAnimation : config.get(name, mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); const completed = await overlayAnimation(overlay, animationBuilder, overlay.el, opts); if (completed) { overlay.didPresent.emit(); } /** * When an overlay that steals focus * is dismissed, focus should be returned * to the element that was focused * prior to the overlay opening. Toast * does not steal focus and is excluded * from returning focus as a result. */ if (overlay.el.tagName !== 'ION-TOAST') { focusPreviousElementOnDismiss(overlay.el); } if (overlay.keyboardClose) { overlay.el.focus(); } }; /** * When an overlay component is dismissed, * focus should be returned to the element * that presented the overlay. Otherwise * focus will be set on the body which * means that people using screen readers * or tabbing will need to re-navigate * to where they were before they * opened the overlay. */ const focusPreviousElementOnDismiss = async (overlayEl) => { let previousElement = document.activeElement; if (!previousElement) { return; } const shadowRoot = previousElement && previousElement.shadowRoot; if (shadowRoot) { // If there are no inner focusable elements, just focus the host element. previousElement = shadowRoot.querySelector(innerFocusableQueryString) || previousElement; } await overlayEl.onDidDismiss(); previousElement.focus(); }; export const dismiss = async (overlay, data, role, name, iosLeaveAnimation, mdLeaveAnimation, opts) => { if (!overlay.presented) { return false; } setRootAriaHidden(false); overlay.presented = false; try { // Overlay contents should not be clickable during dismiss overlay.el.style.setProperty('pointer-events', 'none'); overlay.willDismiss.emit({ data, role }); const mode = getIonMode(overlay); const animationBuilder = (overlay.leaveAnimation) ? overlay.leaveAnimation : config.get(name, mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation); // If dismissed via gesture, no need to play leaving animation again if (role !== 'gesture') { await overlayAnimation(overlay, animationBuilder, overlay.el, opts); } overlay.didDismiss.emit({ data, role }); activeAnimations.delete(overlay); } catch (err) { console.error(err); } overlay.el.remove(); return true; }; const getAppRoot = (doc) => { return doc.querySelector('ion-app') || doc.body; }; const overlayAnimation = async (overlay, animationBuilder, baseEl, opts) => { // Make overlay visible in case it's hidden baseEl.classList.remove('overlay-hidden'); const aniRoot = baseEl.shadowRoot || overlay.el; const animation = animationBuilder(aniRoot, opts); if (!overlay.animated || !config.getBoolean('animated', true)) { animation.duration(0); } if (overlay.keyboardClose) { animation.beforeAddWrite(() => { const activeElement = baseEl.ownerDocument.activeElement; if (activeElement && activeElement.matches('input, ion-input, ion-textarea')) { activeElement.blur(); } }); } const activeAni = activeAnimations.get(overlay) || []; activeAnimations.set(overlay, [...activeAni, animation]); await animation.play(); return true; }; export const eventMethod = (element, eventName) => { let resolve; const promise = new Promise(r => resolve = r); onceEvent(element, eventName, (event) => { resolve(event.detail); }); return promise; }; export const onceEvent = (element, eventName, callback) => { const handler = (ev) => { removeEventListener(element, eventName, handler); callback(ev); }; addEventListener(element, eventName, handler); }; export const isCancel = (role) => { return role === 'cancel' || role === BACKDROP; }; const defaultGate = (h) => h(); export const safeCall = (handler, arg) => { if (typeof handler === 'function') { const jmp = config.get('_zoneGate', defaultGate); return jmp(() => { try { return handler(arg); } catch (e) { console.error(e); } }); } return undefined; }; export const BACKDROP = 'backdrop';