ckeditor5-elements-schema 49.0.0-alpha.2 → 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.
- package/README.md +28 -2
- package/dist/augmentation.d.ts +7 -1
- package/dist/augmentation.d.ts.map +1 -1
- package/dist/commands/elementBlockCommand.d.ts +46 -0
- package/dist/commands/elementBlockCommand.d.ts.map +1 -0
- package/dist/commands/elementBlockCommand.js +71 -0
- package/dist/commands/elementBlockCommand.js.map +1 -0
- package/dist/commands/elementContainerCommand.d.ts +36 -0
- package/dist/commands/elementContainerCommand.d.ts.map +1 -0
- package/dist/commands/elementContainerCommand.js +67 -0
- package/dist/commands/elementContainerCommand.js.map +1 -0
- package/dist/commands/elementInlineCommand.d.ts +87 -0
- package/dist/commands/elementInlineCommand.d.ts.map +1 -0
- package/dist/commands/elementInlineCommand.js +253 -0
- package/dist/commands/elementInlineCommand.js.map +1 -0
- package/dist/commands/elementInsertCommand.d.ts +33 -0
- package/dist/commands/elementInsertCommand.d.ts.map +1 -0
- package/dist/commands/elementInsertCommand.js +61 -0
- package/dist/commands/elementInsertCommand.js.map +1 -0
- package/dist/commands/elementSchemaCommand.d.ts +62 -0
- package/dist/commands/elementSchemaCommand.d.ts.map +1 -0
- package/dist/commands/elementSchemaCommand.js +107 -0
- package/dist/commands/elementSchemaCommand.js.map +1 -0
- package/dist/{convertElementSchema.d.ts → conversion/convertElementSchema.d.ts} +3 -2
- package/dist/conversion/convertElementSchema.d.ts.map +1 -0
- package/dist/{convertElementSchema.js → conversion/convertElementSchema.js} +16 -4
- package/dist/conversion/convertElementSchema.js.map +1 -0
- package/dist/conversion/inlineTextElement.d.ts +24 -0
- package/dist/conversion/inlineTextElement.d.ts.map +1 -0
- package/dist/conversion/inlineTextElement.js +28 -0
- package/dist/conversion/inlineTextElement.js.map +1 -0
- package/dist/definition/canHold.d.ts.map +1 -0
- package/dist/definition/canHold.js.map +1 -0
- package/dist/definition/compileElementSchema.d.ts.map +1 -0
- package/dist/definition/compileElementSchema.js.map +1 -0
- package/dist/definition/elementSchemaError.d.ts.map +1 -0
- package/dist/definition/elementSchemaError.js.map +1 -0
- package/dist/definition/readDefinition.d.ts.map +1 -0
- package/dist/definition/readDefinition.js.map +1 -0
- package/dist/definition/suggest.d.ts.map +1 -0
- package/dist/definition/suggest.js.map +1 -0
- package/dist/definition/types.d.ts.map +1 -0
- package/dist/definition/types.js.map +1 -0
- package/dist/definition/validateElementSchema.d.ts.map +1 -0
- package/dist/definition/validateElementSchema.js.map +1 -0
- package/dist/elementSchema.js +4 -4
- package/dist/elementSchema.js.map +1 -1
- package/dist/elementSchemaCommands.d.ts +83 -0
- package/dist/elementSchemaCommands.d.ts.map +1 -0
- package/dist/elementSchemaCommands.js +137 -0
- package/dist/elementSchemaCommands.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/typing/twoStepCaretMovement.d.ts +11 -0
- package/dist/typing/twoStepCaretMovement.d.ts.map +1 -0
- package/dist/typing/twoStepCaretMovement.js +76 -0
- package/dist/typing/twoStepCaretMovement.js.map +1 -0
- package/dist/validate.d.ts +4 -4
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +3 -3
- package/dist/validate.js.map +1 -1
- package/package.json +2 -2
- package/src/augmentation.ts +8 -1
- package/src/commands/elementBlockCommand.ts +87 -0
- package/src/commands/elementContainerCommand.ts +83 -0
- package/src/commands/elementInlineCommand.ts +311 -0
- package/src/commands/elementInsertCommand.ts +74 -0
- package/src/commands/elementSchemaCommand.ts +140 -0
- package/src/{convertElementSchema.ts → conversion/convertElementSchema.ts} +19 -5
- package/src/conversion/inlineTextElement.ts +31 -0
- package/src/elementSchema.ts +5 -5
- package/src/elementSchemaCommands.ts +202 -0
- package/src/index.ts +17 -1
- package/src/typing/twoStepCaretMovement.ts +119 -0
- package/src/validate.ts +4 -4
- package/dist/canHold.d.ts.map +0 -1
- package/dist/canHold.js.map +0 -1
- package/dist/compileElementSchema.d.ts.map +0 -1
- package/dist/compileElementSchema.js.map +0 -1
- package/dist/convertElementSchema.d.ts.map +0 -1
- package/dist/convertElementSchema.js.map +0 -1
- package/dist/elementSchemaError.d.ts.map +0 -1
- package/dist/elementSchemaError.js.map +0 -1
- package/dist/readDefinition.d.ts.map +0 -1
- package/dist/readDefinition.js.map +0 -1
- package/dist/suggest.d.ts.map +0 -1
- package/dist/suggest.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validateElementSchema.d.ts.map +0 -1
- package/dist/validateElementSchema.js.map +0 -1
- /package/dist/{canHold.d.ts → definition/canHold.d.ts} +0 -0
- /package/dist/{canHold.js → definition/canHold.js} +0 -0
- /package/dist/{compileElementSchema.d.ts → definition/compileElementSchema.d.ts} +0 -0
- /package/dist/{compileElementSchema.js → definition/compileElementSchema.js} +0 -0
- /package/dist/{elementSchemaError.d.ts → definition/elementSchemaError.d.ts} +0 -0
- /package/dist/{elementSchemaError.js → definition/elementSchemaError.js} +0 -0
- /package/dist/{readDefinition.d.ts → definition/readDefinition.d.ts} +0 -0
- /package/dist/{readDefinition.js → definition/readDefinition.js} +0 -0
- /package/dist/{suggest.d.ts → definition/suggest.d.ts} +0 -0
- /package/dist/{suggest.js → definition/suggest.js} +0 -0
- /package/dist/{types.d.ts → definition/types.d.ts} +0 -0
- /package/dist/{types.js → definition/types.js} +0 -0
- /package/dist/{validateElementSchema.d.ts → definition/validateElementSchema.d.ts} +0 -0
- /package/dist/{validateElementSchema.js → definition/validateElementSchema.js} +0 -0
- /package/src/{canHold.ts → definition/canHold.ts} +0 -0
- /package/src/{compileElementSchema.ts → definition/compileElementSchema.ts} +0 -0
- /package/src/{elementSchemaError.ts → definition/elementSchemaError.ts} +0 -0
- /package/src/{readDefinition.ts → definition/readDefinition.ts} +0 -0
- /package/src/{suggest.ts → definition/suggest.ts} +0 -0
- /package/src/{types.ts → definition/types.ts} +0 -0
- /package/src/{validateElementSchema.ts → definition/validateElementSchema.ts} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ModelDocumentSelection, ModelSelection } from 'ckeditor5';
|
|
2
|
+
import { selectNewElement, type ElementOrPosition } from 'ckeditor5-elements-core';
|
|
3
|
+
import ElementSchemaCommand from './elementSchemaCommand.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Inserts the element, as CKEditor's horizontal line and image commands do: an element that is not typed around text,
|
|
7
|
+
* e.g. an `empty` element of the element schema (a `divider`, or an inline `badge`), or an inline `container` (a `card`).
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* editor.execute('divider'); // In place of the selection.
|
|
11
|
+
* editor.commands.get('divider').executeFor(element); // After the element.
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* For the selection, it replaces the selected content, as typing does; at a position, it is inserted there; for an
|
|
15
|
+
* element, after it. Where the element cannot be, the blocks are split up to where it can, e.g. a block-level one in the
|
|
16
|
+
* middle of a paragraph. Then it is selected, as `selectNewElement()` does: an `empty` element as a whole, and an inline
|
|
17
|
+
* container gets a paragraph, with the cursor in it.
|
|
18
|
+
*
|
|
19
|
+
* It does not toggle: it is never "on".
|
|
20
|
+
*/
|
|
21
|
+
export default class ElementInsertCommand extends ElementSchemaCommand {
|
|
22
|
+
/**
|
|
23
|
+
* Inserts a new element at the target, and selects it.
|
|
24
|
+
*/
|
|
25
|
+
public override execute(elementOrPosition?: ElementOrPosition): void {
|
|
26
|
+
const model = this.editor.model;
|
|
27
|
+
const selectable = this._getSelectable(elementOrPosition);
|
|
28
|
+
|
|
29
|
+
if (!selectable) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
model.change(writer => {
|
|
34
|
+
const element = writer.createElement(this.elementName);
|
|
35
|
+
|
|
36
|
+
model.insertContent(element, selectable);
|
|
37
|
+
|
|
38
|
+
if (element.parent) {
|
|
39
|
+
selectNewElement(writer, element);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Enabled when the target can be edited, and the element can be there, or in one of its ancestors.
|
|
46
|
+
*/
|
|
47
|
+
protected override _isEnabledFor(elementOrPosition?: ElementOrPosition): boolean {
|
|
48
|
+
const position = this._getSelectable(elementOrPosition)?.getFirstPosition();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
!!position &&
|
|
52
|
+
super._isEnabledFor(elementOrPosition) &&
|
|
53
|
+
!!this.editor.model.schema.findAllowedParent(position, this.elementName)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns where the element goes: the selection, or the given position, or the position after the given element.
|
|
59
|
+
*/
|
|
60
|
+
private _getSelectable(elementOrPosition?: ElementOrPosition): ModelSelection | ModelDocumentSelection | null {
|
|
61
|
+
const model = this.editor.model;
|
|
62
|
+
const target = this._getElementOrPosition(elementOrPosition);
|
|
63
|
+
|
|
64
|
+
if (!target) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!elementOrPosition) {
|
|
69
|
+
return model.document.selection;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return model.createSelection(target.is('position') ? target : model.createPositionAfter(target));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Editor, ModelElement, ModelDocumentFragment, ModelItem, ModelPosition, ModelRange } from 'ckeditor5';
|
|
2
|
+
import { ElementOrPositionCommand, type ElementOrPosition } from 'ckeditor5-elements-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The base of the commands of the elements of a schema, one for each kind of element: what they share is the element,
|
|
6
|
+
* the state, and how they find their targets and check the schema.
|
|
7
|
+
*
|
|
8
|
+
* * {@link ElementInlineCommand}: an inline element with text, as bold (an inline `block`);
|
|
9
|
+
* * {@link ElementBlockCommand}: a block, as a heading (a `block`);
|
|
10
|
+
* * {@link ElementContainerCommand}: a container of blocks, as a block quote (a `container`);
|
|
11
|
+
* * {@link ElementInsertCommand}: an element that is inserted, as a horizontal line, an inline image, or a card (an
|
|
12
|
+
* `empty` element, and an inline `container`).
|
|
13
|
+
*
|
|
14
|
+
* As every {@link ElementOrPositionCommand}, it is for the selection, or for a given position or element, e.g. of a
|
|
15
|
+
* context menu.
|
|
16
|
+
*/
|
|
17
|
+
export default class ElementSchemaCommand extends ElementOrPositionCommand {
|
|
18
|
+
/**
|
|
19
|
+
* Whether the target is in the element, or is one, for the commands that toggle it.
|
|
20
|
+
*
|
|
21
|
+
* @observable
|
|
22
|
+
*/
|
|
23
|
+
declare public isOn: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The name of the model element of the command.
|
|
27
|
+
*/
|
|
28
|
+
public readonly elementName: string;
|
|
29
|
+
|
|
30
|
+
constructor(editor: Editor, elementName: string) {
|
|
31
|
+
super(editor);
|
|
32
|
+
|
|
33
|
+
this.elementName = elementName;
|
|
34
|
+
this.set('isOn', false);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public override refresh(elementOrPosition?: ElementOrPosition): void {
|
|
38
|
+
this.isOn = this._isOnFor(elementOrPosition);
|
|
39
|
+
super.refresh(elementOrPosition);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a node is an element of the command.
|
|
44
|
+
*/
|
|
45
|
+
public isElement(node: ModelItem | ModelDocumentFragment | null): node is ModelElement {
|
|
46
|
+
return !!node?.is('element', this.elementName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the closest element of the command that the target is, or is in: by default, the selection. Never a root.
|
|
51
|
+
*/
|
|
52
|
+
protected _findElement(elementOrPosition?: ElementOrPosition): ModelElement | null {
|
|
53
|
+
return this._getAncestors(elementOrPosition).find(element => this.isElement(element)) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the elements the target is, or is in (by default, the selection), the inner first: an element and its
|
|
58
|
+
* ancestors, or the parent of a position and its ancestors. Not the root.
|
|
59
|
+
*/
|
|
60
|
+
protected _getAncestors(elementOrPosition?: ElementOrPosition): ModelElement[] {
|
|
61
|
+
const target = this._getElementOrPosition(elementOrPosition);
|
|
62
|
+
const element = target && (target.is('position') ? target.parent : target);
|
|
63
|
+
|
|
64
|
+
return element?.is('element')
|
|
65
|
+
? element
|
|
66
|
+
.getAncestors({ includeSelf: true, parentFirst: true })
|
|
67
|
+
.filter((node): node is ModelElement => node.is('element') && !node.is('rootElement'))
|
|
68
|
+
: [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the ranges of the selected content, when the command is for the selection, and the selection is content,
|
|
73
|
+
* not a cursor nor an element.
|
|
74
|
+
*/
|
|
75
|
+
protected _getSelectedRanges(elementOrPosition?: ElementOrPosition): ModelRange[] | null {
|
|
76
|
+
const selection = this.editor.model.document.selection;
|
|
77
|
+
|
|
78
|
+
return !elementOrPosition && !selection.isCollapsed && !selection.getSelectedElement()
|
|
79
|
+
? Array.from(selection.getRanges())
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the blocks of the target, by default of the selection: the selected blocks, or the block-level element the
|
|
85
|
+
* target is, or is in (e.g. the paragraph with the cursor, a selected horizontal line, or a block quote).
|
|
86
|
+
*/
|
|
87
|
+
protected _getBlocks(elementOrPosition?: ElementOrPosition): ModelElement[] {
|
|
88
|
+
const selection = this.editor.model.document.selection;
|
|
89
|
+
const blocks =
|
|
90
|
+
elementOrPosition || selection.getSelectedElement() ? [] : Array.from(selection.getSelectedBlocks());
|
|
91
|
+
|
|
92
|
+
if (blocks.length) {
|
|
93
|
+
return blocks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const block = this._getAncestors(elementOrPosition).find(
|
|
97
|
+
element => !this.editor.model.schema.isInline(element)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return block ? [block] : [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether an element (by default, the one of the command) can be at a position, and hold the items there: the check
|
|
105
|
+
* for wrapping them, or for renaming their parent.
|
|
106
|
+
*/
|
|
107
|
+
protected _canHold(position: ModelPosition, items: Iterable<ModelItem>, name = this.elementName): boolean {
|
|
108
|
+
const schema = this.editor.model.schema;
|
|
109
|
+
|
|
110
|
+
if (!schema.checkChild(position, name)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const parent = position.parent as ModelElement;
|
|
115
|
+
const context = [...parent.getAncestors({ includeSelf: true }).filter(node => node.is('element')), name];
|
|
116
|
+
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
if (!schema.checkChild(context, item.is('$textProxy') ? '$text' : item)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether the children of an element can be in its place: the check for unwrapping it.
|
|
128
|
+
*/
|
|
129
|
+
protected _canUnwrap(element: ModelElement): boolean {
|
|
130
|
+
const model = this.editor.model;
|
|
131
|
+
|
|
132
|
+
if (!element.parent) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const position = model.createPositionBefore(element);
|
|
137
|
+
|
|
138
|
+
return Array.from(element.getChildren()).every(child => model.schema.checkChild(position, child));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Editor } from 'ckeditor5';
|
|
2
|
-
import type { ElementSchemaElement, ElementSchemaInput } from '
|
|
2
|
+
import type { ElementSchemaElement, ElementSchemaInput } from '../definition/types.js';
|
|
3
|
+
import { createInlineTextElement } from './inlineTextElement.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Returns the tag of an element in the data: a `span` for what is in a line of text (an inline element that is empty or
|
|
@@ -14,15 +15,17 @@ export function getViewTag({ type, isInline }: Pick<ElementSchemaElement, 'type'
|
|
|
14
15
|
* {@link getViewTag}), with its name as the class. Its attributes are `data-*` attributes. The data is loaded as it is
|
|
15
16
|
* saved. The editing view is the same, except for the tag, when the element has an `editingTag`.
|
|
16
17
|
*
|
|
17
|
-
* The `empty` elements are empty in the data too: `<div class="divider"></div>`, with no filler.
|
|
18
|
+
* The `empty` elements are empty in the data too: `<div class="divider"></div>`, with no filler. The inline elements with
|
|
19
|
+
* text have no block filler, so the cursor can go in an empty one (see `withoutBlockFiller()`).
|
|
18
20
|
*/
|
|
19
21
|
export function convertElementSchema(editor: Editor, input: ElementSchemaInput): void {
|
|
20
22
|
const { conversion } = editor;
|
|
21
23
|
|
|
22
24
|
for (const [name, element] of Object.entries(input)) {
|
|
23
|
-
const { type, editingTag, allowAttributes } = element as ElementSchemaElement;
|
|
25
|
+
const { type, isInline, editingTag, allowAttributes } = element as ElementSchemaElement;
|
|
24
26
|
const tag = getViewTag(element as ElementSchemaElement);
|
|
25
27
|
const view = { name: tag, classes: name };
|
|
28
|
+
const isInlineText = type === 'block' && !!isInline;
|
|
26
29
|
|
|
27
30
|
for (const key of allowAttributes ?? []) {
|
|
28
31
|
// The names of HTML attributes are lowercase, so the data has them in lowercase; the model keeps the key as it is.
|
|
@@ -40,10 +43,21 @@ export function convertElementSchema(editor: Editor, input: ElementSchemaInput):
|
|
|
40
43
|
});
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
if (
|
|
46
|
+
if (isInlineText) {
|
|
47
|
+
conversion.for('dataDowncast').elementToElement({
|
|
48
|
+
model: name,
|
|
49
|
+
view: (_element, { writer }) => createInlineTextElement(writer, tag, name),
|
|
50
|
+
converterPriority: 'high'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (editingTag || isInlineText) {
|
|
44
55
|
conversion.for('editingDowncast').elementToElement({
|
|
45
56
|
model: name,
|
|
46
|
-
view:
|
|
57
|
+
view: (_element, { writer }) =>
|
|
58
|
+
isInlineText
|
|
59
|
+
? createInlineTextElement(writer, editingTag ?? tag, name)
|
|
60
|
+
: writer.createContainerElement(editingTag ?? tag, { class: name }),
|
|
47
61
|
converterPriority: 'high'
|
|
48
62
|
});
|
|
49
63
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ViewContainerElement, ViewDowncastWriter } from 'ckeditor5';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Makes a view element have no block filler, and returns it: for the view of an inline element with text, e.g. a bold
|
|
5
|
+
* that is an element, so that the cursor can go in it when it is empty.
|
|
6
|
+
*
|
|
7
|
+
* CKEditor puts a block filler, a `<br>`, in an empty container element, so that an empty paragraph has a height. In an
|
|
8
|
+
* inline element, the browser does not type there: the text goes before the element. Without the block filler, CKEditor
|
|
9
|
+
* puts its inline filler where the cursor is, as in the empty `<strong>` of its own bold, and the text goes in.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* conversion.for('downcast').elementToElement({
|
|
13
|
+
* model: 'bold',
|
|
14
|
+
* view: (element, { writer }) => withoutBlockFiller(writer.createContainerElement('strong'))
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The converters of the element schema (`elementSchemaConverters`) and the `view` of `ElementSchemaCommands` do it already.
|
|
19
|
+
*/
|
|
20
|
+
export function withoutBlockFiller<T extends ViewContainerElement>(element: T): T {
|
|
21
|
+
element.getFillerOffset = () => null;
|
|
22
|
+
|
|
23
|
+
return element;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates the view of an inline element with text: an element with no block filler (see {@link withoutBlockFiller}).
|
|
28
|
+
*/
|
|
29
|
+
export function createInlineTextElement(writer: ViewDowncastWriter, tag: string, name?: string): ViewContainerElement {
|
|
30
|
+
return withoutBlockFiller(writer.createContainerElement(tag, name ? { class: name } : {}));
|
|
31
|
+
}
|
package/src/elementSchema.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Plugin, type ModelSchema } from 'ckeditor5';
|
|
2
2
|
import { findPlugin } from 'ckeditor5-elements-core';
|
|
3
|
-
import { compileElementSchema } from './compileElementSchema.js';
|
|
4
|
-
import { convertElementSchema } from './convertElementSchema.js';
|
|
5
|
-
import ElementSchemaError from './elementSchemaError.js';
|
|
6
|
-
import type { ElementSchemaInput, ElementSchemaKind } from './types.js';
|
|
7
|
-
import { validateElementSchema } from './validateElementSchema.js';
|
|
3
|
+
import { compileElementSchema } from './definition/compileElementSchema.js';
|
|
4
|
+
import { convertElementSchema } from './conversion/convertElementSchema.js';
|
|
5
|
+
import ElementSchemaError from './definition/elementSchemaError.js';
|
|
6
|
+
import type { ElementSchemaInput, ElementSchemaKind } from './definition/types.js';
|
|
7
|
+
import { validateElementSchema } from './definition/validateElementSchema.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Declares elements in the model schema of the editor from a simple definition, the `elementSchema` editor option: a
|