import { AlloyComponent, AlloyEvents, AlloyTriggers, CustomEvent, Keying, NativeEvents, Reflecting, Representing } from '@ephox/alloy'; import { Dialog, DialogManager } from '@ephox/bridge'; import { Result, Fun } from '@ephox/katamari'; import { Attribute, Compare, Focus, SugarElement, SugarShadowDom } from '@ephox/sugar'; import { formActionEvent, FormActionEvent, formBlockEvent, FormBlockEvent, FormCancelEvent, formCancelEvent, FormChangeEvent, formChangeEvent, FormCloseEvent, formCloseEvent, FormSubmitEvent, formSubmitEvent, formTabChangeEvent, FormTabChangeEvent, formUnblockEvent, FormUnblockEvent } from '../general/FormEvents'; import * as NavigableObject from '../general/NavigableObject'; export interface ExtraListeners { readonly onBlock: (blockEvent: FormBlockEvent) => void; readonly onUnblock: () => void; readonly onClose: () => void; } interface EventSpec { readonly onClose: () => void; readonly onCancel: (api: A) => void; } type FireApiCallback, E extends CustomEvent> = (api: A, spec: S, event: E, self: AlloyComponent) => void; type FireApiFunc> = (name: string, f: FireApiCallback) => AlloyEvents.AlloyEventKeyAndHandler; const initCommonEvents = >(fireApiEvent: FireApiFunc, extras: ExtraListeners): AlloyEvents.AlloyEventKeyAndHandler[] => [ // When focus moves onto a tab-placeholder, skip to the next thing in the tab sequence AlloyEvents.runWithTarget(NativeEvents.focusin(), NavigableObject.onFocus), // TODO: Test if disabled first. fireApiEvent(formCloseEvent, (_api: A, spec: S, _event, self) => { // TINY-9148: Safari scrolls down to the sink if the dialog is selected before removing, // so we should blur the currently active element beforehand. Focus.active(SugarShadowDom.getRootNode(self.element)).fold(Fun.noop, Focus.blur); extras.onClose(); spec.onClose(); }), // TODO: Test if disabled first. fireApiEvent(formCancelEvent, (api, spec, _event, self) => { spec.onCancel(api); AlloyTriggers.emit(self, formCloseEvent); }), AlloyEvents.run(formUnblockEvent, (_c, _se) => extras.onUnblock()), AlloyEvents.run(formBlockEvent, (_c, se) => extras.onBlock(se.event)) ]; const initUrlDialog = (getInstanceApi: () => Dialog.UrlDialogInstanceApi, extras: ExtraListeners): AlloyEvents.AlloyEventKeyAndHandler[] => { const fireApiEvent: FireApiFunc = (eventName, f) => AlloyEvents.run(eventName, (c, se) => { withSpec(c, (spec, _c) => { f(getInstanceApi(), spec, se.event, c); }); }); const withSpec = (c: AlloyComponent, f: (spec: Dialog.UrlDialog, c: AlloyComponent) => void): void => { Reflecting.getState(c).get().each((currentDialog: Dialog.UrlDialog) => { f(currentDialog, c); }); }; return [ ...initCommonEvents(fireApiEvent, extras), fireApiEvent(formActionEvent, (api, spec, event) => { spec.onAction(api, { name: event.name }); }) ]; }; const initDialog = (getInstanceApi: () => Dialog.DialogInstanceApi, extras: ExtraListeners, getSink: () => Result): AlloyEvents.AlloyEventKeyAndHandler[] => { const fireApiEvent: FireApiFunc, Dialog.Dialog> = (eventName, f) => AlloyEvents.run(eventName, (c, se) => { withSpec(c, (spec, _c) => { f(getInstanceApi(), spec, se.event, c); }); }); const withSpec = (c: AlloyComponent, f: (spec: Dialog.Dialog, c: AlloyComponent) => void): void => { Reflecting.getState(c).get().each((currentDialogInit: DialogManager.DialogInit) => { f(currentDialogInit.internalDialog, c); }); }; return [ ...initCommonEvents, Dialog.Dialog>(fireApiEvent, extras), fireApiEvent(formSubmitEvent, (api, spec) => spec.onSubmit(api)), fireApiEvent>(formChangeEvent, (api, spec, event) => { spec.onChange(api, { name: event.name }); }), fireApiEvent(formActionEvent, (api, spec, event, component) => { const focusIn = () => Keying.focusIn(component); const isDisabled = (focused: SugarElement) => Attribute.has(focused, 'disabled') || Attribute.getOpt(focused, 'aria-disabled').exists((val) => val === 'true'); const rootNode = SugarShadowDom.getRootNode(component.element); const current = Focus.active(rootNode); spec.onAction(api, { name: event.name, value: event.value }); Focus.active(rootNode).fold(focusIn, (focused) => { // We need to check if the focused element is disabled because apparently firefox likes to leave focus on disabled elements. if (isDisabled(focused)) { focusIn(); // And we need the below check for IE, which likes to leave focus on the parent of disabled elements } else if (current.exists((cur) => Compare.contains(focused, cur) && isDisabled(cur))) { focusIn(); // Lastly if something outside the sink has focus then return the focus back to the dialog } else { getSink().toOptional() .filter((sink) => !Compare.contains(sink.element, focused)) .each(focusIn); } }); }), fireApiEvent(formTabChangeEvent, (api, spec, event) => { spec.onTabChange(api, { newTabName: event.name, oldTabName: event.oldName }); }), // When the dialog is being closed, store the current state of the form AlloyEvents.runOnDetached((component) => { const api = getInstanceApi(); Representing.setValue(component, api.getData()); }) ]; }; export const SilverDialogEvents = { initUrlDialog, initDialog };