element.js 10 KB

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