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
};