@ckeditor/ckeditor5-engine 46.1.1 → 47.0.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": "46.1.1",
3
+ "version": "47.0.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": "46.1.1",
27
+ "@ckeditor/ckeditor5-utils": "47.0.0-alpha.1",
28
28
  "es-toolkit": "1.39.5"
29
29
  },
30
30
  "author": "CKSource (http://cksource.com/)",
@@ -1881,7 +1881,7 @@ function prepareDescriptor(highlightDescriptor, data, conversionApi) {
1881
1881
  // If passed descriptor is a creator function, call it. If not, just use passed value.
1882
1882
  const descriptor = typeof highlightDescriptor == 'function' ?
1883
1883
  highlightDescriptor(data, conversionApi) :
1884
- highlightDescriptor;
1884
+ { ...highlightDescriptor };
1885
1885
  if (!descriptor) {
1886
1886
  return null;
1887
1887
  }
@@ -15,7 +15,7 @@ import { ModelRange } from './range.js';
15
15
  * changed elements, after all changes are applied on the model document. Calculates the diff between saved
16
16
  * elements and new ones and returns a change set.
17
17
  */
18
- class Differ {
18
+ export class Differ {
19
19
  /**
20
20
  * Priority of the {@link ~Differ#_elementState element states}. States on higher indexes of the array can overwrite states on the lower
21
21
  * indexes.
@@ -1144,7 +1144,6 @@ class Differ {
1144
1144
  }
1145
1145
  }
1146
1146
  }
1147
- export { Differ };
1148
1147
  /**
1149
1148
  * Returns a snapshot for the specified child node. Text node snapshots have the `name` property set to `$text`.
1150
1149
  */
@@ -133,7 +133,7 @@ export class MergeOperation extends Operation {
133
133
  */
134
134
  throw new CKEditorError('merge-operation-target-position-invalid', this);
135
135
  }
136
- else if (this.howMany != sourceElement.maxOffset) {
136
+ else if (this.howMany !== Number.NEGATIVE_INFINITY && this.howMany != sourceElement.maxOffset) {
137
137
  /**
138
138
  * Merge operation specifies wrong number of nodes to move.
139
139
  *
@@ -141,6 +141,7 @@ export class MergeOperation extends Operation {
141
141
  */
142
142
  throw new CKEditorError('merge-operation-how-many-invalid', this);
143
143
  }
144
+ this.howMany = this.sourcePosition.parent.maxOffset;
144
145
  }
145
146
  /**
146
147
  * @inheritDoc
@@ -84,7 +84,7 @@ export class RenameOperation extends Operation {
84
84
  */
85
85
  throw new CKEditorError('rename-operation-wrong-position', this);
86
86
  }
87
- else if (element.name !== this.oldName) {
87
+ else if (this.oldName !== '' && element.name !== this.oldName) {
88
88
  /**
89
89
  * Element to change has different name than operation's old name.
90
90
  *
@@ -92,6 +92,7 @@ export class RenameOperation extends Operation {
92
92
  */
93
93
  throw new CKEditorError('rename-operation-wrong-name', this);
94
94
  }
95
+ this.oldName = element.name;
95
96
  }
96
97
  /**
97
98
  * @inheritDoc
@@ -138,7 +138,7 @@ export class SplitOperation extends Operation {
138
138
  */
139
139
  throw new CKEditorError('split-operation-split-in-root', this);
140
140
  }
141
- else if (this.howMany != element.maxOffset - this.splitPosition.offset) {
141
+ else if (this.howMany !== Number.NEGATIVE_INFINITY && this.howMany != element.maxOffset - this.splitPosition.offset) {
142
142
  /**
143
143
  * Split operation specifies wrong number of nodes to move.
144
144
  *
@@ -154,6 +154,7 @@ export class SplitOperation extends Operation {
154
154
  */
155
155
  throw new CKEditorError('split-operation-graveyard-position-invalid', this);
156
156
  }
157
+ this.howMany = this.splitPosition.parent.maxOffset - this.splitPosition.offset;
157
158
  }
158
159
  /**
159
160
  * @inheritDoc
@@ -20,7 +20,7 @@ const DEFAULT_PRIORITY = 10;
20
20
  * To create a new attribute element instance use the
21
21
  * {@link module:engine/view/downcastwriter~ViewDowncastWriter#createAttributeElement `ViewDowncastWriter#createAttributeElement()`} method.
22
22
  */
23
- class ViewAttributeElement extends ViewElement {
23
+ export class ViewAttributeElement extends ViewElement {
24
24
  static DEFAULT_PRIORITY = DEFAULT_PRIORITY;
25
25
  /**
26
26
  * Element priority. Decides in what order elements are wrapped by {@link module:engine/view/downcastwriter~ViewDowncastWriter}.
@@ -173,7 +173,6 @@ class ViewAttributeElement extends ViewElement {
173
173
  return super._canSubtractAttributesOf(otherElement);
174
174
  }
175
175
  }
176
- export { ViewAttributeElement };
177
176
  // The magic of type inference using `is` method is centralized in `TypeCheckable` class.
178
177
  // Proper overload would interfere with that.
179
178
  ViewAttributeElement.prototype.is = function (type, name) {
@@ -7,7 +7,9 @@
7
7
  */
8
8
  import { CKEditorError, EmitterMixin, EventInfo, toArray } from '@ckeditor/ckeditor5-utils';
9
9
  import { BubblingEventInfo } from './bubblingeventinfo.js';
10
- const contextsSymbol = Symbol('bubbling contexts');
10
+ const bubblingEmitterSymbol = Symbol('bubblingEmitter');
11
+ const callbackMapSymbol = Symbol('bubblingCallbacks');
12
+ const contextsSymbol = Symbol('bubblingContexts');
11
13
  /**
12
14
  * Bubbling emitter mixin for the view document as described in the {@link ~BubblingEmitter} interface.
13
15
  *
@@ -29,42 +31,27 @@ export function BubblingEmitterMixin(base) {
29
31
  fire(eventOrInfo, ...eventArgs) {
30
32
  try {
31
33
  const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo(this, eventOrInfo);
32
- const eventContexts = getBubblingContexts(this);
33
- if (!eventContexts.size) {
34
- return;
35
- }
34
+ const bubblingEmitter = getBubblingEmitter(this);
35
+ const customContexts = getCustomContexts(this);
36
36
  updateEventInfo(eventInfo, 'capturing', this);
37
37
  // The capture phase of the event.
38
- if (fireListenerFor(eventContexts, '$capture', eventInfo, ...eventArgs)) {
38
+ if (fireListenerFor(bubblingEmitter, '$capture', eventInfo, ...eventArgs)) {
39
39
  return eventInfo.return;
40
40
  }
41
41
  const startRange = eventInfo.startRange || this.selection.getFirstRange();
42
42
  const selectedElement = startRange ? startRange.getContainedElement() : null;
43
- const isCustomContext = selectedElement ? Boolean(getCustomContext(eventContexts, selectedElement)) : false;
43
+ const isCustomContext = selectedElement ? hasMatchingCustomContext(customContexts, selectedElement) : false;
44
44
  let node = selectedElement || getDeeperRangeParent(startRange);
45
45
  updateEventInfo(eventInfo, 'atTarget', node);
46
46
  // For the not yet bubbling event trigger for $text node if selection can be there and it's not a custom context selected.
47
47
  if (!isCustomContext) {
48
- if (fireListenerFor(eventContexts, '$text', eventInfo, ...eventArgs)) {
48
+ if (fireListenerFor(bubblingEmitter, '$text', eventInfo, ...eventArgs)) {
49
49
  return eventInfo.return;
50
50
  }
51
51
  updateEventInfo(eventInfo, 'bubbling', node);
52
52
  }
53
53
  while (node) {
54
- // Root node handling.
55
- if (node.is('rootElement')) {
56
- if (fireListenerFor(eventContexts, '$root', eventInfo, ...eventArgs)) {
57
- return eventInfo.return;
58
- }
59
- }
60
- // Element node handling.
61
- else if (node.is('element')) {
62
- if (fireListenerFor(eventContexts, node.name, eventInfo, ...eventArgs)) {
63
- return eventInfo.return;
64
- }
65
- }
66
- // Check custom contexts (i.e., a widget).
67
- if (fireListenerFor(eventContexts, node, eventInfo, ...eventArgs)) {
54
+ if (node.is('element') && fireListenerFor(bubblingEmitter, node, eventInfo, ...eventArgs)) {
68
55
  return eventInfo.return;
69
56
  }
70
57
  node = node.parent;
@@ -72,7 +59,7 @@ export function BubblingEmitterMixin(base) {
72
59
  }
73
60
  updateEventInfo(eventInfo, 'bubbling', this);
74
61
  // Document context.
75
- fireListenerFor(eventContexts, '$document', eventInfo, ...eventArgs);
62
+ fireListenerFor(bubblingEmitter, '$document', eventInfo, ...eventArgs);
76
63
  return eventInfo.return;
77
64
  }
78
65
  catch (err) {
@@ -83,20 +70,27 @@ export function BubblingEmitterMixin(base) {
83
70
  }
84
71
  _addEventListener(event, callback, options) {
85
72
  const contexts = toArray(options.context || '$document');
86
- const eventContexts = getBubblingContexts(this);
73
+ const bubblingEmitter = getBubblingEmitter(this);
74
+ const callbacksMap = getCallbackMap(this);
87
75
  for (const context of contexts) {
88
- let emitter = eventContexts.get(context);
89
- if (!emitter) {
90
- emitter = new (EmitterMixin())();
91
- eventContexts.set(context, emitter);
76
+ if (typeof context == 'function') {
77
+ getCustomContexts(this).add(context);
92
78
  }
93
- this.listenTo(emitter, event, callback, options);
94
79
  }
80
+ // Wrap callback with current target match.
81
+ const wrappedCallback = wrapCallback(this, contexts, callback);
82
+ // Store for later removing of listeners.
83
+ callbacksMap.set(callback, wrappedCallback);
84
+ // Listen for the event.
85
+ this.listenTo(bubblingEmitter, event, wrappedCallback, options);
95
86
  }
96
87
  _removeEventListener(event, callback) {
97
- const eventContexts = getBubblingContexts(this);
98
- for (const emitter of eventContexts.values()) {
99
- this.stopListening(emitter, event, callback);
88
+ const bubblingEmitter = getBubblingEmitter(this);
89
+ const callbacksMap = getCallbackMap(this);
90
+ const wrappedCallback = callbacksMap.get(callback);
91
+ if (wrappedCallback) {
92
+ callbacksMap.delete(callback);
93
+ this.stopListening(bubblingEmitter, event, wrappedCallback);
100
94
  }
101
95
  }
102
96
  }
@@ -122,34 +116,90 @@ function updateEventInfo(eventInfo, eventPhase, currentTarget) {
122
116
  * @param eventArgs Additional arguments to be passed to the callbacks.
123
117
  * @returns True if event stop was called.
124
118
  */
125
- function fireListenerFor(eventContexts, context, eventInfo, ...eventArgs) {
126
- const emitter = typeof context == 'string' ? eventContexts.get(context) : getCustomContext(eventContexts, context);
127
- if (!emitter) {
128
- return false;
119
+ function fireListenerFor(emitter, currentTarget, eventInfo, ...eventArgs) {
120
+ emitter.fire(eventInfo, {
121
+ currentTarget,
122
+ eventArgs
123
+ });
124
+ // Similar to DOM Event#stopImmediatePropagation() this does not fire events
125
+ // for other contexts on the same node.
126
+ if (eventInfo.stop.called) {
127
+ return true;
129
128
  }
130
- emitter.fire(eventInfo, ...eventArgs);
131
- return eventInfo.stop.called;
129
+ return false;
132
130
  }
133
131
  /**
134
- * Returns an emitter for a specified view node.
132
+ * Returns an event callback wrapped with context check condition.
135
133
  */
136
- function getCustomContext(eventContexts, node) {
137
- for (const [context, emitter] of eventContexts) {
138
- if (typeof context == 'function' && context(node)) {
139
- return emitter;
134
+ function wrapCallback(emitter, contexts, callback) {
135
+ return function (event, data) {
136
+ const { currentTarget, eventArgs } = data;
137
+ // Quick path for string based context ($capture, $text, $document).
138
+ if (typeof currentTarget == 'string') {
139
+ if (contexts.includes(currentTarget)) {
140
+ callback.call(emitter, event, ...eventArgs);
141
+ }
142
+ return;
143
+ }
144
+ // The current target is a view element.
145
+ // Special case for the root element as it could be handled ac $root context.
146
+ // Note that it could also be matched later by custom context.
147
+ if (currentTarget.is('rootElement') && contexts.includes('$root')) {
148
+ callback.call(emitter, event, ...eventArgs);
149
+ return;
140
150
  }
151
+ // Check if it is a context for this element name.
152
+ if (contexts.includes(currentTarget.name)) {
153
+ callback.call(emitter, event, ...eventArgs);
154
+ return;
155
+ }
156
+ // Check if dynamic context matches.
157
+ for (const context of contexts) {
158
+ if (typeof context == 'function' && context(currentTarget)) {
159
+ callback.call(emitter, event, ...eventArgs);
160
+ return;
161
+ }
162
+ }
163
+ };
164
+ }
165
+ /**
166
+ * Returns bubbling emitter for the source (emitter).
167
+ */
168
+ function getBubblingEmitter(source) {
169
+ if (!source[bubblingEmitterSymbol]) {
170
+ source[bubblingEmitterSymbol] = new (EmitterMixin())();
171
+ }
172
+ return source[bubblingEmitterSymbol];
173
+ }
174
+ /**
175
+ * Returns map of callbacks (original to wrapped one).
176
+ */
177
+ function getCallbackMap(source) {
178
+ if (!source[callbackMapSymbol]) {
179
+ source[callbackMapSymbol] = new Map();
141
180
  }
142
- return null;
181
+ return source[callbackMapSymbol];
143
182
  }
144
183
  /**
145
- * Returns bubbling contexts map for the source (emitter).
184
+ * Returns the set of registered custom contexts.
146
185
  */
147
- function getBubblingContexts(source) {
186
+ function getCustomContexts(source) {
148
187
  if (!source[contextsSymbol]) {
149
- source[contextsSymbol] = new Map();
188
+ source[contextsSymbol] = new Set();
150
189
  }
151
190
  return source[contextsSymbol];
152
191
  }
192
+ /**
193
+ * Returns true if any of custom context match the given element.
194
+ */
195
+ function hasMatchingCustomContext(customContexts, element) {
196
+ for (const context of customContexts) {
197
+ if (context(element)) {
198
+ return true;
199
+ }
200
+ }
201
+ return false;
202
+ }
153
203
  /**
154
204
  * Returns the deeper parent element for the range.
155
205
  */