8
0

nodelist.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/nodelist
  7. */
  8. import Node from './node';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. /**
  11. * Provides an interface to operate on a list of {@link module:engine/model/node~Node nodes}. `NodeList` is used internally
  12. * in classes like {@link module:engine/model/element~Element Element}
  13. * or {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}.
  14. */
  15. export default class NodeList {
  16. /**
  17. * Creates an empty node list.
  18. *
  19. * @protected
  20. * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes contained in this node list.
  21. */
  22. constructor( nodes ) {
  23. /**
  24. * Nodes contained in this node list.
  25. *
  26. * @private
  27. * @member {Array.<module:engine/model/node~Node>}
  28. */
  29. this._nodes = [];
  30. if ( nodes ) {
  31. this._insertNodes( 0, nodes );
  32. }
  33. }
  34. /**
  35. * Iterable interface.
  36. *
  37. * Iterates over all nodes contained inside this node list.
  38. *
  39. * @returns {Iterable.<module:engine/model/node~Node>}
  40. */
  41. [ Symbol.iterator ]() {
  42. return this._nodes[ Symbol.iterator ]();
  43. }
  44. /**
  45. * Number of nodes contained inside this node list.
  46. *
  47. * @readonly
  48. * @type {Number}
  49. */
  50. get length() {
  51. return this._nodes.length;
  52. }
  53. /**
  54. * Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes contained inside this node list.
  55. *
  56. * @readonly
  57. * @type {Number}
  58. */
  59. get maxOffset() {
  60. return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
  61. }
  62. /**
  63. * Gets the node at the given index. Returns `null` if incorrect index was passed.
  64. *
  65. * @param {Number} index Index of node.
  66. * @returns {module:engine/model/node~Node|null} Node at given index.
  67. */
  68. getNode( index ) {
  69. return this._nodes[ index ] || null;
  70. }
  71. /**
  72. * Returns an index of the given node. Returns `null` if given node is not inside this node list.
  73. *
  74. * @param {module:engine/model/node~Node} node Child node to look for.
  75. * @returns {Number|null} Child node's index.
  76. */
  77. getNodeIndex( node ) {
  78. const index = this._nodes.indexOf( node );
  79. return index == -1 ? null : index;
  80. }
  81. /**
  82. * Returns the starting offset of given node. Starting offset is equal to the sum of
  83. * {module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
  84. *
  85. * @param {module:engine/model/node~Node} node Node to look for.
  86. * @returns {Number|null} Node's starting offset.
  87. */
  88. getNodeStartOffset( node ) {
  89. const index = this.getNodeIndex( node );
  90. return index === null ? null : this._nodes.slice( 0, index ).reduce( ( sum, node ) => sum + node.offsetSize, 0 );
  91. }
  92. /**
  93. * Converts index to offset in node list.
  94. *
  95. * Returns starting offset of a node that is at given index. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
  96. * `model-nodelist-index-out-of-bounds` if given index is less than `0` or more than {@link #length}.
  97. *
  98. * @param {Number} index Node's index.
  99. * @returns {Number} Node's starting offset.
  100. */
  101. indexToOffset( index ) {
  102. if ( index == this._nodes.length ) {
  103. return this.maxOffset;
  104. }
  105. const node = this._nodes[ index ];
  106. if ( !node ) {
  107. /**
  108. * Given index cannot be found in the node list.
  109. *
  110. * @error nodelist-index-out-of-bounds
  111. */
  112. throw new CKEditorError( 'model-nodelist-index-out-of-bounds: Given index cannot be found in the node list.' );
  113. }
  114. return this.getNodeStartOffset( node );
  115. }
  116. /**
  117. * Converts offset in node list to index.
  118. *
  119. * Returns index of a node that occupies given offset. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
  120. * `model-nodelist-offset-out-of-bounds` if given offset is less than `0` or more than {@link #maxOffset}.
  121. *
  122. * @param {Number} offset Offset to look for.
  123. * @returns {Number} Index of a node that occupies given offset.
  124. */
  125. offsetToIndex( offset ) {
  126. let totalOffset = 0;
  127. for ( const node of this._nodes ) {
  128. if ( offset >= totalOffset && offset < totalOffset + node.offsetSize ) {
  129. return this.getNodeIndex( node );
  130. }
  131. totalOffset += node.offsetSize;
  132. }
  133. if ( totalOffset != offset ) {
  134. /**
  135. * Given offset cannot be found in the node list.
  136. *
  137. * @error nodelist-offset-out-of-bounds
  138. */
  139. throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.' );
  140. }
  141. return this.length;
  142. }
  143. /**
  144. * Inserts given nodes at given index.
  145. *
  146. * @protected
  147. * @param {Number} index Index at which nodes should be inserted.
  148. * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
  149. */
  150. _insertNodes( index, nodes ) {
  151. // Validation.
  152. for ( const node of nodes ) {
  153. if ( !( node instanceof Node ) ) {
  154. /**
  155. * Trying to insert an object which is not a Node instance.
  156. *
  157. * @error nodelist-insertNodes-not-node
  158. */
  159. throw new CKEditorError( 'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' );
  160. }
  161. }
  162. this._nodes.splice( index, 0, ...nodes );
  163. }
  164. /**
  165. * Removes one or more nodes starting at the given index.
  166. *
  167. * @protected
  168. * @param {Number} indexStart Index of the first node to remove.
  169. * @param {Number} [howMany=1] Number of nodes to remove.
  170. * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
  171. */
  172. _removeNodes( indexStart, howMany = 1 ) {
  173. return this._nodes.splice( indexStart, howMany );
  174. }
  175. /**
  176. * Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes
  177. * are also converted to their plain object representation.
  178. *
  179. * @returns {Array.<module:engine/model/node~Node>} `NodeList` instance converted to `Array`.
  180. */
  181. toJSON() {
  182. return this._nodes.map( node => node.toJSON() );
  183. }
  184. }