markercollection.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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/markercollection
  7. */
  8. import LiveRange from './liverange';
  9. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  10. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  11. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  12. /**
  13. * The collection of all {@link module:engine/model/markercollection~Marker markers} attached to the document.
  14. * It lets you {@link module:engine/model/markercollection~MarkerCollection#get get} markers or track them using
  15. * {@link module:engine/model/markercollection~MarkerCollection#event:update} event.
  16. *
  17. * To create, change or remove makers use {@link module:engine/model/writer~Writer model writers'} methods:
  18. * {@link module:engine/model/writer~Writer#addMarker} or {@link module:engine/model/writer~Writer#removeMarker}. Since
  19. * the writer is the only proper way to change the data model it is not possible to change markers directly using this
  20. * collection. All markers created by the writer will be automatically added to this collection.
  21. *
  22. * By default there is one marker collection available as {@link module:engine/model/model~Model#markers model property}.
  23. *
  24. * @see module:engine/model/markercollection~Marker
  25. */
  26. export default class MarkerCollection {
  27. /**
  28. * Creates a markers collection.
  29. */
  30. constructor() {
  31. /**
  32. * Stores {@link ~Marker markers} added to the collection.
  33. *
  34. * @private
  35. * @member {Map} #_markers
  36. */
  37. this._markers = new Map();
  38. }
  39. /**
  40. * Iterable interface.
  41. *
  42. * Iterates over all {@link ~Marker markers} added to the collection.
  43. *
  44. * @returns {Iterable}
  45. */
  46. [ Symbol.iterator ]() {
  47. return this._markers.values();
  48. }
  49. /**
  50. * Checks if marker with given `markerName` is in the collection.
  51. *
  52. * @param {String} markerName Marker name.
  53. * @returns {Boolean} `true` if marker with given `markerName` is in the collection, `false` otherwise.
  54. */
  55. has( markerName ) {
  56. return this._markers.has( markerName );
  57. }
  58. /**
  59. * Returns {@link ~Marker marker} with given `markerName`.
  60. *
  61. * @param {String} markerName Name of marker to get.
  62. * @returns {module:engine/model/markercollection~Marker|null} Marker with given name or `null` if such marker was
  63. * not added to the collection.
  64. */
  65. get( markerName ) {
  66. return this._markers.get( markerName ) || null;
  67. }
  68. /**
  69. * Creates and adds a {@link ~Marker marker} to the `MarkerCollection` with given name on given
  70. * {@link module:engine/model/range~Range range}.
  71. *
  72. * If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed), the marker in
  73. * collection is updated and {@link module:engine/model/markercollection~MarkerCollection#event:update} event is fired
  74. * but only if there was a change (marker range or {@link module:engine/model/markercollection~Marker#managedUsingOperations}
  75. * flag has changed.
  76. *
  77. * @protected
  78. * @fires module:engine/model/markercollection~MarkerCollection#event:update
  79. * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
  80. * @param {module:engine/model/range~Range} range Marker range.
  81. * @param {Boolean} [managedUsingOperations=false] Specifies whether the marker is managed using operations.
  82. * @param {Boolean} [affectsData=false] Specifies whether the marker affects the data produced by the data pipeline
  83. * (is persisted in the editor's data).
  84. * @returns {module:engine/model/markercollection~Marker} `Marker` instance which was added or updated.
  85. */
  86. _set( markerOrName, range, managedUsingOperations = false, affectsData = false ) {
  87. const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
  88. if ( markerName.includes( ',' ) ) {
  89. /**
  90. * Marker name cannot contain the "," character.
  91. *
  92. * @error markercollection-incorrect-marker-name
  93. */
  94. throw new CKEditorError( 'markercollection-incorrect-marker-name: Marker name cannot contain "," character.', this );
  95. }
  96. const oldMarker = this._markers.get( markerName );
  97. if ( oldMarker ) {
  98. const oldRange = oldMarker.getRange();
  99. let hasChanged = false;
  100. if ( !oldRange.isEqual( range ) ) {
  101. oldMarker._attachLiveRange( LiveRange.fromRange( range ) );
  102. hasChanged = true;
  103. }
  104. if ( managedUsingOperations != oldMarker.managedUsingOperations ) {
  105. oldMarker._managedUsingOperations = managedUsingOperations;
  106. hasChanged = true;
  107. }
  108. if ( typeof affectsData === 'boolean' && affectsData != oldMarker.affectsData ) {
  109. oldMarker._affectsData = affectsData;
  110. hasChanged = true;
  111. }
  112. if ( hasChanged ) {
  113. this.fire( 'update:' + markerName, oldMarker, oldRange, range );
  114. }
  115. return oldMarker;
  116. }
  117. const liveRange = LiveRange.fromRange( range );
  118. const marker = new Marker( markerName, liveRange, managedUsingOperations, affectsData );
  119. this._markers.set( markerName, marker );
  120. this.fire( 'update:' + markerName, marker, null, range );
  121. return marker;
  122. }
  123. /**
  124. * Removes given {@link ~Marker marker} or a marker with given name from the `MarkerCollection`.
  125. *
  126. * @protected
  127. * @fires module:engine/model/markercollection~MarkerCollection#event:update
  128. * @param {String} markerOrName Marker or name of a marker to remove.
  129. * @returns {Boolean} `true` if marker was found and removed, `false` otherwise.
  130. */
  131. _remove( markerOrName ) {
  132. const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
  133. const oldMarker = this._markers.get( markerName );
  134. if ( oldMarker ) {
  135. this._markers.delete( markerName );
  136. this.fire( 'update:' + markerName, oldMarker, oldMarker.getRange(), null );
  137. this._destroyMarker( oldMarker );
  138. return true;
  139. }
  140. return false;
  141. }
  142. /**
  143. * Fires an {@link module:engine/model/markercollection~MarkerCollection#event:update} event for the given {@link ~Marker marker}
  144. * but does not change the marker. Useful to force {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast
  145. * conversion} for the marker.
  146. *
  147. * @protected
  148. * @fires module:engine/model/markercollection~MarkerCollection#event:update
  149. * @param {String} markerOrName Marker or name of a marker to refresh.
  150. */
  151. _refresh( markerOrName ) {
  152. const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
  153. const marker = this._markers.get( markerName );
  154. if ( !marker ) {
  155. throw new CKEditorError( 'markercollection-refresh-marker-not-exists: Marker with provided name does not exists.', this );
  156. }
  157. const range = marker.getRange();
  158. this.fire( 'update:' + markerName, marker, range, range, marker.managedUsingOperations, marker.affectsData );
  159. }
  160. /**
  161. * Returns iterator that iterates over all markers, which ranges contain given {@link module:engine/model/position~Position position}.
  162. *
  163. * @param {module:engine/model/position~Position} position
  164. * @returns {Iterable.<module:engine/model/markercollection~Marker>}
  165. */
  166. * getMarkersAtPosition( position ) {
  167. for ( const marker of this ) {
  168. if ( marker.getRange().containsPosition( position ) ) {
  169. yield marker;
  170. }
  171. }
  172. }
  173. /**
  174. * Returns iterator that iterates over all markers, which intersects with given {@link module:engine/model/range~Range range}.
  175. *
  176. * @param {module:engine/model/range~Range} range
  177. * @returns {Iterable.<module:engine/model/markercollection~Marker>}
  178. */
  179. * getMarkersIntersectingRange( range ) {
  180. for ( const marker of this ) {
  181. if ( marker.getRange().getIntersection( range ) !== null ) {
  182. yield marker;
  183. }
  184. }
  185. }
  186. /**
  187. * Destroys marker collection and all markers inside it.
  188. */
  189. destroy() {
  190. for ( const marker of this._markers.values() ) {
  191. this._destroyMarker( marker );
  192. }
  193. this._markers = null;
  194. this.stopListening();
  195. }
  196. /**
  197. * Iterates over all markers that starts with given `prefix`.
  198. *
  199. * const markerFooA = markersCollection.set( 'foo:a', rangeFooA );
  200. * const markerFooB = markersCollection.set( 'foo:b', rangeFooB );
  201. * const markerBarA = markersCollection.set( 'bar:a', rangeBarA );
  202. * const markerFooBarA = markersCollection.set( 'foobar:a', rangeFooBarA );
  203. * Array.from( markersCollection.getMarkersGroup( 'foo' ) ); // [ markerFooA, markerFooB ]
  204. * Array.from( markersCollection.getMarkersGroup( 'a' ) ); // []
  205. *
  206. * @param prefix
  207. * @returns {Iterable.<module:engine/model/markercollection~Marker>}
  208. */
  209. * getMarkersGroup( prefix ) {
  210. for ( const marker of this._markers.values() ) {
  211. if ( marker.name.startsWith( prefix + ':' ) ) {
  212. yield marker;
  213. }
  214. }
  215. }
  216. /**
  217. * Destroys the marker.
  218. *
  219. * @private
  220. * @param {module:engine/model/markercollection~Marker} marker Marker to destroy.
  221. */
  222. _destroyMarker( marker ) {
  223. marker.stopListening();
  224. marker._detachLiveRange();
  225. }
  226. /**
  227. * Fired whenever marker is added, updated or removed from `MarkerCollection`.
  228. *
  229. * @event update
  230. * @param {module:engine/model/markercollection~Marker} marker Updated Marker.
  231. * @param {module:engine/model/range~Range|null} oldRange Marker range before the update. When is not defined it
  232. * means that marker is just added.
  233. * @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it
  234. * means that marker is just removed.
  235. */
  236. }
  237. mix( MarkerCollection, EmitterMixin );
  238. /**
  239. * `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
  240. * part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
  241. * model document tree, markers are not stored directly in document tree but in
  242. * {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
  243. * additional meaning to the part of a model document between marker start and marker end.
  244. *
  245. * In this sense, markers are similar to adding and converting attributes on nodes. The difference is that attribute is
  246. * connected with a given node (e.g. a character is bold no matter if it gets moved or content around it changes).
  247. * Markers on the other hand are continuous ranges and are characterized by their start and end position. This means that
  248. * any character in the marker is marked by the marker. For example, if a character is moved outside of marker it stops being
  249. * "special" and the marker is shrunk. Similarly, when a character is moved into the marker from other place in document
  250. * model, it starts being "special" and the marker is enlarged.
  251. *
  252. * Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes
  253. * and then trying to find that part of document would require traversing whole document tree. Marker gives instant access
  254. * to the range which it is marking at the moment.
  255. *
  256. * Markers are built from a name and a range.
  257. *
  258. * Range of the marker is updated automatically when document changes, using
  259. * {@link module:engine/model/liverange~LiveRange live range} mechanism.
  260. *
  261. * Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
  262. * using common prefixes, separated with `:`, for example: `user:john` or `search:3`. That's useful in term of creating
  263. * namespaces for custom elements (e.g. comments, highlights). You can use this prefixes in
  264. * {@link module:engine/model/markercollection~MarkerCollection#event:update} listeners to listen on changes in a group of markers.
  265. * For instance: `model.markers.on( 'update:user', callback );` will be called whenever any `user:*` markers changes.
  266. *
  267. * There are two types of markers.
  268. *
  269. * 1. Markers managed directly, without using operations. They are added directly by {@link module:engine/model/writer~Writer}
  270. * to the {@link module:engine/model/markercollection~MarkerCollection} without any additional mechanism. They can be used
  271. * as bookmarks or visual markers. They are great for showing results of the find, or select link when the focus is in the input.
  272. *
  273. * 1. Markers managed using operations. These markers are also stored in {@link module:engine/model/markercollection~MarkerCollection}
  274. * but changes in these markers is managed the same way all other changes in the model structure - using operations.
  275. * Therefore, they are handled in the undo stack and synchronized between clients if the collaboration plugin is enabled.
  276. * This type of markers is useful for solutions like spell checking or comments.
  277. *
  278. * Both type of them should be added / updated by {@link module:engine/model/writer~Writer#addMarker}
  279. * and removed by {@link module:engine/model/writer~Writer#removeMarker} methods.
  280. *
  281. * model.change( ( writer ) => {
  282. * const marker = writer.addMarker( name, { range, usingOperation: true } );
  283. *
  284. * // ...
  285. *
  286. * writer.removeMarker( marker );
  287. * } );
  288. *
  289. * See {@link module:engine/model/writer~Writer} to find more examples.
  290. *
  291. * Since markers need to track change in the document, for efficiency reasons, it is best to create and keep as little
  292. * markers as possible and remove them as soon as they are not needed anymore.
  293. *
  294. * Markers can be downcasted and upcasted.
  295. *
  296. * Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and
  297. * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events.
  298. * Use {@link module:engine/conversion/downcasthelpers downcast converters} or attach a custom converter to mentioned events.
  299. * For {@link module:engine/controller/datacontroller~DataController data pipeline}, marker should be downcasted to an element.
  300. * Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcasthelpers upcast converters} or
  301. * attach a custom converter to {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element}.
  302. *
  303. * `Marker` instances are created and destroyed only by {@link ~MarkerCollection MarkerCollection}.
  304. */
  305. class Marker {
  306. /**
  307. * Creates a marker instance.
  308. *
  309. * @param {String} name Marker name.
  310. * @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
  311. * @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
  312. * @param {Boolean} affectsData Specifies whether the marker affects the data produced by the data pipeline
  313. * (is persisted in the editor's data).
  314. */
  315. constructor( name, liveRange, managedUsingOperations, affectsData ) {
  316. /**
  317. * Marker's name.
  318. *
  319. * @readonly
  320. * @type {String}
  321. */
  322. this.name = name;
  323. /**
  324. * Range marked by the marker.
  325. *
  326. * @protected
  327. * @member {module:engine/model/liverange~LiveRange}
  328. */
  329. this._liveRange = this._attachLiveRange( liveRange );
  330. /**
  331. * Flag indicates if the marker is managed using operations or not.
  332. *
  333. * @private
  334. * @member {Boolean}
  335. */
  336. this._managedUsingOperations = managedUsingOperations;
  337. /**
  338. * Specifies whether the marker affects the data produced by the data pipeline
  339. * (is persisted in the editor's data).
  340. *
  341. * @private
  342. * @member {Boolean}
  343. */
  344. this._affectsData = affectsData;
  345. }
  346. /**
  347. * A value indicating if the marker is managed using operations.
  348. * See {@link ~Marker marker class description} to learn more about marker types.
  349. * See {@link module:engine/model/writer~Writer#addMarker}.
  350. *
  351. * @returns {Boolean}
  352. */
  353. get managedUsingOperations() {
  354. if ( !this._liveRange ) {
  355. throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
  356. }
  357. return this._managedUsingOperations;
  358. }
  359. /**
  360. * A value indicating if the marker changes the data.
  361. *
  362. * @returns {Boolean}
  363. */
  364. get affectsData() {
  365. if ( !this._liveRange ) {
  366. throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
  367. }
  368. return this._affectsData;
  369. }
  370. /**
  371. * Returns current marker start position.
  372. *
  373. * @returns {module:engine/model/position~Position}
  374. */
  375. getStart() {
  376. if ( !this._liveRange ) {
  377. throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
  378. }
  379. return this._liveRange.start.clone();
  380. }
  381. /**
  382. * Returns current marker end position.
  383. *
  384. * @returns {module:engine/model/position~Position}
  385. */
  386. getEnd() {
  387. if ( !this._liveRange ) {
  388. throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
  389. }
  390. return this._liveRange.end.clone();
  391. }
  392. /**
  393. * Returns a range that represents the current state of the marker.
  394. *
  395. * Keep in mind that returned value is a {@link module:engine/model/range~Range Range}, not a
  396. * {@link module:engine/model/liverange~LiveRange LiveRange}. This means that it is up-to-date and relevant only
  397. * until next model document change. Do not store values returned by this method. Instead, store {@link ~Marker#name}
  398. * and get `Marker` instance from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection} every
  399. * time there is a need to read marker properties. This will guarantee that the marker has not been removed and
  400. * that it's data is up-to-date.
  401. *
  402. * @returns {module:engine/model/range~Range}
  403. */
  404. getRange() {
  405. if ( !this._liveRange ) {
  406. throw new CKEditorError( 'marker-destroyed: Cannot use a destroyed marker instance.', this );
  407. }
  408. return this._liveRange.toRange();
  409. }
  410. /**
  411. * Checks whether this object is of the given.
  412. *
  413. * marker.is( 'marker' ); // -> true
  414. * marker.is( 'model:marker' ); // -> true
  415. *
  416. * marker.is( 'view:element' ); // -> false
  417. * marker.is( 'documentSelection' ); // -> false
  418. *
  419. * {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
  420. *
  421. * @param {String} type
  422. * @returns {Boolean}
  423. */
  424. is( type ) {
  425. return type === 'marker' || type === 'model:marker';
  426. }
  427. /**
  428. * Binds new live range to the marker and detach the old one if is attached.
  429. *
  430. * @protected
  431. * @param {module:engine/model/liverange~LiveRange} liveRange Live range to attach
  432. * @returns {module:engine/model/liverange~LiveRange} Attached live range.
  433. */
  434. _attachLiveRange( liveRange ) {
  435. if ( this._liveRange ) {
  436. this._detachLiveRange();
  437. }
  438. // Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
  439. liveRange.delegate( 'change:range' ).to( this );
  440. liveRange.delegate( 'change:content' ).to( this );
  441. this._liveRange = liveRange;
  442. return liveRange;
  443. }
  444. /**
  445. * Unbinds and destroys currently attached live range.
  446. *
  447. * @protected
  448. */
  449. _detachLiveRange() {
  450. this._liveRange.stopDelegating( 'change:range', this );
  451. this._liveRange.stopDelegating( 'change:content', this );
  452. this._liveRange.detach();
  453. this._liveRange = null;
  454. }
  455. /**
  456. * Fired whenever {@link ~Marker#_liveRange marker range} is changed due to changes on {@link module:engine/model/document~Document}.
  457. * This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:range LiveRange change:range event}.
  458. *
  459. * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
  460. * all event listeners listening to it should be removed. It is best to do it on
  461. * {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
  462. *
  463. * @see module:engine/model/liverange~LiveRange#event:change:range
  464. * @event change:range
  465. * @param {module:engine/model/range~Range} oldRange
  466. * @param {Object} data
  467. */
  468. /**
  469. * Fired whenever change on {@link module:engine/model/document~Document} is done inside {@link ~Marker#_liveRange marker range}.
  470. * This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:content LiveRange change:content event}.
  471. *
  472. * When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
  473. * all event listeners listening to it should be removed. It is best to do it on
  474. * {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
  475. *
  476. * @see module:engine/model/liverange~LiveRange#event:change:content
  477. * @event change:content
  478. * @param {module:engine/model/range~Range} oldRange
  479. * @param {Object} data
  480. */
  481. }
  482. mix( Marker, EmitterMixin );
  483. /**
  484. * Cannot use a {@link module:engine/model/markercollection~MarkerCollection#destroy destroyed marker} instance.
  485. *
  486. * @error marker-destroyed
  487. */