/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module engine/view/documentfragment */ import Text from './text'; import TextProxy from './textproxy'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; /** * Document fragment. * * To create a new document fragment instance use the * {@link module:engine/view/upcastwriter~UpcastWriter#createDocumentFragment `UpcastWriter#createDocumentFragment()`} * method. */ export default class DocumentFragment { /** * Creates new DocumentFragment instance. * * @protected * @param {module:engine/view/document~Document} document The document to which this document fragment belongs. * @param {module:engine/view/node~Node|Iterable.} [children] * A list of nodes to be inserted into the created document fragment. */ constructor( document, children ) { /** * The document to which this document fragment belongs. * * @readonly * @member {module:engine/view/document~Document} */ this.document = document; /** * Array of child nodes. * * @protected * @member {Array.} module:engine/view/documentfragment~DocumentFragment#_children */ this._children = []; if ( children ) { this._insertChild( 0, children ); } } /** * Iterable interface. * * Iterates over nodes added to this document fragment. * * @returns {Iterable.} */ [ Symbol.iterator ]() { return this._children[ Symbol.iterator ](); } /** * Number of child nodes in this document fragment. * * @readonly * @type {Number} */ get childCount() { return this._children.length; } /** * Is `true` if there are no nodes inside this document fragment, `false` otherwise. * * @readonly * @type {Boolean} */ get isEmpty() { return this.childCount === 0; } /** * Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons. * * @readonly * @type {module:engine/model/documentfragment~DocumentFragment} */ get root() { return this; } /** * Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons. * * @readonly * @type {null} */ get parent() { return null; } /** * Checks whether this object is of the given type. * * docFrag.is( 'documentFragment' ); // -> true * docFrag.is( 'view:documentFragment' ); // -> true * * docFrag.is( 'model:documentFragment' ); // -> false * docFrag.is( 'element' ); // -> false * docFrag.is( 'node' ); // -> false * * {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method. * * @param {String} type * @returns {Boolean} */ is( type ) { return type === 'documentFragment' || type === 'view:documentFragment'; } /** * {@link module:engine/view/documentfragment~DocumentFragment#_insertChild Insert} a child node or a list of child nodes at the end * and sets the parent of these nodes to this fragment. * * @param {module:engine/view/item~Item|Iterable.} items Items to be inserted. * @returns {Number} Number of appended nodes. */ _appendChild( items ) { return this._insertChild( this.childCount, items ); } /** * Gets child at the given index. * * @param {Number} index Index of child. * @returns {module:engine/view/node~Node} Child node. */ getChild( index ) { return this._children[ index ]; } /** * Gets index of the given child node. Returns `-1` if child node is not found. * * @param {module:engine/view/node~Node} node Child node. * @returns {Number} Index of the child node. */ getChildIndex( node ) { return this._children.indexOf( node ); } /** * Gets child nodes iterator. * * @returns {Iterable.} Child nodes iterator. */ getChildren() { return this._children[ Symbol.iterator ](); } /** * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to * this fragment. * * @param {Number} index Position where nodes should be inserted. * @param {module:engine/view/item~Item|Iterable.} items Items to be inserted. * @returns {Number} Number of inserted nodes. */ _insertChild( index, items ) { this._fireChange( 'children', this ); let count = 0; const nodes = normalize( this.document, items ); for ( const node of nodes ) { // If node that is being added to this element is already inside another element, first remove it from the old parent. if ( node.parent !== null ) { node._remove(); } node.parent = this; this._children.splice( index, 0, node ); index++; count++; } return count; } /** * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`. * * @param {Number} index Number of the first node to remove. * @param {Number} [howMany=1] Number of nodes to remove. * @returns {Array.} The array of removed nodes. */ _removeChildren( index, howMany = 1 ) { this._fireChange( 'children', this ); for ( let i = index; i < index + howMany; i++ ) { this._children[ i ].parent = null; } return this._children.splice( index, howMany ); } /** * Fires `change` event with given type of the change. * * @private * @param {module:engine/view/document~ChangeType} type Type of the change. * @param {module:engine/view/node~Node} node Changed node. * @fires module:engine/view/node~Node#change */ _fireChange( type, node ) { this.fire( 'change:' + type, node ); } // @if CK_DEBUG_ENGINE // printTree() { // @if CK_DEBUG_ENGINE // let string = 'ViewDocumentFragment: ['; // @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) { // @if CK_DEBUG_ENGINE // if ( child.is( 'text' ) ) { // @if CK_DEBUG_ENGINE // string += '\n' + '\t'.repeat( 1 ) + child.data; // @if CK_DEBUG_ENGINE // } else { // @if CK_DEBUG_ENGINE // string += '\n' + child.printTree( 1 ); // @if CK_DEBUG_ENGINE // } // @if CK_DEBUG_ENGINE // } // @if CK_DEBUG_ENGINE // string += '\n]'; // @if CK_DEBUG_ENGINE // return string; // @if CK_DEBUG_ENGINE // } // @if CK_DEBUG_ENGINE // logTree() { // @if CK_DEBUG_ENGINE // console.log( this.printTree() ); // @if CK_DEBUG_ENGINE // } } mix( DocumentFragment, EmitterMixin ); // Converts strings to Text and non-iterables to arrays. // // @param {String|module:engine/view/item~Item|Iterable.} // @returns {Iterable.} function normalize( document, nodes ) { // Separate condition because string is iterable. if ( typeof nodes == 'string' ) { return [ new Text( document, nodes ) ]; } if ( !isIterable( nodes ) ) { nodes = [ nodes ]; } // Array.from to enable .map() on non-arrays. return Array.from( nodes ) .map( node => { if ( typeof node == 'string' ) { return new Text( document, node ); } if ( node instanceof TextProxy ) { return new Text( document, node.data ); } return node; } ); }