@ckeditor/ckeditor5-enter
Version:
Enter feature for CKEditor 5.
380 lines (372 loc) • 12.7 kB
JavaScript
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
import { Command, Plugin } from "@ckeditor/ckeditor5-core";
import { BubblingEventInfo, Observer, ViewDocumentDomEventData } from "@ckeditor/ckeditor5-engine";
import { env } from "@ckeditor/ckeditor5-utils";
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* Returns attributes that should be preserved on the enter keystroke.
*
* Filtering is realized based on `copyOnEnter` attribute property. Read more about attribute properties
* {@link module:engine/model/schema~ModelSchema#setAttributeProperties here}.
*
* @param schema Model's schema.
* @param allAttributes Attributes to filter.
* @internal
*/
function* getCopyOnEnterAttributes(schema, allAttributes) {
for (const attribute of allAttributes) if (attribute && schema.getAttributeProperties(attribute[0]).copyOnEnter) yield attribute;
}
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module enter/entercommand
*/
/**
* Enter command used by the {@link module:enter/enter~Enter Enter feature} to handle the <kbd>Enter</kbd> keystroke.
*/
var EnterCommand = class extends Command {
/**
* @inheritDoc
*/
execute() {
this.editor.model.change((writer) => {
this.enterBlock(writer);
this.fire("afterExecute", { writer });
});
}
/**
* Splits a block where the document selection is placed, in the way how the <kbd>Enter</kbd> key is expected to work:
*
* ```
* <p>Foo[]bar</p> -> <p>Foo</p><p>[]bar</p>
* <p>Foobar[]</p> -> <p>Foobar</p><p>[]</p>
* <p>Fo[ob]ar</p> -> <p>Fo</p><p>[]ar</p>
* ```
*
* In some cases, the split will not happen:
*
* ```
* // The selection parent is a limit element:
* <figcaption>A[bc]d</figcaption> -> <figcaption>A[]d</figcaption>
*
* // The selection spans over multiple elements:
* <h>x[x</h><p>y]y<p> -> <h>x</h><p>[]y</p>
* ```
*
* @param writer Writer to use when performing the enter action.
* @returns Boolean indicating if the block was split.
*/
enterBlock(writer) {
const model = this.editor.model;
const selection = model.document.selection;
const schema = model.schema;
const isSelectionEmpty = selection.isCollapsed;
const range = selection.getFirstRange();
const startElement = range.start.parent;
const endElement = range.end.parent;
if (schema.isLimit(startElement) || schema.isLimit(endElement)) {
if (!isSelectionEmpty && startElement == endElement) model.deleteContent(selection);
return false;
}
if (isSelectionEmpty) {
const attributesToCopy = getCopyOnEnterAttributes(writer.model.schema, selection.getAttributes());
splitBlock(writer, range.start);
writer.setSelectionAttribute(attributesToCopy);
return true;
} else {
const leaveUnmerged = !(range.start.isAtStart && range.end.isAtEnd);
const isContainedWithinOneElement = startElement == endElement;
model.deleteContent(selection, { leaveUnmerged });
if (leaveUnmerged) if (isContainedWithinOneElement) {
splitBlock(writer, selection.focus);
return true;
} else writer.setSelection(endElement, 0);
}
return false;
}
};
function splitBlock(writer, splitPos) {
writer.split(splitPos);
writer.setSelection(splitPos.parent.nextSibling, 0);
}
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module enter/enterobserver
*/
const ENTER_EVENT_TYPES = {
insertParagraph: { isSoft: false },
insertLineBreak: { isSoft: true }
};
/**
* Enter observer introduces the {@link module:engine/view/document~ViewDocument#event:enter `Document#enter`} event.
*/
var EnterObserver = class extends Observer {
/**
* @inheritDoc
*/
constructor(view) {
super(view);
const doc = this.document;
let shiftPressed = false;
doc.on("keydown", (evt, data) => {
shiftPressed = data.shiftKey;
});
doc.on("beforeinput", (evt, data) => {
if (!this.isEnabled) return;
let inputType = data.inputType;
if (env.isSafari && shiftPressed && inputType == "insertParagraph") inputType = "insertLineBreak";
const domEvent = data.domEvent;
const enterEventSpec = ENTER_EVENT_TYPES[inputType];
if (!enterEventSpec) return;
const event = new BubblingEventInfo(doc, "enter", data.targetRanges[0]);
doc.fire(event, new ViewDocumentDomEventData(view, domEvent, { isSoft: enterEventSpec.isSoft }));
if (event.stop.called) evt.stop();
});
}
/**
* @inheritDoc
*/
observe() {}
/**
* @inheritDoc
*/
stopObserving() {}
};
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module enter/enter
*/
/**
* This plugin handles the <kbd>Enter</kbd> keystroke (hard line break) in the editor.
*
* See also the {@link module:enter/shiftenter~ShiftEnter} plugin.
*
* For more information about this feature see the {@glink api/enter package page}.
*/
var Enter = class extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return "Enter";
}
/**
* @inheritDoc
*/
static get isOfficialPlugin() {
return true;
}
init() {
const editor = this.editor;
const view = editor.editing.view;
const viewDocument = view.document;
const t = this.editor.t;
view.addObserver(EnterObserver);
editor.commands.add("enter", new EnterCommand(editor));
this.listenTo(viewDocument, "enter", (evt, data) => {
if (!viewDocument.isComposing) data.preventDefault();
if (data.isSoft) return;
editor.execute("enter");
view.scrollToTheSelection();
}, { priority: "low" });
editor.accessibility.addKeystrokeInfos({ keystrokes: [{
label: t("Insert a hard break (a new paragraph)"),
keystroke: "Enter"
}] });
}
};
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module enter/shiftentercommand
*/
/**
* ShiftEnter command. It is used by the {@link module:enter/shiftenter~ShiftEnter ShiftEnter feature} to handle
* the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke.
*/
var ShiftEnterCommand = class extends Command {
/**
* @inheritDoc
*/
execute() {
const model = this.editor.model;
const doc = model.document;
model.change((writer) => {
softBreakAction(model, writer, doc.selection);
this.fire("afterExecute", { writer });
});
}
/**
* @inheritDoc
*/
refresh() {
const model = this.editor.model;
const doc = model.document;
this.isEnabled = isEnabled(model.schema, doc.selection);
}
};
/**
* Checks whether the ShiftEnter command should be enabled in the specified selection.
*/
function isEnabled(schema, selection) {
if (selection.rangeCount > 1) return false;
const anchorPos = selection.anchor;
if (!anchorPos || !schema.checkChild(anchorPos, "softBreak")) return false;
const range = selection.getFirstRange();
const startElement = range.start.parent;
const endElement = range.end.parent;
if ((isInsideLimitElement(startElement, schema) || isInsideLimitElement(endElement, schema)) && startElement !== endElement) return false;
return true;
}
/**
* Creates a break in the way that the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke is expected to work.
*/
function softBreakAction(model, writer, selection) {
const isSelectionEmpty = selection.isCollapsed;
const range = selection.getFirstRange();
const startElement = range.start.parent;
const endElement = range.end.parent;
const isContainedWithinOneElement = startElement == endElement;
if (isSelectionEmpty) insertBreak(model, writer, range.end, selection.getAttributes());
else {
const leaveUnmerged = !(range.start.isAtStart && range.end.isAtEnd);
model.deleteContent(selection, { leaveUnmerged });
if (isContainedWithinOneElement) insertBreak(model, writer, selection.focus, selection.getAttributes());
else if (leaveUnmerged) writer.setSelection(endElement, 0);
}
}
/**
* Inserts a softBreak with applicable attributes.
*/
function insertBreak(model, writer, position, selectionAttributes) {
const attributes = Array.from(getCopyOnEnterAttributes(model.schema, selectionAttributes));
const breakLineElement = writer.createElement("softBreak", attributes);
model.insertContent(breakLineElement, position);
writer.setSelection(breakLineElement, "after");
}
/**
* Checks whether the specified `element` is a child of the limit element.
*
* Checking whether the `<p>` element is inside a limit element:
* - `<$root><p>Text.</p></$root> => false`
* - `<$root><limitElement><p>Text</p></limitElement></$root> => true`
*/
function isInsideLimitElement(element, schema) {
if (element.is("rootElement")) return false;
return schema.isLimit(element) || isInsideLimitElement(element.parent, schema);
}
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module enter/shiftenter
*/
/**
* This plugin handles the <kbd>Shift</kbd>+<kbd>Enter</kbd> keystroke (soft line break) in the editor.
*
* See also the {@link module:enter/enter~Enter} plugin.
*
* For more information about this feature see the {@glink api/enter package page}.
*/
var ShiftEnter = class extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return "ShiftEnter";
}
/**
* @inheritDoc
*/
static get isOfficialPlugin() {
return true;
}
init() {
const editor = this.editor;
const schema = editor.model.schema;
const conversion = editor.conversion;
const view = editor.editing.view;
const viewDocument = view.document;
const t = this.editor.t;
schema.register("softBreak", {
allowWhere: "$text",
allowAttributesOf: "$text",
isInline: true
});
conversion.for("upcast").elementToElement({
model: "softBreak",
view: "br"
});
conversion.for("downcast").elementToElement({
model: "softBreak",
view: (modelElement, { writer }) => writer.createEmptyElement("br")
});
view.addObserver(EnterObserver);
editor.commands.add("shiftEnter", new ShiftEnterCommand(editor));
editor.model.document.registerPostFixer((writer) => removeStaleSoftBreakAttributes(writer));
this.listenTo(viewDocument, "enter", (evt, data) => {
if (!viewDocument.isComposing) data.preventDefault();
if (!data.isSoft) return;
editor.execute("shiftEnter");
view.scrollToTheSelection();
}, { priority: "low" });
editor.accessibility.addKeystrokeInfos({ keystrokes: [{
label: t("Insert a soft break (a <code><br></code> element)"),
keystroke: "Shift+Enter"
}] });
}
};
/**
* The post-fixer that removes attributes from stale soft-break elements.
*/
function removeStaleSoftBreakAttributes(writer) {
const parentsToCheck = /* @__PURE__ */ new Set();
for (const change of writer.model.document.differ.getChanges()) if (change.type == "insert" || change.type == "remove") {
/* v8 ignore else -- @preserve */
if (change.position.parent.is("element")) parentsToCheck.add(change.position.parent);
} else if (change.type == "attribute") {
/* v8 ignore else -- @preserve */
if (change.range.start.parent.is("element")) parentsToCheck.add(change.range.start.parent);
}
let wasChanged = false;
for (const parent of parentsToCheck) for (const child of parent.getChildren()) {
if (!child.is("element", "softBreak")) continue;
const nextSibling = child.nextSibling;
if (!nextSibling || nextSibling.is("element")) continue;
const attributesToRemove = Array.from(child.getAttributes()).filter(([key, value]) => !hasSameAttribute(nextSibling, key, value));
for (const [key] of attributesToRemove) {
writer.removeAttribute(key, child);
wasChanged = true;
}
}
return wasChanged;
}
/**
* Returns `true` if the given node has the same attribute as the one provided in the parameters.
*/
function hasSameAttribute(node, key, value) {
return node?.getAttribute(key) === value;
}
/**
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
export { Enter, EnterCommand, EnterObserver, ShiftEnter, ShiftEnterCommand, getCopyOnEnterAttributes as _getCopyOnEnterAttributes };
//# sourceMappingURL=index.js.map