markercollection.js 19 KB

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