8
0

documentfragment.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module module:engine/model/documentfragment
  7. */
  8. import NodeList from './nodelist';
  9. import Element from './element';
  10. import Text from './text';
  11. import isIterable from 'ckeditor5-utils/src/isiterable';
  12. /**
  13. * DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
  14. * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
  15. */
  16. export default class DocumentFragment {
  17. /**
  18. * Creates an empty `DocumentFragment`.
  19. *
  20. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
  21. * Nodes to be contained inside the `DocumentFragment`.
  22. */
  23. constructor( children ) {
  24. /**
  25. * List of nodes contained inside the document fragment.
  26. *
  27. * @private
  28. * @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children
  29. */
  30. this._children = new NodeList();
  31. if ( children ) {
  32. this.insertChildren( 0, children );
  33. }
  34. }
  35. /**
  36. * Returns an iterator that iterates over all nodes contained inside this document fragment.
  37. *
  38. * @returns {Iterator.<module:engine/model/node~Node>}
  39. */
  40. [ Symbol.iterator ]() {
  41. return this.getChildren();
  42. }
  43. /**
  44. * Number of this document fragment's children.
  45. *
  46. * @readonly
  47. * @type {Number}
  48. */
  49. get childCount() {
  50. return this._children.length;
  51. }
  52. /**
  53. * Sum of {module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children.
  54. *
  55. * @readonly
  56. * @type {Number}
  57. */
  58. get maxOffset() {
  59. return this._children.maxOffset;
  60. }
  61. /**
  62. * Is `true` if there are no nodes inside this document fragment, `false` otherwise.
  63. *
  64. * @readonly
  65. * @type {Boolean}
  66. */
  67. get isEmpty() {
  68. return this.childCount === 0;
  69. }
  70. /**
  71. * Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
  72. *
  73. * @readonly
  74. * @type {module:engine/model/documentfragment~DocumentFragment}
  75. */
  76. get root() {
  77. return this;
  78. }
  79. /**
  80. * Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
  81. *
  82. * @readonly
  83. * @type {null}
  84. */
  85. get parent() {
  86. return null;
  87. }
  88. /**
  89. * Gets the child at the given index. Returns `null` if incorrect index was passed.
  90. *
  91. * @param {Number} index Index of child.
  92. * @returns {module:engine/model/node~Node|null} Child node.
  93. */
  94. getChild( index ) {
  95. return this._children.getNode( index );
  96. }
  97. /**
  98. * Returns an iterator that iterates over all of this document fragment's children.
  99. *
  100. * @returns {Iterable.<module:engine/model/node~Node>}
  101. */
  102. getChildren() {
  103. return this._children[ Symbol.iterator ]();
  104. }
  105. /**
  106. * Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
  107. *
  108. * @param {module:engine/model/node~Node} node Child node to look for.
  109. * @returns {Number|null} Child node's index.
  110. */
  111. getChildIndex( node ) {
  112. return this._children.getNodeIndex( node );
  113. }
  114. /**
  115. * Returns the starting offset of given child. Starting offset is equal to the sum of
  116. * {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
  117. * given node is not a child of this document fragment.
  118. *
  119. * @param {module:engine/model/node~Node} node Child node to look for.
  120. * @returns {Number|null} Child node's starting offset.
  121. */
  122. getChildStartOffset( node ) {
  123. return this._children.getNodeStartOffset( node );
  124. }
  125. /**
  126. * Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
  127. *
  128. * @returns {Array}
  129. */
  130. getPath() {
  131. return [];
  132. }
  133. /**
  134. * Converts offset "position" to index "position".
  135. *
  136. * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
  137. * too high, returns index after last child}.
  138. *
  139. * const textNode = new Text( 'foo' );
  140. * const pElement = new Element( 'p' );
  141. * const docFrag = new DocumentFragment( [ textNode, pElement ] );
  142. * docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
  143. * docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
  144. * docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
  145. * docFrag.offsetToIndex( 2 ); // Returns 0.
  146. * docFrag.offsetToIndex( 3 ); // Returns 1.
  147. * docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
  148. *
  149. * @param {Number} offset Offset to look for.
  150. * @returns {Number} Index of a node that occupies given offset.
  151. */
  152. offsetToIndex( offset ) {
  153. return this._children.offsetToIndex( offset );
  154. }
  155. /**
  156. * {@link #insertChildren Inserts} one or more nodes at the end of this document fragment.
  157. *
  158. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
  159. */
  160. appendChildren( nodes ) {
  161. this.insertChildren( this.childCount, nodes );
  162. }
  163. /**
  164. * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
  165. * to this document fragment.
  166. *
  167. * @param {Number} index Index at which nodes should be inserted.
  168. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
  169. */
  170. insertChildren( index, nodes ) {
  171. nodes = normalize( nodes );
  172. for ( let node of nodes ) {
  173. node.parent = this;
  174. }
  175. this._children.insertNodes( index, nodes );
  176. }
  177. /**
  178. * Removes one or more nodes starting at the given index
  179. * and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
  180. *
  181. * @param {Number} index Index of the first node to remove.
  182. * @param {Number} [howMany=1] Number of nodes to remove.
  183. * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
  184. */
  185. removeChildren( index, howMany = 1 ) {
  186. const nodes = this._children.removeNodes( index, howMany );
  187. for ( let node of nodes ) {
  188. node.parent = null;
  189. }
  190. return nodes;
  191. }
  192. /**
  193. * Converts `DocumentFragment` instance to plain object and returns it.
  194. * Takes care of converting all of this document fragment's children.
  195. *
  196. * @returns {Object} `DocumentFragment` instance converted to plain object.
  197. */
  198. toJSON() {
  199. let json = [];
  200. for ( let node of this._children ) {
  201. json.push( node.toJSON() );
  202. }
  203. return json;
  204. }
  205. /**
  206. * Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
  207. * Converts `DocumentFragment` children to proper nodes.
  208. *
  209. * @param {Object} json Plain object to be converted to `DocumentFragment`.
  210. * @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object.
  211. */
  212. static fromJSON( json ) {
  213. let children = [];
  214. for ( let child of json ) {
  215. if ( child.name ) {
  216. // If child has name property, it is an Element.
  217. children.push( Element.fromJSON( child ) );
  218. } else {
  219. // Otherwise, it is a Text node.
  220. children.push( Text.fromJSON( child ) );
  221. }
  222. }
  223. return new DocumentFragment( children );
  224. }
  225. }
  226. // Converts strings to Text and non-iterables to arrays.
  227. //
  228. // @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>}
  229. // @return {Iterable.<module:engine/model/node~Node>}
  230. function normalize( nodes ) {
  231. // Separate condition because string is iterable.
  232. if ( typeof nodes == 'string' ) {
  233. return [ new Text( nodes ) ];
  234. }
  235. if ( !isIterable( nodes ) ) {
  236. nodes = [ nodes ];
  237. }
  238. // Array.from to enable .map() on non-arrays.
  239. return Array.from( nodes ).map( ( node ) => typeof node == 'string' ? new Text( node ) : node );
  240. }