8
0

documentfragment.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * @license Copyright (c) 2003-2017, 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 '@ckeditor/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. * DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
  17. * will be set to the {@link module:engine/model/document~Document#markers document markers} by a
  18. * {@link module:engine/model/writer~writer.insert} function.
  19. */
  20. export default class DocumentFragment {
  21. /**
  22. * Creates an empty `DocumentFragment`.
  23. *
  24. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
  25. * Nodes to be contained inside the `DocumentFragment`.
  26. */
  27. constructor( children ) {
  28. /**
  29. * DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
  30. * which will be set as Markers to {@link module:engine/model/document~Document#markers document markers collection}
  31. * when DocumentFragment will be inserted to the document.
  32. *
  33. * @member {Map<String, {module:engine/model/range~Range}>} module:engine/model/documentfragment~DocumentFragment#markers
  34. */
  35. this.markers = new Map();
  36. /**
  37. * List of nodes contained inside the document fragment.
  38. *
  39. * @private
  40. * @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children
  41. */
  42. this._children = new NodeList();
  43. if ( children ) {
  44. this.insertChildren( 0, children );
  45. }
  46. }
  47. /**
  48. * Returns an iterator that iterates over all nodes contained inside this document fragment.
  49. *
  50. * @returns {Iterator.<module:engine/model/node~Node>}
  51. */
  52. [ Symbol.iterator ]() {
  53. return this.getChildren();
  54. }
  55. /**
  56. * Number of this document fragment's children.
  57. *
  58. * @readonly
  59. * @type {Number}
  60. */
  61. get childCount() {
  62. return this._children.length;
  63. }
  64. /**
  65. * Sum of {module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children.
  66. *
  67. * @readonly
  68. * @type {Number}
  69. */
  70. get maxOffset() {
  71. return this._children.maxOffset;
  72. }
  73. /**
  74. * Is `true` if there are no nodes inside this document fragment, `false` otherwise.
  75. *
  76. * @readonly
  77. * @type {Boolean}
  78. */
  79. get isEmpty() {
  80. return this.childCount === 0;
  81. }
  82. /**
  83. * Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
  84. *
  85. * @readonly
  86. * @type {module:engine/model/documentfragment~DocumentFragment}
  87. */
  88. get root() {
  89. return this;
  90. }
  91. /**
  92. * Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
  93. *
  94. * @readonly
  95. * @type {null}
  96. */
  97. get parent() {
  98. return null;
  99. }
  100. /**
  101. * Checks whether given model tree object is of given type.
  102. *
  103. * Read more in {@link module:engine/model/node~Node#is}.
  104. *
  105. * @param {String} type
  106. * @returns {Boolean}
  107. */
  108. is( type ) {
  109. return type == 'documentFragment';
  110. }
  111. /**
  112. * Gets the child at the given index. Returns `null` if incorrect index was passed.
  113. *
  114. * @param {Number} index Index of child.
  115. * @returns {module:engine/model/node~Node|null} Child node.
  116. */
  117. getChild( index ) {
  118. return this._children.getNode( index );
  119. }
  120. /**
  121. * Returns an iterator that iterates over all of this document fragment's children.
  122. *
  123. * @returns {Iterable.<module:engine/model/node~Node>}
  124. */
  125. getChildren() {
  126. return this._children[ Symbol.iterator ]();
  127. }
  128. /**
  129. * Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
  130. *
  131. * @param {module:engine/model/node~Node} node Child node to look for.
  132. * @returns {Number|null} Child node's index.
  133. */
  134. getChildIndex( node ) {
  135. return this._children.getNodeIndex( node );
  136. }
  137. /**
  138. * Returns the starting offset of given child. Starting offset is equal to the sum of
  139. * {module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
  140. * given node is not a child of this document fragment.
  141. *
  142. * @param {module:engine/model/node~Node} node Child node to look for.
  143. * @returns {Number|null} Child node's starting offset.
  144. */
  145. getChildStartOffset( node ) {
  146. return this._children.getNodeStartOffset( node );
  147. }
  148. /**
  149. * Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
  150. *
  151. * @returns {Array}
  152. */
  153. getPath() {
  154. return [];
  155. }
  156. /**
  157. * Returns a descendant node by its path relative to this element.
  158. *
  159. * // <this>a<b>c</b></this>
  160. * this.getNodeByPath( [ 0 ] ); // -> "a"
  161. * this.getNodeByPath( [ 1 ] ); // -> <b>
  162. * this.getNodeByPath( [ 1, 0 ] ); // -> "c"
  163. *
  164. * @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
  165. * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
  166. */
  167. getNodeByPath( relativePath ) {
  168. let node = this; // eslint-disable-line consistent-this
  169. for ( const index of relativePath ) {
  170. node = node.getChild( index );
  171. }
  172. return node;
  173. }
  174. /**
  175. * Converts offset "position" to index "position".
  176. *
  177. * Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
  178. * too high, returns index after last child}.
  179. *
  180. * const textNode = new Text( 'foo' );
  181. * const pElement = new Element( 'p' );
  182. * const docFrag = new DocumentFragment( [ textNode, pElement ] );
  183. * docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
  184. * docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
  185. * docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
  186. * docFrag.offsetToIndex( 2 ); // Returns 0.
  187. * docFrag.offsetToIndex( 3 ); // Returns 1.
  188. * docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
  189. *
  190. * @param {Number} offset Offset to look for.
  191. * @returns {Number} Index of a node that occupies given offset.
  192. */
  193. offsetToIndex( offset ) {
  194. return this._children.offsetToIndex( offset );
  195. }
  196. /**
  197. * {@link #insertChildren Inserts} one or more nodes at the end of this document fragment.
  198. *
  199. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
  200. */
  201. appendChildren( nodes ) {
  202. this.insertChildren( this.childCount, nodes );
  203. }
  204. /**
  205. * Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
  206. * to this document fragment.
  207. *
  208. * @param {Number} index Index at which nodes should be inserted.
  209. * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
  210. */
  211. insertChildren( index, nodes ) {
  212. nodes = normalize( nodes );
  213. for ( const node of nodes ) {
  214. node.parent = this;
  215. }
  216. this._children.insertNodes( index, nodes );
  217. }
  218. /**
  219. * Removes one or more nodes starting at the given index
  220. * and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
  221. *
  222. * @param {Number} index Index of the first node to remove.
  223. * @param {Number} [howMany=1] Number of nodes to remove.
  224. * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
  225. */
  226. removeChildren( index, howMany = 1 ) {
  227. const nodes = this._children.removeNodes( index, howMany );
  228. for ( const node of nodes ) {
  229. node.parent = null;
  230. }
  231. return nodes;
  232. }
  233. /**
  234. * Converts `DocumentFragment` instance to plain object and returns it.
  235. * Takes care of converting all of this document fragment's children.
  236. *
  237. * @returns {Object} `DocumentFragment` instance converted to plain object.
  238. */
  239. toJSON() {
  240. const json = [];
  241. for ( const node of this._children ) {
  242. json.push( node.toJSON() );
  243. }
  244. return json;
  245. }
  246. /**
  247. * Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
  248. * Converts `DocumentFragment` children to proper nodes.
  249. *
  250. * @param {Object} json Plain object to be converted to `DocumentFragment`.
  251. * @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object.
  252. */
  253. static fromJSON( json ) {
  254. const children = [];
  255. for ( const child of json ) {
  256. if ( child.name ) {
  257. // If child has name property, it is an Element.
  258. children.push( Element.fromJSON( child ) );
  259. } else {
  260. // Otherwise, it is a Text node.
  261. children.push( Text.fromJSON( child ) );
  262. }
  263. }
  264. return new DocumentFragment( children );
  265. }
  266. }
  267. // Converts strings to Text and non-iterables to arrays.
  268. //
  269. // @param {String|module:engine/model/node~Node|Iterable.<String|module:engine/model/node~Node>}
  270. // @return {Iterable.<module:engine/model/node~Node>}
  271. function normalize( nodes ) {
  272. // Separate condition because string is iterable.
  273. if ( typeof nodes == 'string' ) {
  274. return [ new Text( nodes ) ];
  275. }
  276. if ( !isIterable( nodes ) ) {
  277. nodes = [ nodes ];
  278. }
  279. // Array.from to enable .map() on non-arrays.
  280. return Array.from( nodes )
  281. .map( node => {
  282. return typeof node == 'string' ? new Text( node ) : node;
  283. } );
  284. }