element.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import Node from './node.js';
  7. import utils from '../../utils/utils.js';
  8. import isPlainObject from '../../utils/lib/lodash/isPlainObject.js';
  9. /**
  10. * Tree view element.
  11. *
  12. * Editing engine does not define fixed HTML DTD. This is why the type of the {@link engine.treeView.Element} need to
  13. * be defined by the feature developer. Creating an element you should use {@link engine.treeView.ContainerElement}
  14. * class or {@link engine.treeView.AttributeElement}.
  15. *
  16. * Note that for view elements which are not created from model, like elements from mutations, paste or
  17. * {@link engine.treeController.DataController#set data.set} it is not possible to define the type of the element, so
  18. * these will be instances of the {@link engine.treeView.Element}.
  19. *
  20. * @memberOf engine.treeView
  21. * @extends engine.treeView.Node
  22. */
  23. export default class Element extends Node {
  24. /**
  25. * Creates a tree view element.
  26. *
  27. * Attributes can be passed in various formats:
  28. *
  29. * new Element( 'div', { 'class': 'editor', 'contentEditable': 'true' } ); // object
  30. * new Element( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
  31. * new Element( 'div', mapOfAttributes ); // map
  32. *
  33. * @param {String} name Node name.
  34. * @param {Object|Iterable} [attrs] Collection of attributes.
  35. * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} [children] List of nodes to be inserted into created element.
  36. */
  37. constructor( name, attrs, children ) {
  38. super();
  39. /**
  40. * Name of the element.
  41. *
  42. * @readonly
  43. * @member {String} engine.treeView.Element#name
  44. */
  45. this.name = name;
  46. /**
  47. * Map of attributes, where attributes names are keys and attributes values are values.
  48. *
  49. * @protected
  50. * @member {Map} engine.treeView.Element#_attrs
  51. */
  52. if ( isPlainObject( attrs ) ) {
  53. this._attrs = utils.objectToMap( attrs );
  54. } else {
  55. this._attrs = new Map( attrs );
  56. }
  57. /**
  58. * Array of child nodes.
  59. *
  60. * @protected
  61. * @member {Array.<engine.treeView.Node>} engine.treeView.Element#_children
  62. */
  63. this._children = [];
  64. if ( children ) {
  65. this.insertChildren( 0, children );
  66. }
  67. /**
  68. * Set of classes associated with element instance.
  69. *
  70. * @protected
  71. * @member {Set} engine.treeView.Element#_classes
  72. */
  73. this._classes = new Set();
  74. if ( this._attrs.has( 'class' ) ) {
  75. // Remove class attribute and handle it by class set.
  76. const classString = this._attrs.get( 'class' );
  77. parseClasses( this._classes, classString );
  78. this._attrs.delete( 'class' );
  79. }
  80. /**
  81. * Map of styles.
  82. *
  83. * @protected
  84. * @member {Set} engine.treeView.Element#_styles
  85. */
  86. this._styles = new Map();
  87. if ( this._attrs.has( 'style' ) ) {
  88. // Remove style attribute and handle it by styles map.
  89. parseInlineStyles( this._styles, this._attrs.get( 'style' ) );
  90. this._attrs.delete( 'style' );
  91. }
  92. }
  93. /**
  94. * Clones provided element.
  95. *
  96. * @param {Boolean} deep If set to `true` clones element and all its children recursively. When set to `false`,
  97. * element will be cloned without any children.
  98. * @returns {Element} Clone of this element.
  99. */
  100. clone( deep ) {
  101. const childrenClone = [];
  102. if ( deep ) {
  103. for ( let child of this.getChildren() ) {
  104. childrenClone.push( child.clone( deep ) );
  105. }
  106. }
  107. // ContainerElement and AttributeElement should be also cloned properly.
  108. const cloned = new this.constructor( this.name, this._attrs, childrenClone );
  109. // Classes and styles are cloned separately - this solution is faster than adding them back to attributes and
  110. // parse once again in constructor.
  111. cloned._classes = new Set( this._classes );
  112. cloned._styles = new Map( this._styles );
  113. return cloned;
  114. }
  115. /**
  116. * {@link engine.treeView.Element#insert Insert} a child node or a list of child nodes at the end of this node and sets
  117. * the parent of these nodes to this element.
  118. *
  119. * @fires engine.treeView.Node#change
  120. * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or the list of nodes to be inserted.
  121. * @returns {Number} Number of appended nodes.
  122. */
  123. appendChildren( nodes ) {
  124. return this.insertChildren( this.getChildCount(), nodes );
  125. }
  126. /**
  127. * Gets child at the given index.
  128. *
  129. * @param {Number} index Index of child.
  130. * @returns {engine.treeView.Node} Child node.
  131. */
  132. getChild( index ) {
  133. return this._children[ index ];
  134. }
  135. /**
  136. * Gets the number of element's children.
  137. *
  138. * @returns {Number} The number of element's children.
  139. */
  140. getChildCount() {
  141. return this._children.length;
  142. }
  143. /**
  144. * Gets index of the given child node. Returns `-1` if child node is not found.
  145. *
  146. * @param {engine.treeView.Node} node Child node.
  147. * @returns {Number} Index of the child node.
  148. */
  149. getChildIndex( node ) {
  150. return this._children.indexOf( node );
  151. }
  152. /**
  153. * Gets child nodes iterator.
  154. *
  155. * @returns {Iterable.<engine.treeView.Node>} Child nodes iterator.
  156. */
  157. getChildren() {
  158. return this._children[ Symbol.iterator ]();
  159. }
  160. /**
  161. * Returns an iterator that contains the keys for attributes.
  162. * Order of inserting attributes is not preserved.
  163. *
  164. * @returns {Iterator.<String>} Keys for attributes.
  165. */
  166. *getAttributeKeys() {
  167. if ( this._classes.size > 0 ) {
  168. yield 'class';
  169. }
  170. if ( this._styles.size > 0 ) {
  171. yield 'style';
  172. }
  173. yield* this._attrs.keys();
  174. }
  175. /**
  176. * Gets attribute by key. If attribute is not present - returns undefined.
  177. *
  178. * @param {String} key Attribute key.
  179. * @returns {String|undefined} Attribute value.
  180. */
  181. getAttribute( key ) {
  182. if ( key == 'class' ) {
  183. if ( this._classes.size > 0 ) {
  184. return [ ...this._classes ].join( ' ' );
  185. }
  186. return undefined;
  187. }
  188. if ( key == 'style' ) {
  189. if ( this._styles.size > 0 ) {
  190. let styleString = '';
  191. for ( let [ property, value ] of this._styles ) {
  192. styleString += `${ property }:${ value };`;
  193. }
  194. return styleString;
  195. }
  196. return undefined;
  197. }
  198. return this._attrs.get( key );
  199. }
  200. /**
  201. * Returns a boolean indicating whether an attribute with the specified key exists in the element.
  202. *
  203. * @param {String} key Attribute key.
  204. * @returns {Boolean} `true` if attribute with the specified key exists in the element, false otherwise.
  205. */
  206. hasAttribute( key ) {
  207. if ( key == 'class' ) {
  208. return this._classes.size > 0;
  209. }
  210. if ( key == 'style' ) {
  211. return this._styles.size > 0;
  212. }
  213. return this._attrs.has( key );
  214. }
  215. /**
  216. * Adds or overwrite attribute with a specified key and value.
  217. *
  218. * @param {String} key Attribute key.
  219. * @param {String} value Attribute value.
  220. * @fires engine.treeView.Node#change
  221. */
  222. setAttribute( key, value ) {
  223. this._fireChange( 'ATTRIBUTES', this );
  224. if ( key == 'class' ) {
  225. parseClasses( this._classes, value );
  226. } else if ( key == 'style' ) {
  227. parseInlineStyles( this._styles, value );
  228. } else {
  229. this._attrs.set( key, value );
  230. }
  231. }
  232. /**
  233. * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
  234. * this element.
  235. *
  236. * @param {Number} index Position where nodes should be inserted.
  237. * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or the list of nodes to be inserted.
  238. * @fires engine.treeView.Node#change
  239. * @returns {Number} Number of inserted nodes.
  240. */
  241. insertChildren( index, nodes ) {
  242. this._fireChange( 'CHILDREN', this );
  243. let count = 0;
  244. if ( !utils.isIterable( nodes ) ) {
  245. nodes = [ nodes ];
  246. }
  247. for ( let node of nodes ) {
  248. node.parent = this;
  249. this._children.splice( index, 0, node );
  250. index++;
  251. count++;
  252. }
  253. return count;
  254. }
  255. /**
  256. * Removes attribute from the element.
  257. *
  258. * @param {String} key Attribute key.
  259. * @returns {Boolean} Returns true if an attribute existed and has been removed.
  260. * @fires engine.treeView.Node#change
  261. */
  262. removeAttribute( key ) {
  263. this._fireChange( 'ATTRIBUTES', this );
  264. // Remove class attribute.
  265. if ( key == 'class' ) {
  266. if ( this._classes.size > 0 ) {
  267. this._classes.clear();
  268. return true;
  269. }
  270. return false;
  271. }
  272. // Remove style attribute.
  273. if ( key == 'style' ) {
  274. if ( this._styles.size > 0 ) {
  275. this._styles.clear();
  276. return true;
  277. }
  278. return false;
  279. }
  280. // Remove other attributes.
  281. return this._attrs.delete( key );
  282. }
  283. /**
  284. * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`.
  285. *
  286. * @param {Number} index Number of the first node to remove.
  287. * @param {Number} [howMany=1] Number of nodes to remove.
  288. * @returns {Array.<engine.treeView.Node>} The array of removed nodes.
  289. * @fires engine.treeView.Node#change
  290. */
  291. removeChildren( index, howMany = 1 ) {
  292. this._fireChange( 'CHILDREN', this );
  293. for ( let i = index; i < index + howMany; i++ ) {
  294. this._children[ i ].parent = null;
  295. }
  296. return this._children.splice( index, howMany );
  297. }
  298. /**
  299. * Checks if this element is similar to other element.
  300. * Both elements should have the same name and attributes to be considered as similar. Two similar elements
  301. * can contain different set of children nodes.
  302. *
  303. * @param {Element} otherElement
  304. * @returns {Boolean}
  305. */
  306. isSimilar( otherElement ) {
  307. if ( !( otherElement instanceof Element ) ) {
  308. return false;
  309. }
  310. // If exactly the same Element is provided - return true immediately.
  311. if ( this === otherElement ) {
  312. return true;
  313. }
  314. // Check element name.
  315. if ( this.name != otherElement.name ) {
  316. return false;
  317. }
  318. // Check number of attributes, classes and styles.
  319. if ( this._attrs.size !== otherElement._attrs.size || this._classes.size !== otherElement._classes.size ||
  320. this._styles.size !== otherElement._styles.size ) {
  321. return false;
  322. }
  323. // Check if attributes are the same.
  324. for ( let [ key, value ] of this._attrs ) {
  325. if ( !otherElement._attrs.has( key ) || otherElement._attrs.get( key ) !== value ) {
  326. return false;
  327. }
  328. }
  329. // Check if classes are the same.
  330. for ( let className of this._classes ) {
  331. if ( !otherElement._classes.has( className ) ) {
  332. return false;
  333. }
  334. }
  335. // Check if styles are the same.
  336. for ( let [ property, value ] of this._styles ) {
  337. if ( !otherElement._styles.has( property ) || otherElement._styles.get( property ) !== value ) {
  338. return false;
  339. }
  340. }
  341. return true;
  342. }
  343. /**
  344. * Adds specified class.
  345. *
  346. * element.addClass( 'foo' ); // Adds 'foo' class.
  347. * element.addClass( 'foo', 'bar' ); // Adds 'foo' and 'bar' classes.
  348. *
  349. * @param {...String} className
  350. * @fires engine.treeView.Node#change
  351. */
  352. addClass( ...className ) {
  353. this._fireChange( 'ATTRIBUTES', this );
  354. className.forEach( name => this._classes.add( name ) );
  355. }
  356. /**
  357. * Removes specified class.
  358. *
  359. * element.removeClass( 'foo' ); // Removes 'foo' class.
  360. * element.removeClass( 'foo', 'bar' ); // Removes both 'foo' and 'bar' classes.
  361. *
  362. * @param {...String} className
  363. * @fires engine.treeView.Node#change
  364. */
  365. removeClass( ...className ) {
  366. this._fireChange( 'ATTRIBUTES', this );
  367. className.forEach( name => this._classes.delete( name ) );
  368. }
  369. /**
  370. * Returns true if class is present.
  371. * If more then one class is provided - returns true only when all classes are present.
  372. *
  373. * element.hasClass( 'foo' ); // Returns true if 'foo' class is present.
  374. * element.hasClass( 'foo', 'bar' ); // Returns true if 'foo' and 'bar' classes are both present.
  375. *
  376. * @param {...String} className
  377. */
  378. hasClass( ...className ) {
  379. for ( let name of className ) {
  380. if ( !this._classes.has( name ) ) {
  381. return false;
  382. }
  383. }
  384. return true;
  385. }
  386. /**
  387. * Returns iterator that contains all class names.
  388. *
  389. * @returns {Iterator.<String>}
  390. */
  391. getClassNames() {
  392. return this._classes.keys();
  393. }
  394. /**
  395. * Adds style to the element.
  396. *
  397. * element.setStyle( 'color', 'red' );
  398. * element.setStyle( {
  399. * color: 'red',
  400. * position: 'fixed'
  401. * } );
  402. *
  403. * @param {String|Object} property Property name or object with key - value pairs.
  404. * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
  405. * @fires engine.treeView.Node#change
  406. */
  407. setStyle( property, value ) {
  408. this._fireChange( 'ATTRIBUTES', this );
  409. if ( isPlainObject( property ) ) {
  410. const keys = Object.keys( property );
  411. for ( let key of keys ) {
  412. this._styles.set( key, property[ key ] );
  413. }
  414. } else {
  415. this._styles.set( property, value );
  416. }
  417. }
  418. /**
  419. * Returns style value for given property.
  420. * Undefined is returned if style does not exist.
  421. *
  422. * @param {String} property
  423. * @returns {String|undefined}
  424. */
  425. getStyle( property ) {
  426. return this._styles.get( property );
  427. }
  428. /**
  429. * Returns iterator that contains all style names.
  430. *
  431. * @returns {Iterator.<String>}
  432. */
  433. getStyleNames() {
  434. return this._styles.keys();
  435. }
  436. /**
  437. * Returns true if style keys are present.
  438. * If more then one style property is provided - returns true only when all properties are present.
  439. *
  440. * element.hasStyle( 'color' ); // Returns true if 'border-top' style is present.
  441. * element.hasStyle( 'color', 'border-top' ); // Returns true if 'color' and 'border-top' styles are both present.
  442. *
  443. * @param {...String} property
  444. */
  445. hasStyle( ...property ) {
  446. for ( let name of property ) {
  447. if ( !this._styles.has( name ) ) {
  448. return false;
  449. }
  450. }
  451. return true;
  452. }
  453. /**
  454. * Removes specified style.
  455. *
  456. * element.removeStyle( 'color' ); // Removes 'color' style.
  457. * element.removeStyle( 'color', 'border-top' ); // Removes both 'color' and 'border-top' styles.
  458. *
  459. * @param {...String} property
  460. * @fires engine.treeView.Node#change
  461. */
  462. removeStyle( ...property ) {
  463. this._fireChange( 'ATTRIBUTES', this );
  464. property.forEach( name => this._styles.delete( name ) );
  465. }
  466. }
  467. // Parses inline styles and puts property - value pairs into styles map.
  468. // Styles map is cleared before insertion.
  469. //
  470. // @param {Map.<String, String>} stylesMap Map to insert parsed properties and values.
  471. // @param {String} stylesString Styles to parse.
  472. function parseInlineStyles( stylesMap, stylesString ) {
  473. const regex = /\s*([^:;\s]+)\s*:\s*([^;]+)\s*(?=;|$)/g;
  474. let matchStyle;
  475. stylesMap.clear();
  476. while ( ( matchStyle = regex.exec( stylesString ) ) !== null ) {
  477. stylesMap.set( matchStyle[ 1 ], matchStyle[ 2 ].trim() );
  478. }
  479. }
  480. // Parses class attribute and puts all classes into classes set.
  481. // Classes set s cleared before insertion.
  482. //
  483. // @param {Set.<String>} classesSet Set to insert parsed classes.
  484. // @param {String} classesString String with classes to parse.
  485. function parseClasses( classesSet, classesString ) {
  486. const classArray = classesString.split( /\s+/ );
  487. classesSet.clear();
  488. classArray.forEach( name => classesSet.add( name ) );
  489. }