| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module module:engine/model/documentfragment
- */
- import NodeList from './nodelist';
- import Element from './element';
- import Text from './text';
- import TextProxy from './textproxy';
- import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
- /**
- * DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
- * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
- *
- * DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
- * will be set to the {@link module:engine/model/model~Model#markers model markers} by a
- * {@link module:engine/model/writer~Writer#insert} function.
- */
- export default class DocumentFragment {
- /**
- * Creates an empty `DocumentFragment`.
- *
- * **Note:** Constructor of this class shouldn't be used directly in the code.
- * Use the {@link module:engine/model/writer~Writer#createDocumentFragment} method instead.
- *
- * @protected
- * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
- * Nodes to be contained inside the `DocumentFragment`.
- */
- constructor( children ) {
- /**
- * DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
- * which will be set as Markers to {@link module:engine/model/model~Model#markers model markers collection}
- * when DocumentFragment will be inserted to the document.
- *
- * @readonly
- * @member {Map<String,module:engine/model/range~Range>} module:engine/model/documentfragment~DocumentFragment#markers
- */
- this.markers = new Map();
- /**
- * List of nodes contained inside the document fragment.
- *
- * @private
- * @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children
- */
- this._children = new NodeList();
- if ( children ) {
- this._insertChild( 0, children );
- }
- }
- /**
- * Returns an iterator that iterates over all nodes contained inside this document fragment.
- *
- * @returns {Iterable.<module:engine/model/node~Node>}
- */
- [ Symbol.iterator ]() {
- return this.getChildren();
- }
- /**
- * Number of this document fragment's children.
- *
- * @readonly
- * @type {Number}
- */
- get childCount() {
- return this._children.length;
- }
- /**
- * Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children.
- *
- * @readonly
- * @type {Number}
- */
- get maxOffset() {
- return this._children.maxOffset;
- }
- /**
- * 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 given model tree object is of given type.
- *
- * Read more in {@link module:engine/model/node~Node#is}.
- *
- * @param {String} type
- * @returns {Boolean}
- */
- is( type ) {
- return type == 'documentFragment' || type == 'model:documentFragment';
- }
- /**
- * Gets the child at the given index. Returns `null` if incorrect index was passed.
- *
- * @param {Number} index Index of child.
- * @returns {module:engine/model/node~Node|null} Child node.
- */
- getChild( index ) {
- return this._children.getNode( index );
- }
- /**
- * Returns an iterator that iterates over all of this document fragment's children.
- *
- * @returns {Iterable.<module:engine/model/node~Node>}
- */
- getChildren() {
- return this._children[ Symbol.iterator ]();
- }
- /**
- * Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
- *
- * @param {module:engine/model/node~Node} node Child node to look for.
- * @returns {Number|null} Child node's index.
- */
- getChildIndex( node ) {
- return this._children.getNodeIndex( node );
- }
- /**
- * Returns the starting offset of given child. Starting offset is equal to the sum of
- * {@link module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
- * given node is not a child of this document fragment.
- *
- * @param {module:engine/model/node~Node} node Child node to look for.
- * @returns {Number|null} Child node's starting offset.
- */
- getChildStartOffset( node ) {
- return this._children.getNodeStartOffset( node );
- }
- /**
- * Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
- *
- * @returns {Array}
- */
- getPath() {
- return [];
- }
- /**
- * Returns a descendant node by its path relative to this element.
- *
- * // <this>a<b>c</b></this>
- * this.getNodeByPath( [ 0 ] ); // -> "a"
- * this.getNodeByPath( [ 1 ] ); // -> <b>
- * this.getNodeByPath( [ 1, 0 ] ); // -> "c"
- *
- * @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
- * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
- */
- getNodeByPath( relativePath ) {
- let node = this; // eslint-disable-line consistent-this
- for ( const index of relativePath ) {
- node = node.getChild( node.offsetToIndex( index ) );
- }
- return node;
- }
- /**
- * Converts offset "position" to index "position".
- *
- * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
- * too high, returns index after last child}.
- *
- * const textNode = new Text( 'foo' );
- * const pElement = new Element( 'p' );
- * const docFrag = new DocumentFragment( [ textNode, pElement ] );
- * docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
- * docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
- * docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
- * docFrag.offsetToIndex( 2 ); // Returns 0.
- * docFrag.offsetToIndex( 3 ); // Returns 1.
- * docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
- *
- * @param {Number} offset Offset to look for.
- * @returns {Number} Index of a node that occupies given offset.
- */
- offsetToIndex( offset ) {
- return this._children.offsetToIndex( offset );
- }
- /**
- * Converts `DocumentFragment` instance to plain object and returns it.
- * Takes care of converting all of this document fragment's children.
- *
- * @returns {Object} `DocumentFragment` instance converted to plain object.
- */
- toJSON() {
- const json = [];
- for ( const node of this._children ) {
- json.push( node.toJSON() );
- }
- return json;
- }
- /**
- * Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
- * Converts `DocumentFragment` children to proper nodes.
- *
- * @param {Object} json Plain object to be converted to `DocumentFragment`.
- * @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object.
- */
- static fromJSON( json ) {
- const children = [];
- for ( const child of json ) {
- if ( child.name ) {
- // If child has name property, it is an Element.
- children.push( Element.fromJSON( child ) );
- } else {
- // Otherwise, it is a Text node.
- children.push( Text.fromJSON( child ) );
- }
- }
- return new DocumentFragment( children );
- }
- /**
- * {@link #_insertChild Inserts} one or more nodes at the end of this document fragment.
- *
- * @protected
- * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
- */
- _appendChild( items ) {
- this._insertChild( this.childCount, items );
- }
- /**
- * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
- * to this document fragment.
- *
- * @protected
- * @param {Number} index Index at which nodes should be inserted.
- * @param {module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>} items Items to be inserted.
- */
- _insertChild( index, items ) {
- const nodes = normalize( 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._insertNodes( index, nodes );
- }
- /**
- * Removes one or more nodes starting at the given index
- * and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
- *
- * @protected
- * @param {Number} index Index of the first node to remove.
- * @param {Number} [howMany=1] Number of nodes to remove.
- * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
- */
- _removeChildren( index, howMany = 1 ) {
- const nodes = this._children._removeNodes( index, howMany );
- for ( const node of nodes ) {
- node.parent = null;
- }
- return nodes;
- }
- }
- // Converts strings to Text and non-iterables to arrays.
- //
- // @param {String|module:engine/model/item~Item|Iterable.<module:engine/model/item~Item>}
- // @returns {Iterable.<module:engine/model/node~Node>}
- function normalize( nodes ) {
- // Separate condition because string is iterable.
- if ( typeof nodes == 'string' ) {
- return [ new Text( 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( node );
- }
- if ( node instanceof TextProxy ) {
- return new Text( node.data, node.getAttributes() );
- }
- return node;
- } );
- }
|