model-to-view-converters.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ModelRange from '../model/range';
  6. import ModelSelection from '../model/selection';
  7. import ModelElement from '../model/element';
  8. import ViewElement from '../view/element';
  9. import ViewAttributeElement from '../view/attributeelement';
  10. import ViewText from '../view/text';
  11. import ViewRange from '../view/range';
  12. import viewWriter from '../view/writer';
  13. /**
  14. * Contains model to view converters for
  15. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
  16. *
  17. * @module engine/conversion/model-to-view-converters
  18. */
  19. /**
  20. * Function factory, creates a converter that converts node insertion changes from the model to the view.
  21. * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
  22. * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
  23. * dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}.
  24. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  25. * The result of the function will be inserted to the view.
  26. *
  27. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  28. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
  29. *
  30. * modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
  31. *
  32. * modelDispatcher.on(
  33. * 'insert:myElem',
  34. * insertElement( ( data, consumable, conversionApi ) => {
  35. * let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
  36. *
  37. * // Do something fancy with myElem using data/consumable/conversionApi ...
  38. *
  39. * return myElem;
  40. * }
  41. * ) );
  42. *
  43. * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
  44. * will be inserted.
  45. * @returns {Function} Insert element event converter.
  46. */
  47. export function insertElement( elementCreator ) {
  48. return ( evt, data, consumable, conversionApi ) => {
  49. const viewElement = ( elementCreator instanceof ViewElement ) ?
  50. elementCreator.clone( true ) :
  51. elementCreator( data, consumable, conversionApi );
  52. if ( !viewElement ) {
  53. return;
  54. }
  55. if ( !consumable.consume( data.item, 'insert' ) ) {
  56. return;
  57. }
  58. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  59. conversionApi.mapper.bindElements( data.item, viewElement );
  60. viewWriter.insert( viewPosition, viewElement );
  61. };
  62. }
  63. /**
  64. * Function factory, creates a default model-to-view converter for text insertion changes.
  65. *
  66. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  67. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  68. *
  69. * modelDispatcher.on( 'insert:$text', insertText() );
  70. *
  71. * @returns {Function} Insert text event converter.
  72. */
  73. export function insertText() {
  74. return ( evt, data, consumable, conversionApi ) => {
  75. if ( !consumable.consume( data.item, 'insert' ) ) {
  76. return;
  77. }
  78. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  79. const viewText = new ViewText( data.item.data );
  80. viewWriter.insert( viewPosition, viewText );
  81. };
  82. }
  83. /**
  84. * Function factory, creates a default model-to-view converter for node remove changes.
  85. *
  86. * modelDispatcher.on( 'remove', remove() );
  87. *
  88. * @returns {Function} Remove event converter.
  89. */
  90. export function remove() {
  91. return ( evt, data, conversionApi ) => {
  92. // Find view range start position by mapping model position at which the remove happened.
  93. const viewStart = conversionApi.mapper.toViewPosition( data.position );
  94. const modelEnd = data.position.getShiftedBy( data.length );
  95. const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } );
  96. const viewRange = new ViewRange( viewStart, viewEnd );
  97. // Trim the range to remove in case some UI elements are on the view range boundaries.
  98. const removed = viewWriter.remove( viewRange.getTrimmed() );
  99. // After the range is removed, unbind all view elements from the model.
  100. // Range inside view document fragment is used to unbind deeply.
  101. for ( const child of ViewRange.createIn( removed ).getItems() ) {
  102. conversionApi.mapper.unbindViewElement( child );
  103. }
  104. };
  105. }
  106. /**
  107. * Function factory, creates a converter that converts marker adding change to the view ui element.
  108. * The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
  109. * In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
  110. * and at the end of the range.
  111. *
  112. * **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
  113. * uses marker as "model source of data". This means that view ui element does not have corresponding model element.
  114. *
  115. * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which
  116. * will be inserted.
  117. * @returns {Function} Insert element event converter.
  118. */
  119. export function insertUIElement( elementCreator ) {
  120. return ( evt, data, consumable, conversionApi ) => {
  121. let viewStartElement, viewEndElement;
  122. // Create two view elements. One will be inserted at the beginning of marker, one at the end.
  123. // If marker is collapsed, only "opening" element will be inserted.
  124. if ( elementCreator instanceof ViewElement ) {
  125. viewStartElement = elementCreator.clone( true );
  126. viewEndElement = elementCreator.clone( true );
  127. } else {
  128. data.isOpening = true;
  129. viewStartElement = elementCreator( data, conversionApi );
  130. data.isOpening = false;
  131. viewEndElement = elementCreator( data, conversionApi );
  132. }
  133. if ( !viewStartElement || !viewEndElement ) {
  134. return;
  135. }
  136. const markerRange = data.markerRange;
  137. // Marker that is collapsed has consumable build differently that non-collapsed one.
  138. // For more information see `addMarker` event description.
  139. // If marker's range is collapsed - check if it can be consumed.
  140. if ( markerRange.isCollapsed && !consumable.consume( markerRange, evt.name ) ) {
  141. return;
  142. }
  143. // If marker's range is not collapsed - consume all items inside.
  144. for ( const value of markerRange ) {
  145. if ( !consumable.consume( value.item, evt.name ) ) {
  146. return;
  147. }
  148. }
  149. const mapper = conversionApi.mapper;
  150. // Add "opening" element.
  151. viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement );
  152. // Add "closing" element only if range is not collapsed.
  153. if ( !markerRange.isCollapsed ) {
  154. viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement );
  155. }
  156. evt.stop();
  157. };
  158. }
  159. /**
  160. * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
  161. * basing on marker remove change.
  162. *
  163. * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
  164. * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
  165. * @returns {Function} Remove ui element converter.
  166. */
  167. export function removeUIElement( elementCreator ) {
  168. return ( evt, data, conversionApi ) => {
  169. let viewStartElement, viewEndElement;
  170. // Create two view elements. One will be used to remove "opening element", the other for "closing element".
  171. // If marker was collapsed, only "opening" element will be removed.
  172. if ( elementCreator instanceof ViewElement ) {
  173. viewStartElement = elementCreator.clone( true );
  174. viewEndElement = elementCreator.clone( true );
  175. } else {
  176. data.isOpening = true;
  177. viewStartElement = elementCreator( data, conversionApi );
  178. data.isOpening = false;
  179. viewEndElement = elementCreator( data, conversionApi );
  180. }
  181. if ( !viewStartElement || !viewEndElement ) {
  182. return;
  183. }
  184. const markerRange = data.markerRange;
  185. // When removing the ui elements, we map the model range to view twice, because that view range
  186. // may change after the first clearing.
  187. if ( !markerRange.isCollapsed ) {
  188. viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement );
  189. }
  190. // Remove "opening" element.
  191. viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement );
  192. evt.stop();
  193. };
  194. }
  195. /**
  196. * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
  197. *
  198. * Attributes from model are converted to the view element attributes in the view. You may provide a custom function to generate
  199. * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view elements
  200. * attributes on 1-to-1 basis.
  201. *
  202. * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
  203. *
  204. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  205. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  206. *
  207. * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( data ) => {
  208. * // Change attribute key from `customAttr` to `class` in view.
  209. * const key = 'class';
  210. * let value = data.attributeNewValue;
  211. *
  212. * // Force attribute value to 'empty' if the model element is empty.
  213. * if ( data.item.childCount === 0 ) {
  214. * value = 'empty';
  215. * }
  216. *
  217. * // Return key-value pair.
  218. * return { key, value };
  219. * } ) );
  220. *
  221. * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  222. * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
  223. * The function is passed all the parameters of the
  224. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute} event.
  225. * @returns {Function} Set/change attribute converter.
  226. */
  227. export function changeAttribute( attributeCreator ) {
  228. attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
  229. return ( evt, data, consumable, conversionApi ) => {
  230. if ( !consumable.consume( data.item, evt.name ) ) {
  231. return;
  232. }
  233. const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
  234. if ( data.attributeNewValue !== null ) {
  235. conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
  236. } else {
  237. conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
  238. }
  239. };
  240. }
  241. /**
  242. * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view.
  243. * Also can be used to convert selection attributes. In that case, an empty attribute element will be created and the
  244. * selection will be put inside it.
  245. *
  246. * Attributes from model are converted to a view element that will be wrapping those view nodes that are bound to
  247. * model elements having given attribute. This is useful for attributes like `bold`, which may be set on text nodes in model
  248. * but are represented as an element in the view:
  249. *
  250. * [paragraph] MODEL ====> VIEW <p>
  251. * |- a {bold: true} |- <b>
  252. * |- b {bold: true} | |- ab
  253. * |- c |- c
  254. *
  255. * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
  256. * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
  257. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute event}.
  258. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  259. * The result of the function will be the wrapping element.
  260. * When provided `Function` does not return element, then will be no conversion.
  261. *
  262. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  263. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  264. *
  265. * modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
  266. *
  267. * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  268. * be used for wrapping.
  269. * @returns {Function} Set/change attribute converter.
  270. */
  271. export function wrap( elementCreator ) {
  272. return ( evt, data, consumable, conversionApi ) => {
  273. // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed
  274. // or the attribute was removed.
  275. const oldViewElement = ( elementCreator instanceof ViewElement ) ?
  276. elementCreator.clone( true ) :
  277. elementCreator( data.attributeOldValue, data, consumable, conversionApi );
  278. // Create node to wrap with.
  279. const newViewElement = ( elementCreator instanceof ViewElement ) ?
  280. elementCreator.clone( true ) :
  281. elementCreator( data.attributeNewValue, data, consumable, conversionApi );
  282. if ( !oldViewElement && !newViewElement ) {
  283. return;
  284. }
  285. if ( !consumable.consume( data.item, evt.name ) ) {
  286. return;
  287. }
  288. if ( data.item instanceof ModelSelection ) {
  289. // Selection attribute conversion.
  290. viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection );
  291. } else {
  292. // Node attribute conversion.
  293. let viewRange = conversionApi.mapper.toViewRange( data.range );
  294. // First, unwrap the range from current wrapper.
  295. if ( data.attributeOldValue !== null ) {
  296. viewRange = viewWriter.unwrap( viewRange, oldViewElement );
  297. }
  298. if ( data.attributeNewValue !== null ) {
  299. viewWriter.wrap( viewRange, newViewElement );
  300. }
  301. }
  302. };
  303. }
  304. /**
  305. * Function factory, creates converter that converts text inside marker's range. Converter wraps the text with
  306. * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
  307. * See {link module:engine/conversion/model-to-view-converters~createViewElementFromHighlightDescriptor}.
  308. *
  309. * Also can be used to convert selection that is inside a marker. In that case, an empty attribute element will be
  310. * created and the selection will be put inside it.
  311. *
  312. * If the highlight descriptor will not provide `priority` property, `10` will be used.
  313. *
  314. * If the highlight descriptor will not provide `id` property, name of the marker will be used.
  315. *
  316. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
  317. * @return {Function}
  318. */
  319. export function highlightText( highlightDescriptor ) {
  320. return ( evt, data, consumable, conversionApi ) => {
  321. if ( data.markerRange.isCollapsed ) {
  322. return;
  323. }
  324. if ( !( data.item instanceof ModelSelection ) && !data.item.is( 'textProxy' ) ) {
  325. return;
  326. }
  327. const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
  328. if ( !descriptor ) {
  329. return;
  330. }
  331. if ( !consumable.consume( data.item, evt.name ) ) {
  332. return;
  333. }
  334. const viewElement = createViewElementFromHighlightDescriptor( descriptor );
  335. if ( data.item instanceof ModelSelection ) {
  336. viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection );
  337. } else {
  338. const viewRange = conversionApi.mapper.toViewRange( data.range );
  339. viewWriter.wrap( viewRange, viewElement );
  340. }
  341. };
  342. }
  343. /**
  344. * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range.
  345. *
  346. * The converter checks if an element has `addHighlight` function stored as
  347. * {@link module:engine/view/element~Element#setCustomProperty custom property} and, if so, uses it to apply the highlight.
  348. * In such case converter will consume all element's children, assuming that they were handled by element itself.
  349. *
  350. * When `addHighlight` custom property is not present, element is not converted in any special way.
  351. * This means that converters will proceed to convert element's child nodes.
  352. *
  353. * If the highlight descriptor will not provide `priority` property, `10` will be used.
  354. *
  355. * If the highlight descriptor will not provide `id` property, name of the marker will be used.
  356. *
  357. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
  358. * @return {Function}
  359. */
  360. export function highlightElement( highlightDescriptor ) {
  361. return ( evt, data, consumable, conversionApi ) => {
  362. if ( data.markerRange.isCollapsed ) {
  363. return;
  364. }
  365. if ( !( data.item instanceof ModelElement ) ) {
  366. return;
  367. }
  368. const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
  369. if ( !descriptor ) {
  370. return;
  371. }
  372. if ( !consumable.test( data.item, evt.name ) ) {
  373. return;
  374. }
  375. const viewElement = conversionApi.mapper.toViewElement( data.item );
  376. if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) {
  377. // Consume element itself.
  378. consumable.consume( data.item, evt.name );
  379. // Consume all children nodes.
  380. for ( const value of ModelRange.createIn( data.item ) ) {
  381. consumable.consume( value.item, evt.name );
  382. }
  383. viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor );
  384. }
  385. };
  386. }
  387. /**
  388. * Function factory, creates a converter that converts model marker remove to the view.
  389. *
  390. * Both text nodes and elements are handled by this converter by they are handled a bit differently.
  391. *
  392. * Text nodes are unwrapped using {@link module:engine/view/attributeelement~AttributeElement} created from provided
  393. * highlight descriptor. See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
  394. *
  395. * For elements, the converter checks if an element has `removeHighlight` function stored as
  396. * {@link module:engine/view/element~Element#setCustomProperty custom property}. If so, it uses it to remove the highlight.
  397. * In such case, children of that element will not be converted.
  398. *
  399. * When `removeHighlight` is not present, element is not converted in any special way.
  400. * Instead converter will proceed to convert element's child nodes.
  401. *
  402. * If the highlight descriptor will not provide `priority` property, `10` will be used.
  403. *
  404. * If the highlight descriptor will not provide `id` property, name of the marker will be used.
  405. *
  406. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor
  407. * @return {Function}
  408. */
  409. export function removeHighlight( highlightDescriptor ) {
  410. return ( evt, data, conversionApi ) => {
  411. // This conversion makes sense only for non-collapsed range.
  412. if ( data.markerRange.isCollapsed ) {
  413. return;
  414. }
  415. const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi );
  416. if ( !descriptor ) {
  417. return;
  418. }
  419. const viewRange = conversionApi.mapper.toViewRange( data.markerRange );
  420. // Retrieve all items in the affected range. We will process them and remove highlight from them appropriately.
  421. const items = new Set( viewRange.getItems() );
  422. // First, iterate through all items and remove highlight from those container elements that have custom highlight handling.
  423. for ( const item of items ) {
  424. if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) {
  425. item.getCustomProperty( 'removeHighlight' )( item, descriptor.id );
  426. // If container element had custom handling, remove all it's children from further processing.
  427. for ( const descendant of ViewRange.createIn( item ) ) {
  428. items.delete( descendant );
  429. }
  430. }
  431. }
  432. // Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end
  433. // to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged).
  434. const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor );
  435. for ( const item of Array.from( items ).reverse() ) {
  436. if ( item.is( 'textProxy' ) ) {
  437. viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement );
  438. }
  439. }
  440. };
  441. }
  442. // Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter.
  443. function _prepareDescriptor( highlightDescriptor, data, conversionApi ) {
  444. // If passed descriptor is a creator function, call it. If not, just use passed value.
  445. const descriptor = typeof highlightDescriptor == 'function' ?
  446. highlightDescriptor( data, conversionApi ) :
  447. highlightDescriptor;
  448. if ( !descriptor ) {
  449. return null;
  450. }
  451. // Apply default descriptor priority.
  452. if ( !descriptor.priority ) {
  453. descriptor.priority = 10;
  454. }
  455. // Default descriptor id is marker name.
  456. if ( !descriptor.id ) {
  457. descriptor.id = data.markerName;
  458. }
  459. return descriptor;
  460. }
  461. /**
  462. * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
  463. * provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority
  464. * is not provided in descriptor - default priority will be used.
  465. *
  466. * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor
  467. * @return {module:engine/conversion/model-to-view-converters~HighlightAttributeElement}
  468. */
  469. export function createViewElementFromHighlightDescriptor( descriptor ) {
  470. const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes );
  471. if ( descriptor.class ) {
  472. const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ];
  473. viewElement.addClass( ...cssClasses );
  474. }
  475. if ( descriptor.priority ) {
  476. viewElement.priority = descriptor.priority;
  477. }
  478. viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id );
  479. return viewElement;
  480. }
  481. /**
  482. * Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in
  483. * marker-to-highlight conversion.
  484. *
  485. * The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement}
  486. * is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method.
  487. *
  488. * For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight
  489. * conversion basing on {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object.
  490. * `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar.
  491. */
  492. class HighlightAttributeElement extends ViewAttributeElement {
  493. isSimilar( otherElement ) {
  494. if ( otherElement.is( 'attributeElement' ) ) {
  495. return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' );
  496. }
  497. return false;
  498. }
  499. }
  500. /**
  501. * Object describing how the content highlight should be created in the view.
  502. *
  503. * Each text node contained in the highlight will be wrapped with `span` element with CSS class(es), attributes and priority
  504. * described by this object.
  505. *
  506. * Each element can handle displaying the highlight separately by providing `addHighlight` and `removeHighlight` custom
  507. * properties:
  508. * * `HighlightDescriptor` is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to
  509. * the element,
  510. * * descriptor id is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given
  511. * id from the element.
  512. *
  513. * @typedef {Object} module:engine/conversion/model-to-view-converters~HighlightDescriptor
  514. *
  515. * @property {String|Array.<String>} class CSS class or array of classes to set. If descriptor is used to
  516. * create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set
  517. * on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element,
  518. * usually those class will be set on that element, however this depends on how the element converts the descriptor.
  519. *
  520. * @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name.
  521. *
  522. * @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create
  523. * {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's
  524. * {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element,
  525. * the priority will be used to determine which descriptor is more important.
  526. *
  527. * @property {Object} [attributes] Attributes to set. If descriptor is used to create
  528. * {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that
  529. * {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those
  530. * attributes will be set on that element, however this depends on how the element converts the descriptor.
  531. */