markercollection.js 15 KB

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