import { AddEventsBehaviour, AlloyComponent, AlloyEvents, AlloyTriggers, Behaviour, Boxes, Docking, GuiFactory, HotspotAnchorSpec, InlineView, Keying, MakeshiftAnchorSpec, ModalDialog, NodeAnchorSpec, SelectionAnchorSpec, SystemEvents } from '@ephox/alloy'; import { StructureProcessor, StructureSchema } from '@ephox/boulder'; import { Dialog, DialogManager } from '@ephox/bridge'; import { Optional, Singleton } from '@ephox/katamari'; import { SelectorExists, SugarBody, SugarElement } from '@ephox/sugar'; import Editor from 'tinymce/core/api/Editor'; import { WindowManagerImpl, WindowParams } from 'tinymce/core/api/WindowManager'; import * as Options from '../../api/Options'; import { UiFactoryBackstagePair } from '../../backstage/Backstage'; import { formCancelEvent } from '../general/FormEvents'; import { renderDialog } from '../window/SilverDialog'; import { renderInlineDialog } from '../window/SilverInlineDialog'; import { renderUrlDialog } from '../window/SilverUrlDialog'; import * as AlertDialog from './AlertDialog'; import * as ConfirmDialog from './ConfirmDialog'; export interface WindowManagerSetup { readonly backstages: UiFactoryBackstagePair; readonly editor: Editor; } type InlineDialogAnchor = HotspotAnchorSpec | MakeshiftAnchorSpec | NodeAnchorSpec | SelectionAnchorSpec; const validateData = (data: Partial, validator: StructureProcessor) => StructureSchema.getOrDie(StructureSchema.asRaw('data', validator, data)); const isAlertOrConfirmDialog = (target: SugarElement): boolean => SelectorExists.closest(target, '.tox-alert-dialog') || SelectorExists.closest(target, '.tox-confirm-dialog'); const inlineAdditionalBehaviours = (editor: Editor, isStickyToolbar: boolean, isToolbarLocationTop: boolean): Behaviour.NamedConfiguredBehaviour[] => { // When using sticky toolbars it already handles the docking behaviours so applying docking would // do nothing except add additional processing when scrolling, so we don't want to include it here // (Except when the toolbar is located at the bottom since the anchor will be at the top) if (isStickyToolbar && isToolbarLocationTop) { return [ ]; } else { return [ Docking.config({ contextual: { lazyContext: () => Optional.some(Boxes.box(SugarElement.fromDom(editor.getContentAreaContainer()))), fadeInClass: 'tox-dialog-dock-fadein', fadeOutClass: 'tox-dialog-dock-fadeout', transitionClass: 'tox-dialog-dock-transition' }, modes: [ 'top' ] }) ]; } }; const setup = (extras: WindowManagerSetup): WindowManagerImpl => { const editor = extras.editor; const isStickyToolbar = Options.isStickyToolbar(editor); // Alert and Confirm dialogs are Modal Dialogs const alertDialog = AlertDialog.setup(extras.backstages.dialog); const confirmDialog = ConfirmDialog.setup(extras.backstages.dialog); const open = (config: Dialog.DialogSpec, params: WindowParams | undefined, closeWindow: (dialogApi: Dialog.DialogInstanceApi) => void): Dialog.DialogInstanceApi => { if (params !== undefined && params.inline === 'toolbar') { return openInlineDialog(config, extras.backstages.popup.shared.anchors.inlineDialog(), closeWindow, params.ariaAttrs); } else if (params !== undefined && params.inline === 'cursor') { return openInlineDialog(config, extras.backstages.popup.shared.anchors.cursor(), closeWindow, params.ariaAttrs); } else { return openModalDialog(config, closeWindow); } }; const openUrl = (config: Dialog.UrlDialogSpec, closeWindow: (dialogApi: Dialog.UrlDialogInstanceApi) => void) => openModalUrlDialog(config, closeWindow); const openModalUrlDialog = (config: Dialog.UrlDialogSpec, closeWindow: (dialogApi: Dialog.UrlDialogInstanceApi) => void) => { const factory = (contents: Dialog.UrlDialog): Dialog.UrlDialogInstanceApi => { const dialog = renderUrlDialog( contents, { closeWindow: () => { ModalDialog.hide(dialog.dialog); closeWindow(dialog.instanceApi); } }, editor, extras.backstages.dialog ); ModalDialog.show(dialog.dialog); return dialog.instanceApi; }; return DialogManager.DialogManager.openUrl(factory, config); }; const openModalDialog = (config: Dialog.DialogSpec, closeWindow: (dialogApi: Dialog.DialogInstanceApi) => void): Dialog.DialogInstanceApi => { const factory = (contents: Dialog.Dialog, internalInitialData: Partial, dataValidator: StructureProcessor): Dialog.DialogInstanceApi => { // We used to validate data here, but it's done by the instanceApi.setData call below. const initialData = internalInitialData; const dialogInit: DialogManager.DialogInit = { dataValidator, initialData, internalDialog: contents }; const dialog = renderDialog( dialogInit, { redial: DialogManager.DialogManager.redial, closeWindow: () => { ModalDialog.hide(dialog.dialog); closeWindow(dialog.instanceApi); } }, extras.backstages.dialog ); ModalDialog.show(dialog.dialog); dialog.instanceApi.setData(initialData); return dialog.instanceApi; }; return DialogManager.DialogManager.open(factory, config); }; const openInlineDialog = (config: Dialog.DialogSpec, anchor: InlineDialogAnchor, closeWindow: (dialogApi: Dialog.DialogInstanceApi) => void, ariaAttrs: boolean = false): Dialog.DialogInstanceApi => { const factory = (contents: Dialog.Dialog, internalInitialData: Partial, dataValidator: StructureProcessor): Dialog.DialogInstanceApi => { const initialData = validateData(internalInitialData, dataValidator); const inlineDialog = Singleton.value(); const isToolbarLocationTop = extras.backstages.popup.shared.header.isPositionedAtTop(); const dialogInit = { dataValidator, initialData, internalDialog: contents }; const refreshDocking = () => inlineDialog.on((dialog) => { InlineView.reposition(dialog); Docking.refresh(dialog); }); const dialogUi = renderInlineDialog( dialogInit, { redial: DialogManager.DialogManager.redial, closeWindow: () => { inlineDialog.on(InlineView.hide); editor.off('ResizeEditor', refreshDocking); inlineDialog.clear(); closeWindow(dialogUi.instanceApi); } }, extras.backstages.popup, ariaAttrs ); const inlineDialogComp = GuiFactory.build(InlineView.sketch({ lazySink: extras.backstages.popup.shared.getSink, dom: { tag: 'div', classes: [ ] }, // Fires the default dismiss event. fireDismissalEventInstead: { }, ...isToolbarLocationTop ? { } : { fireRepositionEventInstead: { }}, inlineBehaviours: Behaviour.derive([ AddEventsBehaviour.config('window-manager-inline-events', [ AlloyEvents.run(SystemEvents.dismissRequested(), (_comp, _se) => { AlloyTriggers.emit(dialogUi.dialog, formCancelEvent); }) ]), ...inlineAdditionalBehaviours(editor, isStickyToolbar, isToolbarLocationTop) ]), // Treat alert or confirm dialogs as part of the inline dialog isExtraPart: (_comp, target) => isAlertOrConfirmDialog(target) })); inlineDialog.set(inlineDialogComp); // Position the inline dialog InlineView.showWithin( inlineDialogComp, GuiFactory.premade(dialogUi.dialog), { anchor }, Optional.some(SugarBody.body()) ); // Refresh the docking position if not using a sticky toolbar if (!isStickyToolbar || !isToolbarLocationTop) { Docking.refresh(inlineDialogComp); // Bind to the editor resize event and update docking as needed. We don't need to worry about // 'ResizeWindow` as that's handled by docking already. editor.on('ResizeEditor', refreshDocking); } // Set the initial data in the dialog and focus the first focusable item dialogUi.instanceApi.setData(initialData); Keying.focusIn(dialogUi.dialog); return dialogUi.instanceApi; }; return DialogManager.DialogManager.open(factory, config); }; const confirm = (message: string, callback: (state: boolean) => void) => { confirmDialog.open(message, callback); }; const alert = (message: string, callback: () => void) => { alertDialog.open(message, callback); }; const close = (instanceApi: Dialog.DialogInstanceApi | Dialog.UrlDialogInstanceApi) => { instanceApi.close(); }; return { open, openUrl, alert, close, confirm }; }; export { setup };