markercollection.js 12 KB

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