|
| 1 | +import { DOM } from 'aurelia-pal'; |
| 2 | +import { transient } from 'aurelia-dependency-injection'; |
| 3 | +import { Renderer } from './renderer'; |
| 4 | +import { DialogController } from './dialog-controller'; |
| 5 | +import { transitionEvent, hasTransition } from './dialog-renderer'; |
| 6 | + |
| 7 | +const containerTagName = 'dialog'; |
| 8 | +let body: HTMLBodyElement; |
| 9 | + |
| 10 | +@transient() |
| 11 | +export class NativeDialogRenderer implements Renderer { |
| 12 | + public static dialogControllers: DialogController[] = []; |
| 13 | + |
| 14 | + public static keyboardEventHandler(e: KeyboardEvent) { |
| 15 | + const key = (e.code || e.key) === 'Enter' || e.keyCode === 13 |
| 16 | + ? 'Enter' |
| 17 | + : undefined; |
| 18 | + |
| 19 | + if (!key) { return; } |
| 20 | + const top = NativeDialogRenderer.dialogControllers[NativeDialogRenderer.dialogControllers.length - 1]; |
| 21 | + if (!top || !top.settings.keyboard) { return; } |
| 22 | + const keyboard = top.settings.keyboard; |
| 23 | + if (key === 'Enter' && (keyboard === key || (Array.isArray(keyboard) && keyboard.indexOf(key) > -1))) { |
| 24 | + top.ok(); |
| 25 | + } |
| 26 | + } |
| 27 | + |
| 28 | + public static trackController(dialogController: DialogController): void { |
| 29 | + if (!NativeDialogRenderer.dialogControllers.length) { |
| 30 | + DOM.addEventListener('keyup', NativeDialogRenderer.keyboardEventHandler, false); |
| 31 | + } |
| 32 | + NativeDialogRenderer.dialogControllers.push(dialogController); |
| 33 | + } |
| 34 | + |
| 35 | + public static untrackController(dialogController: DialogController): void { |
| 36 | + const i = NativeDialogRenderer.dialogControllers.indexOf(dialogController); |
| 37 | + if (i !== -1) { |
| 38 | + NativeDialogRenderer.dialogControllers.splice(i, 1); |
| 39 | + } |
| 40 | + if (!NativeDialogRenderer.dialogControllers.length) { |
| 41 | + DOM.removeEventListener('keyup', NativeDialogRenderer.keyboardEventHandler, false); |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + private stopPropagation: (e: MouseEvent & { _aureliaDialogHostClicked: boolean }) => void; |
| 46 | + private closeDialogClick: (e: MouseEvent & { _aureliaDialogHostClicked: boolean }) => void; |
| 47 | + private dialogCancel: (e: Event) => void; |
| 48 | + |
| 49 | + public dialogContainer: HTMLDialogElement; |
| 50 | + public host: Element; |
| 51 | + public anchor: Element; |
| 52 | + |
| 53 | + private getOwnElements(parent: Element, selector: string): Element[] { |
| 54 | + const elements = parent.querySelectorAll(selector); |
| 55 | + const own: Element[] = []; |
| 56 | + for (let i = 0; i < elements.length; i++) { |
| 57 | + if (elements[i].parentElement === parent) { |
| 58 | + own.push(elements[i]); |
| 59 | + } |
| 60 | + } |
| 61 | + return own; |
| 62 | + } |
| 63 | + |
| 64 | + private attach(dialogController: DialogController): void { |
| 65 | + const spacingWrapper = DOM.createElement('div'); // TODO: check if redundant |
| 66 | + spacingWrapper.appendChild(this.anchor); |
| 67 | + this.dialogContainer = DOM.createElement(containerTagName) as HTMLDialogElement; |
| 68 | + if ((window as any).dialogPolyfill) { |
| 69 | + (window as any).dialogPolyfill.registerDialog(this.dialogContainer); |
| 70 | + } |
| 71 | + |
| 72 | + this.dialogContainer.appendChild(spacingWrapper); |
| 73 | + |
| 74 | + const lastContainer = this.getOwnElements(this.host, containerTagName).pop(); |
| 75 | + if (lastContainer && lastContainer.parentElement) { |
| 76 | + this.host.insertBefore(this.dialogContainer, lastContainer.nextSibling); |
| 77 | + } else { |
| 78 | + this.host.insertBefore(this.dialogContainer, this.host.firstChild); |
| 79 | + } |
| 80 | + dialogController.controller.attached(); |
| 81 | + this.host.classList.add('ux-dialog-open'); |
| 82 | + } |
| 83 | + |
| 84 | + private detach(dialogController: DialogController): void { |
| 85 | + // This check only seems required for the polyfill |
| 86 | + if (this.dialogContainer.hasAttribute('open')) { |
| 87 | + this.dialogContainer.close(); |
| 88 | + } |
| 89 | + |
| 90 | + this.host.removeChild(this.dialogContainer); |
| 91 | + dialogController.controller.detached(); |
| 92 | + if (!NativeDialogRenderer.dialogControllers.length) { |
| 93 | + this.host.classList.remove('ux-dialog-open'); |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + private setAsActive(): void { |
| 98 | + this.dialogContainer.showModal(); |
| 99 | + this.dialogContainer.classList.add('active'); |
| 100 | + } |
| 101 | + |
| 102 | + private setAsInactive(): void { |
| 103 | + this.dialogContainer.classList.remove('active'); |
| 104 | + } |
| 105 | + |
| 106 | + private setupEventHandling(dialogController: DialogController): void { |
| 107 | + this.stopPropagation = e => { e._aureliaDialogHostClicked = true; }; |
| 108 | + this.closeDialogClick = e => { |
| 109 | + if (dialogController.settings.overlayDismiss && !e._aureliaDialogHostClicked) { |
| 110 | + dialogController.cancel(); |
| 111 | + } |
| 112 | + }; |
| 113 | + this.dialogCancel = e => { |
| 114 | + const keyboard = dialogController.settings.keyboard; |
| 115 | + const key = 'Escape'; |
| 116 | + |
| 117 | + if (keyboard === true || keyboard === key || (Array.isArray(keyboard) && keyboard.indexOf(key) > -1)) { |
| 118 | + dialogController.cancel(); |
| 119 | + } else { |
| 120 | + e.preventDefault(); |
| 121 | + } |
| 122 | + }; |
| 123 | + this.dialogContainer.addEventListener('click', this.closeDialogClick); |
| 124 | + this.dialogContainer.addEventListener('cancel', this.dialogCancel); |
| 125 | + this.anchor.addEventListener('click', this.stopPropagation); |
| 126 | + } |
| 127 | + |
| 128 | + private clearEventHandling(): void { |
| 129 | + this.dialogContainer.removeEventListener('click', this.closeDialogClick); |
| 130 | + this.dialogContainer.removeEventListener('cancel', this.dialogCancel); |
| 131 | + this.anchor.removeEventListener('click', this.stopPropagation); |
| 132 | + } |
| 133 | + |
| 134 | + private awaitTransition(setActiveInactive: () => void, ignore: boolean): Promise<void> { |
| 135 | + return new Promise<void>(resolve => { |
| 136 | + // tslint:disable-next-line:no-this-assignment |
| 137 | + const renderer = this; |
| 138 | + const eventName = transitionEvent(); |
| 139 | + function onTransitionEnd(e: TransitionEvent): void { |
| 140 | + if (e.target !== renderer.dialogContainer) { |
| 141 | + return; |
| 142 | + } |
| 143 | + renderer.dialogContainer.removeEventListener(eventName, onTransitionEnd); |
| 144 | + resolve(); |
| 145 | + } |
| 146 | + |
| 147 | + if (ignore || !hasTransition(this.dialogContainer)) { |
| 148 | + resolve(); |
| 149 | + } else { |
| 150 | + this.dialogContainer.addEventListener(eventName, onTransitionEnd); |
| 151 | + } |
| 152 | + setActiveInactive(); |
| 153 | + }); |
| 154 | + } |
| 155 | + |
| 156 | + public getDialogContainer(): Element { |
| 157 | + return this.anchor || (this.anchor = DOM.createElement('div')); |
| 158 | + } |
| 159 | + |
| 160 | + public showDialog(dialogController: DialogController): Promise<void> { |
| 161 | + if (!body) { |
| 162 | + body = DOM.querySelectorAll('body')[0] as HTMLBodyElement; |
| 163 | + } |
| 164 | + if (dialogController.settings.host) { |
| 165 | + this.host = dialogController.settings.host; |
| 166 | + } else { |
| 167 | + this.host = body; |
| 168 | + } |
| 169 | + const settings = dialogController.settings; |
| 170 | + this.attach(dialogController); |
| 171 | + |
| 172 | + if (typeof settings.position === 'function') { |
| 173 | + settings.position(this.dialogContainer); |
| 174 | + } |
| 175 | + |
| 176 | + NativeDialogRenderer.trackController(dialogController); |
| 177 | + this.setupEventHandling(dialogController); |
| 178 | + return this.awaitTransition(() => this.setAsActive(), dialogController.settings.ignoreTransitions as boolean); |
| 179 | + } |
| 180 | + |
| 181 | + public hideDialog(dialogController: DialogController): Promise<void> { |
| 182 | + this.clearEventHandling(); |
| 183 | + NativeDialogRenderer.untrackController(dialogController); |
| 184 | + return this.awaitTransition(() => this.setAsInactive(), dialogController.settings.ignoreTransitions as boolean) |
| 185 | + .then(() => { this.detach(dialogController); }); |
| 186 | + } |
| 187 | +} |
0 commit comments