|
@@ -32,6 +32,15 @@ export default class Writer {
|
|
|
* @type {module:engine/view/document~Document}
|
|
* @type {module:engine/view/document~Document}
|
|
|
*/
|
|
*/
|
|
|
this.document = document;
|
|
this.document = document;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
|
|
|
+ * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @type {Map}
|
|
|
|
|
+ */
|
|
|
|
|
+ this._cloneGroups = new Map();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -135,15 +144,28 @@ export default class Writer {
|
|
|
* writer.createAttributeElement( 'strong' );
|
|
* writer.createAttributeElement( 'strong' );
|
|
|
* writer.createAttributeElement( 'strong', { 'alignment': 'center' } );
|
|
* writer.createAttributeElement( 'strong', { 'alignment': 'center' } );
|
|
|
*
|
|
*
|
|
|
|
|
+ * // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
|
|
|
|
|
+ * writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
|
|
|
|
|
+ *
|
|
|
|
|
+ * // Set `id` of a marker element so it is not joined or merged with "normal" elements.
|
|
|
|
|
+ * writer.createAttributeElement( 'span', { class: 'myMarker' }, { id: 'marker:my' } );
|
|
|
|
|
+ *
|
|
|
* @param {String} name Name of the element.
|
|
* @param {String} name Name of the element.
|
|
|
- * @param {Object} [attributes] Elements attributes.
|
|
|
|
|
|
|
+ * @param {Object} [attributes] Element's attributes.
|
|
|
|
|
+ * @param {Object} [options] Element's options.
|
|
|
|
|
+ * @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
|
|
|
|
|
+ * @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
|
* @returns {module:engine/view/attributeelement~AttributeElement} Created element.
|
|
* @returns {module:engine/view/attributeelement~AttributeElement} Created element.
|
|
|
*/
|
|
*/
|
|
|
- createAttributeElement( name, attributes, priority ) {
|
|
|
|
|
|
|
+ createAttributeElement( name, attributes, options = {} ) {
|
|
|
const attributeElement = new AttributeElement( name, attributes );
|
|
const attributeElement = new AttributeElement( name, attributes );
|
|
|
|
|
|
|
|
- if ( priority ) {
|
|
|
|
|
- attributeElement._priority = priority;
|
|
|
|
|
|
|
+ if ( options.priority ) {
|
|
|
|
|
+ attributeElement._priority = options.priority;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ( options.id ) {
|
|
|
|
|
+ attributeElement._id = options.id;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return attributeElement;
|
|
return attributeElement;
|
|
@@ -166,10 +188,9 @@ export default class Writer {
|
|
|
/**
|
|
/**
|
|
|
* Creates new {@link module:engine/view/editableelement~EditableElement}.
|
|
* Creates new {@link module:engine/view/editableelement~EditableElement}.
|
|
|
*
|
|
*
|
|
|
- * writer.createEditableElement( document, 'div' );
|
|
|
|
|
- * writer.createEditableElement( document, 'div', { 'alignment': 'center' } );
|
|
|
|
|
|
|
+ * writer.createEditableElement( 'div' );
|
|
|
|
|
+ * writer.createEditableElement( 'div', { 'alignment': 'center' } );
|
|
|
*
|
|
*
|
|
|
- * @param {module:engine/view/document~Document} document View document.
|
|
|
|
|
* @param {String} name Name of the element.
|
|
* @param {String} name Name of the element.
|
|
|
* @param {Object} [attributes] Elements attributes.
|
|
* @param {Object} [attributes] Elements attributes.
|
|
|
* @returns {module:engine/view/editableelement~EditableElement} Created element.
|
|
* @returns {module:engine/view/editableelement~EditableElement} Created element.
|
|
@@ -384,9 +405,9 @@ export default class Writer {
|
|
|
*/
|
|
*/
|
|
|
breakAttributes( positionOrRange ) {
|
|
breakAttributes( positionOrRange ) {
|
|
|
if ( positionOrRange instanceof Position ) {
|
|
if ( positionOrRange instanceof Position ) {
|
|
|
- return _breakAttributes( positionOrRange );
|
|
|
|
|
|
|
+ return this._breakAttributes( positionOrRange );
|
|
|
} else {
|
|
} else {
|
|
|
- return _breakAttributesRange( positionOrRange );
|
|
|
|
|
|
|
+ return this._breakAttributesRange( positionOrRange );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -491,7 +512,9 @@ export default class Writer {
|
|
|
if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
|
|
if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
|
|
|
const parent = positionParent.parent;
|
|
const parent = positionParent.parent;
|
|
|
const offset = positionParent.index;
|
|
const offset = positionParent.index;
|
|
|
|
|
+
|
|
|
positionParent._remove();
|
|
positionParent._remove();
|
|
|
|
|
+ this._removeFromClonedElementsGroup( positionParent );
|
|
|
|
|
|
|
|
return this.mergeAttributes( new Position( parent, offset ) );
|
|
return this.mergeAttributes( new Position( parent, offset ) );
|
|
|
}
|
|
}
|
|
@@ -508,12 +531,14 @@ export default class Writer {
|
|
|
if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
|
|
if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
|
|
|
return mergeTextNodes( nodeBefore, nodeAfter );
|
|
return mergeTextNodes( nodeBefore, nodeAfter );
|
|
|
}
|
|
}
|
|
|
- // When selection is between two same attribute elements.
|
|
|
|
|
|
|
+ // When position is between two same attribute elements.
|
|
|
else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
|
|
else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
|
|
|
// Move all children nodes from node placed after selection and remove that node.
|
|
// Move all children nodes from node placed after selection and remove that node.
|
|
|
const count = nodeBefore.childCount;
|
|
const count = nodeBefore.childCount;
|
|
|
nodeBefore._appendChildren( nodeAfter.getChildren() );
|
|
nodeBefore._appendChildren( nodeAfter.getChildren() );
|
|
|
|
|
+
|
|
|
nodeAfter._remove();
|
|
nodeAfter._remove();
|
|
|
|
|
+ this._removeFromClonedElementsGroup( nodeAfter );
|
|
|
|
|
|
|
|
// New position is located inside the first node, before new nodes.
|
|
// New position is located inside the first node, before new nodes.
|
|
|
// Call this method recursively to merge again if needed.
|
|
// Call this method recursively to merge again if needed.
|
|
@@ -600,9 +625,13 @@ export default class Writer {
|
|
|
throw new CKEditorError( 'view-writer-invalid-position-container' );
|
|
throw new CKEditorError( 'view-writer-invalid-position-container' );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const insertionPosition = _breakAttributes( position, true );
|
|
|
|
|
-
|
|
|
|
|
|
|
+ const insertionPosition = this._breakAttributes( position, true );
|
|
|
const length = container._insertChildren( insertionPosition.offset, nodes );
|
|
const length = container._insertChildren( insertionPosition.offset, nodes );
|
|
|
|
|
+
|
|
|
|
|
+ for ( const node of nodes ) {
|
|
|
|
|
+ this._addToClonedElementsGroup( node );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const endPosition = insertionPosition.getShiftedBy( length );
|
|
const endPosition = insertionPosition.getShiftedBy( length );
|
|
|
const start = this.mergeAttributes( insertionPosition );
|
|
const start = this.mergeAttributes( insertionPosition );
|
|
|
|
|
|
|
@@ -641,7 +670,7 @@ export default class Writer {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Break attributes at range start and end.
|
|
// Break attributes at range start and end.
|
|
|
- const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
|
|
|
|
|
|
|
+ const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
|
|
|
const parentContainer = breakStart.parent;
|
|
const parentContainer = breakStart.parent;
|
|
|
|
|
|
|
|
const count = breakEnd.offset - breakStart.offset;
|
|
const count = breakEnd.offset - breakStart.offset;
|
|
@@ -649,6 +678,10 @@ export default class Writer {
|
|
|
// Remove nodes in range.
|
|
// Remove nodes in range.
|
|
|
const removed = parentContainer._removeChildren( breakStart.offset, count );
|
|
const removed = parentContainer._removeChildren( breakStart.offset, count );
|
|
|
|
|
|
|
|
|
|
+ for ( const node of removed ) {
|
|
|
|
|
+ this._removeFromClonedElementsGroup( node );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Merge after removing.
|
|
// Merge after removing.
|
|
|
const mergePosition = this.mergeAttributes( breakStart );
|
|
const mergePosition = this.mergeAttributes( breakStart );
|
|
|
range.start = mergePosition;
|
|
range.start = mergePosition;
|
|
@@ -733,12 +766,12 @@ export default class Writer {
|
|
|
let nodes;
|
|
let nodes;
|
|
|
|
|
|
|
|
if ( targetPosition.isAfter( sourceRange.end ) ) {
|
|
if ( targetPosition.isAfter( sourceRange.end ) ) {
|
|
|
- targetPosition = _breakAttributes( targetPosition, true );
|
|
|
|
|
|
|
+ targetPosition = this._breakAttributes( targetPosition, true );
|
|
|
|
|
|
|
|
const parent = targetPosition.parent;
|
|
const parent = targetPosition.parent;
|
|
|
const countBefore = parent.childCount;
|
|
const countBefore = parent.childCount;
|
|
|
|
|
|
|
|
- sourceRange = _breakAttributesRange( sourceRange, true );
|
|
|
|
|
|
|
+ sourceRange = this._breakAttributesRange( sourceRange, true );
|
|
|
|
|
|
|
|
nodes = this.remove( sourceRange );
|
|
nodes = this.remove( sourceRange );
|
|
|
|
|
|
|
@@ -829,7 +862,7 @@ export default class Writer {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Break attributes at range start and end.
|
|
// Break attributes at range start and end.
|
|
|
- const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
|
|
|
|
|
|
|
+ const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
|
|
|
|
|
|
|
|
// Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
|
|
// Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
|
|
|
// For example:
|
|
// For example:
|
|
@@ -916,8 +949,9 @@ export default class Writer {
|
|
|
if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
|
|
if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
|
|
|
// Clone attribute.
|
|
// Clone attribute.
|
|
|
const newAttribute = attribute._clone();
|
|
const newAttribute = attribute._clone();
|
|
|
|
|
+ this._addToClonedElementsGroup( newAttribute );
|
|
|
|
|
|
|
|
- // Wrap current node with new attribute;
|
|
|
|
|
|
|
+ // Wrap current node with new attribute.
|
|
|
child._remove();
|
|
child._remove();
|
|
|
newAttribute._appendChildren( child );
|
|
newAttribute._appendChildren( child );
|
|
|
parent._insertChildren( i, newAttribute );
|
|
parent._insertChildren( i, newAttribute );
|
|
@@ -980,6 +1014,8 @@ export default class Writer {
|
|
|
|
|
|
|
|
// Replace wrapper element with its children
|
|
// Replace wrapper element with its children
|
|
|
child._remove();
|
|
child._remove();
|
|
|
|
|
+ this._removeFromClonedElementsGroup( child );
|
|
|
|
|
+
|
|
|
parent._insertChildren( i, unwrapped );
|
|
parent._insertChildren( i, unwrapped );
|
|
|
|
|
|
|
|
// Save start and end position of moved items.
|
|
// Save start and end position of moved items.
|
|
@@ -1049,7 +1085,7 @@ export default class Writer {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Break attributes at range start and end.
|
|
// Break attributes at range start and end.
|
|
|
- const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
|
|
|
|
|
|
|
+ const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
|
|
|
|
|
|
|
|
// Range around one element.
|
|
// Range around one element.
|
|
|
if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
|
|
if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
|
|
@@ -1152,6 +1188,10 @@ export default class Writer {
|
|
|
* @returns {Boolean} Returns `true` if elements are merged.
|
|
* @returns {Boolean} Returns `true` if elements are merged.
|
|
|
*/
|
|
*/
|
|
|
_wrapAttributeElement( wrapper, toWrap ) {
|
|
_wrapAttributeElement( wrapper, toWrap ) {
|
|
|
|
|
+ if ( !canBeJoined( wrapper, toWrap ) ) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Can't merge if name or priority differs.
|
|
// Can't merge if name or priority differs.
|
|
|
if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
|
|
if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
|
|
|
return false;
|
|
return false;
|
|
@@ -1216,6 +1256,10 @@ export default class Writer {
|
|
|
* @returns {Boolean} Returns `true` if elements are unwrapped.
|
|
* @returns {Boolean} Returns `true` if elements are unwrapped.
|
|
|
**/
|
|
**/
|
|
|
_unwrapAttributeElement( wrapper, toUnwrap ) {
|
|
_unwrapAttributeElement( wrapper, toUnwrap ) {
|
|
|
|
|
+ if ( !canBeJoined( wrapper, toUnwrap ) ) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Can't unwrap if name or priority differs.
|
|
// Can't unwrap if name or priority differs.
|
|
|
if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
|
|
if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
|
|
|
return false;
|
|
return false;
|
|
@@ -1265,6 +1309,199 @@ export default class Writer {
|
|
|
|
|
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Helper function used by other `Writer` methods. Breaks attribute elements at the boundaries of given range.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
|
|
|
|
|
+ * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
|
|
|
|
|
+ * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
|
|
|
|
|
+ * @returns {module:engine/view/range~Range} New range with located at break positions.
|
|
|
|
|
+ */
|
|
|
|
|
+ _breakAttributesRange( range, forceSplitText = false ) {
|
|
|
|
|
+ const rangeStart = range.start;
|
|
|
|
|
+ const rangeEnd = range.end;
|
|
|
|
|
+
|
|
|
|
|
+ validateRangeContainer( range );
|
|
|
|
|
+
|
|
|
|
|
+ // Break at the collapsed position. Return new collapsed range.
|
|
|
|
|
+ if ( range.isCollapsed ) {
|
|
|
|
|
+ const position = this._breakAttributes( range.start, forceSplitText );
|
|
|
|
|
+
|
|
|
|
|
+ return new Range( position, position );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const breakEnd = this._breakAttributes( rangeEnd, forceSplitText );
|
|
|
|
|
+ const count = breakEnd.parent.childCount;
|
|
|
|
|
+ const breakStart = this._breakAttributes( rangeStart, forceSplitText );
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate new break end offset.
|
|
|
|
|
+ breakEnd.offset += breakEnd.parent.childCount - count;
|
|
|
|
|
+
|
|
|
|
|
+ return new Range( breakStart, breakEnd );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Helper function used by other `Writer` methods. Breaks attribute elements at given position.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
|
|
|
|
|
+ * is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
|
|
|
|
|
+ * is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @param {module:engine/view/position~Position} position Position where to break attributes.
|
|
|
|
|
+ * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
|
|
|
|
|
+ * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
|
|
|
|
|
+ * @returns {module:engine/view/position~Position} New position after breaking the attributes.
|
|
|
|
|
+ */
|
|
|
|
|
+ _breakAttributes( position, forceSplitText = false ) {
|
|
|
|
|
+ const positionOffset = position.offset;
|
|
|
|
|
+ const positionParent = position.parent;
|
|
|
|
|
+
|
|
|
|
|
+ // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
|
|
|
|
|
+ if ( position.parent.is( 'emptyElement' ) ) {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Cannot break inside EmptyElement instance.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @error view-writer-cannot-break-empty-element
|
|
|
|
|
+ */
|
|
|
|
|
+ throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // If position is placed inside UIElement - throw an exception as we cannot break inside.
|
|
|
|
|
+ if ( position.parent.is( 'uiElement' ) ) {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Cannot break inside UIElement instance.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @error view-writer-cannot-break-ui-element
|
|
|
|
|
+ */
|
|
|
|
|
+ throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // There are no attributes to break and text nodes breaking is not forced.
|
|
|
|
|
+ if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
|
|
|
|
|
+ return Position.createFromPosition( position );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Position's parent is container, so no attributes to break.
|
|
|
|
|
+ if ( isContainerOrFragment( positionParent ) ) {
|
|
|
|
|
+ return Position.createFromPosition( position );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Break text and start again in new position.
|
|
|
|
|
+ if ( positionParent.is( 'text' ) ) {
|
|
|
|
|
+ return this._breakAttributes( breakTextNode( position ), forceSplitText );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const length = positionParent.childCount;
|
|
|
|
|
+
|
|
|
|
|
+ // <p>foo<b><u>bar{}</u></b></p>
|
|
|
|
|
+ // <p>foo<b><u>bar</u>[]</b></p>
|
|
|
|
|
+ // <p>foo<b><u>bar</u></b>[]</p>
|
|
|
|
|
+ if ( positionOffset == length ) {
|
|
|
|
|
+ const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
|
|
|
|
|
+
|
|
|
|
|
+ return this._breakAttributes( newPosition, forceSplitText );
|
|
|
|
|
+ } else
|
|
|
|
|
+ // <p>foo<b><u>{}bar</u></b></p>
|
|
|
|
|
+ // <p>foo<b>[]<u>bar</u></b></p>
|
|
|
|
|
+ // <p>foo{}<b><u>bar</u></b></p>
|
|
|
|
|
+ if ( positionOffset === 0 ) {
|
|
|
|
|
+ const newPosition = new Position( positionParent.parent, positionParent.index );
|
|
|
|
|
+
|
|
|
|
|
+ return this._breakAttributes( newPosition, forceSplitText );
|
|
|
|
|
+ }
|
|
|
|
|
+ // <p>foo<b><u>b{}ar</u></b></p>
|
|
|
|
|
+ // <p>foo<b><u>b[]ar</u></b></p>
|
|
|
|
|
+ // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
|
|
|
|
|
+ // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
|
|
|
|
|
+ else {
|
|
|
|
|
+ const offsetAfter = positionParent.index + 1;
|
|
|
|
|
+
|
|
|
|
|
+ // Break element.
|
|
|
|
|
+ const clonedNode = positionParent._clone();
|
|
|
|
|
+ this._addToClonedElementsGroup( clonedNode );
|
|
|
|
|
+
|
|
|
|
|
+ // Insert cloned node to position's parent node.
|
|
|
|
|
+ positionParent.parent._insertChildren( offsetAfter, clonedNode );
|
|
|
|
|
+
|
|
|
|
|
+ // Get nodes to move.
|
|
|
|
|
+ const count = positionParent.childCount - positionOffset;
|
|
|
|
|
+ const nodesToMove = positionParent._removeChildren( positionOffset, count );
|
|
|
|
|
+
|
|
|
|
|
+ // Move nodes to cloned node.
|
|
|
|
|
+ clonedNode._appendChildren( nodesToMove );
|
|
|
|
|
+
|
|
|
|
|
+ // Create new position to work on.
|
|
|
|
|
+ const newPosition = new Position( positionParent.parent, offsetAfter );
|
|
|
|
|
+
|
|
|
|
|
+ return this._breakAttributes( newPosition, forceSplitText );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Stores the information that an {@link module:engine/view/attributeelement~AttributeElement attribute element} was
|
|
|
|
|
+ * added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
|
|
|
|
|
+ * from the group now keep a reference to the given attribute element.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The clones group can be obtained using {@link module:engine/view/attributeelement~AttributeElement#getElementsWithSameId}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Does nothing if added element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
|
|
|
|
|
+ */
|
|
|
|
|
+ _addToClonedElementsGroup( element ) {
|
|
|
|
|
+ const id = element.id;
|
|
|
|
|
+
|
|
|
|
|
+ if ( !id ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let group = this._cloneGroups.get( id );
|
|
|
|
|
+
|
|
|
|
|
+ if ( !group ) {
|
|
|
|
|
+ group = new Set();
|
|
|
|
|
+ this._cloneGroups.set( id, group );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ group.add( element );
|
|
|
|
|
+ element._clonesGroup = group;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Removes all the information about the given {@link module:engine/view/attributeelement~AttributeElement attribute element}
|
|
|
|
|
+ * from its clones group.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
|
|
|
|
|
+ * This allows to reference the whole group even if the element was already removed from the tree.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Does nothing if the element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
|
|
|
|
|
+ */
|
|
|
|
|
+ _removeFromClonedElementsGroup( element ) {
|
|
|
|
|
+ const id = element.id;
|
|
|
|
|
+
|
|
|
|
|
+ if ( !id ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const group = this._cloneGroups.get( id );
|
|
|
|
|
+
|
|
|
|
|
+ group.delete( element );
|
|
|
|
|
+ // Not removing group from element on purpose!
|
|
|
|
|
+ // If other parts of code have reference to this element, they will be able to get references to other elements from the group.
|
|
|
|
|
+ // If all other elements are removed from the set, everything will be garbage collected.
|
|
|
|
|
+
|
|
|
|
|
+ if ( group.size === 0 ) {
|
|
|
|
|
+ this._cloneGroups.delete( id );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
|
|
// Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
|
|
@@ -1298,135 +1535,6 @@ function getParentContainer( position ) {
|
|
|
return parent;
|
|
return parent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Function used by both public breakAttributes (without splitting text nodes) and by other methods (with
|
|
|
|
|
-// splitting text nodes).
|
|
|
|
|
-//
|
|
|
|
|
-// @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
|
|
|
|
|
-// @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
|
|
|
|
|
-// container element. This behavior will result in incorrect view state, but is needed by other view writing methods
|
|
|
|
|
-// which then fixes view state. Defaults to `false`.
|
|
|
|
|
-// @returns {module:engine/view/range~Range} New range with located at break positions.
|
|
|
|
|
-function _breakAttributesRange( range, forceSplitText = false ) {
|
|
|
|
|
- const rangeStart = range.start;
|
|
|
|
|
- const rangeEnd = range.end;
|
|
|
|
|
-
|
|
|
|
|
- validateRangeContainer( range );
|
|
|
|
|
-
|
|
|
|
|
- // Break at the collapsed position. Return new collapsed range.
|
|
|
|
|
- if ( range.isCollapsed ) {
|
|
|
|
|
- const position = _breakAttributes( range.start, forceSplitText );
|
|
|
|
|
-
|
|
|
|
|
- return new Range( position, position );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const breakEnd = _breakAttributes( rangeEnd, forceSplitText );
|
|
|
|
|
- const count = breakEnd.parent.childCount;
|
|
|
|
|
- const breakStart = _breakAttributes( rangeStart, forceSplitText );
|
|
|
|
|
-
|
|
|
|
|
- // Calculate new break end offset.
|
|
|
|
|
- breakEnd.offset += breakEnd.parent.childCount - count;
|
|
|
|
|
-
|
|
|
|
|
- return new Range( breakStart, breakEnd );
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// Function used by public breakAttributes (without splitting text nodes) and by other methods (with
|
|
|
|
|
-// splitting text nodes).
|
|
|
|
|
-//
|
|
|
|
|
-// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
|
|
|
|
|
-// is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
|
|
|
|
|
-//
|
|
|
|
|
-// Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
|
|
|
|
|
-// is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
|
|
|
|
|
-//
|
|
|
|
|
-// @param {module:engine/view/position~Position} position Position where to break attributes.
|
|
|
|
|
-// @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
|
|
|
|
|
-// container element. This behavior will result in incorrect view state, but is needed by other view writing methods
|
|
|
|
|
-// which then fixes view state. Defaults to `false`.
|
|
|
|
|
-// @returns {module:engine/view/position~Position} New position after breaking the attributes.
|
|
|
|
|
-function _breakAttributes( position, forceSplitText = false ) {
|
|
|
|
|
- const positionOffset = position.offset;
|
|
|
|
|
- const positionParent = position.parent;
|
|
|
|
|
-
|
|
|
|
|
- // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
|
|
|
|
|
- if ( position.parent.is( 'emptyElement' ) ) {
|
|
|
|
|
- /**
|
|
|
|
|
- * Cannot break inside EmptyElement instance.
|
|
|
|
|
- *
|
|
|
|
|
- * @error view-writer-cannot-break-empty-element
|
|
|
|
|
- */
|
|
|
|
|
- throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // If position is placed inside UIElement - throw an exception as we cannot break inside.
|
|
|
|
|
- if ( position.parent.is( 'uiElement' ) ) {
|
|
|
|
|
- /**
|
|
|
|
|
- * Cannot break inside UIElement instance.
|
|
|
|
|
- *
|
|
|
|
|
- * @error view-writer-cannot-break-ui-element
|
|
|
|
|
- */
|
|
|
|
|
- throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // There are no attributes to break and text nodes breaking is not forced.
|
|
|
|
|
- if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
|
|
|
|
|
- return Position.createFromPosition( position );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Position's parent is container, so no attributes to break.
|
|
|
|
|
- if ( isContainerOrFragment( positionParent ) ) {
|
|
|
|
|
- return Position.createFromPosition( position );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Break text and start again in new position.
|
|
|
|
|
- if ( positionParent.is( 'text' ) ) {
|
|
|
|
|
- return _breakAttributes( breakTextNode( position ), forceSplitText );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const length = positionParent.childCount;
|
|
|
|
|
-
|
|
|
|
|
- // <p>foo<b><u>bar{}</u></b></p>
|
|
|
|
|
- // <p>foo<b><u>bar</u>[]</b></p>
|
|
|
|
|
- // <p>foo<b><u>bar</u></b>[]</p>
|
|
|
|
|
- if ( positionOffset == length ) {
|
|
|
|
|
- const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
|
|
|
|
|
-
|
|
|
|
|
- return _breakAttributes( newPosition, forceSplitText );
|
|
|
|
|
- } else
|
|
|
|
|
- // <p>foo<b><u>{}bar</u></b></p>
|
|
|
|
|
- // <p>foo<b>[]<u>bar</u></b></p>
|
|
|
|
|
- // <p>foo{}<b><u>bar</u></b></p>
|
|
|
|
|
- if ( positionOffset === 0 ) {
|
|
|
|
|
- const newPosition = new Position( positionParent.parent, positionParent.index );
|
|
|
|
|
-
|
|
|
|
|
- return _breakAttributes( newPosition, forceSplitText );
|
|
|
|
|
- }
|
|
|
|
|
- // <p>foo<b><u>b{}ar</u></b></p>
|
|
|
|
|
- // <p>foo<b><u>b[]ar</u></b></p>
|
|
|
|
|
- // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
|
|
|
|
|
- // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
|
|
|
|
|
- else {
|
|
|
|
|
- const offsetAfter = positionParent.index + 1;
|
|
|
|
|
-
|
|
|
|
|
- // Break element.
|
|
|
|
|
- const clonedNode = positionParent._clone();
|
|
|
|
|
-
|
|
|
|
|
- // Insert cloned node to position's parent node.
|
|
|
|
|
- positionParent.parent._insertChildren( offsetAfter, clonedNode );
|
|
|
|
|
-
|
|
|
|
|
- // Get nodes to move.
|
|
|
|
|
- const count = positionParent.childCount - positionOffset;
|
|
|
|
|
- const nodesToMove = positionParent._removeChildren( positionOffset, count );
|
|
|
|
|
-
|
|
|
|
|
- // Move nodes to cloned node.
|
|
|
|
|
- clonedNode._appendChildren( nodesToMove );
|
|
|
|
|
-
|
|
|
|
|
- // Create new position to work on.
|
|
|
|
|
- const newPosition = new Position( positionParent.parent, offsetAfter );
|
|
|
|
|
-
|
|
|
|
|
- return _breakAttributes( newPosition, forceSplitText );
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
|
|
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
|
|
|
// can be wrapped otuside second element. It is done by comparing elements'
|
|
// can be wrapped otuside second element. It is done by comparing elements'
|
|
|
// {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
|
|
// {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
|
|
@@ -1592,3 +1700,14 @@ function validateRangeContainer( range ) {
|
|
|
throw new CKEditorError( 'view-writer-invalid-range-container' );
|
|
throw new CKEditorError( 'view-writer-invalid-range-container' );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// Checks if two attribute elements can be joined together. Elements can be joined together if, and only if
|
|
|
|
|
+// they do not have ids specified.
|
|
|
|
|
+//
|
|
|
|
|
+// @private
|
|
|
|
|
+// @param {module:engine/view/element~Element} a
|
|
|
|
|
+// @param {module:engine/view/element~Element} b
|
|
|
|
|
+// @returns {Boolean}
|
|
|
|
|
+function canBeJoined( a, b ) {
|
|
|
|
|
+ return a.id === null && b.id === null;
|
|
|
|
|
+}
|