ckeditor5-elements-schema 0.0.0-stage → 49.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -2
  3. package/dist/augmentation.d.ts +25 -0
  4. package/dist/augmentation.d.ts.map +1 -0
  5. package/dist/augmentation.js +2 -0
  6. package/dist/augmentation.js.map +1 -0
  7. package/dist/commands/elementBlockCommand.d.ts +46 -0
  8. package/dist/commands/elementBlockCommand.d.ts.map +1 -0
  9. package/dist/commands/elementBlockCommand.js +71 -0
  10. package/dist/commands/elementBlockCommand.js.map +1 -0
  11. package/dist/commands/elementContainerCommand.d.ts +36 -0
  12. package/dist/commands/elementContainerCommand.d.ts.map +1 -0
  13. package/dist/commands/elementContainerCommand.js +67 -0
  14. package/dist/commands/elementContainerCommand.js.map +1 -0
  15. package/dist/commands/elementInlineCommand.d.ts +87 -0
  16. package/dist/commands/elementInlineCommand.d.ts.map +1 -0
  17. package/dist/commands/elementInlineCommand.js +253 -0
  18. package/dist/commands/elementInlineCommand.js.map +1 -0
  19. package/dist/commands/elementInsertCommand.d.ts +33 -0
  20. package/dist/commands/elementInsertCommand.d.ts.map +1 -0
  21. package/dist/commands/elementInsertCommand.js +61 -0
  22. package/dist/commands/elementInsertCommand.js.map +1 -0
  23. package/dist/commands/elementSchemaCommand.d.ts +62 -0
  24. package/dist/commands/elementSchemaCommand.d.ts.map +1 -0
  25. package/dist/commands/elementSchemaCommand.js +107 -0
  26. package/dist/commands/elementSchemaCommand.js.map +1 -0
  27. package/dist/conversion/convertElementSchema.d.ts +17 -0
  28. package/dist/conversion/convertElementSchema.d.ts.map +1 -0
  29. package/dist/conversion/convertElementSchema.js +55 -0
  30. package/dist/conversion/convertElementSchema.js.map +1 -0
  31. package/dist/conversion/inlineTextElement.d.ts +24 -0
  32. package/dist/conversion/inlineTextElement.d.ts.map +1 -0
  33. package/dist/conversion/inlineTextElement.js +28 -0
  34. package/dist/conversion/inlineTextElement.js.map +1 -0
  35. package/dist/definition/canHold.d.ts +10 -0
  36. package/dist/definition/canHold.d.ts.map +1 -0
  37. package/dist/definition/canHold.js +22 -0
  38. package/dist/definition/canHold.js.map +1 -0
  39. package/dist/definition/compileElementSchema.d.ts +41 -0
  40. package/dist/definition/compileElementSchema.d.ts.map +1 -0
  41. package/dist/definition/compileElementSchema.js +60 -0
  42. package/dist/definition/compileElementSchema.js.map +1 -0
  43. package/dist/definition/elementSchemaError.d.ts +12 -0
  44. package/dist/definition/elementSchemaError.d.ts.map +1 -0
  45. package/dist/definition/elementSchemaError.js +16 -0
  46. package/dist/definition/elementSchemaError.js.map +1 -0
  47. package/dist/definition/readDefinition.d.ts +18 -0
  48. package/dist/definition/readDefinition.d.ts.map +1 -0
  49. package/dist/definition/readDefinition.js +60 -0
  50. package/dist/definition/readDefinition.js.map +1 -0
  51. package/dist/definition/suggest.d.ts +5 -0
  52. package/dist/definition/suggest.d.ts.map +1 -0
  53. package/dist/definition/suggest.js +27 -0
  54. package/dist/definition/suggest.js.map +1 -0
  55. package/dist/definition/types.d.ts +105 -0
  56. package/dist/definition/types.d.ts.map +1 -0
  57. package/dist/definition/types.js +2 -0
  58. package/dist/definition/types.js.map +1 -0
  59. package/dist/definition/validateElementSchema.d.ts +26 -0
  60. package/dist/definition/validateElementSchema.d.ts.map +1 -0
  61. package/dist/definition/validateElementSchema.js +145 -0
  62. package/dist/definition/validateElementSchema.js.map +1 -0
  63. package/dist/elementSchema.d.ts +47 -0
  64. package/dist/elementSchema.d.ts.map +1 -0
  65. package/dist/elementSchema.js +108 -0
  66. package/dist/elementSchema.js.map +1 -0
  67. package/dist/elementSchemaCommands.d.ts +83 -0
  68. package/dist/elementSchemaCommands.d.ts.map +1 -0
  69. package/dist/elementSchemaCommands.js +137 -0
  70. package/dist/elementSchemaCommands.js.map +1 -0
  71. package/dist/index.d.ts +18 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +22 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/typing/twoStepCaretMovement.d.ts +11 -0
  76. package/dist/typing/twoStepCaretMovement.d.ts.map +1 -0
  77. package/dist/typing/twoStepCaretMovement.js +76 -0
  78. package/dist/typing/twoStepCaretMovement.js.map +1 -0
  79. package/dist/validate.d.ts +12 -0
  80. package/dist/validate.d.ts.map +1 -0
  81. package/dist/validate.js +11 -0
  82. package/dist/validate.js.map +1 -0
  83. package/package.json +49 -3
  84. package/src/augmentation.ts +28 -0
  85. package/src/commands/elementBlockCommand.ts +87 -0
  86. package/src/commands/elementContainerCommand.ts +83 -0
  87. package/src/commands/elementInlineCommand.ts +311 -0
  88. package/src/commands/elementInsertCommand.ts +74 -0
  89. package/src/commands/elementSchemaCommand.ts +140 -0
  90. package/src/conversion/convertElementSchema.ts +65 -0
  91. package/src/conversion/inlineTextElement.ts +31 -0
  92. package/src/definition/canHold.ts +29 -0
  93. package/src/definition/compileElementSchema.ts +88 -0
  94. package/src/definition/elementSchemaError.ts +21 -0
  95. package/src/definition/readDefinition.ts +98 -0
  96. package/src/definition/suggest.ts +38 -0
  97. package/src/definition/types.ts +123 -0
  98. package/src/definition/validateElementSchema.ts +238 -0
  99. package/src/elementSchema.ts +133 -0
  100. package/src/elementSchemaCommands.ts +202 -0
  101. package/src/index.ts +30 -0
  102. package/src/typing/twoStepCaretMovement.ts +119 -0
  103. package/src/validate.ts +20 -0
@@ -0,0 +1,76 @@
1
+ import { isForwardArrowKeyCode, keyCodes } from 'ckeditor5';
2
+ /**
3
+ * Makes the left and right arrow keys move in two steps at the ends of inline elements, as CKEditor's two-step caret
4
+ * movement does at the ends of a bold: the first step goes into the element (or out of it), where the cursor looks the
5
+ * same, the next one moves in the text. `a[]<b>bc</b>` is `a<b>[]bc</b>`, then `a<b>b[]c</b>`. The browser would move by
6
+ * a character at once, and the positions on both sides of the end of an element would be out of reach.
7
+ *
8
+ * What is typed then goes where the cursor is. The listeners are the plugin's.
9
+ */
10
+ export function enableTwoStepCaretMovement(plugin, names) {
11
+ const editor = plugin.editor;
12
+ const viewDocument = editor.editing.view.document;
13
+ const isElement = (node) => !!node?.is('element') && names.has(node.name);
14
+ // Before the other features: e.g. `ElementSelectable` selects an inline object that the cursor reaches, and it may
15
+ // be just after the end of an element.
16
+ plugin.listenTo(viewDocument, 'keydown', (evt, data) => {
17
+ const isPlain = !data.shiftKey && !data.ctrlKey && !data.altKey && !data.metaKey;
18
+ const isHorizontal = data.keyCode === keyCodes.arrowleft || data.keyCode === keyCodes.arrowright;
19
+ const isForward = isForwardArrowKeyCode(data.keyCode, editor.locale.contentLanguageDirection);
20
+ const position = isPlain && isHorizontal ? getStep(editor, isForward, isElement) : null;
21
+ if (position) {
22
+ editor.model.change(writer => {
23
+ writer.setSelection(position);
24
+ });
25
+ data.preventDefault();
26
+ evt.stop();
27
+ }
28
+ }, { priority: 'highest' });
29
+ // The browser types at a place of its own at an end of an element, e.g. at the end of the text before it, whichever
30
+ // side of the end the cursor is on. When that place differs from the cursor only by ends of elements, the text goes
31
+ // where the cursor is: at the view selection, which is the model one. The browser must not type it at its place, as
32
+ // CKEditor does for a fake selection; a composition is left to the browser.
33
+ plugin.listenTo(viewDocument, 'insertText', (evt, data) => {
34
+ const target = data.selection?.rangeCount === 1 ? data.selection.getFirstRange() : null;
35
+ const position = target?.isCollapsed ? editor.editing.mapper.toModelPosition(target.start) : null;
36
+ if (position && !data.isComposing && isAtCursor(editor, position, isElement)) {
37
+ data.selection = viewDocument.selection;
38
+ data.preventDefault();
39
+ }
40
+ }, { priority: 'high' });
41
+ }
42
+ /**
43
+ * Returns where the cursor goes with one step in the direction, when it is at an end of an element: into the element
44
+ * next to it, or out of the element it is in. `null` elsewhere, or for a selection that is not collapsed.
45
+ */
46
+ function getStep(editor, isForward, isElement) {
47
+ const model = editor.model;
48
+ const selection = model.document.selection;
49
+ const position = selection.getFirstPosition();
50
+ if (!selection.isCollapsed || !position) {
51
+ return null;
52
+ }
53
+ const next = isForward ? position.nodeAfter : position.nodeBefore;
54
+ if (isElement(next)) {
55
+ return model.createPositionAt(next, isForward ? 0 : 'end');
56
+ }
57
+ if ((isForward ? position.isAtEnd : position.isAtStart) && isElement(position.parent)) {
58
+ return isForward ? model.createPositionAfter(position.parent) : model.createPositionBefore(position.parent);
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Whether a position differs from the cursor only by ends of elements: `a[]<b>c</b>` and `a<b>[]c</b>` look the same.
64
+ */
65
+ function isAtCursor(editor, position, isElement) {
66
+ const model = editor.model;
67
+ const selection = model.document.selection;
68
+ const cursor = selection.getFirstPosition();
69
+ // Where the cursor is exactly, the browser types as usual.
70
+ if (!selection.isCollapsed || !cursor || cursor.isEqual(position)) {
71
+ return false;
72
+ }
73
+ const range = cursor.isBefore(position) ? model.createRange(cursor, position) : model.createRange(position, cursor);
74
+ return Array.from(range.getWalker()).every(({ item }) => item.is('element') && isElement(item));
75
+ }
76
+ //# sourceMappingURL=twoStepCaretMovement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twoStepCaretMovement.js","sourceRoot":"","sources":["../../src/typing/twoStepCaretMovement.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The part of the element schema that needs no editor and no browser: the validation, the compiling to schema
3
+ * definitions, the error and the types. It is in its own module, so that it can be used in Node.js, e.g. to validate a
4
+ * schema file in a test or in CI: the main entry loads CKEditor, which needs a DOM.
5
+ *
6
+ * @module ckeditor5-elements-schema/validate
7
+ */
8
+ export { validateElementSchema } from './definition/validateElementSchema.js';
9
+ export { compileElementSchema, type CompiledElementSchema } from './definition/compileElementSchema.js';
10
+ export { default as ElementSchemaError } from './definition/elementSchemaError.js';
11
+ export type { ElementSchemaDefinition, ElementSchemaElement, ElementSchemaInput, ElementSchemaIssue, ElementSchemaKind, ElementSchemaType, ValidateElementSchemaOptions } from './definition/types.js';
12
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AACxG,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACnF,YAAY,EACX,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,4BAA4B,EAC5B,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The part of the element schema that needs no editor and no browser: the validation, the compiling to schema
3
+ * definitions, the error and the types. It is in its own module, so that it can be used in Node.js, e.g. to validate a
4
+ * schema file in a test or in CI: the main entry loads CKEditor, which needs a DOM.
5
+ *
6
+ * @module ckeditor5-elements-schema/validate
7
+ */
8
+ export { validateElementSchema } from './definition/validateElementSchema.js';
9
+ export { compileElementSchema } from './definition/compileElementSchema.js';
10
+ export { default as ElementSchemaError } from './definition/elementSchemaError.js';
11
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAA8B,MAAM,sCAAsC,CAAC;AACxG,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,oCAAoC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "ckeditor5-elements-schema",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "49.0.0-alpha.4",
4
+ "description": "Element schema for CKEditor 5: declare elements in the model schema from a simple definition, e.g. a JSON file, with three types of elements and a validation.",
5
+ "keywords": [
6
+ "ckeditor",
7
+ "ckeditor5",
8
+ "ckeditor 5",
9
+ "ckeditor5-feature",
10
+ "ckeditor5-plugin"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Ivan Kildyushov",
14
+ "homepage": "https://ivankild.github.io/ckeditor5-elements/guide/schema",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/ivankild/ckeditor5-elements.git",
18
+ "directory": "packages/ckeditor5-elements-schema"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/ivankild/ckeditor5-elements/issues"
22
+ },
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./validate": {
32
+ "types": "./dist/validate.d.ts",
33
+ "import": "./dist/validate.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "README.md"
41
+ ],
42
+ "sideEffects": false,
43
+ "dependencies": {
44
+ "ckeditor5-elements-core": "49.0.0-alpha.4"
45
+ },
46
+ "peerDependencies": {
47
+ "ckeditor5": "^49.0.0"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc --build"
51
+ }
6
52
  }
@@ -0,0 +1,28 @@
1
+ import type { ElementSchema, ElementSchemaCommands, ElementSchemaCommandsConfig, ElementSchemaInput } from './index.js';
2
+
3
+ declare module 'ckeditor5' {
4
+ interface PluginsMap {
5
+ [ElementSchema.pluginName]: ElementSchema;
6
+ [ElementSchemaCommands.pluginName]: ElementSchemaCommands;
7
+ }
8
+
9
+ interface EditorConfig {
10
+ /**
11
+ * The elements to declare in the model schema: an object, e.g. a JSON file. See {@link ElementSchema}.
12
+ */
13
+ elementSchema?: ElementSchemaInput;
14
+
15
+ /**
16
+ * Whether to declare the converters of the elements of `elementSchema`, one to one: `div` or `span` with the name as
17
+ * the class, and the attributes as `data-*` attributes, in the data and in the editing view; `editingTag` of an
18
+ * element sets another tag for the editing view. See {@link ElementSchema}. Default: `false`.
19
+ */
20
+ elementSchemaConverters?: boolean;
21
+
22
+ /**
23
+ * The commands and the buttons of elements of the schema, as CKEditor's bold, heading, block quote or horizontal
24
+ * line, by the name of the command. See {@link ElementSchemaCommands}.
25
+ */
26
+ elementSchemaCommands?: ElementSchemaCommandsConfig;
27
+ }
28
+ }
@@ -0,0 +1,87 @@
1
+ import type { ModelElement } from 'ckeditor5';
2
+ import type { ElementOrPosition } from 'ckeditor5-elements-core';
3
+ import ElementSchemaCommand from './elementSchemaCommand.js';
4
+
5
+ /**
6
+ * Turns blocks into the element, as CKEditor's heading command does, e.g. a `title` that is a `block` of the element
7
+ * schema: the selected blocks, or the block of the target, are renamed, and keep their content.
8
+ *
9
+ * ```ts
10
+ * editor.execute('title'); // The blocks of the selection.
11
+ * editor.commands.get('title').executeFor(paragraph);
12
+ * ```
13
+ *
14
+ * When all the blocks are the element already, it turns them back into paragraphs (the {@link defaultElement}). The
15
+ * blocks that cannot be renamed, e.g. where the element is not allowed, are left as they are.
16
+ *
17
+ * It is "on" when all the blocks are the element.
18
+ */
19
+ export default class ElementBlockCommand extends ElementSchemaCommand {
20
+ public override isToggleable = true;
21
+
22
+ /**
23
+ * The element the blocks are turned back into.
24
+ */
25
+ public defaultElement = 'paragraph';
26
+
27
+ /**
28
+ * Renames the blocks of the target: to the element, or back to the {@link defaultElement} when they are all the
29
+ * element.
30
+ */
31
+ public override execute(elementOrPosition?: ElementOrPosition): void {
32
+ const name = this._getNewName(elementOrPosition);
33
+ const schema = this.editor.model.schema;
34
+
35
+ this.editor.model.change(writer => {
36
+ for (const block of this._getBlocks(elementOrPosition)) {
37
+ if (this._canRename(block, name)) {
38
+ writer.rename(block, name);
39
+ schema.removeDisallowedAttributes([block], writer);
40
+ }
41
+ }
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Enabled when the target can be edited, and one of its blocks can be renamed.
47
+ */
48
+ protected override _isEnabledFor(elementOrPosition?: ElementOrPosition): boolean {
49
+ const name = this._getNewName(elementOrPosition);
50
+
51
+ return (
52
+ super._isEnabledFor(elementOrPosition) &&
53
+ this._getBlocks(elementOrPosition).some(block => this._canRename(block, name))
54
+ );
55
+ }
56
+
57
+ /**
58
+ * "On" when all the blocks of the target are the element.
59
+ */
60
+ protected override _isOnFor(elementOrPosition?: ElementOrPosition): boolean {
61
+ const blocks = this._getBlocks(elementOrPosition);
62
+
63
+ return blocks.length > 0 && blocks.every(block => this.isElement(block));
64
+ }
65
+
66
+ /**
67
+ * Returns the name the blocks get: the element, or the {@link defaultElement} when they are the element already.
68
+ */
69
+ private _getNewName(elementOrPosition?: ElementOrPosition): string {
70
+ return this._isOnFor(elementOrPosition) ? this.defaultElement : this.elementName;
71
+ }
72
+
73
+ /**
74
+ * Whether a block can be renamed: it is not an object, nor the name already, and the schema allows it with its
75
+ * content.
76
+ */
77
+ private _canRename(block: ModelElement, name: string): boolean {
78
+ const schema = this.editor.model.schema;
79
+
80
+ return (
81
+ block.name !== name &&
82
+ !schema.isObject(block) &&
83
+ schema.isRegistered(name) &&
84
+ this._canHold(this.editor.model.createPositionBefore(block), block.getChildren(), name)
85
+ );
86
+ }
87
+ }
@@ -0,0 +1,83 @@
1
+ import type { ModelElement, ModelRange } from 'ckeditor5';
2
+ import type { ElementOrPosition } from 'ckeditor5-elements-core';
3
+ import ElementSchemaCommand from './elementSchemaCommand.js';
4
+
5
+ /**
6
+ * Wraps blocks with the element, as CKEditor's block quote command does, e.g. a `callout` that is a `container` of the
7
+ * element schema: the selected blocks, or the block-level element of the target, go in a new element.
8
+ *
9
+ * ```ts
10
+ * editor.execute('callout'); // The blocks of the selection.
11
+ * editor.commands.get('callout').executeFor(paragraph);
12
+ * ```
13
+ *
14
+ * In the element, it unwraps it: the whole element, not only the selected blocks.
15
+ *
16
+ * It is "on" when the target is in the element.
17
+ */
18
+ export default class ElementContainerCommand extends ElementSchemaCommand {
19
+ public override isToggleable = true;
20
+
21
+ /**
22
+ * Unwraps the element the target is in, or wraps the blocks of the target with a new one.
23
+ */
24
+ public override execute(elementOrPosition?: ElementOrPosition): void {
25
+ const element = this._findElement(elementOrPosition);
26
+ const range = element ? null : this._getWrapRange(elementOrPosition);
27
+
28
+ this.editor.model.change(writer => {
29
+ if (element) {
30
+ writer.unwrap(element);
31
+ } else if (range) {
32
+ writer.wrap(range, this.elementName);
33
+ }
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Enabled when the target can be edited, and the element it is in can be unwrapped, or its blocks can be wrapped.
39
+ */
40
+ protected override _isEnabledFor(elementOrPosition?: ElementOrPosition): boolean {
41
+ const element = this._findElement(elementOrPosition);
42
+
43
+ return (
44
+ super._isEnabledFor(elementOrPosition) &&
45
+ (element ? this._canUnwrap(element) : !!this._getWrapRange(elementOrPosition))
46
+ );
47
+ }
48
+
49
+ /**
50
+ * "On" when the target is in the element.
51
+ */
52
+ protected override _isOnFor(elementOrPosition?: ElementOrPosition): boolean {
53
+ return !!this._findElement(elementOrPosition);
54
+ }
55
+
56
+ /**
57
+ * Returns the range to wrap: from the first block of the target to the last, as siblings: the blocks themselves, or
58
+ * their ancestors in the parent they share. `null` when the schema does not allow the element there.
59
+ */
60
+ private _getWrapRange(elementOrPosition?: ElementOrPosition): ModelRange | null {
61
+ const model = this.editor.model;
62
+ const blocks = this._getBlocks(elementOrPosition);
63
+ const first = blocks[0] as ModelElement | undefined;
64
+ const last = blocks.at(-1);
65
+
66
+ if (!first || !last) {
67
+ return null;
68
+ }
69
+
70
+ const parent = first === last ? first.parent : first.getCommonAncestor(last);
71
+ const [start, end] = [first, last].map(block =>
72
+ block.getAncestors({ includeSelf: true }).find((node): node is ModelElement => node.parent === parent)
73
+ );
74
+
75
+ if (!start || !end) {
76
+ return null;
77
+ }
78
+
79
+ const range = model.createRange(model.createPositionBefore(start), model.createPositionAfter(end));
80
+
81
+ return this._canHold(range.start, range.getItems({ shallow: true })) ? range : null;
82
+ }
83
+ }
@@ -0,0 +1,311 @@
1
+ import {
2
+ ModelLivePosition,
3
+ ModelLiveRange,
4
+ type ModelDocumentFragment,
5
+ type ModelElement,
6
+ type ModelNode,
7
+ type ModelRange,
8
+ type ModelWriter
9
+ } from 'ckeditor5';
10
+ import type { ElementOrPosition } from 'ckeditor5-elements-core';
11
+ import ElementSchemaCommand from './elementSchemaCommand.js';
12
+
13
+ /**
14
+ * Toggles an inline element with text, as CKEditor's `bold` toggles an attribute of text, but with an element, e.g. a
15
+ * `bold` that is an inline `block` of the element schema.
16
+ *
17
+ * ```ts
18
+ * editor.execute('bold'); // The selection.
19
+ * editor.commands.get('bold').executeFor(element); // An element, e.g. of a context menu.
20
+ * ```
21
+ *
22
+ * * On selected text, it wraps the text with the element, in each block, and merges it with the elements next to it.
23
+ * When all the text is in the element already, it unwraps the elements: the whole ones, not only the selected part.
24
+ * * On the cursor, it inserts an empty element with the cursor in it. In the element, it moves the cursor out of it, at
25
+ * its start or end, or it unwraps it.
26
+ * * On an element (selected, or given), it unwraps the element it is in, or wraps it.
27
+ *
28
+ * It is "on" ({@link isOn}) when the target is in the element: for selected text, when all of it is.
29
+ */
30
+ export default class ElementInlineCommand extends ElementSchemaCommand {
31
+ public override isToggleable = true;
32
+
33
+ /**
34
+ * Puts the target in the element, or out of it: by default, the selection, also selected text.
35
+ */
36
+ public override execute(elementOrPosition?: ElementOrPosition): void {
37
+ const ranges = this._getSelectedRanges(elementOrPosition);
38
+ const target = this._getElementOrPosition(elementOrPosition);
39
+
40
+ this.editor.model.change(writer => {
41
+ if (ranges) {
42
+ if (this._isOnRanges(ranges)) {
43
+ this._unwrapRanges(writer, ranges);
44
+ } else {
45
+ this._wrapRanges(writer, ranges);
46
+ }
47
+ } else if (target) {
48
+ const element = this._findElement(target);
49
+
50
+ if (element) {
51
+ this._leave(writer, element, target);
52
+ } else {
53
+ this._insert(writer, target);
54
+ }
55
+ }
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Enabled when the target can be edited, and is in the element, or the element can be there.
61
+ */
62
+ protected override _isEnabledFor(elementOrPosition?: ElementOrPosition): boolean {
63
+ const ranges = this._getSelectedRanges(elementOrPosition);
64
+ const target = this._getElementOrPosition(elementOrPosition);
65
+
66
+ if (!target || !super._isEnabledFor(target)) {
67
+ return false;
68
+ }
69
+
70
+ if (ranges) {
71
+ return this._isOnRanges(ranges) || this._getWrapRanges(ranges).length > 0;
72
+ }
73
+
74
+ if (this._findElement(target)) {
75
+ return true;
76
+ }
77
+
78
+ return target.is('position')
79
+ ? this.editor.model.schema.checkChild(target, this.elementName)
80
+ : this._canHold(this.editor.model.createPositionBefore(target), [target]);
81
+ }
82
+
83
+ /**
84
+ * "On" when the target is in the element: for selected text, when all of it is.
85
+ */
86
+ protected override _isOnFor(elementOrPosition?: ElementOrPosition): boolean {
87
+ const ranges = this._getSelectedRanges(elementOrPosition);
88
+ const target = this._getElementOrPosition(elementOrPosition);
89
+
90
+ return ranges ? this._isOnRanges(ranges) : !!target && !!this._findElement(target);
91
+ }
92
+
93
+ /**
94
+ * Whether all the text of the ranges is in the element: there are elements, and nothing to wrap.
95
+ */
96
+ private _isOnRanges(ranges: ModelRange[]): boolean {
97
+ return this._getElementsIn(ranges).length > 0 && !this._getWrapRanges(ranges).length;
98
+ }
99
+
100
+ /**
101
+ * Returns the elements of the command that a node is, or is in, in its line of text: the node and its ancestors, up to
102
+ * the first that is not inline, e.g. the paragraph. The inner first.
103
+ */
104
+ private _getElementsAround(node: ModelElement | ModelDocumentFragment | null): ModelElement[] {
105
+ const schema = this.editor.model.schema;
106
+ const elements: ModelElement[] = [];
107
+
108
+ for (let element = node; element?.is('element') && schema.isInline(element); element = element.parent) {
109
+ if (this.isElement(element)) {
110
+ elements.push(element);
111
+ }
112
+ }
113
+
114
+ return elements;
115
+ }
116
+
117
+ /**
118
+ * Returns the closest element of the command that the target is, or is in, in its line of text.
119
+ */
120
+ protected override _findElement(elementOrPosition?: ElementOrPosition): ModelElement | null {
121
+ const target = this._getElementOrPosition(elementOrPosition);
122
+
123
+ return target ? (this._getElementsAround(target.is('position') ? target.parent : target)[0] ?? null) : null;
124
+ }
125
+
126
+ /**
127
+ * Returns the elements of the command in ranges, and around their ends.
128
+ */
129
+ private _getElementsIn(ranges: ModelRange[]): ModelElement[] {
130
+ const elements = new Set<ModelElement>();
131
+
132
+ for (const range of ranges) {
133
+ for (const element of [
134
+ ...this._getElementsAround(range.start.parent),
135
+ ...this._getElementsAround(range.end.parent)
136
+ ]) {
137
+ elements.add(element);
138
+ }
139
+
140
+ for (const item of range.getItems()) {
141
+ if (this.isElement(item)) {
142
+ elements.add(item);
143
+ }
144
+ }
145
+ }
146
+
147
+ return Array.from(elements);
148
+ }
149
+
150
+ /**
151
+ * Returns the ranges to wrap with the element to put the text of ranges in it: in each parent the element is allowed
152
+ * in, the runs of nodes it can hold, which are not in the element already. The other elements are looked into, e.g.
153
+ * the paragraphs of a selection across blocks.
154
+ */
155
+ private _getWrapRanges(ranges: ModelRange[]): ModelRange[] {
156
+ const model = this.editor.model;
157
+ const wrapRanges: ModelRange[] = [];
158
+
159
+ const collect = (flatRange: ModelRange): void => {
160
+ const parent = flatRange.start.parent as ModelElement;
161
+
162
+ if (this._getElementsAround(parent).length) {
163
+ return;
164
+ }
165
+
166
+ let run: ModelRange | null = null;
167
+ const close = (): void => {
168
+ if (run) {
169
+ wrapRanges.push(run);
170
+ run = null;
171
+ }
172
+ };
173
+
174
+ for (const item of flatRange.getItems({ shallow: true })) {
175
+ if (this.isElement(item)) {
176
+ close();
177
+ } else if (this._canHold(flatRange.start, [item])) {
178
+ run = model.createRange(
179
+ run?.start ?? model.createPositionBefore(item),
180
+ model.createPositionAfter(item)
181
+ );
182
+ } else {
183
+ close();
184
+
185
+ if (item.is('element')) {
186
+ collect(model.createRangeIn(item));
187
+ }
188
+ }
189
+ }
190
+
191
+ close();
192
+ };
193
+
194
+ for (const range of ranges) {
195
+ range.getMinimalFlatRanges().forEach(collect);
196
+ }
197
+
198
+ return wrapRanges;
199
+ }
200
+
201
+ /**
202
+ * Wraps the text of ranges with new elements, merged with the ones next to them, and keeps the selection on the text.
203
+ */
204
+ private _wrapRanges(writer: ModelWriter, ranges: ModelRange[]): void {
205
+ // The last first: wrapping one does not move the ones before it.
206
+ const wrapRanges = this._getWrapRanges(ranges).reverse();
207
+
208
+ this._keepSelection(writer, ranges, () => {
209
+ for (const range of wrapRanges) {
210
+ const element = writer.createElement(this.elementName);
211
+
212
+ writer.wrap(range, element);
213
+ this._mergeWithSiblings(writer, element);
214
+ }
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Unwraps the elements in ranges and around their ends, and keeps the selection on the text.
220
+ */
221
+ private _unwrapRanges(writer: ModelWriter, ranges: ModelRange[]): void {
222
+ const elements = this._getElementsIn(ranges);
223
+
224
+ this._keepSelection(writer, ranges, () => {
225
+ for (const element of elements) {
226
+ writer.unwrap(element);
227
+ }
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Changes the content of the selected ranges, and selects the same text then: the ends of the ranges stick to the
233
+ * text, so they follow it into the elements and out of them.
234
+ */
235
+ private _keepSelection(writer: ModelWriter, ranges: ModelRange[], change: () => void): void {
236
+ const backward = this.editor.model.document.selection.isBackward;
237
+ const ends = ranges.map(range => ({
238
+ start: ModelLivePosition.fromPosition(range.start, 'toNext'),
239
+ end: ModelLivePosition.fromPosition(range.end, 'toPrevious')
240
+ }));
241
+
242
+ change();
243
+ writer.setSelection(
244
+ ends.map(({ start, end }) => writer.createRange(start, end)),
245
+ { backward }
246
+ );
247
+
248
+ for (const { start, end } of ends) {
249
+ start.detach();
250
+ end.detach();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Merges a new element with the elements of the command next to it that have no attributes, as it has none.
256
+ */
257
+ private _mergeWithSiblings(writer: ModelWriter, element: ModelElement): void {
258
+ const isPlain = (node: ModelNode | null): boolean =>
259
+ this.isElement(node) && node.getAttributeKeys().next().done === true;
260
+
261
+ if (isPlain(element.nextSibling)) {
262
+ writer.merge(writer.createPositionAfter(element));
263
+ }
264
+
265
+ if (isPlain(element.previousSibling)) {
266
+ writer.merge(writer.createPositionBefore(element));
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Puts the target out of the element it is in: a cursor at the start or the end of the element goes before or after
272
+ * it, so that what is typed next is out of it; otherwise the element is unwrapped. A selected element that is
273
+ * unwrapped leaves its content selected.
274
+ */
275
+ private _leave(writer: ModelWriter, element: ModelElement, target: ElementOrPosition): void {
276
+ if (target.is('position') && target.parent === element && !element.isEmpty) {
277
+ if (target.isAtEnd || target.isAtStart) {
278
+ writer.setSelection(element, target.isAtEnd ? 'after' : 'before');
279
+
280
+ return;
281
+ }
282
+ }
283
+
284
+ const isSelected = this.editor.model.document.selection.getSelectedElement() === element;
285
+ const content = ModelLiveRange.fromRange(writer.createRangeIn(element));
286
+
287
+ writer.unwrap(element);
288
+
289
+ if (isSelected) {
290
+ writer.setSelection(content.toRange());
291
+ }
292
+
293
+ content.detach();
294
+ }
295
+
296
+ /**
297
+ * Puts the target in a new element: at a position, an empty element with the cursor in it; an element, e.g. a
298
+ * selected inline object, is wrapped, and selected.
299
+ */
300
+ private _insert(writer: ModelWriter, target: ElementOrPosition): void {
301
+ if (target.is('position')) {
302
+ const element = writer.createElement(this.elementName);
303
+
304
+ writer.insert(element, target);
305
+ writer.setSelection(element, 0);
306
+ } else {
307
+ writer.wrap(writer.createRangeOn(target), this.elementName);
308
+ writer.setSelection(target, 'on');
309
+ }
310
+ }
311
+ }