upcastwriter.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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 module:engine/view/upcastwriter
  7. */
  8. import DocumentFragment from './documentfragment';
  9. import Element from './element';
  10. import Text from './text';
  11. import { isPlainObject } from 'lodash-es';
  12. import Position from './position';
  13. import Range from './range';
  14. import Selection from './selection';
  15. /**
  16. * View upcast writer. It provides a set of methods used to manipulate non-semantic view trees.
  17. *
  18. * It should be used only while working on a non-semantic view
  19. * (e.g. a view created from HTML string on paste).
  20. * To manipulate a view which was or is being downcasted from the the model use the
  21. * {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
  22. *
  23. * Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
  24. * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
  25. *
  26. * Unlike `DowncastWriter`, which is available in the {@link module:engine/view/view~View#change `View#change()`} block,
  27. * `UpcastWriter` can be created wherever you need it:
  28. *
  29. * const writer = new UpcastWriter( viewDocument );
  30. * const text = writer.createText( 'foo!' );
  31. *
  32. * writer.appendChild( text, someViewElement );
  33. */
  34. export default class UpcastWriter {
  35. /**
  36. * @param {module:engine/view/document~Document} document The view document instance in which this upcast writer operates.
  37. */
  38. constructor( document ) {
  39. /**
  40. * The view document instance in which this upcast writer operates.
  41. *
  42. * @readonly
  43. * @type {module:engine/view/document~Document}
  44. */
  45. this.document = document;
  46. }
  47. /**
  48. * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
  49. *
  50. * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
  51. * A list of nodes to be inserted into the created document fragment.
  52. * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
  53. */
  54. createDocumentFragment( children ) {
  55. return new DocumentFragment( this.document, children );
  56. }
  57. /**
  58. * Creates a new {@link module:engine/view/element~Element} instance.
  59. *
  60. * Attributes can be passed in various formats:
  61. *
  62. * upcastWriter.createElement( 'div', { class: 'editor', contentEditable: 'true' } ); // object
  63. * upcastWriter.createElement( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
  64. * upcastWriter.createElement( 'div', mapOfAttributes ); // map
  65. *
  66. * @param {String} name Node name.
  67. * @param {Object|Iterable} [attrs] Collection of attributes.
  68. * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
  69. * A list of nodes to be inserted into created element.
  70. * @returns {module:engine/view/element~Element} Created element.
  71. */
  72. createElement( name, attrs, children ) {
  73. return new Element( this.document, name, attrs, children );
  74. }
  75. /**
  76. * Creates a new {@link module:engine/view/text~Text} instance.
  77. *
  78. * @param {String} data The text's data.
  79. * @returns {module:engine/view/text~Text} The created text node.
  80. */
  81. createText( data ) {
  82. return new Text( this.document, data );
  83. }
  84. /**
  85. * Clones the provided element.
  86. *
  87. * @see module:engine/view/element~Element#_clone
  88. * @param {module:engine/view/element~Element} element Element to be cloned.
  89. * @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,
  90. * element will be cloned without any children.
  91. * @returns {module:engine/view/element~Element} Clone of this element.
  92. */
  93. clone( element, deep = false ) {
  94. return element._clone( deep );
  95. }
  96. /**
  97. * Appends a child node or a list of child nodes at the end of this node
  98. * and sets the parent of these nodes to this element.
  99. *
  100. * @see module:engine/view/element~Element#_appendChild
  101. * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
  102. * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} element Element
  103. * to which items will be appended.
  104. * @fires module:engine/view/node~Node#event:change
  105. * @returns {Number} Number of appended nodes.
  106. */
  107. appendChild( items, element ) {
  108. return element._appendChild( items );
  109. }
  110. /**
  111. * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to
  112. * this element.
  113. *
  114. * @see module:engine/view/element~Element#_insertChild
  115. * @param {Number} index Offset at which nodes should be inserted.
  116. * @param {module:engine/view/item~Item|Iterable.<module:engine/view/item~Item>} items Items to be inserted.
  117. * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} element Element
  118. * to which items will be inserted.
  119. * @fires module:engine/view/node~Node#event:change
  120. * @returns {Number} Number of inserted nodes.
  121. */
  122. insertChild( index, items, element ) {
  123. return element._insertChild( index, items );
  124. }
  125. /**
  126. * Removes the given number of child nodes starting at the given index and set the parent of these nodes to `null`.
  127. *
  128. * @see module:engine/view/element~Element#_removeChildren
  129. * @param {Number} index Offset from which nodes will be removed.
  130. * @param {Number} howMany Number of nodes to remove.
  131. * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} element Element
  132. * which children will be removed.
  133. * @fires module:engine/view/node~Node#event:change
  134. * @returns {Array.<module:engine/view/node~Node>} The array containing removed nodes.
  135. */
  136. removeChildren( index, howMany, element ) {
  137. return element._removeChildren( index, howMany );
  138. }
  139. /**
  140. * Removes given element from the view structure. Will not have effect on detached elements.
  141. *
  142. * @param {module:engine/view/element~Element} element Element which will be removed.
  143. * @returns {Array.<module:engine/view/node~Node>} The array containing removed nodes.
  144. */
  145. remove( element ) {
  146. const parent = element.parent;
  147. if ( parent ) {
  148. return this.removeChildren( parent.getChildIndex( element ), 1, parent );
  149. }
  150. return [];
  151. }
  152. /**
  153. * Replaces given element with the new one in the view structure. Will not have effect on detached elements.
  154. *
  155. * @param {module:engine/view/element~Element} oldElement Element which will be replaced.
  156. * @param {module:engine/view/element~Element} newElement Element which will be inserted in the place of the old element.
  157. * @returns {Boolean} Whether old element was successfully replaced.
  158. */
  159. replace( oldElement, newElement ) {
  160. const parent = oldElement.parent;
  161. if ( parent ) {
  162. const index = parent.getChildIndex( oldElement );
  163. this.removeChildren( index, 1, parent );
  164. this.insertChild( index, newElement, parent );
  165. return true;
  166. }
  167. return false;
  168. }
  169. /**
  170. * Removes given element from view structure and places its children in its position.
  171. * It does nothing if element has no parent.
  172. *
  173. * @param {module:engine/view/element~Element} element Element to unwrap.
  174. */
  175. unwrapElement( element ) {
  176. const parent = element.parent;
  177. if ( parent ) {
  178. const index = parent.getChildIndex( element );
  179. this.remove( element );
  180. this.insertChild( index, element.getChildren(), parent );
  181. }
  182. }
  183. /**
  184. * Renames element by creating a copy of a given element but with its name changed and then moving contents of the
  185. * old element to the new one.
  186. *
  187. * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
  188. *
  189. * @param {String} newName New element name.
  190. * @param {module:engine/view/element~Element} element Element to be renamed.
  191. * @returns {module:engine/view/element~Element|null} New element or null if the old element
  192. * was not replaced (happens for detached elements).
  193. */
  194. rename( newName, element ) {
  195. const newElement = new Element( this.document, newName, element.getAttributes(), element.getChildren() );
  196. return this.replace( element, newElement ) ? newElement : null;
  197. }
  198. /**
  199. * Adds or overwrites element's attribute with a specified key and value.
  200. *
  201. * writer.setAttribute( linkElement, 'href', 'http://ckeditor.com' );
  202. *
  203. * @see module:engine/view/element~Element#_setAttribute
  204. * @param {String} key Attribute key.
  205. * @param {String} value Attribute value.
  206. * @param {module:engine/view/element~Element} element Element for which attribute will be set.
  207. */
  208. setAttribute( key, value, element ) {
  209. element._setAttribute( key, value );
  210. }
  211. /**
  212. * Removes attribute from the element.
  213. *
  214. * writer.removeAttribute( linkElement, 'href' );
  215. *
  216. * @see module:engine/view/element~Element#_removeAttribute
  217. * @param {String} key Attribute key.
  218. * @param {module:engine/view/element~Element} element Element from which attribute will be removed.
  219. */
  220. removeAttribute( key, element ) {
  221. element._removeAttribute( key );
  222. }
  223. /**
  224. * Adds specified class to the element.
  225. *
  226. * writer.addClass( linkElement, 'foo' );
  227. * writer.addClass( linkElement, [ 'foo', 'bar' ] );
  228. *
  229. * @see module:engine/view/element~Element#_addClass
  230. * @param {Array.<String>|String} className Single class name or array of class names which will be added.
  231. * @param {module:engine/view/element~Element} element Element for which class will be added.
  232. */
  233. addClass( className, element ) {
  234. element._addClass( className );
  235. }
  236. /**
  237. * Removes specified class from the element.
  238. *
  239. * writer.removeClass( linkElement, 'foo' );
  240. * writer.removeClass( linkElement, [ 'foo', 'bar' ] );
  241. *
  242. * @see module:engine/view/element~Element#_removeClass
  243. * @param {Array.<String>|String} className Single class name or array of class names which will be removed.
  244. * @param {module:engine/view/element~Element} element Element from which class will be removed.
  245. */
  246. removeClass( className, element ) {
  247. element._removeClass( className );
  248. }
  249. /**
  250. * Adds style to the element.
  251. *
  252. * writer.setStyle( element, 'color', 'red' );
  253. * writer.setStyle( element, {
  254. * color: 'red',
  255. * position: 'fixed'
  256. * } );
  257. *
  258. * **Note**: This method can work with normalized style names if
  259. * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
  260. * See {@link module:engine/view/stylesmap~StylesMap#set `StylesMap#set()`} for details.
  261. *
  262. * @see module:engine/view/element~Element#_setStyle
  263. * @param {String|Object} property Property name or object with key - value pairs.
  264. * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
  265. * @param {module:engine/view/element~Element} element Element for which style will be added.
  266. */
  267. setStyle( property, value, element ) {
  268. if ( isPlainObject( property ) && element === undefined ) {
  269. element = value;
  270. }
  271. element._setStyle( property, value );
  272. }
  273. /**
  274. * Removes specified style from the element.
  275. *
  276. * writer.removeStyle( element, 'color' ); // Removes 'color' style.
  277. * writer.removeStyle( element, [ 'color', 'border-top' ] ); // Removes both 'color' and 'border-top' styles.
  278. *
  279. * **Note**: This method can work with normalized style names if
  280. * {@link module:engine/controller/datacontroller~DataController#addStyleProcessorRules a particular style processor rule is enabled}.
  281. * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
  282. *
  283. * @see module:engine/view/element~Element#_removeStyle
  284. * @param {Array.<String>|String} property Style property name or names to be removed.
  285. * @param {module:engine/view/element~Element} element Element from which style will be removed.
  286. */
  287. removeStyle( property, element ) {
  288. element._removeStyle( property );
  289. }
  290. /**
  291. * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
  292. * so they can be used to add special data to elements.
  293. *
  294. * @see module:engine/view/element~Element#_setCustomProperty
  295. * @param {String|Symbol} key Custom property name/key.
  296. * @param {*} value Custom property value to be stored.
  297. * @param {module:engine/view/element~Element} element Element for which custom property will be set.
  298. */
  299. setCustomProperty( key, value, element ) {
  300. element._setCustomProperty( key, value );
  301. }
  302. /**
  303. * Removes a custom property stored under the given key.
  304. *
  305. * @see module:engine/view/element~Element#_removeCustomProperty
  306. * @param {String|Symbol} key Name/key of the custom property to be removed.
  307. * @param {module:engine/view/element~Element} element Element from which the custom property will be removed.
  308. * @returns {Boolean} Returns true if property was removed.
  309. */
  310. removeCustomProperty( key, element ) {
  311. return element._removeCustomProperty( key );
  312. }
  313. /**
  314. * Creates position at the given location. The location can be specified as:
  315. *
  316. * * a {@link module:engine/view/position~Position position},
  317. * * parent element and offset (offset defaults to `0`),
  318. * * parent element and `'end'` (sets position at the end of that element),
  319. * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
  320. *
  321. * This method is a shortcut to other constructors such as:
  322. *
  323. * * {@link #createPositionBefore},
  324. * * {@link #createPositionAfter},
  325. *
  326. * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
  327. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  328. * first parameter is a {@link module:engine/view/item~Item view item}.
  329. */
  330. createPositionAt( itemOrPosition, offset ) {
  331. return Position._createAt( itemOrPosition, offset );
  332. }
  333. /**
  334. * Creates a new position after given view item.
  335. *
  336. * @param {module:engine/view/item~Item} item View item after which the position should be located.
  337. * @returns {module:engine/view/position~Position}
  338. */
  339. createPositionAfter( item ) {
  340. return Position._createAfter( item );
  341. }
  342. /**
  343. * Creates a new position before given view item.
  344. *
  345. * @param {module:engine/view/item~Item} item View item before which the position should be located.
  346. * @returns {module:engine/view/position~Position}
  347. */
  348. createPositionBefore( item ) {
  349. return Position._createBefore( item );
  350. }
  351. /**
  352. * Creates a range spanning from `start` position to `end` position.
  353. *
  354. * **Note:** This factory method creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
  355. *
  356. * @param {module:engine/view/position~Position} start Start position.
  357. * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
  358. * @returns {module:engine/view/range~Range}
  359. */
  360. createRange( start, end ) {
  361. return new Range( start, end );
  362. }
  363. /**
  364. * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
  365. *
  366. * @param {module:engine/view/item~Item} item
  367. * @returns {module:engine/view/range~Range}
  368. */
  369. createRangeOn( item ) {
  370. return Range._createOn( item );
  371. }
  372. /**
  373. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  374. * that element and ends after the last child of that element.
  375. *
  376. * @param {module:engine/view/element~Element} element Element which is a parent for the range.
  377. * @returns {module:engine/view/range~Range}
  378. */
  379. createRangeIn( element ) {
  380. return Range._createIn( element );
  381. }
  382. /**
  383. * Creates a new {@link module:engine/view/selection~Selection} instance.
  384. *
  385. * // Creates empty selection without ranges.
  386. * const selection = writer.createSelection();
  387. *
  388. * // Creates selection at the given range.
  389. * const range = writer.createRange( start, end );
  390. * const selection = writer.createSelection( range );
  391. *
  392. * // Creates selection at the given ranges
  393. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
  394. * const selection = writer.createSelection( ranges );
  395. *
  396. * // Creates selection from the other selection.
  397. * const otherSelection = writer.createSelection();
  398. * const selection = writer.createSelection( otherSelection );
  399. *
  400. * // Creates selection from the document selection.
  401. * const selection = writer.createSelection( editor.editing.view.document.selection );
  402. *
  403. * // Creates selection at the given position.
  404. * const position = writer.createPositionFromPath( root, path );
  405. * const selection = writer.createSelection( position );
  406. *
  407. * // Creates collapsed selection at the position of given item and offset.
  408. * const paragraph = writer.createContainerElement( 'paragraph' );
  409. * const selection = writer.createSelection( paragraph, offset );
  410. *
  411. * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
  412. * // first child of that element and ends after the last child of that element.
  413. * const selection = writer.createSelection( paragraph, 'in' );
  414. *
  415. * // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
  416. * // just after the item.
  417. * const selection = writer.createSelection( paragraph, 'on' );
  418. *
  419. * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
  420. *
  421. * // Creates backward selection.
  422. * const selection = writer.createSelection( range, { backward: true } );
  423. *
  424. * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
  425. * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
  426. * represented in other way, for example by applying proper CSS class.
  427. *
  428. * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
  429. * (and be properly handled by screen readers).
  430. *
  431. * // Creates fake selection with label.
  432. * const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
  433. *
  434. * @param {module:engine/view/selection~Selectable} [selectable=null]
  435. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
  436. * @param {Object} [options]
  437. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  438. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
  439. * @param {String} [options.label] Label for the fake selection.
  440. * @returns {module:engine/view/selection~Selection}
  441. */
  442. createSelection( selectable, placeOrOffset, options ) {
  443. return new Selection( selectable, placeOrOffset, options );
  444. }
  445. }