nodelist.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 model-nodelist-offset-out-of-bounds
  138. * @param {Number} offset
  139. * @param {module:engine/model/nodelist~NodeList} nodeList Stringified node list.
  140. */
  141. throw new CKEditorError( 'model-nodelist-offset-out-of-bounds: Given offset cannot be found in the node list.', {
  142. offset,
  143. nodeList: this
  144. } );
  145. }
  146. return this.length;
  147. }
  148. /**
  149. * Inserts given nodes at given index.
  150. *
  151. * @protected
  152. * @param {Number} index Index at which nodes should be inserted.
  153. * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes to be inserted.
  154. */
  155. _insertNodes( index, nodes ) {
  156. // Validation.
  157. for ( const node of nodes ) {
  158. if ( !( node instanceof Node ) ) {
  159. /**
  160. * Trying to insert an object which is not a Node instance.
  161. *
  162. * @error nodelist-insertNodes-not-node
  163. */
  164. throw new CKEditorError( 'model-nodelist-insertNodes-not-node: Trying to insert an object which is not a Node instance.' );
  165. }
  166. }
  167. this._nodes.splice( index, 0, ...nodes );
  168. }
  169. /**
  170. * Removes one or more nodes starting at the given index.
  171. *
  172. * @protected
  173. * @param {Number} indexStart Index of the first node to remove.
  174. * @param {Number} [howMany=1] Number of nodes to remove.
  175. * @returns {Array.<module:engine/model/node~Node>} Array containing removed nodes.
  176. */
  177. _removeNodes( indexStart, howMany = 1 ) {
  178. return this._nodes.splice( indexStart, howMany );
  179. }
  180. /**
  181. * Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes
  182. * are also converted to their plain object representation.
  183. *
  184. * @returns {Array.<module:engine/model/node~Node>} `NodeList` instance converted to `Array`.
  185. */
  186. toJSON() {
  187. return this._nodes.map( node => node.toJSON() );
  188. }
  189. }