nodelist.js 5.7 KB

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