model.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. /**
  7. * The base MVC model class.
  8. *
  9. * @class Model
  10. * @mixins EventEmitter
  11. */
  12. CKEDITOR.define( [ 'emittermixin', 'ckeditorerror', 'utils' ], ( EmitterMixin, CKEditorError, utils ) => {
  13. class Model {
  14. /**
  15. * Creates a new Model instance.
  16. *
  17. * @param {Object} [attributes] The model state attributes to be set during the instance creation.
  18. * @param {Object} [properties] The properties to be appended to the instance during creation.
  19. * @method constructor
  20. */
  21. constructor( attributes, properties ) {
  22. /**
  23. * The internal hash containing the model's state.
  24. *
  25. * @property _attributes
  26. * @private
  27. */
  28. this._attributes = {};
  29. /**
  30. * Map containing bindings of this model to external models.
  31. * See {@link #bind}.
  32. *
  33. * @private
  34. * @property {Map}
  35. */
  36. this._boundTo = new Map();
  37. /**
  38. * Object that stores which attributes of this model are bound.
  39. * See {@link #bind}.
  40. *
  41. * @private
  42. * @property {Object}
  43. */
  44. this._bound = {};
  45. // Extend this instance with the additional (out of state) properties.
  46. if ( properties ) {
  47. utils.extend( this, properties );
  48. }
  49. // Initialize the attributes.
  50. if ( attributes ) {
  51. this.set( attributes );
  52. }
  53. }
  54. /**
  55. * Creates and sets the value of a model attribute of this object. This attribute will be part of the model
  56. * state and will be observable.
  57. *
  58. * It accepts also a single object literal containing key/value pairs with attributes to be set.
  59. *
  60. * This method throws the {@link model-set-cannot-override} error if the model instance already
  61. * have a property with a given attribute name. This prevents from mistakenly overriding existing
  62. * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
  63. *
  64. * @param {String} name The attributes name.
  65. * @param {*} value The attributes value.
  66. */
  67. set( name, value ) {
  68. // If the first parameter is an Object, we gonna interact through its properties.
  69. if ( utils.isObject( name ) ) {
  70. Object.keys( name ).forEach( ( attr ) => {
  71. this.set( attr, name[ attr ] );
  72. }, this );
  73. return;
  74. }
  75. if ( ( name in this ) && !( name in this._attributes ) ) {
  76. /**
  77. * Cannot override an existing property.
  78. *
  79. * This error is thrown when trying to {@link Model#set set} an attribute with
  80. * a name of an already existing property. For example:
  81. *
  82. * let model = new Model();
  83. * model.property = 1;
  84. * model.set( 'property', 2 ); // throws
  85. *
  86. * model.set( 'attr', 1 );
  87. * model.set( 'attr', 2 ); // ok, because this is an existing attribute.
  88. *
  89. * @error model-set-cannot-override
  90. */
  91. throw new CKEditorError( 'model-set-cannot-override: Cannot override an existing property.' );
  92. }
  93. Object.defineProperty( this, name, {
  94. enumerable: true,
  95. configurable: true,
  96. get: () => {
  97. return this._attributes[ name ];
  98. },
  99. set: ( value ) => {
  100. const oldValue = this._attributes[ name ];
  101. if ( oldValue !== value ) {
  102. this._attributes[ name ] = value;
  103. this.fire( 'change', name, value, oldValue );
  104. this.fire( 'change:' + name, value, oldValue );
  105. }
  106. }
  107. } );
  108. this[ name ] = value;
  109. }
  110. /**
  111. * Binds model attributes to another Model instance.
  112. *
  113. * Once bound, the model will immediately share the current state of attributes
  114. * of the model it is bound to and react to the changes to these attributes
  115. * in the future.
  116. *
  117. * To release the binding use {@link #unbind}.
  118. *
  119. * A.bind( 'a' ).to( B );
  120. * A.bind( 'a' ).to( B, 'b' );
  121. * A.bind( 'a', 'b' ).to( B, 'c', 'd' );
  122. * A.bind( 'a' ).to( B, 'b' ).to( C, 'd' ).as( ( Bb, Cd ) => Bb + Cd );
  123. *
  124. * @param {String...} bindAttrs Model attributes use that will be bound to another model(s).
  125. * @returns {BindChain}
  126. */
  127. bind( ...bindAttrs ) {
  128. if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
  129. /**
  130. * All attributes must be strings.
  131. *
  132. * @error model-bind-wrong-attrs
  133. */
  134. throw new CKEditorError( 'model-bind-wrong-attrs: All attributes must be strings.' );
  135. }
  136. if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
  137. /**
  138. * Attributes must be unique.
  139. *
  140. * @error model-bind-duplicate-attrs
  141. */
  142. throw new CKEditorError( 'model-bind-duplicate-attrs: Attributes must be unique.' );
  143. }
  144. bindAttrs.forEach( attrName => {
  145. if ( attrName in this._bound ) {
  146. /**
  147. * Cannot bind the same attribute more that once.
  148. *
  149. * @error model-bind-rebind
  150. */
  151. throw new CKEditorError( 'model-bind-rebind: Cannot bind the same attribute more that once.' );
  152. }
  153. this._bound[ attrName ] = true;
  154. } );
  155. /**
  156. * @typedef BindChain
  157. * @type Object
  158. * @property {Model} _bindModel The model which initializes the binding.
  159. * @property {Array} _bindAttrs Array of `_bindModel` attributes to be bound.
  160. * @property {Array} _boundTo Array of `to()` model–attributes (`{ model: toModel, attrs: ...toAttrs }`).
  161. * @property {Object} _current The arguments of the last `to( toModel, ...toAttrs )` call, also
  162. * the last item of `_boundTo`.
  163. * @property {Function} to See {@link #_bindTo}.
  164. * @property {Function} as See {@link #_bindAs} (available after `to()` called in chain).
  165. */
  166. return {
  167. _bindModel: this,
  168. _bindAttrs: bindAttrs,
  169. _boundTo: [],
  170. get _current() {
  171. return this._boundTo[ this._boundTo.length - 1 ];
  172. },
  173. to: this._bindTo
  174. };
  175. }
  176. /**
  177. * A chaining for {@link #bind} providing `.to()` interface.
  178. *
  179. * @protected
  180. * @param {Model} model A model used for binding.
  181. * @param {String...} [toAttrs] Attributes of the model used for binding.
  182. * @returns {BindChain}
  183. */
  184. _bindTo( toModel, ...toAttrs ) {
  185. if ( !toModel || !( toModel instanceof Model ) ) {
  186. /**
  187. * An instance of Model is required.
  188. *
  189. * @error model-bind-to-wrong-model
  190. */
  191. throw new CKEditorError( 'model-bind-to-wrong-model: An instance of Model is required.' );
  192. }
  193. if ( !isStringArray( toAttrs ) ) {
  194. /**
  195. * Model attributes must be strings.
  196. *
  197. * @error model-bind-to-wrong-attrs
  198. */
  199. throw new CKEditorError( 'model-bind-to-wrong-attrs: Model attributes must be strings.' );
  200. }
  201. // Eliminate A.bind( 'x' ).to( B, 'y', 'z' )
  202. // Eliminate A.bind( 'x', 'y' ).to( B, 'z' )
  203. if ( toAttrs.length && toAttrs.length !== this._bindAttrs.length ) {
  204. /**
  205. * The number of attributes must match.
  206. *
  207. * @error model-bind-to-attrs-length
  208. */
  209. throw new CKEditorError( 'model-bind-to-attrs-length: The number of attributes must match.' );
  210. }
  211. // Eliminate A.bind( 'x' ).to( B, 'y' ), when B.y == undefined.
  212. // Eliminate A.bind( 'x' ).to( B ), when B.x == undefined.
  213. if ( !hasAttributes( toModel, toAttrs ) || ( !toAttrs.length && !hasAttributes( toModel, this._bindAttrs ) ) ) {
  214. /**
  215. * Model has no such attribute(s).
  216. *
  217. * @error model-bind-to-missing-attr
  218. */
  219. throw new CKEditorError( 'model-bind-to-missing-attr: Model has no such attribute(s).' );
  220. }
  221. // Eliminate A.bind( 'x', 'y' ).to( B ).to( C ) when no trailing .as().
  222. // Eliminate A.bind( 'x', 'y' ).to( B, 'x', 'y' ).to( C, 'x', 'y' ).
  223. if ( this._boundTo.length && ( toAttrs.length > 1 || this._bindAttrs.length > 1 ) ) {
  224. /**
  225. * Chaining only allowed for a single attribute.
  226. *
  227. * @error model-bind-to-chain-multiple-attrs
  228. */
  229. throw new CKEditorError( 'model-bind-to-chain-multiple-attrs: Chaining only allowed for a single attribute.' );
  230. }
  231. // When no toAttrs specified, observing MODEL attributes, like MODEL.bind( 'foo' ).to( TOMODEL )
  232. if ( !toAttrs.length ) {
  233. toAttrs = this._bindAttrs;
  234. }
  235. // Extend current chain with the new binding information.
  236. this._boundTo.push( { model: toModel, attrs: toAttrs } );
  237. setupBinding( this );
  238. if ( !this.as ) {
  239. this.as = this._bindModel._bindAs;
  240. }
  241. return this;
  242. }
  243. /**
  244. * A chaining for {@link #bind} providing `.as()` interface.
  245. *
  246. * @protected
  247. * @param {Function} callback A callback to combine model's attributes.
  248. */
  249. _bindAs( callback ) {
  250. if ( !callback || typeof callback !== 'function' ) {
  251. /**
  252. * Callback must be a Function.
  253. *
  254. * @error model-bind-as-wrong-callback
  255. */
  256. throw new CKEditorError( 'model-bind-as-wrong-callback: Callback must be a Function.' );
  257. }
  258. this._callback = callback;
  259. updateModelAttrs( this, this._bindAttrs[ 0 ] );
  260. }
  261. /**
  262. * Removes the binding created with {@link #bind}.
  263. *
  264. * A.unbind( 'a' );
  265. * A.unbind();
  266. *
  267. * @param {String...} [bindAttrs] Model attributes to unbound. All the bindings will
  268. * be released if not attributes provided.
  269. */
  270. unbind( ...unbindAttrs ) {
  271. if ( unbindAttrs.length ) {
  272. if ( !isStringArray( unbindAttrs ) ) {
  273. /**
  274. * Attributes must be strings.
  275. *
  276. * @error model-unbind-wrong-attrs
  277. */
  278. throw new CKEditorError( 'model-unbind-wrong-attrs: Attributes must be strings.' );
  279. }
  280. unbindAttrs.forEach( attrName => {
  281. for ( let to of this._boundTo ) {
  282. // TODO, ES6 destructuring.
  283. const boundModel = to[ 0 ];
  284. const bindings = to[ 1 ];
  285. for ( let boundAttrName in bindings ) {
  286. if ( bindings[ boundAttrName ].has( attrName ) ) {
  287. bindings[ boundAttrName ].delete( attrName );
  288. }
  289. if ( !bindings[ boundAttrName ].size ) {
  290. delete bindings[ boundAttrName ];
  291. }
  292. if ( !Object.keys( bindings ).length ) {
  293. this._boundTo.delete( boundModel );
  294. this.stopListening( boundModel, 'change' );
  295. }
  296. }
  297. }
  298. delete this._bound[ attrName ];
  299. } );
  300. } else {
  301. this._boundTo.forEach( ( bindings, boundModel ) => {
  302. this.stopListening( boundModel, 'change' );
  303. this._boundTo.delete( boundModel );
  304. } );
  305. this._bound = {};
  306. }
  307. }
  308. }
  309. /**
  310. * Check if the `model` has given `attrs`.
  311. *
  312. * @private
  313. * @param {Model} model Model to be checked.
  314. * @param {Array} arr An array of `String`.
  315. * @returns {Boolean}
  316. */
  317. function hasAttributes( model, attrs ) {
  318. return attrs.every( a => a in model._attributes );
  319. }
  320. /**
  321. * Check if all entries of the array are of `String` type.
  322. *
  323. * @private
  324. * @param {Array} arr An array to be checked.
  325. * @returns {Boolean}
  326. */
  327. function isStringArray( arr ) {
  328. return arr.every( a => typeof a == 'string' );
  329. }
  330. /**
  331. * Returns all bindings of the `chain._bindModel` to `chain._current.model`
  332. * set by {@link #updateModelBindingsToCurrent}.
  333. *
  334. * // Given that A == _bindModel and B == _current.model
  335. * A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  336. *
  337. * // The following object is returned
  338. * { x: [ 'a', 'c' ], y: [ 'b' ] }
  339. *
  340. *
  341. * @private
  342. * @param {BindChain} chain The chain initialized by {@link Model#bind}.
  343. * @returns {Object}
  344. */
  345. function getModelBindingsToCurrent( chain ) {
  346. return chain._bindModel._boundTo.get( chain._current.model );
  347. }
  348. /**
  349. * Updates `chain._bindModel._boundTo` with a binding for `chain._current`.
  350. * The binding can be then retrieved by {@link #getModelBindingsToCurrent}.
  351. *
  352. * @private
  353. * @param {BindChain} chain The chain initialized by {@link Model#bind}.
  354. * @returns {Object}
  355. */
  356. function updateModelBindingsToCurrent( chain ) {
  357. const currentBindings = getModelBindingsToCurrent( chain );
  358. const bindings = currentBindings || {};
  359. chain._current.attrs.forEach( ( attrName, index ) => {
  360. ( bindings[ attrName ] || ( bindings[ attrName ] = new Set() ) )
  361. .add( chain._bindAttrs[ index ] );
  362. } );
  363. if ( !currentBindings ) {
  364. chain._bindModel._boundTo.set( chain._current.model, bindings );
  365. }
  366. }
  367. /**
  368. * Updates the model attribute with given value. If an attribute does not exist,
  369. * it is created on the fly.
  370. *
  371. * @private
  372. * @param {Model} model The model which attribute is updated.
  373. * @param {String} attrName The name of the attribute.
  374. * @param {*} value The value of the attribute.
  375. */
  376. function updateModelAttr( model, attrName, value ) {
  377. if ( model[ attrName ] ) {
  378. model[ attrName ] = value;
  379. } else {
  380. model.set( attrName, value );
  381. }
  382. }
  383. /**
  384. * Updates all bound attributes of `chain._bindModel` with the `value` of
  385. * `attrName` of `chain._current` model.
  386. *
  387. * // Given that A == _bindModel and B == _current.model
  388. * A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  389. *
  390. * // The following is updated
  391. * A.a = A.c = B.x;
  392. * A.b = B.y;
  393. *
  394. * @private
  395. * @param {BindChain} chain The chain initialized by {@link Model#bind}.
  396. * @param {String} attrName One of the attributes of `chain._current`.
  397. * @param {*} value The value of the attribute.
  398. */
  399. function updateModelAttrs( chain, attrName, value ) {
  400. const boundAttrs = getModelBindingsToCurrent( chain )[ attrName ];
  401. if ( !boundAttrs ) {
  402. return;
  403. } else if ( chain._callback ) {
  404. // MODEL.bind( 'a' ).to( TOMODEL1, 'b1' )[ .to( TOMODELn, 'bn' ) ].as( callback )
  405. // \-> Collect specific attribute value in the boundTo.model (TOMODELn.bn).
  406. //
  407. // MODEL.bind( 'a' ).to( TOMODEL1 )[ .to( TOMODELn ) ].as( callback )
  408. // \-> Use model attribute name to collect boundTo attribute value (TOMODELn.a).
  409. const values = chain._boundTo.map( boundTo => boundTo.model[ boundTo.attrs[ 0 ] ] );
  410. // Pass collected attribute values to the callback function.
  411. // Whatever is returned it becomes the value of the model's attribute.
  412. updateModelAttr(
  413. chain._bindModel,
  414. chain._bindAttrs[ 0 ],
  415. chain._callback.apply( chain._bindModel, values )
  416. );
  417. } else {
  418. // MODEL.bind( 'a' ).to( TOMODEL1 )[ .to( TOMODELn ) ];
  419. // \-> If multiple .to() models but **no** .as( callback ), then the binding is invalid.
  420. if ( !chain._callback && chain._boundTo.length > 1 ) {
  421. value = undefined;
  422. }
  423. for ( let boundAttrName of boundAttrs ) {
  424. updateModelAttr( chain._bindModel, boundAttrName, value );
  425. }
  426. }
  427. }
  428. /**
  429. * Starts listening to changes in `chain._current.model` to update `chain._bindModel`
  430. * attributes. Also sets the initial state of `chain._bindModel` bound attributes.
  431. *
  432. * @private
  433. * @param {BindChain} chain The chain initialized by {@link Model#bind}.
  434. */
  435. function setupBinding( chain ) {
  436. // If there's already a binding between the models (`chain._bindModel` listens to
  437. // `chain._current.model`), there's no need to create another `change` event listener.
  438. if ( !getModelBindingsToCurrent( chain ) ) {
  439. chain._bindModel.listenTo( chain._current.model, 'change', ( evt, attrName, value ) => {
  440. updateModelAttrs( chain, attrName, value );
  441. } );
  442. }
  443. updateModelBindingsToCurrent( chain );
  444. // Set initial model state.
  445. chain._current.attrs.forEach( attrName => {
  446. updateModelAttrs( chain, attrName, chain._current.model[ attrName ] );
  447. } );
  448. }
  449. utils.extend( Model.prototype, EmitterMixin );
  450. return Model;
  451. } );
  452. /**
  453. * Fired when an attribute changed value.
  454. *
  455. * @event change
  456. * @param {String} name The attribute name.
  457. * @param {*} value The new attribute value.
  458. * @param {*} oldValue The previous attribute value.
  459. */
  460. /**
  461. * Fired when an specific attribute changed value.
  462. *
  463. * @event change:{attribute}
  464. * @param {*} value The new attribute value.
  465. * @param {*} oldValue The previous attribute value.
  466. */