8
0

element.js 8.1 KB

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