@ckeditor/ckeditor5-engine 38.2.0-alpha.0 → 39.0.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 (37) hide show
  1. package/README.md +0 -1
  2. package/package.json +3 -4
  3. package/src/controller/editingcontroller.js +2 -2
  4. package/src/conversion/downcastdispatcher.d.ts +15 -0
  5. package/src/conversion/downcastdispatcher.js +28 -19
  6. package/src/conversion/downcasthelpers.d.ts +6 -6
  7. package/src/conversion/downcasthelpers.js +6 -6
  8. package/src/dev-utils/view.js +1 -1
  9. package/src/index.d.ts +1 -0
  10. package/src/index.js +1 -0
  11. package/src/model/differ.d.ts +14 -0
  12. package/src/model/differ.js +70 -11
  13. package/src/model/document.d.ts +9 -1
  14. package/src/model/document.js +14 -9
  15. package/src/model/documentselection.js +8 -2
  16. package/src/model/model.d.ts +0 -1
  17. package/src/model/model.js +0 -1
  18. package/src/model/operation/rootoperation.d.ts +0 -4
  19. package/src/model/operation/rootoperation.js +0 -24
  20. package/src/model/operation/transform.js +2 -2
  21. package/src/model/rootelement.d.ts +6 -0
  22. package/src/model/rootelement.js +6 -0
  23. package/src/model/schema.d.ts +10 -0
  24. package/src/model/schema.js +5 -0
  25. package/src/model/utils/autoparagraphing.js +1 -2
  26. package/src/view/domconverter.d.ts +43 -53
  27. package/src/view/domconverter.js +266 -214
  28. package/src/view/editableelement.d.ts +10 -0
  29. package/src/view/editableelement.js +1 -0
  30. package/src/view/filler.d.ts +2 -2
  31. package/src/view/filler.js +6 -4
  32. package/src/view/observer/selectionobserver.js +2 -2
  33. package/src/view/placeholder.d.ts +13 -5
  34. package/src/view/placeholder.js +21 -12
  35. package/src/view/renderer.js +1 -2
  36. package/src/view/view.d.ts +14 -7
  37. package/src/view/view.js +13 -1
@@ -67,9 +67,9 @@ export default class DomConverter {
67
67
  */
68
68
  this._rawContentElementMatcher = new Matcher();
69
69
  /**
70
- * A set of encountered raw content DOM nodes. It is used for preventing left trimming of the following text node.
70
+ * Matcher for inline object view elements. This is an extension of a simple {@link #inlineObjectElements} array of element names.
71
71
  */
72
- this._encounteredRawContentDomNodes = new WeakSet();
72
+ this._inlineObjectElementMatcher = new Matcher();
73
73
  this.document = document;
74
74
  this.renderingMode = renderingMode;
75
75
  this.blockFillerMode = blockFillerMode || (renderingMode === 'editing' ? 'br' : 'nbsp');
@@ -472,68 +472,22 @@ export default class DomConverter {
472
472
  * or the given node is an empty text node.
473
473
  */
474
474
  domToView(domNode, options = {}) {
475
- if (this.isBlockFiller(domNode)) {
475
+ const inlineNodes = [];
476
+ const generator = this._domToView(domNode, options, inlineNodes);
477
+ // Get the first yielded value or a returned value.
478
+ const node = generator.next().value;
479
+ if (!node) {
476
480
  return null;
477
481
  }
478
- // When node is inside a UIElement or a RawElement return that parent as it's view representation.
479
- const hostElement = this.getHostViewElement(domNode);
480
- if (hostElement) {
481
- return hostElement;
482
- }
483
- if (isComment(domNode) && options.skipComments) {
482
+ // Trigger children handling.
483
+ generator.next();
484
+ // Whitespace cleaning.
485
+ this._processDomInlineNodes(null, inlineNodes, options);
486
+ // Text not got trimmed to an empty string so there is no result node.
487
+ if (node.is('$text') && node.data.length == 0) {
484
488
  return null;
485
489
  }
486
- if (isText(domNode)) {
487
- if (isInlineFiller(domNode)) {
488
- return null;
489
- }
490
- else {
491
- const textData = this._processDataFromDomText(domNode);
492
- return textData === '' ? null : new ViewText(this.document, textData);
493
- }
494
- }
495
- else {
496
- if (this.mapDomToView(domNode)) {
497
- return this.mapDomToView(domNode);
498
- }
499
- let viewElement;
500
- if (this.isDocumentFragment(domNode)) {
501
- // Create view document fragment.
502
- viewElement = new ViewDocumentFragment(this.document);
503
- if (options.bind) {
504
- this.bindDocumentFragments(domNode, viewElement);
505
- }
506
- }
507
- else {
508
- // Create view element.
509
- viewElement = this._createViewElement(domNode, options);
510
- if (options.bind) {
511
- this.bindElements(domNode, viewElement);
512
- }
513
- // Copy element's attributes.
514
- const attrs = domNode.attributes;
515
- if (attrs) {
516
- for (let l = attrs.length, i = 0; i < l; i++) {
517
- viewElement._setAttribute(attrs[i].name, attrs[i].value);
518
- }
519
- }
520
- // Treat this element's content as a raw data if it was registered as such.
521
- // Comment node is also treated as an element with raw data.
522
- if (this._isViewElementWithRawContent(viewElement, options) || isComment(domNode)) {
523
- const rawContent = isComment(domNode) ? domNode.data : domNode.innerHTML;
524
- viewElement._setCustomProperty('$rawContent', rawContent);
525
- // Store a DOM node to prevent left trimming of the following text node.
526
- this._encounteredRawContentDomNodes.add(domNode);
527
- return viewElement;
528
- }
529
- }
530
- if (options.withChildren !== false) {
531
- for (const child of this.domChildrenToView(domNode, options)) {
532
- viewElement._appendChild(child);
533
- }
534
- }
535
- return viewElement;
536
- }
490
+ return node;
537
491
  }
538
492
  /**
539
493
  * Converts children of the DOM element to view nodes using
@@ -542,16 +496,27 @@ export default class DomConverter {
542
496
  *
543
497
  * @param domElement Parent DOM element.
544
498
  * @param options See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
499
+ * @param inlineNodes An array that will be populated with inline nodes. It's used internally for whitespace processing.
545
500
  * @returns View nodes.
546
501
  */
547
- *domChildrenToView(domElement, options) {
502
+ *domChildrenToView(domElement, options = {}, inlineNodes = []) {
548
503
  for (let i = 0; i < domElement.childNodes.length; i++) {
549
504
  const domChild = domElement.childNodes[i];
550
- const viewChild = this.domToView(domChild, options);
505
+ const generator = this._domToView(domChild, options, inlineNodes);
506
+ // Get the first yielded value or a returned value.
507
+ const viewChild = generator.next().value;
551
508
  if (viewChild !== null) {
509
+ // Whitespace cleaning before entering a block element (between block elements).
510
+ if (this._isBlockViewElement(viewChild)) {
511
+ this._processDomInlineNodes(domElement, inlineNodes, options);
512
+ }
552
513
  yield viewChild;
514
+ // Trigger children handling.
515
+ generator.next();
553
516
  }
554
517
  }
518
+ // Whitespace cleaning before leaving a block element (content of block element).
519
+ this._processDomInlineNodes(domElement, inlineNodes, options);
555
520
  }
556
521
  /**
557
522
  * Converts DOM selection to view {@link module:engine/view/selection~Selection}.
@@ -805,6 +770,24 @@ export default class DomConverter {
805
770
  global.window.scrollTo(scrollX, scrollY);
806
771
  }
807
772
  }
773
+ /**
774
+ * Remove DOM selection from blurred editable, so it won't interfere with clicking on dropdowns (especially on iOS).
775
+ *
776
+ * @internal
777
+ */
778
+ _clearDomSelection() {
779
+ const domEditable = this.mapViewToDom(this.document.selection.editableElement);
780
+ if (!domEditable) {
781
+ return;
782
+ }
783
+ // Check if DOM selection is inside editor editable element.
784
+ const domSelection = domEditable.ownerDocument.defaultView.getSelection();
785
+ const newViewSelection = this.domSelectionToView(domSelection);
786
+ const selectionInEditable = newViewSelection && newViewSelection.rangeCount > 0;
787
+ if (selectionInEditable) {
788
+ domSelection.removeAllRanges();
789
+ }
790
+ }
808
791
  /**
809
792
  * Returns `true` when `node.nodeType` equals `Node.ELEMENT_NODE`.
810
793
  *
@@ -925,6 +908,19 @@ export default class DomConverter {
925
908
  registerRawContentMatcher(pattern) {
926
909
  this._rawContentElementMatcher.add(pattern);
927
910
  }
911
+ /**
912
+ * Registers a {@link module:engine/view/matcher~MatcherPattern} for inline object view elements.
913
+ *
914
+ * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and
915
+ * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} process DOM nodes.
916
+ *
917
+ * This is an extension of a simple {@link #inlineObjectElements} array of element names.
918
+ *
919
+ * @param pattern Pattern matching a view element which should be treated as an inline object.
920
+ */
921
+ registerInlineObjectMatcher(pattern) {
922
+ this._inlineObjectElementMatcher.add(pattern);
923
+ }
928
924
  /**
929
925
  * Returns the block {@link module:engine/view/filler filler} node based on the current {@link #blockFillerMode} setting.
930
926
  */
@@ -964,6 +960,194 @@ export default class DomConverter {
964
960
  }
965
961
  return true;
966
962
  }
963
+ /**
964
+ * Internal generator for {@link #domToView}. Also used by {@link #domChildrenToView}.
965
+ * Separates DOM nodes conversion from whitespaces processing.
966
+ *
967
+ * @param domNode DOM node or document fragment to transform.
968
+ * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
969
+ * Used later to process whitespaces.
970
+ */
971
+ *_domToView(domNode, options, inlineNodes) {
972
+ if (this.isBlockFiller(domNode)) {
973
+ return null;
974
+ }
975
+ // When node is inside a UIElement or a RawElement return that parent as it's view representation.
976
+ const hostElement = this.getHostViewElement(domNode);
977
+ if (hostElement) {
978
+ return hostElement;
979
+ }
980
+ if (isComment(domNode) && options.skipComments) {
981
+ return null;
982
+ }
983
+ if (isText(domNode)) {
984
+ if (isInlineFiller(domNode)) {
985
+ return null;
986
+ }
987
+ else {
988
+ const textData = domNode.data;
989
+ if (textData === '') {
990
+ return null;
991
+ }
992
+ const textNode = new ViewText(this.document, textData);
993
+ inlineNodes.push(textNode);
994
+ return textNode;
995
+ }
996
+ }
997
+ else {
998
+ let viewElement = this.mapDomToView(domNode);
999
+ if (viewElement) {
1000
+ if (this._isInlineObjectElement(viewElement)) {
1001
+ inlineNodes.push(viewElement);
1002
+ }
1003
+ return viewElement;
1004
+ }
1005
+ if (this.isDocumentFragment(domNode)) {
1006
+ // Create view document fragment.
1007
+ viewElement = new ViewDocumentFragment(this.document);
1008
+ if (options.bind) {
1009
+ this.bindDocumentFragments(domNode, viewElement);
1010
+ }
1011
+ }
1012
+ else {
1013
+ // Create view element.
1014
+ viewElement = this._createViewElement(domNode, options);
1015
+ if (options.bind) {
1016
+ this.bindElements(domNode, viewElement);
1017
+ }
1018
+ // Copy element's attributes.
1019
+ const attrs = domNode.attributes;
1020
+ if (attrs) {
1021
+ for (let l = attrs.length, i = 0; i < l; i++) {
1022
+ viewElement._setAttribute(attrs[i].name, attrs[i].value);
1023
+ }
1024
+ }
1025
+ // Treat this element's content as a raw data if it was registered as such.
1026
+ if (this._isViewElementWithRawContent(viewElement, options)) {
1027
+ viewElement._setCustomProperty('$rawContent', domNode.innerHTML);
1028
+ if (!this._isBlockViewElement(viewElement)) {
1029
+ inlineNodes.push(viewElement);
1030
+ }
1031
+ return viewElement;
1032
+ }
1033
+ // Comment node is also treated as an element with raw data.
1034
+ if (isComment(domNode)) {
1035
+ viewElement._setCustomProperty('$rawContent', domNode.data);
1036
+ return viewElement;
1037
+ }
1038
+ }
1039
+ // Yield the element first so the flow of nested inline nodes is not reversed inside elements.
1040
+ yield viewElement;
1041
+ const nestedInlineNodes = [];
1042
+ if (options.withChildren !== false) {
1043
+ for (const child of this.domChildrenToView(domNode, options, nestedInlineNodes)) {
1044
+ viewElement._appendChild(child);
1045
+ }
1046
+ }
1047
+ // Check if this is an inline object after processing child nodes so matcher
1048
+ // for inline objects can verify if the element is empty.
1049
+ if (this._isInlineObjectElement(viewElement)) {
1050
+ inlineNodes.push(viewElement);
1051
+ }
1052
+ else {
1053
+ // It's an inline element that is not an object (like <b>, <i>) or a block element.
1054
+ for (const inlineNode of nestedInlineNodes) {
1055
+ inlineNodes.push(inlineNode);
1056
+ }
1057
+ }
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Internal helper that walks the list of inline view nodes already generated from DOM nodes
1062
+ * and handles whitespaces and NBSPs.
1063
+ *
1064
+ * @param domParent The DOM parent of the given inline nodes. This should be a document fragment or
1065
+ * a block element to whitespace processing start cleaning.
1066
+ * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
1067
+ */
1068
+ _processDomInlineNodes(domParent, inlineNodes, options) {
1069
+ if (!inlineNodes.length) {
1070
+ return;
1071
+ }
1072
+ // Process text nodes only after reaching a block or document fragment,
1073
+ // do not alter whitespaces while processing an inline element like <b> or <i>.
1074
+ if (domParent && !this.isDocumentFragment(domParent) && !this._isBlockDomElement(domParent)) {
1075
+ return;
1076
+ }
1077
+ let prevNodeEndsWithSpace = false;
1078
+ for (let i = 0; i < inlineNodes.length; i++) {
1079
+ const node = inlineNodes[i];
1080
+ if (!node.is('$text')) {
1081
+ prevNodeEndsWithSpace = false;
1082
+ continue;
1083
+ }
1084
+ let data;
1085
+ let nodeEndsWithSpace = false;
1086
+ if (_hasViewParentOfType(node, this.preElements)) {
1087
+ data = getDataWithoutFiller(node.data);
1088
+ }
1089
+ else {
1090
+ // Change all consecutive whitespace characters (from the [ \n\t\r] set –
1091
+ // see https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249) to a single space character.
1092
+ // That's how multiple whitespaces are treated when rendered, so we normalize those whitespaces.
1093
+ // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
1094
+ data = node.data.replace(/[ \n\t\r]{1,}/g, ' ');
1095
+ nodeEndsWithSpace = /[^\S\u00A0]/.test(data.charAt(data.length - 1));
1096
+ const prevNode = i > 0 ? inlineNodes[i - 1] : null;
1097
+ const nextNode = i + 1 < inlineNodes.length ? inlineNodes[i + 1] : null;
1098
+ const shouldLeftTrim = !prevNode || prevNode.is('element') && prevNode.name == 'br' || prevNodeEndsWithSpace;
1099
+ const shouldRightTrim = nextNode ? false : !startsWithFiller(node.data);
1100
+ // Do not try to clear whitespaces if this is flat mapping for the purpose of mutation observer and differ in rendering.
1101
+ if (options.withChildren !== false) {
1102
+ // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the
1103
+ // beginning of this text node. Such space character is treated as a whitespace.
1104
+ if (shouldLeftTrim) {
1105
+ data = data.replace(/^ /, '');
1106
+ }
1107
+ // If the next text node does not exist remove space character from the end of this text node.
1108
+ if (shouldRightTrim) {
1109
+ data = data.replace(/ $/, '');
1110
+ }
1111
+ }
1112
+ // At the beginning and end of a block element, Firefox inserts normal space + <br> instead of non-breaking space.
1113
+ // This means that the text node starts/end with normal space instead of non-breaking space.
1114
+ // This causes a problem because the normal space would be removed in `.replace` calls above. To prevent that,
1115
+ // the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
1116
+ data = getDataWithoutFiller(data);
1117
+ // At this point we should have removed all whitespaces from DOM text data.
1118
+ //
1119
+ // Now, We will reverse the process that happens in `_processDataFromViewText`.
1120
+ //
1121
+ // We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
1122
+ // First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
1123
+ // ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to ` `.
1124
+ data = data.replace(/ \u00A0/g, ' ');
1125
+ const isNextNodeInlineObjectElement = nextNode && nextNode.is('element') && nextNode.name != 'br';
1126
+ const isNextNodeStartingWithSpace = nextNode && nextNode.is('$text') && nextNode.data.charAt(0) == ' ';
1127
+ // Then, let's change the last nbsp to a space.
1128
+ if (/[ \u00A0]\u00A0$/.test(data) || !nextNode || isNextNodeInlineObjectElement || isNextNodeStartingWithSpace) {
1129
+ data = data.replace(/\u00A0$/, ' ');
1130
+ }
1131
+ // Then, change &nbsp; character that is at the beginning of the text node to space character.
1132
+ // We do that replacement only if this is the first node or the previous node ends on whitespace character.
1133
+ if (shouldLeftTrim || prevNode && prevNode.is('element') && prevNode.name != 'br') {
1134
+ data = data.replace(/^\u00A0/, ' ');
1135
+ }
1136
+ }
1137
+ // At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
1138
+ // changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
1139
+ if (data.length == 0 && node.parent) {
1140
+ node._remove();
1141
+ inlineNodes.splice(i, 1);
1142
+ i--;
1143
+ }
1144
+ else {
1145
+ node._data = data;
1146
+ prevNodeEndsWithSpace = nodeEndsWithSpace;
1147
+ }
1148
+ }
1149
+ inlineNodes.length = 0;
1150
+ }
967
1151
  /**
968
1152
  * Takes text data from a given {@link module:engine/view/text~Text#data} and processes it so
969
1153
  * it is correctly displayed in the DOM.
@@ -1029,103 +1213,6 @@ export default class DomConverter {
1029
1213
  const data = this._processDataFromViewText(node);
1030
1214
  return data.charAt(data.length - 1) == ' ';
1031
1215
  }
1032
- /**
1033
- * Takes text data from native `Text` node and processes it to a correct {@link module:engine/view/text~Text view text node} data.
1034
- *
1035
- * Following changes are done:
1036
- *
1037
- * * multiple whitespaces are replaced to a single space,
1038
- * * space at the beginning of a text node is removed if it is the first text node in its container
1039
- * element or if the previous text node ends with a space character,
1040
- * * space at the end of the text node is removed if there are two spaces at the end of a node or if next node
1041
- * starts with a space or if it is the last text node in its container
1042
- * * nbsps are converted to spaces.
1043
- *
1044
- * @param node DOM text node to process.
1045
- * @returns Processed data.
1046
- */
1047
- _processDataFromDomText(node) {
1048
- let data = node.data;
1049
- if (_hasDomParentOfType(node, this.preElements)) {
1050
- return getDataWithoutFiller(node);
1051
- }
1052
- // Change all consecutive whitespace characters (from the [ \n\t\r] set –
1053
- // see https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249) to a single space character.
1054
- // That's how multiple whitespaces are treated when rendered, so we normalize those whitespaces.
1055
- // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
1056
- data = data.replace(/[ \n\t\r]{1,}/g, ' ');
1057
- const prevNode = this._getTouchingInlineDomNode(node, false);
1058
- const nextNode = this._getTouchingInlineDomNode(node, true);
1059
- const shouldLeftTrim = this._checkShouldLeftTrimDomText(node, prevNode);
1060
- const shouldRightTrim = this._checkShouldRightTrimDomText(node, nextNode);
1061
- // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning
1062
- // of this text node. Such space character is treated as a whitespace.
1063
- if (shouldLeftTrim) {
1064
- data = data.replace(/^ /, '');
1065
- }
1066
- // If the next text node does not exist remove space character from the end of this text node.
1067
- if (shouldRightTrim) {
1068
- data = data.replace(/ $/, '');
1069
- }
1070
- // At the beginning and end of a block element, Firefox inserts normal space + <br> instead of non-breaking space.
1071
- // This means that the text node starts/end with normal space instead of non-breaking space.
1072
- // This causes a problem because the normal space would be removed in `.replace` calls above. To prevent that,
1073
- // the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
1074
- data = getDataWithoutFiller(new Text(data));
1075
- // At this point we should have removed all whitespaces from DOM text data.
1076
- //
1077
- // Now, We will reverse the process that happens in `_processDataFromViewText`.
1078
- //
1079
- // We have to change &nbsp; chars, that were in DOM text data because of rendering reasons, to spaces.
1080
- // First, change all ` \u00A0` pairs (space + &nbsp;) to two spaces. DOM converter changes two spaces from model/view to
1081
- // ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them back to ` `.
1082
- data = data.replace(/ \u00A0/g, ' ');
1083
- const isNextNodeInlineObjectElement = nextNode && this.isElement(nextNode) && nextNode.tagName != 'BR';
1084
- const isNextNodeStartingWithSpace = nextNode && isText(nextNode) && nextNode.data.charAt(0) == ' ';
1085
- // Then, let's change the last nbsp to a space.
1086
- if (/( |\u00A0)\u00A0$/.test(data) || !nextNode || isNextNodeInlineObjectElement || isNextNodeStartingWithSpace) {
1087
- data = data.replace(/\u00A0$/, ' ');
1088
- }
1089
- // Then, change &nbsp; character that is at the beginning of the text node to space character.
1090
- // We do that replacement only if this is the first node or the previous node ends on whitespace character.
1091
- if (shouldLeftTrim || prevNode && this.isElement(prevNode) && prevNode.tagName != 'BR') {
1092
- data = data.replace(/^\u00A0/, ' ');
1093
- }
1094
- // At this point, all whitespaces should be removed and all &nbsp; created for rendering reasons should be
1095
- // changed to normal space. All left &nbsp; are &nbsp; inserted intentionally.
1096
- return data;
1097
- }
1098
- /**
1099
- * Helper function which checks if a DOM text node, preceded by the given `prevNode` should
1100
- * be trimmed from the left side.
1101
- *
1102
- * @param prevNode Either DOM text or `<br>` or one of `#inlineObjectElements`.
1103
- */
1104
- _checkShouldLeftTrimDomText(node, prevNode) {
1105
- if (!prevNode) {
1106
- return true;
1107
- }
1108
- if (this.isElement(prevNode)) {
1109
- return prevNode.tagName === 'BR';
1110
- }
1111
- // Shouldn't left trim if previous node is a node that was encountered as a raw content node.
1112
- if (this._encounteredRawContentDomNodes.has(node.previousSibling)) {
1113
- return false;
1114
- }
1115
- return /[^\S\u00A0]/.test(prevNode.data.charAt(prevNode.data.length - 1));
1116
- }
1117
- /**
1118
- * Helper function which checks if a DOM text node, succeeded by the given `nextNode` should
1119
- * be trimmed from the right side.
1120
- *
1121
- * @param nextNode Either DOM text or `<br>` or one of `#inlineObjectElements`.
1122
- */
1123
- _checkShouldRightTrimDomText(node, nextNode) {
1124
- if (nextNode) {
1125
- return false;
1126
- }
1127
- return !startsWithFiller(node);
1128
- }
1129
1216
  /**
1130
1217
  * Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling
1131
1218
  * that is contained in the same container element. If there is no such sibling, `null` is returned.
@@ -1140,8 +1227,12 @@ export default class DomConverter {
1140
1227
  direction: getNext ? 'forward' : 'backward'
1141
1228
  });
1142
1229
  for (const value of treeWalker) {
1230
+ // <br> found – it works like a block boundary, so do not scan further.
1231
+ if (value.item.is('element', 'br')) {
1232
+ return null;
1233
+ }
1143
1234
  // Found an inline object (for example an image).
1144
- if (value.item.is('element') && this.inlineObjectElements.includes(value.item.name)) {
1235
+ else if (this._isInlineObjectElement(value.item)) {
1145
1236
  return value.item;
1146
1237
  }
1147
1238
  // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
@@ -1149,10 +1240,6 @@ export default class DomConverter {
1149
1240
  else if (value.item.is('containerElement')) {
1150
1241
  return null;
1151
1242
  }
1152
- // <br> found – it works like a block boundary, so do not scan further.
1153
- else if (value.item.is('element', 'br')) {
1154
- return null;
1155
- }
1156
1243
  // Found a text node in the same container element.
1157
1244
  else if (value.item.is('$textProxy')) {
1158
1245
  return value.item;
@@ -1161,61 +1248,27 @@ export default class DomConverter {
1161
1248
  return null;
1162
1249
  }
1163
1250
  /**
1164
- * Helper function. For the given text node, it finds the closest touching node which is either
1165
- * a text, `<br>` or an {@link #inlineObjectElements inline object}.
1166
- *
1167
- * If no such node is found, `null` is returned.
1168
- *
1169
- * For instance, in the following DOM structure:
1170
- *
1171
- * ```html
1172
- * <p>foo<b>bar</b><br>bom</p>
1173
- * ```
1174
- *
1175
- * * `foo` doesn't have its previous touching inline node (`null` is returned),
1176
- * * `foo`'s next touching inline node is `bar`
1177
- * * `bar`'s next touching inline node is `<br>`
1178
- *
1179
- * This method returns text nodes and `<br>` elements because these types of nodes affect how
1180
- * spaces in the given text node need to be converted.
1251
+ * Returns `true` if a DOM node belongs to {@link #blockElements}. `false` otherwise.
1181
1252
  */
1182
- _getTouchingInlineDomNode(node, getNext) {
1183
- if (!node.parentNode) {
1184
- return null;
1185
- }
1186
- const stepInto = getNext ? 'firstChild' : 'lastChild';
1187
- const stepOver = getNext ? 'nextSibling' : 'previousSibling';
1188
- let skipChildren = true;
1189
- let returnNode = node;
1190
- do {
1191
- if (!skipChildren && returnNode[stepInto]) {
1192
- returnNode = returnNode[stepInto];
1193
- }
1194
- else if (returnNode[stepOver]) {
1195
- returnNode = returnNode[stepOver];
1196
- skipChildren = false;
1197
- }
1198
- else {
1199
- returnNode = returnNode.parentNode;
1200
- skipChildren = true;
1201
- }
1202
- if (!returnNode || this._isBlockElement(returnNode)) {
1203
- return null;
1204
- }
1205
- } while (!(isText(returnNode) || returnNode.tagName == 'BR' || this._isInlineObjectElement(returnNode)));
1206
- return returnNode;
1253
+ _isBlockDomElement(node) {
1254
+ return this.isElement(node) && this.blockElements.includes(node.tagName.toLowerCase());
1207
1255
  }
1208
1256
  /**
1209
- * Returns `true` if a DOM node belongs to {@link #blockElements}. `false` otherwise.
1257
+ * Returns `true` if a view node belongs to {@link #blockElements}. `false` otherwise.
1210
1258
  */
1211
- _isBlockElement(node) {
1212
- return this.isElement(node) && this.blockElements.includes(node.tagName.toLowerCase());
1259
+ _isBlockViewElement(node) {
1260
+ return node.is('element') && this.blockElements.includes(node.name);
1213
1261
  }
1214
1262
  /**
1215
1263
  * Returns `true` if a DOM node belongs to {@link #inlineObjectElements}. `false` otherwise.
1216
1264
  */
1217
1265
  _isInlineObjectElement(node) {
1218
- return this.isElement(node) && this.inlineObjectElements.includes(node.tagName.toLowerCase());
1266
+ if (!node.is('element')) {
1267
+ return false;
1268
+ }
1269
+ return node.name == 'br' ||
1270
+ this.inlineObjectElements.includes(node.name) ||
1271
+ !!this._inlineObjectElementMatcher.match(node);
1219
1272
  }
1220
1273
  /**
1221
1274
  * Creates view element basing on the node type.
@@ -1237,7 +1290,7 @@ export default class DomConverter {
1237
1290
  * @param options Conversion options. See {@link module:engine/view/domconverter~DomConverter#domToView} options parameter.
1238
1291
  */
1239
1292
  _isViewElementWithRawContent(viewElement, options) {
1240
- return options.withChildren !== false && !!this._rawContentElementMatcher.match(viewElement);
1293
+ return options.withChildren !== false && viewElement.is('element') && !!this._rawContentElementMatcher.match(viewElement);
1241
1294
  }
1242
1295
  /**
1243
1296
  * Checks whether a given element name should be renamed in a current rendering mode.
@@ -1276,9 +1329,8 @@ export default class DomConverter {
1276
1329
  *
1277
1330
  * @returns`true` if such parent exists or `false` if it does not.
1278
1331
  */
1279
- function _hasDomParentOfType(node, types) {
1280
- const parents = getAncestors(node);
1281
- return parents.some(parent => parent.tagName && types.includes(parent.tagName.toLowerCase()));
1332
+ function _hasViewParentOfType(node, types) {
1333
+ return node.getAncestors().some(parent => parent.is('element') && types.includes(parent.name));
1282
1334
  }
1283
1335
  /**
1284
1336
  * A helper that executes given callback for each DOM node's ancestor, starting from the given node
@@ -36,6 +36,16 @@ export default class EditableElement extends EditableElement_base {
36
36
  * @observable
37
37
  */
38
38
  isFocused: boolean;
39
+ /**
40
+ * Placeholder of editable element.
41
+ *
42
+ * ```ts
43
+ * editor.editing.view.document.getRoot( 'main' ).placeholder = 'New placeholder';
44
+ * ```
45
+ *
46
+ * @observable
47
+ */
48
+ placeholder?: string;
39
49
  /**
40
50
  * Creates an editable element.
41
51
  *
@@ -31,6 +31,7 @@ export default class EditableElement extends ObservableMixin(ContainerElement) {
31
31
  super(document, name, attributes, children);
32
32
  this.set('isReadOnly', false);
33
33
  this.set('isFocused', false);
34
+ this.set('placeholder', undefined);
34
35
  this.bind('isReadOnly').to(document);
35
36
  this.bind('isFocused').to(document, 'isFocused', isFocused => isFocused && document.selection.editableElement == this);
36
37
  // Update focus state based on selection changes.
@@ -76,7 +76,7 @@ export declare const INLINE_FILLER: string;
76
76
  * @param domNode DOM node.
77
77
  * @returns True if the text node starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
78
78
  */
79
- export declare function startsWithFiller(domNode: Node): boolean;
79
+ export declare function startsWithFiller(domNode: Node | string): boolean;
80
80
  /**
81
81
  * Checks if the text node contains only the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
82
82
  *
@@ -101,7 +101,7 @@ export declare function isInlineFiller(domText: Text): boolean;
101
101
  * @param domText DOM text node, possible with inline filler.
102
102
  * @returns Data without filler.
103
103
  */
104
- export declare function getDataWithoutFiller(domText: Text): string;
104
+ export declare function getDataWithoutFiller(domText: Text | string): string;
105
105
  /**
106
106
  * Assign key observer which move cursor from the end of the inline filler to the beginning of it when
107
107
  * the left arrow is pressed, so the filler does not break navigation.
@@ -86,6 +86,9 @@ export const INLINE_FILLER = '\u2060'.repeat(INLINE_FILLER_LENGTH);
86
86
  * @returns True if the text node starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}.
87
87
  */
88
88
  export function startsWithFiller(domNode) {
89
+ if (typeof domNode == 'string') {
90
+ return domNode.substr(0, INLINE_FILLER_LENGTH) === INLINE_FILLER;
91
+ }
89
92
  return isText(domNode) && (domNode.data.substr(0, INLINE_FILLER_LENGTH) === INLINE_FILLER);
90
93
  }
91
94
  /**
@@ -115,12 +118,11 @@ export function isInlineFiller(domText) {
115
118
  * @returns Data without filler.
116
119
  */
117
120
  export function getDataWithoutFiller(domText) {
121
+ const data = typeof domText == 'string' ? domText : domText.data;
118
122
  if (startsWithFiller(domText)) {
119
- return domText.data.slice(INLINE_FILLER_LENGTH);
120
- }
121
- else {
122
- return domText.data;
123
+ return data.slice(INLINE_FILLER_LENGTH);
123
124
  }
125
+ return data;
124
126
  }
125
127
  /**
126
128
  * Assign key observer which move cursor from the end of the inline filler to the beginning of it when