model.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. /**
  2. * @license Copyright (c) 2003-2016, 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. import utilsLang from './lib/lodash/lang.js';
  10. /**
  11. * The base MVC model class.
  12. *
  13. * @class Model
  14. * @mixins EventEmitter
  15. */
  16. export default class Model {
  17. /**
  18. * Creates a new Model instance.
  19. *
  20. * @param {Object} [attributes] The model state attributes to be set during the instance creation.
  21. * @param {Object} [properties] The properties to be appended to the instance during creation.
  22. * @method constructor
  23. */
  24. constructor( attributes, properties ) {
  25. /**
  26. * The internal hash containing the model's state.
  27. *
  28. * @property _attributes
  29. * @private
  30. */
  31. this._attributes = {};
  32. /**
  33. * Map containing bindings to external models. It shares the binding objects
  34. * (`{ model: A, attr: 'a', to: ... }`) with {@link #_boundAttributes} and
  35. * it is used to observe external models to update own attributes accordingly.
  36. * See {@link #bind}.
  37. *
  38. * A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  39. * console.log( A._boundModels );
  40. *
  41. * Map( {
  42. * B: {
  43. * x: Set( [
  44. * { model: A, attr: 'a', to: [ [ B, 'x' ] ] },
  45. * { model: A, attr: 'c', to: [ [ B, 'x' ] ] }
  46. * ] ),
  47. * y: Set( [
  48. * { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
  49. * ] )
  50. * }
  51. * } )
  52. *
  53. * A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  54. * console.log( A._boundModels );
  55. *
  56. * Map( {
  57. * B: {
  58. * x: Set( [
  59. * { model: A, attr: 'a', to: [ [ B, 'x' ] ] },
  60. * { model: A, attr: 'c', to: [ [ B, 'x' ] ] }
  61. * ] ),
  62. * y: Set( [
  63. * { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
  64. * ] ),
  65. * z: Set( [
  66. * { model: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  67. * ] )
  68. * },
  69. * C: {
  70. * w: Set( [
  71. * { model: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  72. * ] )
  73. * }
  74. * } )
  75. *
  76. * @private
  77. * @property {Map}
  78. */
  79. this._boundModels = new Map();
  80. /**
  81. * Object that stores which attributes of this model are bound and how. It shares
  82. * the binding objects (`{ model: A, attr: 'a', to: ... }`) with {@link #_boundModels}.
  83. * This data structure is a reverse of {@link #_boundModels} and it is helpful for {@link #unbind}.
  84. * See {@link #bind}.
  85. *
  86. * A.bind( 'a', 'b', 'c' ).to( B, 'x', 'y', 'x' );
  87. * console.log( A._boundAttributes );
  88. *
  89. * {
  90. * a: { model: A, attr: 'a', to: [ [ B, 'x' ] ] },
  91. * b: { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
  92. * c: { model: A, attr: 'c', to: [ [ B, 'x' ] ] }
  93. * }
  94. *
  95. * A.bind( 'd' ).to( B, 'z' ).to( C, 'w' ).as( callback );
  96. * console.log( A._boundAttributes );
  97. *
  98. * {
  99. * a: { model: A, attr: 'a', to: [ [ B, 'x' ] ] },
  100. * b: { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
  101. * c: { model: A, attr: 'c', to: [ [ B, 'x' ] ] },
  102. * d: { model: A, attr: 'd', to: [ [ B, 'z' ], [ C, 'w' ] ], callback: callback }
  103. * }
  104. *
  105. * @private
  106. * @property {Object}
  107. */
  108. this._boundAttributes = {};
  109. // Extend this instance with the additional (out of state) properties.
  110. if ( properties ) {
  111. utilsObject.extend( this, properties );
  112. }
  113. // Initialize the attributes.
  114. if ( attributes ) {
  115. this.set( attributes );
  116. }
  117. }
  118. /**
  119. * Creates and sets the value of a model attribute of this object. This attribute will be part of the model
  120. * state and will be observable.
  121. *
  122. * It accepts also a single object literal containing key/value pairs with attributes to be set.
  123. *
  124. * This method throws the {@link model-set-cannot-override} error if the model instance already
  125. * have a property with a given attribute name. This prevents from mistakenly overriding existing
  126. * properties and methods, but means that `foo.set( 'bar', 1 )` may be slightly slower than `foo.bar = 1`.
  127. *
  128. * @param {String} name The attributes name.
  129. * @param {*} value The attributes value.
  130. */
  131. set( name, value ) {
  132. // If the first parameter is an Object, we gonna interact through its properties.
  133. if ( utilsLang.isObject( name ) ) {
  134. Object.keys( name ).forEach( ( attr ) => {
  135. this.set( attr, name[ attr ] );
  136. }, this );
  137. return;
  138. }
  139. if ( ( name in this ) && !( name in this._attributes ) ) {
  140. /**
  141. * Cannot override an existing property.
  142. *
  143. * This error is thrown when trying to {@link Model#set set} an attribute with
  144. * a name of an already existing property. For example:
  145. *
  146. * let model = new Model();
  147. * model.property = 1;
  148. * model.set( 'property', 2 ); // throws
  149. *
  150. * model.set( 'attr', 1 );
  151. * model.set( 'attr', 2 ); // ok, because this is an existing attribute.
  152. *
  153. * @error model-set-cannot-override
  154. */
  155. throw new CKEditorError( 'model-set-cannot-override: Cannot override an existing property.' );
  156. }
  157. Object.defineProperty( this, name, {
  158. enumerable: true,
  159. configurable: true,
  160. get: () => {
  161. return this._attributes[ name ];
  162. },
  163. set: ( value ) => {
  164. const oldValue = this._attributes[ name ];
  165. if ( oldValue !== value ) {
  166. this._attributes[ name ] = value;
  167. this.fire( 'change', name, value, oldValue );
  168. this.fire( 'change:' + name, value, oldValue );
  169. }
  170. }
  171. } );
  172. this[ name ] = value;
  173. }
  174. /**
  175. * Binds model attributes to another {@link Model} instance.
  176. *
  177. * Once bound, the model will immediately share the current state of attributes
  178. * of the model it is bound to and react to the changes to these attributes
  179. * in the future.
  180. *
  181. * **Note**: To release the binding use {@link #unbind}.
  182. *
  183. * A.bind( 'a' ).to( B );
  184. * A.bind( 'a' ).to( B, 'b' );
  185. * A.bind( 'a', 'b' ).to( B, 'c', 'd' );
  186. * A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
  187. *
  188. * @param {String...} bindAttrs Model attributes use that will be bound to another model(s).
  189. * @returns {BindChain}
  190. */
  191. bind( ...bindAttrs ) {
  192. if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
  193. /**
  194. * All attributes must be strings.
  195. *
  196. * @error model-bind-wrong-attrs
  197. */
  198. throw new CKEditorError( 'model-bind-wrong-attrs: All attributes must be strings.' );
  199. }
  200. if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
  201. /**
  202. * Attributes must be unique.
  203. *
  204. * @error model-bind-duplicate-attrs
  205. */
  206. throw new CKEditorError( 'model-bind-duplicate-attrs: Attributes must be unique.' );
  207. }
  208. bindAttrs.forEach( attrName => {
  209. if ( attrName in this._boundAttributes ) {
  210. /**
  211. * Cannot bind the same attribute more that once.
  212. *
  213. * @error model-bind-rebind
  214. */
  215. throw new CKEditorError( 'model-bind-rebind: Cannot bind the same attribute more that once.' );
  216. }
  217. } );
  218. const bindings = {};
  219. /**
  220. * @typedef Binding
  221. * @type Object
  222. * @property {Array} attr Attribute which is bound.
  223. * @property {Array} to Array of model–attribute components of the binding (`{ model: ..., attr: .. }`).
  224. * @property {Array} callback A function which processes `to` components.
  225. */
  226. bindAttrs.forEach( a => {
  227. this._boundAttributes[ a ] = bindings[ a ] = { attr: a, to: [] };
  228. } );
  229. /**
  230. * @typedef BindChain
  231. * @type Object
  232. * @property {Function} to See {@link #_bindTo}.
  233. * @property {Model} _model The model which initializes the binding.
  234. * @property {Array} _bindAttrs Array of `_model` attributes to be bound.
  235. * @property {Array} _to Array of `to()` model–attributes (`{ model: toModel, attrs: ...toAttrs }`).
  236. * @property {Object} _bindings Stores bindings to be kept in {@link #_boundAttributes}/{@link #_boundModels}
  237. * initiated in this binding chain.
  238. */
  239. return {
  240. to: this._bindTo,
  241. _model: this,
  242. _bindAttrs: bindAttrs,
  243. _to: [],
  244. _bindings: bindings
  245. };
  246. }
  247. /**
  248. * Removes the binding created with {@link #bind}.
  249. *
  250. * A.unbind( 'a' );
  251. * A.unbind();
  252. *
  253. * @param {String...} [bindAttrs] Model attributes to unbound. All the bindings will
  254. * be released if not attributes provided.
  255. */
  256. unbind( ...unbindAttrs ) {
  257. if ( unbindAttrs.length ) {
  258. if ( !isStringArray( unbindAttrs ) ) {
  259. /**
  260. * Attributes must be strings.
  261. *
  262. * @error model-unbind-wrong-attrs
  263. */
  264. throw new CKEditorError( 'model-unbind-wrong-attrs: Attributes must be strings.' );
  265. }
  266. unbindAttrs.forEach( attrName => {
  267. const binding = this._boundAttributes[ attrName ];
  268. let toModel, toAttr, toAttrs, toAttrBindings;
  269. binding.to.forEach( to => {
  270. // TODO: ES6 destructuring.
  271. toModel = to[ 0 ];
  272. toAttr = to[ 1 ];
  273. toAttrs = this._boundModels.get( toModel );
  274. toAttrBindings = toAttrs[ toAttr ];
  275. toAttrBindings.delete( binding );
  276. if ( !toAttrBindings.size ) {
  277. delete toAttrs[ toAttr ];
  278. }
  279. if ( !Object.keys( toAttrs ).length ) {
  280. this._boundModels.delete( toModel );
  281. this.stopListening( toModel, 'change' );
  282. }
  283. } );
  284. delete this._boundAttributes[ attrName ];
  285. } );
  286. } else {
  287. this._boundModels.forEach( ( bindings, boundModel ) => {
  288. this.stopListening( boundModel, 'change' );
  289. } );
  290. this._boundModels.clear();
  291. this._boundAttributes = {};
  292. }
  293. }
  294. /**
  295. * A chaining for {@link #bind} providing `.to()` interface.
  296. *
  297. * @protected
  298. * @param {...[Model|String|Function]} args Arguments of the `.to( args )` binding.
  299. */
  300. _bindTo( ...args ) {
  301. const parsedArgs = parseBindToArgs( ...args );
  302. const bindingsKeys = Object.keys( this._bindings );
  303. const numberOfBindings = bindingsKeys.length;
  304. // Eliminate A.bind( 'x' ).to( B, C )
  305. if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
  306. /**
  307. * Binding multiple models only possible with callback.
  308. *
  309. * @error model-bind-no-callback
  310. */
  311. throw new CKEditorError( 'model-bind-to-no-callback: Binding multiple models only possible with callback.' ) ;
  312. }
  313. // Eliminate A.bind( 'x', 'y' ).to( B, callback )
  314. if ( numberOfBindings > 1 && parsedArgs.callback ) {
  315. /**
  316. * Cannot bind multiple attributes and use a callback in one binding.
  317. *
  318. * @error model-bind-to-extra-callback
  319. */
  320. throw new CKEditorError( 'model-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' ) ;
  321. }
  322. parsedArgs.to.forEach( to => {
  323. // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
  324. if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
  325. /**
  326. * The number of attributes must match.
  327. *
  328. * @error model-bind-to-attrs-length
  329. */
  330. throw new CKEditorError( 'model-bind-to-attrs-length: The number of attributes must match.' );
  331. }
  332. // When no to.attrs specified, observing MODEL attributes instead.
  333. if ( !to.attrs.length ) {
  334. to.attrs = this._bindAttrs;
  335. }
  336. // Eliminate A.bind( 'x', 'y' ).to( B, 'a', 'b' ) when B has no 'a'.
  337. if ( !hasAttributes( to.model, to.attrs ) ) {
  338. /*
  339. * Model has no such attribute(s).
  340. *
  341. * @error model-bind-to-missing-attr
  342. */
  343. throw new CKEditorError( 'model-bind-to-missing-attr: Model has no such attribute(s).' );
  344. }
  345. } );
  346. this._to = parsedArgs.to;
  347. // Fill {@link BindChain#_bindings} with callback.
  348. if ( parsedArgs.callback ) {
  349. this._bindings[ bindingsKeys[ 0 ] ].callback = parsedArgs.callback;
  350. }
  351. attachBindToListeners( this._model, this._to );
  352. // Update model._boundAttributes and model._boundModels.
  353. updateBindToBound( this );
  354. // Set initial values of bound attributes.
  355. this._bindAttrs.forEach( attrName => {
  356. updateBoundModelAttr( this._model, attrName );
  357. } );
  358. }
  359. }
  360. /**
  361. * Check if the {@link Model} has given `attrs`.
  362. *
  363. * @private
  364. * @param {Model} model Model to be checked.
  365. * @param {Array} arr An array of `String`.
  366. * @returns {Boolean}
  367. */
  368. function hasAttributes( model, attrs ) {
  369. return attrs.every( a => a in model._attributes );
  370. }
  371. /**
  372. * Check if all entries of the array are of `String` type.
  373. *
  374. * @private
  375. * @param {Array} arr An array to be checked.
  376. * @returns {Boolean}
  377. */
  378. function isStringArray( arr ) {
  379. return arr.every( a => typeof a == 'string' );
  380. }
  381. /**
  382. * Parses and validates {@link Model#bind}`.to( args )` arguments and returns
  383. * an object with a parsed structure. For example
  384. *
  385. * A.bind( 'x' ).to( B, 'a', C, 'b', call );
  386. *
  387. * becomes
  388. *
  389. * {
  390. * to: [
  391. * { model: B, attrs: [ 'a' ] },
  392. * { model: C, attrs: [ 'b' ] },
  393. * ],
  394. * callback: call
  395. * }
  396. *
  397. * @private
  398. * @param {...*} args Arguments of {@link Model#bind}`.to( args )`.
  399. * @returns {Object}
  400. */
  401. function parseBindToArgs( ...args ) {
  402. // Eliminate A.bind( 'x' ).to()
  403. if ( !args.length ) {
  404. /**
  405. * Invalid argument syntax in `to()`.
  406. *
  407. * @error model-bind-to-parse-error
  408. */
  409. throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  410. }
  411. const parsed = { to: [] };
  412. let lastModel;
  413. args.forEach( a => {
  414. // Callback has already been defined.
  415. // Eliminate A.bind( 'x' ).to( B, 'a', callback, C )
  416. if ( parsed.callback ) {
  417. throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  418. } else if ( a instanceof Model ) {
  419. parsed.to.push( ( lastModel = { model: a, attrs: [] } ) );
  420. } else if ( typeof a == 'string' ) {
  421. lastModel.attrs.push( a );
  422. } else if ( typeof a == 'function' ) {
  423. parsed.callback = a;
  424. }
  425. // Eliminate A.bind( 'x' ).to( null, new Date(), etc. )
  426. else {
  427. throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  428. }
  429. } );
  430. return parsed;
  431. }
  432. /**
  433. * Synchronizes {@link Model#_boundModels} with {@link Binding}.
  434. *
  435. * @private
  436. * @param {Binding} binding A binding to store in {@link Model#_boundModels}.
  437. * @param {Model} toModel A model, which is a new component of `binding`.
  438. * @param {String} toAttrName A name of `toModel`'s attribute, a new component of the `binding`.
  439. */
  440. function updateBoundModels( model, binding, toModel, toAttrName ) {
  441. const bindingsToModel = model._boundModels.get( toModel );
  442. const bindings = bindingsToModel || {};
  443. if ( !bindings[ toAttrName ] ) {
  444. bindings[ toAttrName ] = new Set();
  445. }
  446. // Pass the binding to a corresponding Set in `model._boundModels`.
  447. bindings[ toAttrName ].add( binding );
  448. if ( !bindingsToModel ) {
  449. model._boundModels.set( toModel, bindings );
  450. }
  451. }
  452. /**
  453. * Synchronizes {@link Model#_boundAttributes} and {@link Model#_boundModels}
  454. * with {@link BindChain}.
  455. *
  456. * Assuming the following binding being created
  457. *
  458. * A.bind( 'a', 'b' ).to( B, 'x', 'y' );
  459. *
  460. * the following bindings were initialized by {@link Model#bind} in {@link BindChain#_bindings}:
  461. *
  462. * {
  463. * a: { model: A, attr: 'a', to: [] },
  464. * b: { model: A, attr: 'b', to: [] },
  465. * }
  466. *
  467. * Iterate over all bindings in this chain and fill their `to` properties with
  468. * corresponding to( ... ) arguments (components of the binding), so
  469. *
  470. * {
  471. * a: { model: A, attr: 'a', to: [ B, 'x' ] },
  472. * b: { model: A, attr: 'b', to: [ B, 'y' ] },
  473. * }
  474. *
  475. * Then update the structure of {@link Model#_boundModels} with updated
  476. * binding, so it becomes:
  477. *
  478. * Map( {
  479. * B: {
  480. * x: Set( [
  481. * { model: A, attr: 'a', to: [ [ B, 'x' ] ] }
  482. * ] ),
  483. * y: Set( [
  484. * { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
  485. * ] )
  486. * }
  487. * } )
  488. *
  489. * @private
  490. * @param {BindChain} chain The binding initialized by {@link Model#bind}.
  491. */
  492. function updateBindToBound( chain ) {
  493. let binding, toAttr;
  494. for ( let attrName in chain._bindings ) {
  495. binding = chain._bindings[ attrName ];
  496. // Note: For a binding without a callback, this will run only once
  497. // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
  498. // TODO: ES6 destructuring.
  499. chain._to.forEach( to => {
  500. toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
  501. binding.to.push( [ to.model, toAttr ] );
  502. updateBoundModels( chain._model, binding, to.model, toAttr );
  503. } );
  504. }
  505. }
  506. /**
  507. * Updates an attribute of a {@link Model} with a value
  508. * determined by an entry in {@link Model#_boundAttributes}.
  509. *
  510. * @private
  511. * @param {Model} model A model which attribute is to be updated.
  512. * @param {String} attrName An attribute to be updated.
  513. */
  514. function updateBoundModelAttr( model, attrName ) {
  515. const binding = model._boundAttributes[ attrName ];
  516. let attrValue;
  517. // When a binding with callback is created like
  518. //
  519. // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
  520. //
  521. // collect B.b and C.c, then pass them to callback to set A.a.
  522. if ( binding.callback ) {
  523. attrValue = binding.callback.apply( model, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
  524. } else {
  525. attrValue = binding.to[ 0 ];
  526. attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
  527. }
  528. // TODO: Needs update after https://github.com/ckeditor/ckeditor5-core/issues/132.
  529. if ( model.hasOwnProperty( attrName ) ) {
  530. model[ attrName ] = attrValue;
  531. } else {
  532. model.set( attrName, attrValue );
  533. }
  534. }
  535. /**
  536. * Starts listening to changes in {@link BindChain._to} models to update
  537. * {@link BindChain._model} {@link BindChain._bindAttrs}. Also sets the
  538. * initial state of {@link BindChain._model}.
  539. *
  540. * @private
  541. * @param {BindChain} chain The chain initialized by {@link Model#bind}.
  542. */
  543. function attachBindToListeners( model, toBindings ) {
  544. toBindings.forEach( to => {
  545. const boundModels = model._boundModels;
  546. let bindings;
  547. // If there's already a chain between the models (`model` listens to
  548. // `to.model`), there's no need to create another `change` event listener.
  549. if ( !boundModels.get( to.model ) ) {
  550. model.listenTo( to.model, 'change', ( evt, attrName ) => {
  551. bindings = boundModels.get( to.model )[ attrName ];
  552. // Note: to.model will fire for any attribute change, react
  553. // to changes of attributes which are bound only.
  554. if ( bindings ) {
  555. bindings.forEach( binding => {
  556. updateBoundModelAttr( model, binding.attr );
  557. } );
  558. }
  559. } );
  560. }
  561. } );
  562. }
  563. utilsObject.extend( Model.prototype, EmitterMixin );
  564. /**
  565. * Fired when an attribute changed value.
  566. *
  567. * @event change
  568. * @param {String} name The attribute name.
  569. * @param {*} value The new attribute value.
  570. * @param {*} oldValue The previous attribute value.
  571. */
  572. /**
  573. * Fired when an specific attribute changed value.
  574. *
  575. * @event change:{attribute}
  576. * @param {*} value The new attribute value.
  577. * @param {*} oldValue The previous attribute value.
  578. */