@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.
- package/README.md +0 -1
- package/package.json +3 -4
- package/src/controller/editingcontroller.js +2 -2
- package/src/conversion/downcastdispatcher.d.ts +15 -0
- package/src/conversion/downcastdispatcher.js +28 -19
- package/src/conversion/downcasthelpers.d.ts +6 -6
- package/src/conversion/downcasthelpers.js +6 -6
- package/src/dev-utils/view.js +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/model/differ.d.ts +14 -0
- package/src/model/differ.js +70 -11
- package/src/model/document.d.ts +9 -1
- package/src/model/document.js +14 -9
- package/src/model/documentselection.js +8 -2
- package/src/model/model.d.ts +0 -1
- package/src/model/model.js +0 -1
- package/src/model/operation/rootoperation.d.ts +0 -4
- package/src/model/operation/rootoperation.js +0 -24
- package/src/model/operation/transform.js +2 -2
- package/src/model/rootelement.d.ts +6 -0
- package/src/model/rootelement.js +6 -0
- package/src/model/schema.d.ts +10 -0
- package/src/model/schema.js +5 -0
- package/src/model/utils/autoparagraphing.js +1 -2
- package/src/view/domconverter.d.ts +43 -53
- package/src/view/domconverter.js +266 -214
- package/src/view/editableelement.d.ts +10 -0
- package/src/view/editableelement.js +1 -0
- package/src/view/filler.d.ts +2 -2
- package/src/view/filler.js +6 -4
- package/src/view/observer/selectionobserver.js +2 -2
- package/src/view/placeholder.d.ts +13 -5
- package/src/view/placeholder.js +21 -12
- package/src/view/renderer.js +1 -2
- package/src/view/view.d.ts +14 -7
- package/src/view/view.js +13 -1
package/src/view/domconverter.js
CHANGED
|
@@ -67,9 +67,9 @@ export default class DomConverter {
|
|
|
67
67
|
*/
|
|
68
68
|
this._rawContentElementMatcher = new Matcher();
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
70
|
+
* Matcher for inline object view elements. This is an extension of a simple {@link #inlineObjectElements} array of element names.
|
|
71
71
|
*/
|
|
72
|
-
this.
|
|
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
|
-
|
|
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
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
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
|
-
|
|
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
|
|
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 chars, that were in DOM text data because of rendering reasons, to spaces.
|
|
1122
|
+
// First, change all ` \u00A0` pairs (space + ) 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 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 created for rendering reasons should be
|
|
1138
|
+
// changed to normal space. All left are 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 chars, that were in DOM text data because of rendering reasons, to spaces.
|
|
1080
|
-
// First, change all ` \u00A0` pairs (space + ) 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 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 created for rendering reasons should be
|
|
1095
|
-
// changed to normal space. All left are 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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
1183
|
-
|
|
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
|
|
1257
|
+
* Returns `true` if a view node belongs to {@link #blockElements}. `false` otherwise.
|
|
1210
1258
|
*/
|
|
1211
|
-
|
|
1212
|
-
return
|
|
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
|
-
|
|
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
|
|
1280
|
-
|
|
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.
|
package/src/view/filler.d.ts
CHANGED
|
@@ -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.
|
package/src/view/filler.js
CHANGED
|
@@ -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
|
|
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
|