suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
165 lines (142 loc) • 5.54 kB
JavaScript
import { dom, keyCodeMap } from '../../../helper';
import { actionExecutor } from '../executor';
import { makePorts } from '../ports';
import { reduceEnterDown } from '../rules/keydown.rule.enter';
import { useEnterFromBeforeInput } from '../reducers/keydown.reducer';
// The Enter rule/effects never touch the retain-style node cache (a backspace concern) — a local
// placeholder is enough to satisfy `makePorts`.
const _enterStyleNodes = { value: [] };
/**
* @typedef {import('../eventOrchestrator').default} EventManagerThis_handler_ww_input
*/
/**
* @this {EventManagerThis_handler_ww_input}
* @param {SunEditor.FrameContext} fc - Frame context object
* @param {InputEvent} e - Event object
*/
export async function OnBeforeInput_wysiwyg(fc, e) {
if (fc.get('isReadOnly') || fc.get('isDisabled')) {
e.preventDefault();
e.stopPropagation();
return false;
}
// Enter is dispatched here — not on keydown — so the IME has finished committing before the DOM
// mutates (iOS/mobile marked-text stability). `insertParagraph` = Enter, `insertLineBreak` = Shift+Enter.
// ctrl/alt+Enter are shortcuts and never produce these inputTypes, so they stay on the keydown path.
if (
useEnterFromBeforeInput(this.$.store) &&
(e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak')
) {
await dispatchEnter.call(this, fc, e);
return;
}
const data = (e.data === null ? '' : e.data === undefined ? ' ' : e.data) || '';
if (!keyCodeMap.isComposing(e)) {
if (!this.$.char.test(data, false)) {
e.preventDefault();
e.stopPropagation();
return false;
}
this._handledInBefore = true;
} else {
this._handledInBefore = false;
}
// user event
if ((await this.$.eventManager.triggerEvent('onBeforeInput', { frameContext: fc, event: e, data })) === false)
return;
// plugin event
await this._callPluginEventAsync('onBeforeInput', { frameContext: fc, event: e, data });
}
/**
* @this {EventManagerThis_handler_ww_input}
* @description Runs SunEditor's Enter logic from the `beforeinput` event (post-IME-commit) by reusing the
* exact keydown Enter rule + effects — only the dispatch site moves off keydown. See ENTER_FROM_BEFOREINPUT.
* The guards mirror `OnKeyDown_wysiwyg` (selectMenu, input-element, open-dropdown) so a `beforeinput` Enter
* is dropped in the same situations a keydown Enter is.
* @param {SunEditor.FrameContext} fc - Frame context object
* @param {InputEvent} e - The `beforeinput` event (`insertParagraph` | `insertLineBreak`)
*/
async function dispatchEnter(fc, e) {
// Skip while an IME composition is still active
if (e.isComposing || this.isComposing) return;
if (this.$.ui.selectMenuOn) return;
let selectionNode = this.$.selection.getNode();
if (dom.check.isInputElement(selectionNode)) return;
if (this.$.menu.currentDropdownName) return;
if (dom.check.isWysiwygFrame(selectionNode)) {
this._setDefaultLine(this.$.options.get('defaultLine'));
selectionNode = this.$.selection.getNode();
}
const shift = this._enterKeyShift || e.inputType === 'insertLineBreak';
this._enterKeyShift = false;
if (!shift) {
const normalized = this._normalizeEditRange();
if (normalized) selectionNode = normalized;
}
const range = this.$.selection.getRange();
const formatEl = /** @type {HTMLElement} */ (this.$.format.getLine(selectionNode, null) || selectionNode);
/** @type {import('../reducers/keydown.reducer').KeydownReducerCtx} */
const ctx = {
e,
fc,
store: this.$.store,
options: this.$.options,
frameOptions: this.$.frameOptions,
range,
selectionNode,
formatEl,
keyCode: 'Enter',
ctrl: false,
alt: false,
shift,
};
const ports = makePorts(this, { _styleNodes: _enterStyleNodes });
const actions = [];
reduceEnterDown(actions, ports, ctx);
// `beforeinput.preventDefault()` MUST run synchronously (before the first `await` below), or the browser
// commits its native insertParagraph/insertLineBreak on top of ours (duplicate line / cloned container).
if (actions.some((a) => a.t === 'event.prevent' || a.t === 'event.prevent.stop')) {
e.preventDefault();
}
await actionExecutor(actions, { ports, ctx });
}
/**
* @this {EventManagerThis_handler_ww_input}
* @param {SunEditor.FrameContext} fc - Frame context object
* @param {InputEvent} e - Event object
*/
export async function OnInput_wysiwyg(fc, e) {
if (fc.get('isReadOnly') || fc.get('isDisabled')) {
e.preventDefault();
e.stopPropagation();
return false;
}
const range = this.$.selection.getRange();
const selectionNode = this.$.selection.getNode();
const formatEl = this.$.format.getLine(selectionNode, null);
if (
!this.$.format.isNormalLine(formatEl) &&
!this.$.format.isBrLine(formatEl) &&
range.collapsed &&
!this.$.component.is(selectionNode) &&
!dom.check.isList(selectionNode)
) {
const rangeEl = this.$.format.getBlock(selectionNode, null);
this._setDefaultLine(this.$.format.isBlock(rangeEl) ? 'DIV' : this.$.options.get('defaultLine'));
}
this.$.selection.init();
const data = (e.data === null ? '' : e.data === undefined ? ' ' : e.data) || '';
if (!this._handledInBefore) {
if (!this.$.char.test(data, true)) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
this._handledInBefore = false;
// user event
if ((await this.$.eventManager.triggerEvent('onInput', { frameContext: fc, event: e, data })) === false) return;
// plugin event
await this._callPluginEventAsync('onInput', { frameContext: fc, event: e, data });
this.$.history.push(true);
}