@ckeditor/ckeditor5-engine 42.0.2-alpha.2 → 43.0.0-alpha.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +1 -820
  2. package/dist/dev-utils/model.d.ts +2 -0
  3. package/dist/dev-utils/view.d.ts +1 -0
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +466 -271
  6. package/dist/index.js.map +1 -1
  7. package/dist/model/schema.d.ts +149 -51
  8. package/dist/view/observer/focusobserver.d.ts +12 -0
  9. package/dist/view/observer/mutationobserver.d.ts +34 -5
  10. package/dist/view/observer/selectionobserver.d.ts +1 -2
  11. package/dist/view/renderer.d.ts +12 -0
  12. package/dist/view/view.d.ts +1 -4
  13. package/package.json +2 -2
  14. package/src/conversion/upcasthelpers.js +0 -7
  15. package/src/dev-utils/model.d.ts +2 -0
  16. package/src/dev-utils/model.js +4 -2
  17. package/src/dev-utils/utils.js +7 -0
  18. package/src/dev-utils/view.d.ts +1 -0
  19. package/src/dev-utils/view.js +3 -0
  20. package/src/index.d.ts +3 -1
  21. package/src/index.js +2 -0
  22. package/src/model/model.js +1 -5
  23. package/src/model/schema.d.ts +149 -51
  24. package/src/model/schema.js +200 -70
  25. package/src/model/utils/insertcontent.js +21 -65
  26. package/src/view/domconverter.js +13 -9
  27. package/src/view/observer/compositionobserver.js +2 -0
  28. package/src/view/observer/focusobserver.d.ts +12 -0
  29. package/src/view/observer/focusobserver.js +55 -25
  30. package/src/view/observer/inputobserver.js +7 -5
  31. package/src/view/observer/mutationobserver.d.ts +34 -5
  32. package/src/view/observer/mutationobserver.js +8 -11
  33. package/src/view/observer/selectionobserver.d.ts +1 -2
  34. package/src/view/observer/selectionobserver.js +27 -9
  35. package/src/view/renderer.d.ts +12 -0
  36. package/src/view/renderer.js +111 -63
  37. package/src/view/view.d.ts +1 -4
  38. package/src/view/view.js +9 -0
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
- import { logWarning, EmitterMixin, CKEditorError, compareArrays, toArray, toMap, isIterable, ObservableMixin, count, EventInfo, Collection, keyCodes, isText, env, remove as remove$1, insertAt, diff, isNode, isComment, indexOf, fastDiff, global, isValidAttributeName, first, getAncestors, DomEmitterMixin, getCode, isArrowKeyCode, scrollViewportToShowTarget, spliceArray, uid, priorities, isInsideSurrogatePair, isInsideCombinedSymbol, isInsideEmojiSequence } from '@ckeditor/ckeditor5-utils/dist/index.js';
5
+ import { logWarning, EmitterMixin, CKEditorError, compareArrays, toArray, toMap, isIterable, ObservableMixin, count, EventInfo, Collection, keyCodes, isText, env, remove as remove$1, insertAt, diff, fastDiff, isNode, isComment, indexOf, global, isValidAttributeName, first, getAncestors, DomEmitterMixin, getCode, isArrowKeyCode, scrollViewportToShowTarget, spliceArray, uid, priorities, isInsideSurrogatePair, isInsideCombinedSymbol, isInsideEmojiSequence } from '@ckeditor/ckeditor5-utils/dist/index.js';
6
6
  import { clone, isPlainObject, isObject, unset, get, merge, set, extend, debounce, isEqualWith, cloneDeep, isEqual } from 'lodash-es';
7
7
 
8
8
  // Each document stores information about its placeholder elements and check functions.
@@ -7332,6 +7332,7 @@ const validNodesToInsert = [
7332
7332
  this.selection = selection;
7333
7333
  this.set('isFocused', false);
7334
7334
  this.set('isSelecting', false);
7335
+ this.set('isComposing', false);
7335
7336
  // Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes
7336
7337
  // creating the selection in DOM to avoid accidental selection collapsing
7337
7338
  // (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723).
@@ -7343,12 +7344,6 @@ const validNodesToInsert = [
7343
7344
  }
7344
7345
  });
7345
7346
  }
7346
- this.set('isComposing', false);
7347
- this.on('change:isComposing', ()=>{
7348
- if (!this.isComposing) {
7349
- this.render();
7350
- }
7351
- });
7352
7347
  }
7353
7348
  /**
7354
7349
  * Marks a view node to be updated in the DOM by {@link #render `render()`}.
@@ -7402,15 +7397,15 @@ const validNodesToInsert = [
7402
7397
  // and we should not do it because the difference between view and DOM could lead to position mapping problems.
7403
7398
  if (this.isComposing && !env.isAndroid) {
7404
7399
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7405
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing',
7406
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
7400
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Rendering aborted while isComposing.',
7401
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic'
7407
7402
  // @if CK_DEBUG_TYPING // );
7408
7403
  // @if CK_DEBUG_TYPING // }
7409
7404
  return;
7410
7405
  }
7411
7406
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7412
7407
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Rendering',
7413
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
7408
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold'
7414
7409
  // @if CK_DEBUG_TYPING // );
7415
7410
  // @if CK_DEBUG_TYPING // }
7416
7411
  let inlineFillerPosition = null;
@@ -7689,10 +7684,10 @@ const validNodesToInsert = [
7689
7684
  }
7690
7685
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7691
7686
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text',
7692
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
7687
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: normal'
7693
7688
  // @if CK_DEBUG_TYPING // );
7694
7689
  // @if CK_DEBUG_TYPING // }
7695
- updateTextNode(domText, expectedText);
7690
+ this._updateTextNode(domText, expectedText);
7696
7691
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7697
7692
  // @if CK_DEBUG_TYPING // console.groupEnd();
7698
7693
  // @if CK_DEBUG_TYPING // }
@@ -7741,7 +7736,7 @@ const validNodesToInsert = [
7741
7736
  }
7742
7737
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7743
7738
  // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update children',
7744
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
7739
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: normal'
7745
7740
  // @if CK_DEBUG_TYPING // );
7746
7741
  // @if CK_DEBUG_TYPING // }
7747
7742
  // IME on Android inserts a new text node while typing after a link
@@ -7773,6 +7768,11 @@ const validNodesToInsert = [
7773
7768
  // We need to make sure that we update the existing text node and not replace it with another one.
7774
7769
  // The composition and different "language" browser extensions are fragile to text node being completely replaced.
7775
7770
  const actions = this._findUpdateActions(diff, actualDomChildren, expectedDomChildren, areTextNodes);
7771
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping && actions.every( a => a == 'equal' ) ) {
7772
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Nothing to update.',
7773
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic'
7774
+ // @if CK_DEBUG_TYPING // );
7775
+ // @if CK_DEBUG_TYPING // }
7776
7776
  let i = 0;
7777
7777
  const nodesToUnbind = new Set();
7778
7778
  // Handle deletions first.
@@ -7784,9 +7784,22 @@ const validNodesToInsert = [
7784
7784
  for (const action of actions){
7785
7785
  if (action === 'delete') {
7786
7786
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7787
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove node',
7788
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', actualDomChildren[ i ]
7789
- // @if CK_DEBUG_TYPING // );
7787
+ // @if CK_DEBUG_TYPING // const node = actualDomChildren[ i ];
7788
+ // @if CK_DEBUG_TYPING // if ( isText( node ) ) {
7789
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove text node' +
7790
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: ` +
7791
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( node.data ) }%c (${ node.data.length })`,
7792
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold',
7793
+ // @if CK_DEBUG_TYPING // this.isComposing ? 'color: red; font-weight: bold' : '', 'color: blue', ''
7794
+ // @if CK_DEBUG_TYPING // );
7795
+ // @if CK_DEBUG_TYPING // } else {
7796
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Remove element' +
7797
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: `,
7798
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold',
7799
+ // @if CK_DEBUG_TYPING // this.isComposing ? 'color: red; font-weight: bold' : '',
7800
+ // @if CK_DEBUG_TYPING // node
7801
+ // @if CK_DEBUG_TYPING // );
7802
+ // @if CK_DEBUG_TYPING // }
7790
7803
  // @if CK_DEBUG_TYPING // }
7791
7804
  nodesToUnbind.add(actualDomChildren[i]);
7792
7805
  remove$1(actualDomChildren[i]);
@@ -7798,23 +7811,27 @@ const validNodesToInsert = [
7798
7811
  for (const action of actions){
7799
7812
  if (action === 'insert') {
7800
7813
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7801
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert node',
7802
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', expectedDomChildren[ i ]
7803
- // @if CK_DEBUG_TYPING // );
7814
+ // @if CK_DEBUG_TYPING // const node = expectedDomChildren[ i ];
7815
+ // @if CK_DEBUG_TYPING // if ( isText( node ) ) {
7816
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert text node' +
7817
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: ` +
7818
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( node.data ) }%c (${ node.data.length })`,
7819
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold',
7820
+ // @if CK_DEBUG_TYPING // this.isComposing ? 'color: red; font-weight: bold' : '',
7821
+ // @if CK_DEBUG_TYPING // 'color: blue', ''
7822
+ // @if CK_DEBUG_TYPING // );
7823
+ // @if CK_DEBUG_TYPING // } else {
7824
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Insert element:',
7825
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: normal',
7826
+ // @if CK_DEBUG_TYPING // node
7827
+ // @if CK_DEBUG_TYPING // );
7828
+ // @if CK_DEBUG_TYPING // }
7804
7829
  // @if CK_DEBUG_TYPING // }
7805
7830
  insertAt(domElement, i, expectedDomChildren[i]);
7806
7831
  i++;
7807
7832
  } else if (action === 'update') {
7808
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7809
- // @if CK_DEBUG_TYPING // console.group( '%c[Renderer]%c Update text node',
7810
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', ''
7811
- // @if CK_DEBUG_TYPING // );
7812
- // @if CK_DEBUG_TYPING // }
7813
- updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
7833
+ this._updateTextNode(actualDomChildren[i], expectedDomChildren[i].data);
7814
7834
  i++;
7815
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7816
- // @if CK_DEBUG_TYPING // console.groupEnd();
7817
- // @if CK_DEBUG_TYPING // }
7818
7835
  } else if (action === 'equal') {
7819
7836
  // Force updating text nodes inside elements which did not change and do not need to be re-rendered (#1125).
7820
7837
  // Do it here (not in the loop above) because only after insertions the `i` index is correct.
@@ -7890,6 +7907,60 @@ const validNodesToInsert = [
7890
7907
  }
7891
7908
  return newActions.concat(diff(actualSlice, expectedSlice, comparator).map((action)=>action === 'equal' ? 'update' : action));
7892
7909
  }
7910
+ /**
7911
+ * Checks if text needs to be updated and possibly updates it by removing and inserting only parts
7912
+ * of the data from the existing text node to reduce impact on the IME composition.
7913
+ *
7914
+ * @param domText DOM text node to update.
7915
+ * @param expectedText The expected data of a text node.
7916
+ */ _updateTextNode(domText, expectedText) {
7917
+ const actualText = domText.data;
7918
+ if (actualText == expectedText) {
7919
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7920
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:%c ' +
7921
+ // @if CK_DEBUG_TYPING // `${ _escapeTextNodeData( actualText ) }%c (${ actualText.length })`,
7922
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic', 'color: blue', ''
7923
+ // @if CK_DEBUG_TYPING // );
7924
+ // @if CK_DEBUG_TYPING // }
7925
+ return;
7926
+ }
7927
+ // Our approach to interleaving space character with NBSP might differ with the one implemented by the browser.
7928
+ // Avoid modifying the text node in the DOM if only NBSPs and spaces are interchanged.
7929
+ // We should avoid DOM modifications while composing to avoid breakage of composition.
7930
+ // See: https://github.com/ckeditor/ckeditor5/issues/13994.
7931
+ if (env.isAndroid && this.isComposing && actualText.replace(/\u00A0/g, ' ') == expectedText.replace(/\u00A0/g, ' ')) {
7932
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7933
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node ignore NBSP changes while composing: ' +
7934
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( actualText ) }%c (${ actualText.length }) ->` +
7935
+ // @if CK_DEBUG_TYPING // ` %c${ _escapeTextNodeData( expectedText ) }%c (${ expectedText.length })`,
7936
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-style: italic', 'color: blue', '', 'color: blue', ''
7937
+ // @if CK_DEBUG_TYPING // );
7938
+ // @if CK_DEBUG_TYPING // }
7939
+ return;
7940
+ }
7941
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7942
+ // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node' +
7943
+ // @if CK_DEBUG_TYPING // `${ this.isComposing ? ' while composing (may break composition)' : '' }: ` +
7944
+ // @if CK_DEBUG_TYPING // `%c${ _escapeTextNodeData( actualText ) }%c (${ actualText.length }) ->` +
7945
+ // @if CK_DEBUG_TYPING // ` %c${ _escapeTextNodeData( expectedText ) }%c (${ expectedText.length })`,
7946
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', this.isComposing ? 'color: red; font-weight: bold' : '',
7947
+ // @if CK_DEBUG_TYPING // 'color: blue', '', 'color: blue', ''
7948
+ // @if CK_DEBUG_TYPING // );
7949
+ // @if CK_DEBUG_TYPING // }
7950
+ this._updateTextNodeInternal(domText, expectedText);
7951
+ }
7952
+ /**
7953
+ * Part of the `_updateTextNode` method extracted for easier testing.
7954
+ */ _updateTextNodeInternal(domText, expectedText) {
7955
+ const actions = fastDiff(domText.data, expectedText);
7956
+ for (const action of actions){
7957
+ if (action.type === 'insert') {
7958
+ domText.insertData(action.index, action.values.join(''));
7959
+ } else {
7960
+ domText.deleteData(action.index, action.howMany);
7961
+ }
7962
+ }
7963
+ }
7893
7964
  /**
7894
7965
  * Marks text nodes to be synchronized.
7895
7966
  *
@@ -7983,7 +8054,7 @@ const validNodesToInsert = [
7983
8054
  const focus = this.domConverter.viewPositionToDom(this.selection.focus);
7984
8055
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
7985
8056
  // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update DOM selection:',
7986
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '', anchor, focus
8057
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', '', anchor, focus
7987
8058
  // @if CK_DEBUG_TYPING // );
7988
8059
  // @if CK_DEBUG_TYPING // }
7989
8060
  domSelection.setBaseAndExtent(anchor.parent, anchor.offset, focus.parent, focus.offset);
@@ -8183,39 +8254,14 @@ function filterOutFakeSelectionContainer(domChildList, fakeSelectionContainer) {
8183
8254
  // Fill it with a text node so we can update it later.
8184
8255
  container.textContent = '\u00A0';
8185
8256
  return container;
8186
- }
8187
- /**
8188
- * Checks if text needs to be updated and possibly updates it by removing and inserting only parts
8189
- * of the data from the existing text node to reduce impact on the IME composition.
8190
- *
8191
- * @param domText DOM text node to update.
8192
- * @param expectedText The expected data of a text node.
8193
- */ function updateTextNode(domText, expectedText) {
8194
- const actualText = domText.data;
8195
- if (actualText == expectedText) {
8196
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
8197
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Text node does not need update:',
8198
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
8199
- // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length })`
8200
- // @if CK_DEBUG_TYPING // );
8201
- // @if CK_DEBUG_TYPING // }
8202
- return;
8203
- }
8204
- // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
8205
- // @if CK_DEBUG_TYPING // console.info( '%c[Renderer]%c Update text node:',
8206
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', '',
8207
- // @if CK_DEBUG_TYPING // `"${ domText.data }" (${ domText.data.length }) -> "${ expectedText }" (${ expectedText.length })`
8208
- // @if CK_DEBUG_TYPING // );
8209
- // @if CK_DEBUG_TYPING // }
8210
- const actions = fastDiff(actualText, expectedText);
8211
- for (const action of actions){
8212
- if (action.type === 'insert') {
8213
- domText.insertData(action.index, action.values.join(''));
8214
- } else {
8215
- domText.deleteData(action.index, action.howMany);
8216
- }
8217
- }
8218
- }
8257
+ } // @if CK_DEBUG_TYPING // function _escapeTextNodeData( text ) {
8258
+ // @if CK_DEBUG_TYPING // const escapedText = text
8259
+ // @if CK_DEBUG_TYPING // .replace( /&/g, '&' )
8260
+ // @if CK_DEBUG_TYPING // .replace( /\u00A0/g, ' ' )
8261
+ // @if CK_DEBUG_TYPING // .replace( /\u2060/g, '⁠' );
8262
+ // @if CK_DEBUG_TYPING //
8263
+ // @if CK_DEBUG_TYPING // return `"${ escapedText }"`;
8264
+ // @if CK_DEBUG_TYPING // }
8219
8265
 
8220
8266
  const BR_FILLER_REF = BR_FILLER(global.document); // eslint-disable-line new-cap
8221
8267
  const NBSP_FILLER_REF = NBSP_FILLER(global.document); // eslint-disable-line new-cap
@@ -9522,16 +9568,18 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9522
9568
  startPosition: getNext ? Position$1._createAfter(node) : Position$1._createBefore(node),
9523
9569
  direction: getNext ? 'forward' : 'backward'
9524
9570
  });
9525
- for (const value of treeWalker){
9526
- // <br> found – it works like a block boundary, so do not scan further.
9527
- if (value.item.is('element', 'br')) {
9571
+ for (const { item } of treeWalker){
9572
+ // Found a text node in the same container element.
9573
+ if (item.is('$textProxy')) {
9574
+ return item;
9575
+ } else if (item.is('element') && item.getCustomProperty('dataPipeline:transparentRendering')) {
9576
+ continue;
9577
+ } else if (item.is('element', 'br')) {
9528
9578
  return null;
9529
- } else if (this._isInlineObjectElement(value.item)) {
9530
- return value.item;
9531
- } else if (value.item.is('containerElement')) {
9579
+ } else if (this._isInlineObjectElement(item)) {
9580
+ return item;
9581
+ } else if (item.is('containerElement')) {
9532
9582
  return null;
9533
- } else if (value.item.is('$textProxy')) {
9534
- return value.item;
9535
9583
  }
9536
9584
  }
9537
9585
  return null;
@@ -10018,6 +10066,7 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
10018
10066
  }
10019
10067
  }
10020
10068
 
10069
+ // @if CK_DEBUG_TYPING // const { _debouncedLine } = require( '../../dev-utils/utils.js' );
10021
10070
  /**
10022
10071
  * Mutation observer's role is to watch for any DOM changes inside the editor that weren't
10023
10072
  * done by the editor's {@link module:engine/view/renderer~Renderer} itself and reverting these changes.
@@ -10032,9 +10081,6 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
10032
10081
  /**
10033
10082
  * Reference to the {@link module:engine/view/view~View#domConverter}.
10034
10083
  */ domConverter;
10035
- /**
10036
- * Reference to the {@link module:engine/view/view~View#_renderer}.
10037
- */ renderer;
10038
10084
  /**
10039
10085
  * Native mutation observer config.
10040
10086
  */ _config;
@@ -10054,7 +10100,6 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
10054
10100
  subtree: true
10055
10101
  };
10056
10102
  this.domConverter = view.domConverter;
10057
- this.renderer = view._renderer;
10058
10103
  this._domElements = new Set();
10059
10104
  this._mutationObserver = new window.MutationObserver(this._onMutations.bind(this));
10060
10105
  }
@@ -10150,10 +10195,12 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
10150
10195
  }
10151
10196
  // Now we build the list of mutations to mark elements. We did not do it earlier to avoid marking the
10152
10197
  // same node multiple times in case of duplication.
10153
- let hasMutations = false;
10198
+ const mutations = [];
10154
10199
  for (const textNode of mutatedTextNodes){
10155
- hasMutations = true;
10156
- this.renderer.markToSync('text', textNode);
10200
+ mutations.push({
10201
+ type: 'text',
10202
+ node: textNode
10203
+ });
10157
10204
  }
10158
10205
  for (const viewElement of elementsWithMutatedChildren){
10159
10206
  const domElement = domConverter.mapViewToDom(viewElement);
@@ -10164,20 +10211,23 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
10164
10211
  // It may happen that as a result of many changes (sth was inserted and then removed),
10165
10212
  // both elements haven't really changed. #1031
10166
10213
  if (!isEqualWith(viewChildren, newViewChildren, sameNodes)) {
10167
- hasMutations = true;
10168
- this.renderer.markToSync('children', viewElement);
10214
+ mutations.push({
10215
+ type: 'children',
10216
+ node: viewElement
10217
+ });
10169
10218
  }
10170
10219
  }
10171
10220
  // In case only non-relevant mutations were recorded it skips the event and force render (#5600).
10172
- if (hasMutations) {
10221
+ if (mutations.length) {
10173
10222
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10223
+ // @if CK_DEBUG_TYPING // _debouncedLine();
10174
10224
  // @if CK_DEBUG_TYPING // console.group( '%c[MutationObserver]%c Mutations detected',
10175
- // @if CK_DEBUG_TYPING // 'font-weight:bold;color:green', ''
10225
+ // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', 'font-weight: bold'
10176
10226
  // @if CK_DEBUG_TYPING // );
10177
10227
  // @if CK_DEBUG_TYPING // }
10178
- // At this point we have "dirty DOM" (changed) and de-synched view (which has not been changed).
10179
- // In order to "reset DOM" we render the view again.
10180
- this.view.forceRender();
10228
+ this.document.fire('mutations', {
10229
+ mutations
10230
+ });
10181
10231
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10182
10232
  // @if CK_DEBUG_TYPING // console.groupEnd();
10183
10233
  // @if CK_DEBUG_TYPING // }
@@ -10225,7 +10275,7 @@ function sameNodes(child1, child2) {
10225
10275
  */ class FocusObserver extends DomEventObserver {
10226
10276
  /**
10227
10277
  * Identifier of the timeout currently used by focus listener to delay rendering execution.
10228
- */ _renderTimeoutId;
10278
+ */ _renderTimeoutId = null;
10229
10279
  /**
10230
10280
  * Set to `true` if the document is in the process of setting the focus.
10231
10281
  *
@@ -10243,30 +10293,18 @@ function sameNodes(child1, child2) {
10243
10293
  super(view);
10244
10294
  this.useCapture = true;
10245
10295
  const document = this.document;
10246
- document.on('focus', ()=>{
10247
- this._isFocusChanging = true;
10248
- // Unfortunately native `selectionchange` event is fired asynchronously.
10249
- // We need to wait until `SelectionObserver` handle the event and then render. Otherwise rendering will
10250
- // overwrite new DOM selection with selection from the view.
10251
- // See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
10252
- // Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
10253
- //
10254
- // Using `view.change()` instead of `view.forceRender()` to prevent double rendering
10255
- // in a situation where `selectionchange` already caused selection change.
10256
- this._renderTimeoutId = setTimeout(()=>{
10257
- this.flush();
10258
- view.change(()=>{});
10259
- }, 50);
10260
- });
10261
- document.on('blur', (evt, data)=>{
10262
- const selectedEditable = document.selection.editableElement;
10263
- if (selectedEditable === null || selectedEditable === data.target) {
10264
- document.isFocused = false;
10265
- this._isFocusChanging = false;
10266
- // Re-render the document to update view elements
10267
- // (changing document.isFocused already marked view as changed since last rendering).
10268
- view.change(()=>{});
10296
+ document.on('focus', ()=>this._handleFocus());
10297
+ document.on('blur', (evt, data)=>this._handleBlur(data));
10298
+ // Focus the editor in cases where browser dispatches `beforeinput` event to a not-focused editable element.
10299
+ // This is flushed by the beforeinput listener in the `InsertTextObserver`.
10300
+ // Note that focus is set only if the document is not focused yet.
10301
+ // See https://github.com/ckeditor/ckeditor5/issues/14702.
10302
+ document.on('beforeinput', ()=>{
10303
+ if (!document.isFocused) {
10304
+ this._handleFocus();
10269
10305
  }
10306
+ }, {
10307
+ priority: 'highest'
10270
10308
  });
10271
10309
  }
10272
10310
  /**
@@ -10285,10 +10323,47 @@ function sameNodes(child1, child2) {
10285
10323
  /**
10286
10324
  * @inheritDoc
10287
10325
  */ destroy() {
10326
+ this._clearTimeout();
10327
+ super.destroy();
10328
+ }
10329
+ /**
10330
+ * The `focus` event handler.
10331
+ */ _handleFocus() {
10332
+ this._clearTimeout();
10333
+ this._isFocusChanging = true;
10334
+ // Unfortunately native `selectionchange` event is fired asynchronously.
10335
+ // We need to wait until `SelectionObserver` handle the event and then render. Otherwise rendering will
10336
+ // overwrite new DOM selection with selection from the view.
10337
+ // See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
10338
+ // Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
10339
+ //
10340
+ // Using `view.change()` instead of `view.forceRender()` to prevent double rendering
10341
+ // in a situation where `selectionchange` already caused selection change.
10342
+ this._renderTimeoutId = setTimeout(()=>{
10343
+ this._renderTimeoutId = null;
10344
+ this.flush();
10345
+ this.view.change(()=>{});
10346
+ }, 50);
10347
+ }
10348
+ /**
10349
+ * The `blur` event handler.
10350
+ */ _handleBlur(data) {
10351
+ const selectedEditable = this.document.selection.editableElement;
10352
+ if (selectedEditable === null || selectedEditable === data.target) {
10353
+ this.document.isFocused = false;
10354
+ this._isFocusChanging = false;
10355
+ // Re-render the document to update view elements
10356
+ // (changing document.isFocused already marked view as changed since last rendering).
10357
+ this.view.change(()=>{});
10358
+ }
10359
+ }
10360
+ /**
10361
+ * Clears timeout.
10362
+ */ _clearTimeout() {
10288
10363
  if (this._renderTimeoutId) {
10289
10364
  clearTimeout(this._renderTimeoutId);
10365
+ this._renderTimeoutId = null;
10290
10366
  }
10291
- super.destroy();
10292
10367
  }
10293
10368
  }
10294
10369
 
@@ -10367,7 +10442,7 @@ function sameNodes(child1, child2) {
10367
10442
  }
10368
10443
  // Make sure that model selection is up-to-date at the end of selecting process.
10369
10444
  // Sometimes `selectionchange` events could arrive after the `mouseup` event and that selection could be already outdated.
10370
- this._handleSelectionChange(null, domDocument);
10445
+ this._handleSelectionChange(domDocument);
10371
10446
  this.document.isSelecting = false;
10372
10447
  // The safety timeout can be canceled when the document leaves the "is selecting" state.
10373
10448
  this._documentIsSelectingInactivityTimeoutDebounced.cancel();
@@ -10398,10 +10473,11 @@ function sameNodes(child1, child2) {
10398
10473
  });
10399
10474
  this.listenTo(domDocument, 'selectionchange', (evt, domEvent)=>{
10400
10475
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10476
+ // @if CK_DEBUG_TYPING // _debouncedLine();
10401
10477
  // @if CK_DEBUG_TYPING // const domSelection = domDocument.defaultView!.getSelection();
10402
- // @if CK_DEBUG_TYPING // console.group( '%c[SelectionObserver]%c selectionchange', 'color:green', ''
10478
+ // @if CK_DEBUG_TYPING // console.group( '%c[SelectionObserver]%c selectionchange', 'color: green', ''
10403
10479
  // @if CK_DEBUG_TYPING // );
10404
- // @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c DOM Selection:', 'font-weight:bold;color:green', '',
10480
+ // @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c DOM Selection:', 'font-weight: bold; color: green', '',
10405
10481
  // @if CK_DEBUG_TYPING // { node: domSelection!.anchorNode, offset: domSelection!.anchorOffset },
10406
10482
  // @if CK_DEBUG_TYPING // { node: domSelection!.focusNode, offset: domSelection!.focusOffset }
10407
10483
  // @if CK_DEBUG_TYPING // );
@@ -10411,13 +10487,13 @@ function sameNodes(child1, child2) {
10411
10487
  if (this.document.isComposing && !env.isAndroid) {
10412
10488
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10413
10489
  // @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c Selection change ignored (isComposing)',
10414
- // @if CK_DEBUG_TYPING // 'font-weight:bold;color:green', ''
10490
+ // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', ''
10415
10491
  // @if CK_DEBUG_TYPING // );
10416
10492
  // @if CK_DEBUG_TYPING // console.groupEnd();
10417
10493
  // @if CK_DEBUG_TYPING // }
10418
10494
  return;
10419
10495
  }
10420
- this._handleSelectionChange(domEvent, domDocument);
10496
+ this._handleSelectionChange(domDocument);
10421
10497
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10422
10498
  // @if CK_DEBUG_TYPING // console.groupEnd();
10423
10499
  // @if CK_DEBUG_TYPING // }
@@ -10425,6 +10501,26 @@ function sameNodes(child1, child2) {
10425
10501
  // using their mouse).
10426
10502
  this._documentIsSelectingInactivityTimeoutDebounced();
10427
10503
  });
10504
+ // Update the model DocumentSelection just after the Renderer and the SelectionObserver are locked.
10505
+ // We do this synchronously (without waiting for the `selectionchange` DOM event) as browser updates
10506
+ // the DOM selection (but not visually) to span the text that is under composition and could be replaced.
10507
+ this.listenTo(this.view.document, 'compositionstart', ()=>{
10508
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10509
+ // @if CK_DEBUG_TYPING // const domSelection = domDocument.defaultView!.getSelection();
10510
+ // @if CK_DEBUG_TYPING // console.group( '%c[SelectionObserver]%c update selection on compositionstart', 'color: green', ''
10511
+ // @if CK_DEBUG_TYPING // );
10512
+ // @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c DOM Selection:', 'font-weight: bold; color: green', '',
10513
+ // @if CK_DEBUG_TYPING // { node: domSelection!.anchorNode, offset: domSelection!.anchorOffset },
10514
+ // @if CK_DEBUG_TYPING // { node: domSelection!.focusNode, offset: domSelection!.focusOffset }
10515
+ // @if CK_DEBUG_TYPING // );
10516
+ // @if CK_DEBUG_TYPING // }
10517
+ this._handleSelectionChange(domDocument);
10518
+ // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10519
+ // @if CK_DEBUG_TYPING // console.groupEnd();
10520
+ // @if CK_DEBUG_TYPING // }
10521
+ }, {
10522
+ priority: 'lowest'
10523
+ });
10428
10524
  this._documents.add(domDocument);
10429
10525
  }
10430
10526
  /**
@@ -10451,9 +10547,8 @@ function sameNodes(child1, child2) {
10451
10547
  * a selection changes and fires {@link module:engine/view/document~Document#event:selectionChange} event on every change
10452
10548
  * and {@link module:engine/view/document~Document#event:selectionChangeDone} when a selection stop changing.
10453
10549
  *
10454
- * @param domEvent DOM event.
10455
10550
  * @param domDocument DOM document.
10456
- */ _handleSelectionChange(domEvent, domDocument) {
10551
+ */ _handleSelectionChange(domDocument) {
10457
10552
  if (!this.isEnabled) {
10458
10553
  return;
10459
10554
  }
@@ -10501,7 +10596,7 @@ function sameNodes(child1, child2) {
10501
10596
  };
10502
10597
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10503
10598
  // @if CK_DEBUG_TYPING // console.info( '%c[SelectionObserver]%c Fire selection change:',
10504
- // @if CK_DEBUG_TYPING // 'font-weight:bold;color:green', '',
10599
+ // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green', '',
10505
10600
  // @if CK_DEBUG_TYPING // newViewSelection.getFirstRange()
10506
10601
  // @if CK_DEBUG_TYPING // );
10507
10602
  // @if CK_DEBUG_TYPING // }
@@ -10521,6 +10616,7 @@ function sameNodes(child1, child2) {
10521
10616
  }
10522
10617
  }
10523
10618
 
10619
+ // @if CK_DEBUG_TYPING // const { _debouncedLine } = require( '../../dev-utils/utils.js' );
10524
10620
  /**
10525
10621
  * {@link module:engine/view/document~Document#event:compositionstart Compositionstart},
10526
10622
  * {@link module:engine/view/document~Document#event:compositionupdate compositionupdate} and
@@ -10567,6 +10663,7 @@ function sameNodes(child1, child2) {
10567
10663
  * @inheritDoc
10568
10664
  */ onDomEvent(domEvent) {
10569
10665
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10666
+ // @if CK_DEBUG_TYPING // _debouncedLine();
10570
10667
  // @if CK_DEBUG_TYPING // console.group( `%c[CompositionObserver]%c ${ domEvent.type }`, 'color: green', '' );
10571
10668
  // @if CK_DEBUG_TYPING // }
10572
10669
  this.fire(domEvent.type, domEvent, {
@@ -10672,6 +10769,7 @@ function getFiles(nativeDataTransfer) {
10672
10769
  return items.filter((item)=>item.kind === 'file').map((item)=>item.getAsFile());
10673
10770
  }
10674
10771
 
10772
+ // @if CK_DEBUG_TYPING // const { _debouncedLine } = require( '../../dev-utils/utils.js' );
10675
10773
  /**
10676
10774
  * Observer for events connected with data input.
10677
10775
  *
@@ -10685,6 +10783,7 @@ function getFiles(nativeDataTransfer) {
10685
10783
  * @inheritDoc
10686
10784
  */ onDomEvent(domEvent) {
10687
10785
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10786
+ // @if CK_DEBUG_TYPING // _debouncedLine();
10688
10787
  // @if CK_DEBUG_TYPING // console.group( `%c[InputObserver]%c ${ domEvent.type }: ${ domEvent.inputType }`,
10689
10788
  // @if CK_DEBUG_TYPING // 'color: green', 'color: default'
10690
10789
  // @if CK_DEBUG_TYPING // );
@@ -10702,14 +10801,14 @@ function getFiles(nativeDataTransfer) {
10702
10801
  data = domEvent.data;
10703
10802
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10704
10803
  // @if CK_DEBUG_TYPING // console.info( `%c[InputObserver]%c event data: %c${ JSON.stringify( data ) }`,
10705
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', 'color: blue;'
10804
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold', 'color: blue;'
10706
10805
  // @if CK_DEBUG_TYPING // );
10707
10806
  // @if CK_DEBUG_TYPING // }
10708
10807
  } else if (dataTransfer) {
10709
10808
  data = dataTransfer.getData('text/plain');
10710
10809
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10711
10810
  // @if CK_DEBUG_TYPING // console.info( `%c[InputObserver]%c event data transfer: %c${ JSON.stringify( data ) }`,
10712
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', 'color: blue;'
10811
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold', 'color: blue;'
10713
10812
  // @if CK_DEBUG_TYPING // );
10714
10813
  // @if CK_DEBUG_TYPING // }
10715
10814
  }
@@ -10720,7 +10819,7 @@ function getFiles(nativeDataTransfer) {
10720
10819
  targetRanges = Array.from(viewDocument.selection.getRanges());
10721
10820
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10722
10821
  // @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using fake selection:',
10723
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges,
10822
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold', targetRanges,
10724
10823
  // @if CK_DEBUG_TYPING // viewDocument.selection.isFake ? 'fake view selection' : 'fake DOM parent'
10725
10824
  // @if CK_DEBUG_TYPING // );
10726
10825
  // @if CK_DEBUG_TYPING // }
@@ -10740,7 +10839,7 @@ function getFiles(nativeDataTransfer) {
10740
10839
  }).filter((range)=>!!range);
10741
10840
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10742
10841
  // @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using target ranges:',
10743
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
10842
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold', targetRanges
10744
10843
  // @if CK_DEBUG_TYPING // );
10745
10844
  // @if CK_DEBUG_TYPING // }
10746
10845
  } else if (env.isAndroid) {
@@ -10748,7 +10847,7 @@ function getFiles(nativeDataTransfer) {
10748
10847
  targetRanges = Array.from(view.domConverter.domSelectionToView(domSelection).getRanges());
10749
10848
  // @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
10750
10849
  // @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using selection ranges:',
10751
- // @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
10850
+ // @if CK_DEBUG_TYPING // 'color: green; font-weight: bold', 'font-weight: bold', targetRanges
10752
10851
  // @if CK_DEBUG_TYPING // );
10753
10852
  // @if CK_DEBUG_TYPING // }
10754
10853
  }
@@ -10923,8 +11022,6 @@ function getFiles(nativeDataTransfer) {
10923
11022
  */ domRoots = new Map();
10924
11023
  /**
10925
11024
  * Instance of the {@link module:engine/view/renderer~Renderer renderer}.
10926
- *
10927
- * @internal
10928
11025
  */ _renderer;
10929
11026
  /**
10930
11027
  * A DOM root attributes cache. It saves the initial values of DOM root attributes before the DOM element
@@ -11003,6 +11100,19 @@ function getFiles(nativeDataTransfer) {
11003
11100
  }
11004
11101
  });
11005
11102
  }
11103
+ // Listen to external content mutations (directly in the DOM) and mark them to get verified by the renderer.
11104
+ this.listenTo(this.document, 'mutations', (evt, { mutations })=>{
11105
+ mutations.forEach((mutation)=>this._renderer.markToSync(mutation.type, mutation.node));
11106
+ }, {
11107
+ priority: 'low'
11108
+ });
11109
+ // After all mutated nodes were marked to sync we can trigger view to DOM synchronization
11110
+ // to make sure the DOM structure matches the view.
11111
+ this.listenTo(this.document, 'mutations', ()=>{
11112
+ this.forceRender();
11113
+ }, {
11114
+ priority: 'lowest'
11115
+ });
11006
11116
  }
11007
11117
  /**
11008
11118
  * Attaches a DOM root element to the view element and enable all observers on that element.
@@ -20291,14 +20401,7 @@ function getFromAttributeCreator(config) {
20291
20401
  if (data.viewItem.data.trim().length == 0) {
20292
20402
  return;
20293
20403
  }
20294
- // Wrap `$text` in paragraph and include any marker that is directly before `$text`. See #13053.
20295
- const nodeBefore = position.nodeBefore;
20296
20404
  position = wrapInParagraph(position, writer);
20297
- if (nodeBefore && nodeBefore.is('element', '$marker')) {
20298
- // Move `$marker` to the paragraph.
20299
- writer.move(writer.createRangeOn(nodeBefore), position);
20300
- position = writer.createPositionAfter(nodeBefore);
20301
- }
20302
20405
  }
20303
20406
  consumable.consume(data.viewItem);
20304
20407
  const text = writer.createText(data.viewItem.data);
@@ -21782,6 +21885,22 @@ const CONSUMABLE_TYPES = [
21782
21885
  /**
21783
21886
  * A dictionary containing attribute properties.
21784
21887
  */ _attributeProperties = {};
21888
+ /**
21889
+ * Stores additional callbacks registered for schema items, which are evaluated when {@link ~Schema#checkChild} is called.
21890
+ *
21891
+ * Keys are schema item names for which the callbacks are registered. Values are arrays with the callbacks.
21892
+ *
21893
+ * Some checks are added under {@link ~Schema#_genericCheckSymbol} key, these are evaluated for every {@link ~Schema#checkChild} call.
21894
+ */ _customChildChecks = new Map();
21895
+ /**
21896
+ * Stores additional callbacks registered for attribute names, which are evaluated when {@link ~Schema#checkAttribute} is called.
21897
+ *
21898
+ * Keys are schema attribute names for which the callbacks are registered. Values are arrays with the callbacks.
21899
+ *
21900
+ * Some checks are added under {@link ~Schema#_genericCheckSymbol} key, these are evaluated for every
21901
+ * {@link ~Schema#checkAttribute} call.
21902
+ */ _customAttributeChecks = new Map();
21903
+ _genericCheckSymbol = Symbol('$generic');
21785
21904
  _compiledDefinitions;
21786
21905
  /**
21787
21906
  * Creates a schema instance.
@@ -22054,7 +22173,7 @@ const CONSUMABLE_TYPES = [
22054
22173
  return !!(def.isContent || def.isObject);
22055
22174
  }
22056
22175
  /**
22057
- * Checks whether the given node (`child`) can be a child of the given context.
22176
+ * Checks whether the given node can be a child of the given context.
22058
22177
  *
22059
22178
  * ```ts
22060
22179
  * schema.checkChild( model.document.getRoot(), paragraph ); // -> false
@@ -22062,27 +22181,33 @@ const CONSUMABLE_TYPES = [
22062
22181
  * schema.register( 'paragraph', {
22063
22182
  * allowIn: '$root'
22064
22183
  * } );
22184
+ *
22065
22185
  * schema.checkChild( model.document.getRoot(), paragraph ); // -> true
22066
22186
  * ```
22067
22187
  *
22068
- * Note: When verifying whether the given node can be a child of the given context, the
22069
- * schema also verifies the entire context &ndash; from its root to its last element. Therefore, it is possible
22070
- * for `checkChild()` to return `false` even though the context's last element can contain the checked child.
22071
- * It happens if one of the context's elements does not allow its child.
22188
+ * Both {@link module:engine/model/schema~Schema#addChildCheck callback checks} and declarative rules (added when
22189
+ * {@link module:engine/model/schema~Schema#register registering} and {@link module:engine/model/schema~Schema#extend extending} items)
22190
+ * are evaluated when this method is called.
22191
+ *
22192
+ * Note that callback checks have bigger priority than declarative rules checks and may overwrite them.
22193
+ *
22194
+ * Note that when verifying whether the given node can be a child of the given context, the schema also verifies the entire
22195
+ * context &ndash; from its root to its last element. Therefore, it is possible for `checkChild()` to return `false` even though
22196
+ * the `context` last element can contain the checked child. It happens if one of the `context` elements does not allow its child.
22197
+ * When `context` is verified, {@link module:engine/model/schema~Schema#addChildCheck custom checks} are considered as well.
22072
22198
  *
22073
22199
  * @fires checkChild
22074
22200
  * @param context The context in which the child will be checked.
22075
22201
  * @param def The child to check.
22076
22202
  */ checkChild(context, def) {
22077
- // Note: context and child are already normalized here to a SchemaContext and SchemaCompiledItemDefinition.
22203
+ // Note: `context` and `def` are already normalized here to `SchemaContext` and `SchemaCompiledItemDefinition`.
22078
22204
  if (!def) {
22079
22205
  return false;
22080
22206
  }
22081
- return this._checkContextMatch(def, context);
22207
+ return this._checkContextMatch(context, def);
22082
22208
  }
22083
22209
  /**
22084
- * Checks whether the given attribute can be applied in the given context (on the last
22085
- * item of the context).
22210
+ * Checks whether the given attribute can be applied in the given context (on the last item of the context).
22086
22211
  *
22087
22212
  * ```ts
22088
22213
  * schema.checkAttribute( textNode, 'bold' ); // -> false
@@ -22090,22 +22215,36 @@ const CONSUMABLE_TYPES = [
22090
22215
  * schema.extend( '$text', {
22091
22216
  * allowAttributes: 'bold'
22092
22217
  * } );
22218
+ *
22093
22219
  * schema.checkAttribute( textNode, 'bold' ); // -> true
22094
22220
  * ```
22095
22221
  *
22222
+ * Both {@link module:engine/model/schema~Schema#addAttributeCheck callback checks} and declarative rules (added when
22223
+ * {@link module:engine/model/schema~Schema#register registering} and {@link module:engine/model/schema~Schema#extend extending} items)
22224
+ * are evaluated when this method is called.
22225
+ *
22226
+ * Note that callback checks have bigger priority than declarative rules checks and may overwrite them.
22227
+ *
22096
22228
  * @fires checkAttribute
22097
22229
  * @param context The context in which the attribute will be checked.
22230
+ * @param attributeName Name of attribute to check in the given context.
22098
22231
  */ checkAttribute(context, attributeName) {
22232
+ // Note: `context` is already normalized here to `SchemaContext`.
22099
22233
  const def = this.getDefinition(context.last);
22100
22234
  if (!def) {
22101
22235
  return false;
22102
22236
  }
22103
- return def.allowAttributes.includes(attributeName);
22237
+ // First, check all attribute checks declared as callbacks.
22238
+ // Note that `_evaluateAttributeChecks()` will return `undefined` if neither child check was applicable (no decision was made).
22239
+ const isAllowed = this._evaluateAttributeChecks(context, attributeName);
22240
+ // If the decision was not made inside attribute check callbacks, then use declarative rules.
22241
+ return isAllowed !== undefined ? isAllowed : def.allowAttributes.includes(attributeName);
22104
22242
  }
22105
22243
  /**
22106
22244
  * Checks whether the given element (`elementToMerge`) can be merged with the specified base element (`positionOrBaseElement`).
22107
22245
  *
22108
- * In other words &ndash; whether `elementToMerge`'s children {@link #checkChild are allowed} in the `positionOrBaseElement`.
22246
+ * In other words &ndash; both elements are not a limit elements and whether `elementToMerge`'s children
22247
+ * {@link #checkChild are allowed} in the `positionOrBaseElement`.
22109
22248
  *
22110
22249
  * This check ensures that elements merged with {@link module:engine/model/writer~Writer#merge `Writer#merge()`}
22111
22250
  * will be valid.
@@ -22135,6 +22274,9 @@ const CONSUMABLE_TYPES = [
22135
22274
  }
22136
22275
  return this.checkMerge(nodeBefore, nodeAfter);
22137
22276
  }
22277
+ if (this.isLimit(positionOrBaseElement) || this.isLimit(elementToMerge)) {
22278
+ return false;
22279
+ }
22138
22280
  for (const child of elementToMerge.getChildren()){
22139
22281
  if (!this.checkChild(positionOrBaseElement, child)) {
22140
22282
  return false;
@@ -22147,112 +22289,137 @@ const CONSUMABLE_TYPES = [
22147
22289
  *
22148
22290
  * Callbacks allow you to implement rules which are not otherwise possible to achieve
22149
22291
  * by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
22150
- * For example, by using this method you can disallow elements in specific contexts.
22151
22292
  *
22152
- * This method is a shorthand for using the {@link #event:checkChild} event. For even better control,
22153
- * you can use that event instead.
22293
+ * Note that callback checks have bigger priority than declarative rules checks and may overwrite them.
22154
22294
  *
22155
- * Example:
22295
+ * For example, by using this method you can disallow elements in specific contexts:
22156
22296
  *
22157
22297
  * ```ts
22158
- * // Disallow heading1 directly inside a blockQuote.
22298
+ * // Disallow `heading1` inside a `blockQuote` that is inside a table.
22159
22299
  * schema.addChildCheck( ( context, childDefinition ) => {
22160
- * if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'heading1' ) {
22300
+ * if ( context.endsWith( 'tableCell blockQuote' ) ) {
22161
22301
  * return false;
22162
22302
  * }
22303
+ * }, 'heading1' );
22304
+ * ```
22305
+ *
22306
+ * You can skip the optional `itemName` parameter to evaluate the callback for every `checkChild()` call.
22307
+ *
22308
+ * ```ts
22309
+ * // Inside specific custom element, allow only children, which allows for a specific attribute.
22310
+ * schema.addChildCheck( ( context, childDefinition ) => {
22311
+ * if ( context.endsWith( 'myElement' ) ) {
22312
+ * return childDefinition.allowAttributes.includes( 'myAttribute' );
22313
+ * }
22163
22314
  * } );
22164
22315
  * ```
22165
22316
  *
22166
- * Which translates to:
22317
+ * Please note that the generic callbacks may affect the editor performance and should be avoided if possible.
22318
+ *
22319
+ * When one of the callbacks makes a decision (returns `true` or `false`) the processing is finished and other callbacks are not fired.
22320
+ * Callbacks are fired in the order they were added, however generic callbacks are fired before callbacks added for a specified item.
22321
+ *
22322
+ * You can also use `checkChild` event, if you need even better control. The result from the example above could also be
22323
+ * achieved with following event callback:
22167
22324
  *
22168
22325
  * ```ts
22169
22326
  * schema.on( 'checkChild', ( evt, args ) => {
22170
22327
  * const context = args[ 0 ];
22171
22328
  * const childDefinition = args[ 1 ];
22172
22329
  *
22173
- * if ( context.endsWith( 'blockQuote' ) && childDefinition && childDefinition.name == 'heading1' ) {
22330
+ * if ( context.endsWith( 'myElement' ) ) {
22174
22331
  * // Prevent next listeners from being called.
22175
22332
  * evt.stop();
22176
- * // Set the checkChild()'s return value.
22177
- * evt.return = false;
22333
+ * // Set the `checkChild()` return value.
22334
+ * evt.return = childDefinition.allowAttributes.includes( 'myAttribute' );
22178
22335
  * }
22179
22336
  * }, { priority: 'high' } );
22180
22337
  * ```
22181
22338
  *
22339
+ * Note that the callback checks and declarative rules checks are processed on `normal` priority.
22340
+ *
22341
+ * Adding callbacks this way can also negatively impact editor performance.
22342
+ *
22182
22343
  * @param callback The callback to be called. It is called with two parameters:
22183
22344
  * {@link module:engine/model/schema~SchemaContext} (context) instance and
22184
- * {@link module:engine/model/schema~SchemaCompiledItemDefinition} (child-to-check definition).
22185
- * The callback may return `true/false` to override `checkChild()`'s return value. If it does not return
22186
- * a boolean value, the default algorithm (or other callbacks) will define `checkChild()`'s return value.
22187
- */ addChildCheck(callback) {
22188
- this.on('checkChild', (evt, [ctx, childDef])=>{
22189
- // checkChild() was called with a non-registered child.
22190
- // In 99% cases such check should return false, so not to overcomplicate all callbacks
22191
- // don't even execute them.
22192
- if (!childDef) {
22193
- return;
22194
- }
22195
- const retValue = callback(ctx, childDef);
22196
- if (typeof retValue == 'boolean') {
22197
- evt.stop();
22198
- evt.return = retValue;
22199
- }
22200
- }, {
22201
- priority: 'high'
22202
- });
22345
+ * {@link module:engine/model/schema~SchemaCompiledItemDefinition} (definition). The callback may return `true/false` to override
22346
+ * `checkChild()`'s return value. If it does not return a boolean value, the default algorithm (or other callbacks) will define
22347
+ * `checkChild()`'s return value.
22348
+ * @param itemName Name of the schema item for which the callback is registered. If specified, the callback will be run only for
22349
+ * `checkChild()` calls which `def` parameter matches the `itemName`. Otherwise, the callback will run for every `checkChild` call.
22350
+ */ addChildCheck(callback, itemName) {
22351
+ const key = itemName !== undefined ? itemName : this._genericCheckSymbol;
22352
+ const checks = this._customChildChecks.get(key) || [];
22353
+ checks.push(callback);
22354
+ this._customChildChecks.set(key, checks);
22203
22355
  }
22204
22356
  /**
22205
22357
  * Allows registering a callback to the {@link #checkAttribute} method calls.
22206
22358
  *
22207
22359
  * Callbacks allow you to implement rules which are not otherwise possible to achieve
22208
22360
  * by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
22209
- * For example, by using this method you can disallow attribute if node to which it is applied
22210
- * is contained within some other element (e.g. you want to disallow `bold` on `$text` within `heading1`).
22211
22361
  *
22212
- * This method is a shorthand for using the {@link #event:checkAttribute} event. For even better control,
22213
- * you can use that event instead.
22362
+ * Note that callback checks have bigger priority than declarative rules checks and may overwrite them.
22214
22363
  *
22215
- * Example:
22364
+ * For example, by using this method you can disallow setting attributes on nodes in specific contexts:
22216
22365
  *
22217
22366
  * ```ts
22218
- * // Disallow bold on $text inside heading1.
22367
+ * // Disallow setting `bold` on text inside `heading1` element:
22368
+ * schema.addAttributeCheck( context => {
22369
+ * if ( context.endsWith( 'heading1 $text' ) ) {
22370
+ * return false;
22371
+ * }
22372
+ * }, 'bold' );
22373
+ * ```
22374
+ *
22375
+ * You can skip the optional `attributeName` parameter to evaluate the callback for every `checkAttribute()` call.
22376
+ *
22377
+ * ```ts
22378
+ * // Disallow formatting attributes on text inside custom `myTitle` element:
22219
22379
  * schema.addAttributeCheck( ( context, attributeName ) => {
22220
- * if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
22380
+ * if ( context.endsWith( 'myTitle $text' ) && schema.getAttributeProperties( attributeName ).isFormatting ) {
22221
22381
  * return false;
22222
22382
  * }
22223
22383
  * } );
22224
22384
  * ```
22225
22385
  *
22226
- * Which translates to:
22386
+ * Please note that the generic callbacks may affect the editor performance and should be avoided if possible.
22387
+ *
22388
+ * When one of the callbacks makes a decision (returns `true` or `false`) the processing is finished and other callbacks are not fired.
22389
+ * Callbacks are fired in the order they were added, however generic callbacks are fired before callbacks added for a specified item.
22390
+ *
22391
+ * You can also use {@link #event:checkAttribute} event, if you need even better control. The result from the example above could also
22392
+ * be achieved with following event callback:
22227
22393
  *
22228
22394
  * ```ts
22229
22395
  * schema.on( 'checkAttribute', ( evt, args ) => {
22230
22396
  * const context = args[ 0 ];
22231
22397
  * const attributeName = args[ 1 ];
22232
22398
  *
22233
- * if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
22399
+ * if ( context.endsWith( 'myTitle $text' ) && schema.getAttributeProperties( attributeName ).isFormatting ) {
22234
22400
  * // Prevent next listeners from being called.
22235
22401
  * evt.stop();
22236
- * // Set the checkAttribute()'s return value.
22402
+ * // Set the `checkAttribute()` return value.
22237
22403
  * evt.return = false;
22238
22404
  * }
22239
22405
  * }, { priority: 'high' } );
22240
22406
  * ```
22241
22407
  *
22408
+ * Note that the callback checks and declarative rules checks are processed on `normal` priority.
22409
+ *
22410
+ * Adding callbacks this way can also negatively impact editor performance.
22411
+ *
22242
22412
  * @param callback The callback to be called. It is called with two parameters:
22243
- * {@link module:engine/model/schema~SchemaContext} (context) instance and attribute name.
22244
- * The callback may return `true/false` to override `checkAttribute()`'s return value. If it does not return
22245
- * a boolean value, the default algorithm (or other callbacks) will define `checkAttribute()`'s return value.
22246
- */ addAttributeCheck(callback) {
22247
- this.on('checkAttribute', (evt, [ctx, attributeName])=>{
22248
- const retValue = callback(ctx, attributeName);
22249
- if (typeof retValue == 'boolean') {
22250
- evt.stop();
22251
- evt.return = retValue;
22252
- }
22253
- }, {
22254
- priority: 'high'
22255
- });
22413
+ * {@link module:engine/model/schema~SchemaContext `context`} and attribute name. The callback may return `true` or `false`, to
22414
+ * override `checkAttribute()`'s return value. If it does not return a boolean value, the default algorithm (or other callbacks)
22415
+ * will define `checkAttribute()`'s return value.
22416
+ * @param attributeName Name of the attribute for which the callback is registered. If specified, the callback will be run only for
22417
+ * `checkAttribute()` calls with matching `attributeName`. Otherwise, the callback will run for every `checkAttribute()` call.
22418
+ */ addAttributeCheck(callback, attributeName) {
22419
+ const key = attributeName !== undefined ? attributeName : this._genericCheckSymbol;
22420
+ const checks = this._customAttributeChecks.get(key) || [];
22421
+ checks.push(callback);
22422
+ this._customAttributeChecks.set(key, checks);
22256
22423
  }
22257
22424
  /**
22258
22425
  * This method allows assigning additional metadata to the model attributes. For example,
@@ -22579,18 +22746,68 @@ const CONSUMABLE_TYPES = [
22579
22746
  // Compile final definitions. Unnecessary properties are removed and some additional cleaning is applied.
22580
22747
  this._compiledDefinitions = compileDefinitions(definitions);
22581
22748
  }
22582
- _checkContextMatch(def, context, contextItemIndex = context.length - 1) {
22583
- const contextItem = context.getItem(contextItemIndex);
22584
- if (def.allowIn.includes(contextItem.name)) {
22585
- if (contextItemIndex == 0) {
22586
- return true;
22587
- } else {
22588
- const parentRule = this.getDefinition(contextItem);
22589
- return this._checkContextMatch(parentRule, context, contextItemIndex - 1);
22590
- }
22591
- } else {
22749
+ _checkContextMatch(context, def) {
22750
+ const parentItem = context.last;
22751
+ // First, check all child checks declared as callbacks.
22752
+ // Note that `_evaluateChildChecks()` will return `undefined` if neither child check was applicable (no decision was made).
22753
+ let isAllowed = this._evaluateChildChecks(context, def);
22754
+ // If the decision was not made inside child check callbacks, then use declarative rules.
22755
+ isAllowed = isAllowed !== undefined ? isAllowed : def.allowIn.includes(parentItem.name);
22756
+ // If the item is not allowed in the `context`, return `false`.
22757
+ if (!isAllowed) {
22592
22758
  return false;
22593
22759
  }
22760
+ // If the item is allowed, recursively verify the rest of the `context`.
22761
+ const parentItemDefinition = this.getDefinition(parentItem);
22762
+ const parentContext = context.trimLast();
22763
+ // One of the items in the original `context` did not have a definition specified. In this case, the whole context is disallowed.
22764
+ if (!parentItemDefinition) {
22765
+ return false;
22766
+ }
22767
+ // Whole `context` was verified and passed checks.
22768
+ if (parentContext.length == 0) {
22769
+ return true;
22770
+ }
22771
+ // Verify "truncated" parent context. The last item of the original context is now the definition to check.
22772
+ return this._checkContextMatch(parentContext, parentItemDefinition);
22773
+ }
22774
+ /**
22775
+ * Calls child check callbacks to decide whether `def` is allowed in `context`. It uses both generic and specific (defined for `def`
22776
+ * item) callbacks. If neither callback makes a decision, `undefined` is returned.
22777
+ *
22778
+ * Note that the first callback that makes a decision "wins", i.e., if any callback returns `true` or `false`, then the processing
22779
+ * is over and that result is returned.
22780
+ */ _evaluateChildChecks(context, def) {
22781
+ const genericChecks = this._customChildChecks.get(this._genericCheckSymbol) || [];
22782
+ const childChecks = this._customChildChecks.get(def.name) || [];
22783
+ for (const check of [
22784
+ ...genericChecks,
22785
+ ...childChecks
22786
+ ]){
22787
+ const result = check(context, def);
22788
+ if (result !== undefined) {
22789
+ return result;
22790
+ }
22791
+ }
22792
+ }
22793
+ /**
22794
+ * Calls attribute check callbacks to decide whether `attributeName` can be set on the last element of `context`. It uses both
22795
+ * generic and specific (defined for `attributeName`) callbacks. If neither callback makes a decision, `undefined` is returned.
22796
+ *
22797
+ * Note that the first callback that makes a decision "wins", i.e., if any callback returns `true` or `false`, then the processing
22798
+ * is over and that result is returned.
22799
+ */ _evaluateAttributeChecks(context, attributeName) {
22800
+ const genericChecks = this._customAttributeChecks.get(this._genericCheckSymbol) || [];
22801
+ const childChecks = this._customAttributeChecks.get(attributeName) || [];
22802
+ for (const check of [
22803
+ ...genericChecks,
22804
+ ...childChecks
22805
+ ]){
22806
+ const result = check(context, attributeName);
22807
+ if (result !== undefined) {
22808
+ return result;
22809
+ }
22810
+ }
22594
22811
  }
22595
22812
  /**
22596
22813
  * Takes a flat range and an attribute name. Traverses the range recursively and deeply to find and return all ranges
@@ -22768,6 +22985,21 @@ const CONSUMABLE_TYPES = [
22768
22985
  ];
22769
22986
  return ctx;
22770
22987
  }
22988
+ /**
22989
+ * Returns a new schema context that is based on this context but has the last item removed.
22990
+ *
22991
+ * ```ts
22992
+ * const ctxParagraph = new SchemaContext( [ '$root', 'blockQuote', 'paragraph' ] );
22993
+ * const ctxBlockQuote = ctxParagraph.trimLast(); // Items in `ctxBlockQuote` are: `$root` an `blockQuote`.
22994
+ * const ctxRoot = ctxBlockQuote.trimLast(); // Items in `ctxRoot` are: `$root`.
22995
+ * ```
22996
+ *
22997
+ * @returns A new reduced schema context instance.
22998
+ */ trimLast() {
22999
+ const ctx = new SchemaContext([]);
23000
+ ctx._items = this._items.slice(0, -1);
23001
+ return ctx;
23002
+ }
22771
23003
  /**
22772
23004
  * Gets an item on the given index.
22773
23005
  */ getItem(index) {
@@ -33350,25 +33582,14 @@ function removeRangeContent(range, writer) {
33350
33582
  /**
33351
33583
  * Handles insertion of a single node.
33352
33584
  */ _handleNode(node) {
33353
- // Let's handle object in a special way.
33354
- // * They should never be merged with other elements.
33355
- // * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
33356
- if (this.schema.isObject(node)) {
33357
- this._handleObject(node);
33358
- return;
33359
- }
33360
- // Try to find a place for the given node.
33361
- // Check if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
33362
- // Inserts the auto paragraph if it would allow for insertion.
33363
- let isAllowed = this._checkAndAutoParagraphToAllowedPosition(node);
33364
- if (!isAllowed) {
33365
- // Split the position.parent's branch up to a point where the node can be inserted.
33366
- // If it isn't allowed in the whole branch, then of course don't split anything.
33367
- isAllowed = this._checkAndSplitToAllowedPosition(node);
33368
- if (!isAllowed) {
33585
+ // Split the position.parent's branch up to a point where the node can be inserted.
33586
+ // If it isn't allowed in the whole branch, then of course don't split anything.
33587
+ if (!this._checkAndSplitToAllowedPosition(node)) {
33588
+ // Handle element children if it's not an object (strip container).
33589
+ if (!this.schema.isObject(node)) {
33369
33590
  this._handleDisallowedNode(node);
33370
- return;
33371
33591
  }
33592
+ return;
33372
33593
  }
33373
33594
  // Add node to the current temporary DocumentFragment.
33374
33595
  this._appendToFragment(node);
@@ -33404,24 +33625,12 @@ function removeRangeContent(range, writer) {
33404
33625
  this.position = livePosition.toPosition();
33405
33626
  livePosition.detach();
33406
33627
  }
33407
- /**
33408
- * @param node The object element.
33409
- */ _handleObject(node) {
33410
- // Try finding it a place in the tree.
33411
- if (this._checkAndSplitToAllowedPosition(node)) {
33412
- this._appendToFragment(node);
33413
- } else {
33414
- this._tryAutoparagraphing(node);
33415
- }
33416
- }
33417
33628
  /**
33418
33629
  * @param node The disallowed node which needs to be handled.
33419
33630
  */ _handleDisallowedNode(node) {
33420
33631
  // If the node is an element, try inserting its children (strip the parent).
33421
33632
  if (node.is('element')) {
33422
33633
  this.handleNodes(node.getChildren());
33423
- } else {
33424
- this._tryAutoparagraphing(node);
33425
33634
  }
33426
33635
  }
33427
33636
  /**
@@ -33626,35 +33835,8 @@ function removeRangeContent(range, writer) {
33626
33835
  return nextSibling instanceof Element && this.canMergeWith.has(nextSibling) && this.model.schema.checkMerge(node, nextSibling);
33627
33836
  }
33628
33837
  /**
33629
- * Tries wrapping the node in a new paragraph and inserting it this way.
33630
- *
33631
- * @param node The node which needs to be autoparagraphed.
33632
- */ _tryAutoparagraphing(node) {
33633
- const paragraph = this.writer.createElement('paragraph');
33634
- // Do not autoparagraph if the paragraph won't be allowed there,
33635
- // cause that would lead to an infinite loop. The paragraph would be rejected in
33636
- // the next _handleNode() call and we'd be here again.
33637
- if (this._getAllowedIn(this.position.parent, paragraph) && this.schema.checkChild(paragraph, node)) {
33638
- paragraph._appendChild(node);
33639
- this._handleNode(paragraph);
33640
- }
33641
- }
33642
- /**
33643
- * Checks if a node can be inserted in the given position or it would be accepted if a paragraph would be inserted.
33644
- * It also handles inserting the paragraph.
33645
- *
33646
- * @returns Whether an allowed position was found.
33647
- * `false` is returned if the node isn't allowed at the current position or in auto paragraph, `true` if was.
33648
- */ _checkAndAutoParagraphToAllowedPosition(node) {
33649
- if (this.schema.checkChild(this.position.parent, node)) {
33650
- return true;
33651
- }
33652
- // Do not auto paragraph if the paragraph won't be allowed there,
33653
- // cause that would lead to an infinite loop. The paragraph would be rejected in
33654
- // the next _handleNode() call and we'd be here again.
33655
- if (!this.schema.checkChild(this.position.parent, 'paragraph') || !this.schema.checkChild('paragraph', node)) {
33656
- return false;
33657
- }
33838
+ * Inserts a paragraph and moves the insertion position into it.
33839
+ */ _insertAutoParagraph() {
33658
33840
  // Insert nodes collected in temporary DocumentFragment if the position parent needs change to process further nodes.
33659
33841
  this._insertPartialFragment();
33660
33842
  // Insert a paragraph and move insertion position to it.
@@ -33663,7 +33845,6 @@ function removeRangeContent(range, writer) {
33663
33845
  this._setAffectedBoundaries(this.position);
33664
33846
  this._lastAutoParagraph = paragraph;
33665
33847
  this.position = this.writer.createPositionAt(paragraph, 0);
33666
- return true;
33667
33848
  }
33668
33849
  /**
33669
33850
  * @returns Whether an allowed position was found.
@@ -33707,17 +33888,30 @@ function removeRangeContent(range, writer) {
33707
33888
  this.canMergeWith.add(this.position.nodeAfter);
33708
33889
  }
33709
33890
  }
33891
+ // At this point, we split elements up to the parent in which `node` is allowed.
33892
+ // Note that `_getAllowedIn()` checks if the `node` is allowed either directly, or when auto-paragraphed.
33893
+ // So, let's check if the `node` is allowed directly. If not, we need to auto-paragraph it.
33894
+ if (!this.schema.checkChild(this.position.parent, node)) {
33895
+ this._insertAutoParagraph();
33896
+ }
33710
33897
  return true;
33711
33898
  }
33712
33899
  /**
33713
33900
  * Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
33714
33901
  *
33902
+ * It also verifies if auto-paragraphing could help.
33903
+ *
33715
33904
  * @param contextElement The element in which context the node should be checked.
33716
33905
  * @param childNode The node to check.
33717
33906
  */ _getAllowedIn(contextElement, childNode) {
33907
+ // Check if a node can be inserted in the given context...
33718
33908
  if (this.schema.checkChild(contextElement, childNode)) {
33719
33909
  return contextElement;
33720
33910
  }
33911
+ // ...or it would be accepted if a paragraph would be inserted.
33912
+ if (this.schema.checkChild(contextElement, 'paragraph') && this.schema.checkChild('paragraph', childNode)) {
33913
+ return contextElement;
33914
+ }
33721
33915
  // If the child wasn't allowed in the context element and the element is a limit there's no point in
33722
33916
  // checking any further towards the root. This is it: the limit is unsplittable and there's nothing
33723
33917
  // we can do about it. Without this check, the algorithm will analyze parent of the limit and may create
@@ -34117,11 +34311,7 @@ function getSearchRange(start, isForward) {
34117
34311
  // at the end of the conversion. `UpcastDispatcher` or at least `Conversion` class looks like a
34118
34312
  // better place for this registration but both know nothing about `Schema`.
34119
34313
  this.schema.register('$marker');
34120
- this.schema.addChildCheck((context, childDefinition)=>{
34121
- if (childDefinition.name === '$marker') {
34122
- return true;
34123
- }
34124
- });
34314
+ this.schema.addChildCheck(()=>true, '$marker'); // Allow everywhere.
34125
34315
  injectSelectionPostFixer(this);
34126
34316
  // Post-fixer which takes care of adding empty paragraph elements to the empty roots.
34127
34317
  this.document.registerPostFixer(autoParagraphEmptyRoots);
@@ -36494,6 +36684,9 @@ setData$1._parse = parse$1;
36494
36684
  const processor = new XmlDataProcessor(viewDocument, {
36495
36685
  namespaces: Object.keys(allowedTypes)
36496
36686
  });
36687
+ if (options.inlineObjectElements) {
36688
+ processor.domConverter.inlineObjectElements.push(...options.inlineObjectElements);
36689
+ }
36497
36690
  // Convert data to view.
36498
36691
  let view = processor.toView(data);
36499
36692
  // At this point we have a view tree with Elements that could have names like `attribute:b:1`. In the next step
@@ -37159,7 +37352,8 @@ getData._stringify = stringify;
37159
37352
  selectionAttributes: options.selectionAttributes,
37160
37353
  context: [
37161
37354
  modelRoot.name
37162
- ]
37355
+ ],
37356
+ inlineObjectElements: options.inlineObjectElements
37163
37357
  });
37164
37358
  // Retrieve DocumentFragment and Selection from parsed model.
37165
37359
  if ('model' in parsedResult) {
@@ -37330,7 +37524,8 @@ setData._parse = parse;
37330
37524
  // Parse data to view using view utils.
37331
37525
  const parsedResult = parse$1(data, {
37332
37526
  sameSelectionCharacters: true,
37333
- lastRangeBackward: !!options.lastRangeBackward
37527
+ lastRangeBackward: !!options.lastRangeBackward,
37528
+ inlineObjectElements: options.inlineObjectElements
37334
37529
  });
37335
37530
  // Retrieve DocumentFragment and Selection from parsed view.
37336
37531
  let viewDocumentFragment;
@@ -37465,5 +37660,5 @@ function* convertAttributes(attributes, converter) {
37465
37660
  }
37466
37661
  }
37467
37662
 
37468
- export { AttributeElement, AttributeOperation, BubblingEventInfo, ClickObserver, Conversion, DataController, DataTransfer, DocumentFragment, DocumentSelection, DomConverter, DomEventData, DomEventObserver, DowncastWriter, EditingController, View as EditingView, Element, FocusObserver, History, HtmlDataProcessor, InsertOperation, LivePosition, LiveRange, MarkerOperation, Matcher, MergeOperation, Model, MouseObserver, MoveOperation, NoOperation, Observer, OperationFactory, Position, Range, RenameOperation, Renderer, RootAttributeOperation, RootOperation, SplitOperation, StylesMap, StylesProcessor, TabObserver, Text, TextProxy, TreeWalker, UpcastWriter, AttributeElement as ViewAttributeElement, ContainerElement as ViewContainerElement, Document$1 as ViewDocument, DocumentFragment$1 as ViewDocumentFragment, EditableElement as ViewEditableElement, Element$1 as ViewElement, EmptyElement as ViewEmptyElement, RawElement as ViewRawElement, RootEditableElement as ViewRootEditableElement, Text$1 as ViewText, TreeWalker$1 as ViewTreeWalker, UIElement as ViewUIElement, XmlDataProcessor, getData as _getModelData, getData$1 as _getViewData, parse as _parseModel, parse$1 as _parseView, setData as _setModelData, setData$1 as _setViewData, stringify as _stringifyModel, stringify$1 as _stringifyView, addBackgroundRules, addBorderRules, addMarginRules, addPaddingRules, disablePlaceholder, enablePlaceholder, getBoxSidesShorthandValue, getBoxSidesValueReducer, getBoxSidesValues, getFillerOffset$4 as getFillerOffset, getPositionShorthandNormalizer, getShorthandValues, hidePlaceholder, isAttachment, isColor, isLength, isLineStyle, isPercentage, isPosition, isRepeat, isURL, needsPlaceholder, showPlaceholder, transformSets };
37663
+ export { AttributeElement, AttributeOperation, BubblingEventInfo, ClickObserver, Conversion, DataController, DataTransfer, DocumentFragment, DocumentSelection, DomConverter, DomEventData, DomEventObserver, DowncastWriter, EditingController, View as EditingView, Element, FocusObserver, History, HtmlDataProcessor, InsertOperation, LivePosition, LiveRange, MarkerOperation, Matcher, MergeOperation, Model, MouseObserver, MoveOperation, NoOperation, Observer, OperationFactory, Position, Range, RenameOperation, Renderer, RootAttributeOperation, RootOperation, SplitOperation, StylesMap, StylesProcessor, TabObserver, Text, TextProxy, TreeWalker, UpcastWriter, AttributeElement as ViewAttributeElement, ContainerElement as ViewContainerElement, Document$1 as ViewDocument, DocumentFragment$1 as ViewDocumentFragment, EditableElement as ViewEditableElement, Element$1 as ViewElement, EmptyElement as ViewEmptyElement, RawElement as ViewRawElement, RootEditableElement as ViewRootEditableElement, Text$1 as ViewText, TreeWalker$1 as ViewTreeWalker, UIElement as ViewUIElement, XmlDataProcessor, getData as _getModelData, getData$1 as _getViewData, parse as _parseModel, parse$1 as _parseView, setData as _setModelData, setData$1 as _setViewData, stringify as _stringifyModel, stringify$1 as _stringifyView, addBackgroundRules, addBorderRules, addMarginRules, addPaddingRules, autoParagraphEmptyRoots, disablePlaceholder, enablePlaceholder, getBoxSidesShorthandValue, getBoxSidesValueReducer, getBoxSidesValues, getFillerOffset$4 as getFillerOffset, getPositionShorthandNormalizer, getShorthandValues, hidePlaceholder, isAttachment, isColor, isLength, isLineStyle, isParagraphable, isPercentage, isPosition, isRepeat, isURL, needsPlaceholder, showPlaceholder, transformSets, wrapInParagraph };
37469
37664
  //# sourceMappingURL=index.js.map