8
0

element.js 14 KB

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