@ckeditor/ckeditor5-engine 47.1.0 → 47.2.0-alpha.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-engine",
3
- "version": "47.1.0",
3
+ "version": "47.2.0-alpha.1",
4
4
  "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
5
5
  "keywords": [
6
6
  "wysiwyg",
@@ -24,7 +24,7 @@
24
24
  "type": "module",
25
25
  "main": "src/index.js",
26
26
  "dependencies": {
27
- "@ckeditor/ckeditor5-utils": "47.1.0",
27
+ "@ckeditor/ckeditor5-utils": "47.2.0-alpha.1",
28
28
  "es-toolkit": "1.39.5"
29
29
  },
30
30
  "author": "CKSource (http://cksource.com/)",
@@ -210,7 +210,7 @@ export declare class DowncastDispatcher extends /* #__PURE__ */ DowncastDispatch
210
210
  */
211
211
  private _convertAttribute;
212
212
  /**
213
- * Fires re-insertion conversion (with a `reconversion` flag passed to `insert` events)
213
+ * Fires re-insertion conversion (with a `reconversion` flag passed to `remove` and `insert` events)
214
214
  * of a range of elements (only elements on the range depth, without children).
215
215
  *
216
216
  * For each node in the range on its depth (without children), {@link #event:insert `insert` event} is fired.
@@ -328,6 +328,10 @@ export type DowncastReduceChangesEventData = {
328
328
  * A buffered changes to get reduced.
329
329
  */
330
330
  changes: Iterable<DifferItem | DifferItemReinsert>;
331
+ /**
332
+ * Set of items marked for rebuild without reusing view nodes.
333
+ */
334
+ refreshedItems: Set<ModelItem>;
331
335
  };
332
336
  export type DowncastDispatcherEventMap<TItem = ModelItem> = {
333
337
  insert: {
@@ -338,6 +342,7 @@ export type DowncastDispatcherEventMap<TItem = ModelItem> = {
338
342
  remove: {
339
343
  position: ModelPosition;
340
344
  length: number;
345
+ reconversion?: boolean;
341
346
  };
342
347
  attribute: {
343
348
  item: TItem;
@@ -138,13 +138,14 @@ export class DowncastDispatcher extends /* #__PURE__ */ EmitterMixin() {
138
138
  * @param writer The view writer that should be used to modify the view document.
139
139
  */
140
140
  convertChanges(differ, markers, writer) {
141
- const conversionApi = this._createConversionApi(writer, differ.getRefreshedItems());
141
+ const refreshedItems = differ.getRefreshedItems();
142
+ const conversionApi = this._createConversionApi(writer, refreshedItems);
142
143
  // Before the view is updated, remove markers which have changed.
143
144
  for (const change of differ.getMarkersToRemove()) {
144
145
  this._convertMarkerRemove(change.name, change.range, conversionApi);
145
146
  }
146
147
  // Let features modify the change list (for example to allow reconversion).
147
- const changes = this._reduceChanges(differ.getChanges());
148
+ const changes = this._reduceChanges(differ.getChanges(), refreshedItems);
148
149
  // Convert changes that happened on model tree.
149
150
  for (const entry of changes) {
150
151
  if (entry.type === 'insert') {
@@ -315,7 +316,7 @@ export class DowncastDispatcher extends /* #__PURE__ */ EmitterMixin() {
315
316
  }
316
317
  }
317
318
  /**
318
- * Fires re-insertion conversion (with a `reconversion` flag passed to `insert` events)
319
+ * Fires re-insertion conversion (with a `reconversion` flag passed to `remove` and `insert` events)
319
320
  * of a range of elements (only elements on the range depth, without children).
320
321
  *
321
322
  * For each node in the range on its depth (without children), {@link #event:insert `insert` event} is fired.
@@ -333,6 +334,9 @@ export class DowncastDispatcher extends /* #__PURE__ */ EmitterMixin() {
333
334
  this._addConsumablesForInsert(conversionApi.consumable, walkerValues);
334
335
  // Fire a separate insert event for each node and text fragment contained shallowly in the range.
335
336
  for (const data of walkerValues.map(walkerValueToEventData)) {
337
+ // For backward compatibility and handlers that does not recognize reconversion.
338
+ this.fire(`remove:${data.item.is('element') ? data.item.name : '$text'}`, { position: data.range.start, length: data.item.offsetSize, reconversion: true }, conversionApi);
339
+ // Reinsert the view element.
336
340
  this._testAndFire('insert', { ...data, reconversion: true }, conversionApi);
337
341
  }
338
342
  }
@@ -401,8 +405,11 @@ export class DowncastDispatcher extends /* #__PURE__ */ EmitterMixin() {
401
405
  *
402
406
  * @fires reduceChanges
403
407
  */
404
- _reduceChanges(changes) {
405
- const data = { changes };
408
+ _reduceChanges(changes, refreshedItems) {
409
+ const data = {
410
+ changes,
411
+ refreshedItems
412
+ };
406
413
  this.fire('reduceChanges', data);
407
414
  return data.changes;
408
415
  }
@@ -800,6 +800,7 @@ export declare function insertAttributesAndChildren(): (evt: unknown, data: {
800
800
  export declare function remove(): (evt: unknown, data: {
801
801
  position: ModelPosition;
802
802
  length: number;
803
+ reconversion?: boolean;
803
804
  }, conversionApi: DowncastConversionApi) => void;
804
805
  /**
805
806
  * Creates a `<span>` {@link module:engine/view/attributeelement~ViewAttributeElement view attribute element} from the information
@@ -761,18 +761,17 @@ export function insertAttributesAndChildren() {
761
761
  */
762
762
  export function remove() {
763
763
  return (evt, data, conversionApi) => {
764
+ // Ignore reconversion related remove as it is handled in the `insert` of reconversion.
765
+ if (data.reconversion) {
766
+ return;
767
+ }
764
768
  // Find the view range start position by mapping the model position at which the remove happened.
765
769
  const viewStart = conversionApi.mapper.toViewPosition(data.position);
766
770
  const modelEnd = data.position.getShiftedBy(data.length);
767
771
  const viewEnd = conversionApi.mapper.toViewPosition(modelEnd, { isPhantom: true });
768
772
  const viewRange = conversionApi.writer.createRange(viewStart, viewEnd);
769
773
  // Trim the range to remove in case some UI elements are on the view range boundaries.
770
- const removed = conversionApi.writer.remove(viewRange.getTrimmed());
771
- // After the range is removed, unbind all view elements from the model.
772
- // Range inside view document fragment is used to unbind deeply.
773
- for (const child of conversionApi.writer.createRangeIn(removed).getItems()) {
774
- conversionApi.mapper.unbindViewElement(child, { defer: true });
775
- }
774
+ removeRangeAndUnbind(viewRange.getTrimmed(), conversionApi);
776
775
  };
777
776
  }
778
777
  /**
@@ -1019,7 +1018,8 @@ export function insertElement(elementCreator, consumer = defaultConsumer) {
1019
1018
  }
1020
1019
  // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1021
1020
  consumer(data.item, conversionApi.consumable);
1022
- const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
1021
+ const viewPosition = data.reconversion && removeElementAndUnbind(data.item, conversionApi) ||
1022
+ conversionApi.mapper.toViewPosition(data.range.start);
1023
1023
  conversionApi.mapper.bindElements(data.item, viewElement);
1024
1024
  conversionApi.writer.insert(viewPosition, viewElement);
1025
1025
  // Convert attributes before converting children.
@@ -1059,7 +1059,8 @@ export function insertStructure(elementCreator, consumer) {
1059
1059
  validateSlotsChildren(data.item, slotsMap, conversionApi);
1060
1060
  // Consume an element insertion and all present attributes that are specified as a reconversion triggers.
1061
1061
  consumer(data.item, conversionApi.consumable);
1062
- const viewPosition = conversionApi.mapper.toViewPosition(data.range.start);
1062
+ const viewPosition = data.reconversion && removeElementAndUnbind(data.item, conversionApi) ||
1063
+ conversionApi.mapper.toViewPosition(data.range.start);
1063
1064
  conversionApi.mapper.bindElements(data.item, viewElement);
1064
1065
  conversionApi.writer.insert(viewPosition, viewElement);
1065
1066
  // Convert attributes before converting children.
@@ -1119,6 +1120,25 @@ export function insertUIElement(elementCreator) {
1119
1120
  evt.stop();
1120
1121
  };
1121
1122
  }
1123
+ /**
1124
+ * Removes given view range content and unbinds removed elements.
1125
+ */
1126
+ function removeRangeAndUnbind(viewRange, conversionApi) {
1127
+ const removed = conversionApi.writer.remove(viewRange);
1128
+ // After the range is removed, unbind all view elements from the model.
1129
+ // Range inside view document fragment is used to unbind deeply.
1130
+ for (const child of conversionApi.writer.createRangeIn(removed).getItems()) {
1131
+ conversionApi.mapper.unbindViewElement(child, { defer: true });
1132
+ }
1133
+ return viewRange.start;
1134
+ }
1135
+ /**
1136
+ * Removes view element for given model element and unbinds removed view elements.
1137
+ */
1138
+ function removeElementAndUnbind(modelElement, conversionApi) {
1139
+ const viewElement = conversionApi.mapper.toViewElement(modelElement);
1140
+ return viewElement && removeRangeAndUnbind(conversionApi.writer.createRangeOn(viewElement), conversionApi);
1141
+ }
1122
1142
  /**
1123
1143
  * Function factory that returns a default downcast converter for removing a {@link module:engine/view/uielement~ViewUIElement UI element}
1124
1144
  * based on marker remove change.
@@ -1941,10 +1961,14 @@ function createChangeReducer(model) {
1941
1961
  // For attribute use node affected by the change.
1942
1962
  // For insert or remove use parent element because we need to check if it's added/removed child.
1943
1963
  const node = change.type == 'attribute' ? change.range.start.nodeAfter : change.position.parent;
1944
- if (!node || !shouldReplace(node, change)) {
1964
+ if (!node || !shouldReplace(node, change) || change.type == 'reinsert') {
1945
1965
  reducedChanges.push(change);
1946
1966
  continue;
1947
1967
  }
1968
+ // Force to not-reuse view elements renamed in model.
1969
+ if (change.type == 'insert' && change.action == 'rename') {
1970
+ data.refreshedItems.add(change.position.nodeAfter);
1971
+ }
1948
1972
  // If it's already marked for reconversion, so skip this change, otherwise add the diff items.
1949
1973
  if (!data.reconvertedElements.has(node)) {
1950
1974
  data.reconvertedElements.add(node);
@@ -1963,11 +1987,6 @@ function createChangeReducer(model) {
1963
1987
  changeIndex = i;
1964
1988
  }
1965
1989
  reducedChanges.splice(changeIndex, 0, {
1966
- type: 'remove',
1967
- name: node.name,
1968
- position,
1969
- length: 1
1970
- }, {
1971
1990
  type: 'reinsert',
1972
1991
  name: node.name,
1973
1992
  position,
@@ -2071,6 +2090,7 @@ function fillSlots(viewElement, slotsMap, conversionApi, options) {
2071
2090
  // Fill slots with nested view nodes.
2072
2091
  for ([currentSlot, currentSlotNodes] of slotsMap) {
2073
2092
  reinsertOrConvertNodes(viewElement, currentSlotNodes, conversionApi, options);
2093
+ conversionApi.writer.setCustomProperty('$structureSlotParent', true, currentSlot.parent);
2074
2094
  conversionApi.writer.move(conversionApi.writer.createRangeIn(currentSlot), conversionApi.writer.createPositionBefore(currentSlot));
2075
2095
  conversionApi.writer.remove(currentSlot);
2076
2096
  }
@@ -262,7 +262,10 @@ class Insertion {
262
262
  */
263
263
  handleNodes(nodes) {
264
264
  for (const node of Array.from(nodes)) {
265
- this._handleNode(node);
265
+ // Ignore empty nodes, especially empty text nodes.
266
+ if (node.offsetSize > 0) {
267
+ this._handleNode(node);
268
+ }
266
269
  }
267
270
  // Insert nodes collected in temporary ModelDocumentFragment.
268
271
  this._insertPartialFragment();
@@ -344,7 +347,7 @@ class Insertion {
344
347
  return;
345
348
  }
346
349
  // Add node to the current temporary ModelDocumentFragment.
347
- this._appendToFragment(node);
350
+ node = this._appendToFragment(node);
348
351
  // Store the first and last nodes for easy access for merging with sibling nodes.
349
352
  if (!this._firstNode) {
350
353
  this._firstNode = node;
@@ -408,6 +411,11 @@ class Insertion {
408
411
  }
409
412
  this.writer.insert(node, this._documentFragmentPosition);
410
413
  this._documentFragmentPosition = this._documentFragmentPosition.getShiftedBy(node.offsetSize);
414
+ // In case text node was merged with already inserted text node, we need to get the actual node that is in the document.
415
+ // This happens when there is a non-allowed object between text nodes.
416
+ if (!node.parent) {
417
+ node = this._documentFragmentPosition.nodeBefore;
418
+ }
411
419
  // The last inserted object should be selected because we can't put a collapsed selection after it.
412
420
  if (this.schema.isObject(node) && !this.schema.checkChild(this.position, '$text')) {
413
421
  this._nodeToSelect = node;
@@ -416,6 +424,7 @@ class Insertion {
416
424
  this._nodeToSelect = null;
417
425
  }
418
426
  this._filterAttributesOf.push(node);
427
+ return node;
419
428
  }
420
429
  /**
421
430
  * Sets `_affectedStart` and `_affectedEnd` to the given `position`. Should be used before a change is done during insertion process to
@@ -675,12 +684,14 @@ class Insertion {
675
684
  * @param childNode The node to check.
676
685
  */
677
686
  _getAllowedIn(contextElement, childNode) {
687
+ const context = this.schema.createContext(contextElement);
678
688
  // Check if a node can be inserted in the given context...
679
- if (this.schema.checkChild(contextElement, childNode)) {
689
+ if (this.schema.checkChild(context, childNode)) {
680
690
  return contextElement;
681
691
  }
682
692
  // ...or it would be accepted if a paragraph would be inserted.
683
- if (this.schema.checkChild(contextElement, 'paragraph') && this.schema.checkChild('paragraph', childNode)) {
693
+ if (this.schema.checkChild(context, 'paragraph') &&
694
+ this.schema.checkChild(context.push('paragraph'), childNode)) {
684
695
  return contextElement;
685
696
  }
686
697
  // If the child wasn't allowed in the context element and the element is a limit there's no point in
@@ -66,10 +66,11 @@ export function insertObject(model, object, selectable, options = {}) {
66
66
  }
67
67
  let elementToInsert = object;
68
68
  const insertionPositionParent = insertionSelection.anchor.parent;
69
- // Autoparagraphing of an inline objects.
70
- if (!model.schema.checkChild(insertionPositionParent, object) &&
71
- model.schema.checkChild(insertionPositionParent, 'paragraph') &&
72
- model.schema.checkChild('paragraph', object)) {
69
+ const context = model.schema.createContext(insertionPositionParent);
70
+ // Auto-paragraphing of an inline objects.
71
+ if (!model.schema.checkChild(context, object) &&
72
+ model.schema.checkChild(context, 'paragraph') &&
73
+ model.schema.checkChild(context.push('paragraph'), object)) {
73
74
  elementToInsert = writer.createElement('paragraph');
74
75
  writer.insert(object, elementToInsert);
75
76
  }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { DomEventObserver } from './domeventobserver.js';
9
9
  import { type ViewDocumentDomEventData } from './domeventdata.js';
10
+ import type { BubblingEvent } from './bubblingemittermixin.js';
10
11
  import { type KeystrokeInfo } from '@ckeditor/ckeditor5-utils';
11
12
  /**
12
13
  * Observer for events connected with pressing keyboard keys.
@@ -36,10 +37,10 @@ export declare class KeyObserver extends DomEventObserver<'keydown' | 'keyup', K
36
37
  * @see module:engine/view/observer/keyobserver~KeyObserver
37
38
  * @eventName module:engine/view/document~ViewDocument#keydown
38
39
  */
39
- export type ViewDocumentKeyDownEvent = {
40
+ export type ViewDocumentKeyDownEvent = BubblingEvent<{
40
41
  name: 'keydown';
41
42
  args: [data: ViewDocumentKeyEventData];
42
- };
43
+ }>;
43
44
  /**
44
45
  * Fired when a key has been released.
45
46
  *
@@ -51,10 +52,10 @@ export type ViewDocumentKeyDownEvent = {
51
52
  * @see module:engine/view/observer/keyobserver~KeyObserver
52
53
  * @eventName module:engine/view/document~ViewDocument#keyup
53
54
  */
54
- export type ViewDocumentKeyUpEvent = {
55
+ export type ViewDocumentKeyUpEvent = BubblingEvent<{
55
56
  name: 'keyup';
56
57
  args: [data: ViewDocumentKeyEventData];
57
- };
58
+ }>;
58
59
  /**
59
60
  * The value of both events - {@link ~ViewDocumentKeyDownEvent} and {@link ~ViewDocumentKeyUpEvent}.
60
61
  */
@@ -197,6 +197,18 @@ function updateDocumentPlaceholders(placeholders, writer) {
197
197
  continue;
198
198
  }
199
199
  const hostElement = getChildPlaceholderHostSubstitute(element);
200
+ // If host element changed, remove the placeholder from the previous one.
201
+ // This can happen when user replaces the first child element of the parent element
202
+ // with new one, but the previous one is still in the view tree.
203
+ // See:
204
+ // https://github.com/ckeditor/ckeditor5/issues/14354
205
+ // https://github.com/ckeditor/ckeditor5/issues/18149
206
+ if (hostElement !== config.hostElement && config.hostElement) {
207
+ writer.removeAttribute('data-placeholder', config.hostElement);
208
+ hideViewPlaceholder(writer, config.hostElement);
209
+ config.hostElement = null;
210
+ wasViewModified = true;
211
+ }
200
212
  // When not a direct host, it could happen that there is no child element
201
213
  // capable of displaying a placeholder.
202
214
  if (!hostElement) {