collection.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import EmitterMixin from './emittermixin.js';
  7. import CKEditorError from './ckeditorerror.js';
  8. import utilsObject from './lib/lodash/object.js';
  9. /**
  10. * Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
  11. * in the collection (like in an array) or by their ids.
  12. *
  13. * If an object without an `id` property is being added to the collection, the `id` property will be generated
  14. * automatically. Note that the automatically generated id is unique only within this single collection instance.
  15. *
  16. * By default an item in the collection is identified by its `id` property. The name of the identifier can be
  17. * configured through the constructor of the collection.
  18. *
  19. * @class Collection
  20. * @mixins EventEmitter
  21. */
  22. export default class Collection {
  23. /**
  24. * Creates a new Collection instance.
  25. *
  26. * @constructor
  27. * @param {Iterale} [items] Items to be added to the collection.
  28. * @param {Object} options The options object.
  29. * @param {String} [options.idProperty='id'] The name of the property which is considered to identify an item.
  30. */
  31. constructor( options ) {
  32. /**
  33. * The internal list of items in the collection.
  34. *
  35. * @private
  36. * @property {Object[]}
  37. */
  38. this._items = [];
  39. /**
  40. * The internal map of items in the collection.
  41. *
  42. * @private
  43. * @property {Map}
  44. */
  45. this._itemMap = new Map();
  46. /**
  47. * Next id which will be assigned to unidentified item while adding it to the collection.
  48. *
  49. * @private
  50. * @property
  51. */
  52. this._nextId = 0;
  53. /**
  54. * The name of the property which is considered to identify an item.
  55. *
  56. * @private
  57. * @property {String}
  58. */
  59. this._idProperty = options && options.idProperty || 'id';
  60. }
  61. /**
  62. * The number of items available in the collection.
  63. *
  64. * @property length
  65. */
  66. get length() {
  67. return this._items.length;
  68. }
  69. /**
  70. * Adds an item into the collection.
  71. *
  72. * If the item does not have an id, then it will be automatically generated and set on the item.
  73. *
  74. * @chainable
  75. * @param {Object} item
  76. * @param {Number} [index] The position of the item in the collection. The item
  77. * is pushed to the collection when `index` not specified.
  78. */
  79. add( item, index ) {
  80. let itemId;
  81. const idProperty = this._idProperty;
  82. if ( ( idProperty in item ) ) {
  83. itemId = item[ idProperty ];
  84. if ( typeof itemId != 'string' ) {
  85. /**
  86. * This item's id should be a string.
  87. *
  88. * @error collection-add-invalid-id
  89. */
  90. throw new CKEditorError( 'collection-add-invalid-id' );
  91. }
  92. if ( this.get( itemId ) ) {
  93. /**
  94. * This item already exists in the collection.
  95. *
  96. * @error collection-add-item-already-exists
  97. */
  98. throw new CKEditorError( 'collection-add-item-already-exists' );
  99. }
  100. } else {
  101. itemId = this._getNextId();
  102. item[ idProperty ] = itemId;
  103. }
  104. // TODO: Use ES6 default function argument.
  105. if ( index === undefined ) {
  106. index = this._items.length;
  107. } else if ( index > this._items.length || index < 0 ) {
  108. /**
  109. * The index number has invalid value.
  110. *
  111. * @error collection-add-item-bad-index
  112. */
  113. throw new CKEditorError( 'collection-add-item-invalid-index' );
  114. }
  115. this._items.splice( index, 0, item );
  116. this._itemMap.set( itemId, item );
  117. this.fire( 'add', item, index );
  118. return this;
  119. }
  120. /**
  121. * Gets item by its id or index.
  122. *
  123. * @param {String|Number} idOrIndex The item id or index in the collection.
  124. * @returns {Object} The requested item or `null` if such item does not exist.
  125. */
  126. get( idOrIndex ) {
  127. let item;
  128. if ( typeof idOrIndex == 'string' ) {
  129. item = this._itemMap.get( idOrIndex );
  130. } else if ( typeof idOrIndex == 'number' ) {
  131. item = this._items[ idOrIndex ];
  132. } else {
  133. /**
  134. * Index or id must be given.
  135. *
  136. * @error collection-get-invalid-arg
  137. */
  138. throw new CKEditorError( 'collection-get-invalid-arg: Index or id must be given.' );
  139. }
  140. return item || null;
  141. }
  142. /**
  143. * Removes an item from the collection.
  144. *
  145. * @param {Object|Number|String} subject The item to remove, its id or index in the collection.
  146. * @returns {Object} The removed item.
  147. */
  148. remove( subject ) {
  149. let index, id, item;
  150. let itemDoesNotExist = false;
  151. const idProperty = this._idProperty;
  152. if ( typeof subject == 'string' ) {
  153. id = subject;
  154. item = this._itemMap.get( id );
  155. itemDoesNotExist = !item;
  156. if ( item ) {
  157. index = this._items.indexOf( item );
  158. }
  159. } else if ( typeof subject == 'number' ) {
  160. index = subject;
  161. item = this._items[ index ];
  162. itemDoesNotExist = !item;
  163. if ( item ) {
  164. id = item[ idProperty ];
  165. }
  166. } else {
  167. item = subject;
  168. id = item[ idProperty ];
  169. index = this._items.indexOf( item );
  170. itemDoesNotExist = ( index == -1 || !this._itemMap.get( id ) );
  171. }
  172. if ( itemDoesNotExist ) {
  173. /**
  174. * Item not found.
  175. *
  176. * @error collection-remove-404
  177. */
  178. throw new CKEditorError( 'collection-remove-404: Item not found.' );
  179. }
  180. this._items.splice( index, 1 );
  181. this._itemMap.delete( id );
  182. this.fire( 'remove', item );
  183. return item;
  184. }
  185. /**
  186. * Executes the callback for each item in the collection and composes an array or values returned by this callback.
  187. *
  188. * @param {Function} callback
  189. * @param {Item} callback.item
  190. * @param {Number} callback.index
  191. * @params {Object} ctx Context in which the `callback` will be called.
  192. * @returns {Array} The result of mapping.
  193. */
  194. map( callback, ctx ) {
  195. return this._items.map( callback, ctx );
  196. }
  197. /**
  198. * Finds the first item in the collection for which the `callback` returns a true value.
  199. *
  200. * @param {Function} callback
  201. * @param {Object} callback.item
  202. * @param {Number} callback.index
  203. * @returns {Object} The item for which `callback` returned a true value.
  204. * @params {Object} ctx Context in which the `callback` will be called.
  205. */
  206. find( callback, ctx ) {
  207. return this._items.find( callback, ctx );
  208. }
  209. /**
  210. * Returns an array with items for which the `callback` returned a true value.
  211. *
  212. * @param {Function} callback
  213. * @param {Object} callback.item
  214. * @param {Number} callback.index
  215. * @params {Object} ctx Context in which the `callback` will be called.
  216. * @returns {Object[]} The array with matching items.
  217. */
  218. filter( callback, ctx ) {
  219. return this._items.filter( callback, ctx );
  220. }
  221. /**
  222. * Collection iterator.
  223. */
  224. [ Symbol.iterator ]() {
  225. return this._items[ Symbol.iterator ]();
  226. }
  227. /**
  228. * Generates next (not yet used) id for unidentified item being add to the collection.
  229. *
  230. * @returns {String} The next id.
  231. */
  232. _getNextId() {
  233. let id;
  234. do {
  235. id = String( this._nextId++ );
  236. } while ( this._itemMap.has( id ) );
  237. return id;
  238. }
  239. }
  240. utilsObject.extend( Collection.prototype, EmitterMixin );
  241. /**
  242. * Fired when an item is added to the collection.
  243. *
  244. * @event add
  245. * @param {Object} item The added item.
  246. */
  247. /**
  248. * Fired when an item is removed from the collection.
  249. *
  250. * @event remove
  251. * @param {Object} item The removed item.
  252. */