@ckeditor/ckeditor5-engine 37.0.1 → 37.1.0

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": "37.0.1",
3
+ "version": "37.1.0",
4
4
  "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
5
5
  "keywords": [
6
6
  "wysiwyg",
@@ -23,30 +23,30 @@
23
23
  ],
24
24
  "main": "src/index.js",
25
25
  "dependencies": {
26
- "@ckeditor/ckeditor5-utils": "^37.0.1",
26
+ "@ckeditor/ckeditor5-utils": "^37.1.0",
27
27
  "lodash-es": "^4.17.15"
28
28
  },
29
29
  "devDependencies": {
30
- "@ckeditor/ckeditor5-basic-styles": "^37.0.1",
31
- "@ckeditor/ckeditor5-block-quote": "^37.0.1",
32
- "@ckeditor/ckeditor5-clipboard": "^37.0.1",
33
- "@ckeditor/ckeditor5-cloud-services": "^37.0.1",
34
- "@ckeditor/ckeditor5-core": "^37.0.1",
35
- "@ckeditor/ckeditor5-editor-classic": "^37.0.1",
36
- "@ckeditor/ckeditor5-enter": "^37.0.1",
37
- "@ckeditor/ckeditor5-essentials": "^37.0.1",
38
- "@ckeditor/ckeditor5-heading": "^37.0.1",
39
- "@ckeditor/ckeditor5-image": "^37.0.1",
40
- "@ckeditor/ckeditor5-link": "^37.0.1",
41
- "@ckeditor/ckeditor5-list": "^37.0.1",
42
- "@ckeditor/ckeditor5-mention": "^37.0.1",
43
- "@ckeditor/ckeditor5-paragraph": "^37.0.1",
44
- "@ckeditor/ckeditor5-table": "^37.0.1",
45
- "@ckeditor/ckeditor5-theme-lark": "^37.0.1",
46
- "@ckeditor/ckeditor5-typing": "^37.0.1",
47
- "@ckeditor/ckeditor5-ui": "^37.0.1",
48
- "@ckeditor/ckeditor5-undo": "^37.0.1",
49
- "@ckeditor/ckeditor5-widget": "^37.0.1",
30
+ "@ckeditor/ckeditor5-basic-styles": "^37.1.0",
31
+ "@ckeditor/ckeditor5-block-quote": "^37.1.0",
32
+ "@ckeditor/ckeditor5-clipboard": "^37.1.0",
33
+ "@ckeditor/ckeditor5-cloud-services": "^37.1.0",
34
+ "@ckeditor/ckeditor5-core": "^37.1.0",
35
+ "@ckeditor/ckeditor5-editor-classic": "^37.1.0",
36
+ "@ckeditor/ckeditor5-enter": "^37.1.0",
37
+ "@ckeditor/ckeditor5-essentials": "^37.1.0",
38
+ "@ckeditor/ckeditor5-heading": "^37.1.0",
39
+ "@ckeditor/ckeditor5-image": "^37.1.0",
40
+ "@ckeditor/ckeditor5-link": "^37.1.0",
41
+ "@ckeditor/ckeditor5-list": "^37.1.0",
42
+ "@ckeditor/ckeditor5-mention": "^37.1.0",
43
+ "@ckeditor/ckeditor5-paragraph": "^37.1.0",
44
+ "@ckeditor/ckeditor5-table": "^37.1.0",
45
+ "@ckeditor/ckeditor5-theme-lark": "^37.1.0",
46
+ "@ckeditor/ckeditor5-typing": "^37.1.0",
47
+ "@ckeditor/ckeditor5-ui": "^37.1.0",
48
+ "@ckeditor/ckeditor5-undo": "^37.1.0",
49
+ "@ckeditor/ckeditor5-widget": "^37.1.0",
50
50
  "typescript": "^4.8.4",
51
51
  "webpack": "^5.58.1",
52
52
  "webpack-cli": "^4.9.0"
@@ -5,13 +5,14 @@
5
5
  /**
6
6
  * @module engine/controller/editingcontroller
7
7
  */
8
- import { CKEditorError, ObservableMixin } from '@ckeditor/ckeditor5-utils';
8
+ import { CKEditorError, ObservableMixin, env } from '@ckeditor/ckeditor5-utils';
9
9
  import RootEditableElement from '../view/rooteditableelement';
10
10
  import View from '../view/view';
11
11
  import Mapper from '../conversion/mapper';
12
12
  import DowncastDispatcher from '../conversion/downcastdispatcher';
13
13
  import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertAttributesAndChildren, insertText, remove } from '../conversion/downcasthelpers';
14
14
  import { convertSelectionChange } from '../conversion/upcasthelpers';
15
+ import { tryFixingRange } from '../model/utils/selection-post-fixer';
15
16
  // @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
16
17
  /**
17
18
  * A controller for the editing pipeline. The editing pipeline controls the {@link ~EditingController#model model} rendering,
@@ -59,6 +60,8 @@ export default class EditingController extends ObservableMixin() {
59
60
  }, { priority: 'low' });
60
61
  // Convert selection from the view to the model when it changes in the view.
61
62
  this.listenTo(this.view.document, 'selectionChange', convertSelectionChange(this.model, this.mapper));
63
+ // Fix `beforeinput` target ranges so that they map to the valid model ranges.
64
+ this.listenTo(this.view.document, 'beforeinput', fixTargetRanges(this.mapper, this.model.schema, this.view), { priority: 'high' });
62
65
  // Attach default model converters.
63
66
  this.downcastDispatcher.on('insert:$text', insertText(), { priority: 'lowest' });
64
67
  this.downcastDispatcher.on('insert', insertAttributesAndChildren(), { priority: 'lowest' });
@@ -163,3 +166,26 @@ export default class EditingController extends ObservableMixin() {
163
166
  });
164
167
  }
165
168
  }
169
+ /**
170
+ * Checks whether the target ranges provided by the `beforeInput` event can be properly mapped to model ranges and fixes them if needed.
171
+ *
172
+ * This is using the same logic as the selection post-fixer.
173
+ */
174
+ function fixTargetRanges(mapper, schema, view) {
175
+ return (evt, data) => {
176
+ // The Renderer is disabled while composing on non-android browsers, so we can't be sure that target ranges
177
+ // could be properly mapped to view and model because the DOM and view tree drifted apart.
178
+ if (view.document.isComposing && !env.isAndroid) {
179
+ return;
180
+ }
181
+ for (let i = 0; i < data.targetRanges.length; i++) {
182
+ const viewRange = data.targetRanges[i];
183
+ const modelRange = mapper.toModelRange(viewRange);
184
+ const correctedRange = tryFixingRange(modelRange, schema);
185
+ if (!correctedRange || correctedRange.isEqual(modelRange)) {
186
+ continue;
187
+ }
188
+ data.targetRanges[i] = mapper.toViewRange(correctedRange);
189
+ }
190
+ };
191
+ }
@@ -1958,7 +1958,7 @@ function createChangeReducerCallback(model) {
1958
1958
  }
1959
1959
  }
1960
1960
  else {
1961
- /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. */
1961
+ /* istanbul ignore else: This is always true because otherwise it would not register a reducer callback. -- @preserve */
1962
1962
  if (model.children) {
1963
1963
  return true;
1964
1964
  }
@@ -137,7 +137,7 @@ export interface MarkerData {
137
137
  }
138
138
  declare const Marker_base: import("@ckeditor/ckeditor5-utils").Mixed<typeof TypeCheckable, import("@ckeditor/ckeditor5-utils").Emitter>;
139
139
  /**
140
- * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
140
+ * `Marker` is a continuous part of model (like a range), is named and represent some kind of information about marked
141
141
  * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
142
142
  * model document tree, markers are not stored directly in document tree but in
143
143
  * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
@@ -214,7 +214,7 @@ export default class MarkerCollection extends EmitterMixin() {
214
214
  }
215
215
  }
216
216
  /**
217
- * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
217
+ * `Marker` is a continuous part of model (like a range), is named and represent some kind of information about marked
218
218
  * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
219
219
  * model document tree, markers are not stored directly in document tree but in
220
220
  * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
@@ -168,7 +168,7 @@ export default class Model extends ObservableMixin() {
168
168
  }
169
169
  catch (err) {
170
170
  // @if CK_DEBUG // throw err;
171
- /* istanbul ignore next */
171
+ /* istanbul ignore next -- @preserve */
172
172
  CKEditorError.rethrowUnexpectedError(err, this);
173
173
  }
174
174
  }
@@ -191,7 +191,7 @@ export default class Model extends ObservableMixin() {
191
191
  }
192
192
  catch (err) {
193
193
  // @if CK_DEBUG // throw err;
194
- /* istanbul ignore next */
194
+ /* istanbul ignore next -- @preserve */
195
195
  CKEditorError.rethrowUnexpectedError(err, this);
196
196
  }
197
197
  }
@@ -362,13 +362,23 @@ export default class Selection extends Selection_base {
362
362
  * </block>
363
363
  * ```
364
364
  *
365
- * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
366
- * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
365
+ * **Special case**: Selection ignores first and/or last blocks if nothing (from user perspective) is selected in them.
367
366
  *
368
367
  * ```xml
368
+ * // Selection ends and the beginning of the last block.
369
369
  * <paragraph>[a</paragraph>
370
370
  * <paragraph>b</paragraph>
371
- * <paragraph>]c</paragraph> // this block will not be returned
371
+ * <paragraph>]c</paragraph> // This block will not be returned
372
+ *
373
+ * // Selection begins at the end of the first block.
374
+ * <paragraph>a[</paragraph> // This block will not be returned
375
+ * <paragraph>b</paragraph>
376
+ * <paragraph>c]</paragraph>
377
+ *
378
+ * // Selection begings at the end of the first block and ends at the beginning of the last block.
379
+ * <paragraph>a[</paragraph> // This block will not be returned
380
+ * <paragraph>b</paragraph>
381
+ * <paragraph>]c</paragraph> // This block will not be returned
372
382
  * ```
373
383
  */
374
384
  getSelectedBlocks(): IterableIterator<Element>;
@@ -557,13 +557,23 @@ export default class Selection extends EmitterMixin(TypeCheckable) {
557
557
  * </block>
558
558
  * ```
559
559
  *
560
- * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
561
- * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
560
+ * **Special case**: Selection ignores first and/or last blocks if nothing (from user perspective) is selected in them.
562
561
  *
563
562
  * ```xml
563
+ * // Selection ends and the beginning of the last block.
564
564
  * <paragraph>[a</paragraph>
565
565
  * <paragraph>b</paragraph>
566
- * <paragraph>]c</paragraph> // this block will not be returned
566
+ * <paragraph>]c</paragraph> // This block will not be returned
567
+ *
568
+ * // Selection begins at the end of the first block.
569
+ * <paragraph>a[</paragraph> // This block will not be returned
570
+ * <paragraph>b</paragraph>
571
+ * <paragraph>c]</paragraph>
572
+ *
573
+ * // Selection begings at the end of the first block and ends at the beginning of the last block.
574
+ * <paragraph>a[</paragraph> // This block will not be returned
575
+ * <paragraph>b</paragraph>
576
+ * <paragraph>]c</paragraph> // This block will not be returned
567
577
  * ```
568
578
  */
569
579
  *getSelectedBlocks() {
@@ -571,7 +581,7 @@ export default class Selection extends EmitterMixin(TypeCheckable) {
571
581
  for (const range of this.getRanges()) {
572
582
  // Get start block of range in case of a collapsed range.
573
583
  const startBlock = getParentBlock(range.start, visited);
574
- if (startBlock && isTopBlockInRange(startBlock, range)) {
584
+ if (isStartBlockSelected(startBlock, range)) {
575
585
  yield startBlock;
576
586
  }
577
587
  for (const value of range.getWalker()) {
@@ -581,8 +591,7 @@ export default class Selection extends EmitterMixin(TypeCheckable) {
581
591
  }
582
592
  }
583
593
  const endBlock = getParentBlock(range.end, visited);
584
- // #984. Don't return the end block if the range ends right at its beginning.
585
- if (endBlock && !range.end.isTouching(Position._createAt(endBlock, 0)) && isTopBlockInRange(endBlock, range)) {
594
+ if (isEndBlockSelected(endBlock, range)) {
586
595
  yield endBlock;
587
596
  }
588
597
  }
@@ -709,6 +718,62 @@ function isTopBlockInRange(block, range) {
709
718
  const isParentInRange = range.containsRange(Range._createOn(parentBlock), true);
710
719
  return !isParentInRange;
711
720
  }
721
+ /**
722
+ * If a selection starts at the end of a block, that block is not returned as from user perspective this block wasn't selected.
723
+ * See [#11585](https://github.com/ckeditor/ckeditor5/issues/11585) for more details.
724
+ *
725
+ * ```xml
726
+ * <paragraph>a[</paragraph> // This block will not be returned
727
+ * <paragraph>b</paragraph>
728
+ * <paragraph>c]</paragraph>
729
+ * ```
730
+ *
731
+ * Collapsed selection is not affected by it:
732
+ *
733
+ * ```xml
734
+ * <paragraph>a[]</paragraph> // This block will be returned
735
+ * ```
736
+ */
737
+ function isStartBlockSelected(startBlock, range) {
738
+ if (!startBlock) {
739
+ return false;
740
+ }
741
+ if (range.isCollapsed || startBlock.isEmpty) {
742
+ return true;
743
+ }
744
+ if (range.start.isTouching(Position._createAt(startBlock, startBlock.maxOffset))) {
745
+ return false;
746
+ }
747
+ return isTopBlockInRange(startBlock, range);
748
+ }
749
+ /**
750
+ * If a selection ends at the beginning of a block, that block is not returned as from user perspective this block wasn't selected.
751
+ * See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
752
+ *
753
+ * ```xml
754
+ * <paragraph>[a</paragraph>
755
+ * <paragraph>b</paragraph>
756
+ * <paragraph>]c</paragraph> // this block will not be returned
757
+ * ```
758
+ *
759
+ * Collapsed selection is not affected by it:
760
+ *
761
+ * ```xml
762
+ * <paragraph>[]a</paragraph> // this block will be returned
763
+ * ```
764
+ */
765
+ function isEndBlockSelected(endBlock, range) {
766
+ if (!endBlock) {
767
+ return false;
768
+ }
769
+ if (range.isCollapsed || endBlock.isEmpty) {
770
+ return true;
771
+ }
772
+ if (range.end.isTouching(Position._createAt(endBlock, 0))) {
773
+ return false;
774
+ }
775
+ return isTopBlockInRange(endBlock, range);
776
+ }
712
777
  /**
713
778
  * Returns first ancestor block of a node.
714
779
  */
@@ -3,7 +3,7 @@
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
  export default class TypeCheckable {
6
- /* istanbul ignore next */
6
+ /* istanbul ignore next -- @preserve */
7
7
  is() {
8
8
  // There are a lot of overloads above.
9
9
  // Overriding method in derived classes remove them and only `is( type: string ): boolean` is visible which we don't want.
@@ -162,7 +162,7 @@ export default function insertContent(model, content, selectable) {
162
162
  selectionLiveRange.detach();
163
163
  }
164
164
  }
165
- /* istanbul ignore else */
165
+ /* istanbul ignore else -- @preserve */
166
166
  if (newRange) {
167
167
  if (selection instanceof DocumentSelection) {
168
168
  writer.setSelection(newRange);
@@ -253,7 +253,7 @@ class Insertion {
253
253
  // If the real end was after the last auto paragraph then update relevant properties.
254
254
  if (positionAfterNode.isAfter(positionAfterLastNode)) {
255
255
  this._lastNode = node;
256
- /* istanbul ignore if */
256
+ /* istanbul ignore if -- @preserve */
257
257
  if (this.position.parent != node || !this.position.isAtEnd) {
258
258
  // Algorithm's correctness check. We should never end up here but it's good to know that we did.
259
259
  // At this point the insertion position should be at the end of the last auto paragraph.
@@ -386,7 +386,7 @@ class Insertion {
386
386
  * @param node The node to insert.
387
387
  */
388
388
  _appendToFragment(node) {
389
- /* istanbul ignore if */
389
+ /* istanbul ignore if -- @preserve */
390
390
  if (!this.schema.checkChild(this.position, node)) {
391
391
  // Algorithm's correctness check. We should never end up here but it's good to know that we did.
392
392
  // Note that it would often be a silent issue if we insert node in a place where it's not allowed.
@@ -517,7 +517,7 @@ class Insertion {
517
517
  }
518
518
  const mergePosRight = LivePosition._createAfter(node);
519
519
  mergePosRight.stickiness = 'toNext';
520
- /* istanbul ignore if */
520
+ /* istanbul ignore if -- @preserve */
521
521
  if (!this.position.isEqual(mergePosRight)) {
522
522
  // Algorithm's correctness check. We should never end up here but it's good to know that we did.
523
523
  // At this point the insertion position should be after the node we'll merge. If it isn't,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import Range from '../range';
6
6
  import type Model from '../model';
7
+ import type Schema from '../schema';
7
8
  /**
8
9
  * Injects selection post-fixer to the model.
9
10
  *
@@ -56,6 +57,14 @@ import type Model from '../model';
56
57
  * them to select `isLimit=true` elements.
57
58
  */
58
59
  export declare function injectSelectionPostFixer(model: Model): void;
60
+ /**
61
+ * Tries fixing a range if it's incorrect.
62
+ *
63
+ * **Note:** This helper is used by the selection post-fixer and to fix the `beforeinput` target ranges.
64
+ *
65
+ * @returns Returns fixed range or null if range is valid.
66
+ */
67
+ export declare function tryFixingRange(range: Range, schema: Schema): Range | null;
59
68
  /**
60
69
  * Returns a minimal non-intersecting array of ranges without duplicates.
61
70
  *
@@ -97,9 +97,11 @@ function selectionPostFixer(writer, model) {
97
97
  /**
98
98
  * Tries fixing a range if it's incorrect.
99
99
  *
100
+ * **Note:** This helper is used by the selection post-fixer and to fix the `beforeinput` target ranges.
101
+ *
100
102
  * @returns Returns fixed range or null if range is valid.
101
103
  */
102
- function tryFixingRange(range, schema) {
104
+ export function tryFixingRange(range, schema) {
103
105
  if (range.isCollapsed) {
104
106
  return tryFixingCollapsedRange(range, schema);
105
107
  }
@@ -641,7 +641,8 @@ export default class DomConverter {
641
641
  }
642
642
  else {
643
643
  const domBefore = domParent.childNodes[domOffset - 1];
644
- if (isText(domBefore) && isInlineFiller(domBefore)) {
644
+ // Jump over an inline filler (and also on Firefox jump over a block filler while pressing backspace in an empty paragraph).
645
+ if (isText(domBefore) && isInlineFiller(domBefore) || domBefore && this.isBlockFiller(domBefore)) {
645
646
  return this.domPositionToView(domBefore.parentNode, indexOf(domBefore));
646
647
  }
647
648
  const viewBefore = isText(domBefore) ?
@@ -423,7 +423,7 @@ function matchAttributes(patterns, element) {
423
423
  */
424
424
  function matchClasses(patterns, element) {
425
425
  // We don't need `getter` here because patterns for classes are always normalized to `[ className, true ]`.
426
- return matchPatterns(patterns, element.getClassNames(), /* istanbul ignore next */ () => { });
426
+ return matchPatterns(patterns, element.getClassNames(), /* istanbul ignore next -- @preserve */ () => { });
427
427
  }
428
428
  /**
429
429
  * Checks if styles of provided element can be matched against provided patterns.
@@ -77,7 +77,7 @@ export default function BubblingEmitterMixin(base) {
77
77
  }
78
78
  catch (err) {
79
79
  // @if CK_DEBUG // throw err;
80
- /* istanbul ignore next */
80
+ /* istanbul ignore next -- @preserve */
81
81
  CKEditorError.rethrowUnexpectedError(err, this);
82
82
  }
83
83
  }
@@ -116,7 +116,7 @@ export default class SelectionObserver extends Observer {
116
116
  this._fireSelectionChangeDoneDebounced.cancel();
117
117
  this._documentIsSelectingInactivityTimeoutDebounced.cancel();
118
118
  }
119
- /* istanbul ignore next */
119
+ /* istanbul ignore next -- @preserve */
120
120
  _reportInfiniteLoop() {
121
121
  // @if CK_DEBUG // throw new Error(
122
122
  // @if CK_DEBUG // 'Selection change observer detected an infinite rendering loop.\n\n' +
@@ -209,11 +209,8 @@ export default class Position extends TypeCheckable {
209
209
  return 'before';
210
210
  case 'extension':
211
211
  return 'after';
212
- /* istanbul ignore next */
213
- case 'same':
214
- // Already covered by `this.isEqual` above. Added so TypeScript can infer `result` as number in `default` case.
215
- return 'same';
216
212
  default:
213
+ // Cast to number to avoid having 'same' as a type of `result`.
217
214
  return thisPath[result] < otherPath[result] ? 'before' : 'after';
218
215
  }
219
216
  }
@@ -6,7 +6,7 @@
6
6
  * @module engine/view/typecheckable
7
7
  */
8
8
  export default class TypeCheckable {
9
- /* istanbul ignore next */
9
+ /* istanbul ignore next -- @preserve */
10
10
  is() {
11
11
  // There are a lot of overloads above.
12
12
  // Overriding method in derived classes remove them and only `is( type: string ): boolean` is visible which we don't want.
package/src/view/view.js CHANGED
@@ -397,7 +397,7 @@ export default class View extends ObservableMixin() {
397
397
  }
398
398
  catch (err) {
399
399
  // @if CK_DEBUG // throw err;
400
- /* istanbul ignore next */
400
+ /* istanbul ignore next -- @preserve */
401
401
  CKEditorError.rethrowUnexpectedError(err, this);
402
402
  }
403
403
  }