markercollection.js 16 KB

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