element.js 9.3 KB

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