8
0

model.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  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. // Allow undefined as an initial value like A.set( 'x', undefined ) (#132).
  166. // Note: When _attributes has no such own property, then its value is undefined.
  167. if ( oldValue !== value || !this._attributes.hasOwnProperty( name ) ) {
  168. this._attributes[ name ] = value;
  169. this.fire( 'change', name, value, oldValue );
  170. this.fire( 'change:' + name, value, oldValue );
  171. }
  172. }
  173. } );
  174. this[ name ] = value;
  175. }
  176. /**
  177. * Binds model attributes to another {@link Model} instance.
  178. *
  179. * Once bound, the model will immediately share the current state of attributes
  180. * of the model it is bound to and react to the changes to these attributes
  181. * in the future.
  182. *
  183. * **Note**: To release the binding use {@link #unbind}.
  184. *
  185. * A.bind( 'a' ).to( B );
  186. * A.bind( 'a' ).to( B, 'b' );
  187. * A.bind( 'a', 'b' ).to( B, 'c', 'd' );
  188. * A.bind( 'a' ).to( B, 'b', C, 'd', ( b, d ) => b + d );
  189. *
  190. * @param {String...} bindAttrs Model attributes use that will be bound to another model(s).
  191. * @returns {BindChain}
  192. */
  193. bind( ...bindAttrs ) {
  194. if ( !bindAttrs.length || !isStringArray( bindAttrs ) ) {
  195. /**
  196. * All attributes must be strings.
  197. *
  198. * @error model-bind-wrong-attrs
  199. */
  200. throw new CKEditorError( 'model-bind-wrong-attrs: All attributes must be strings.' );
  201. }
  202. if ( ( new Set( bindAttrs ) ).size !== bindAttrs.length ) {
  203. /**
  204. * Attributes must be unique.
  205. *
  206. * @error model-bind-duplicate-attrs
  207. */
  208. throw new CKEditorError( 'model-bind-duplicate-attrs: Attributes must be unique.' );
  209. }
  210. bindAttrs.forEach( attrName => {
  211. if ( attrName in this._boundAttributes ) {
  212. /**
  213. * Cannot bind the same attribute more that once.
  214. *
  215. * @error model-bind-rebind
  216. */
  217. throw new CKEditorError( 'model-bind-rebind: Cannot bind the same attribute more that once.' );
  218. }
  219. } );
  220. const bindings = {};
  221. /**
  222. * @typedef Binding
  223. * @type Object
  224. * @property {Array} attr Attribute which is bound.
  225. * @property {Array} to Array of model–attribute components of the binding (`{ model: ..., attr: .. }`).
  226. * @property {Array} callback A function which processes `to` components.
  227. */
  228. bindAttrs.forEach( a => {
  229. this._boundAttributes[ a ] = bindings[ a ] = { attr: a, to: [] };
  230. } );
  231. /**
  232. * @typedef BindChain
  233. * @type Object
  234. * @property {Function} to See {@link #_bindTo}.
  235. * @property {Model} _model The model which initializes the binding.
  236. * @property {Array} _bindAttrs Array of `_model` attributes to be bound.
  237. * @property {Array} _to Array of `to()` model–attributes (`{ model: toModel, attrs: ...toAttrs }`).
  238. * @property {Object} _bindings Stores bindings to be kept in {@link #_boundAttributes}/{@link #_boundModels}
  239. * initiated in this binding chain.
  240. */
  241. return {
  242. to: this._bindTo,
  243. _model: this,
  244. _bindAttrs: bindAttrs,
  245. _to: [],
  246. _bindings: bindings
  247. };
  248. }
  249. /**
  250. * Removes the binding created with {@link #bind}.
  251. *
  252. * A.unbind( 'a' );
  253. * A.unbind();
  254. *
  255. * @param {String...} [bindAttrs] Model attributes to unbound. All the bindings will
  256. * be released if not attributes provided.
  257. */
  258. unbind( ...unbindAttrs ) {
  259. if ( unbindAttrs.length ) {
  260. if ( !isStringArray( unbindAttrs ) ) {
  261. /**
  262. * Attributes must be strings.
  263. *
  264. * @error model-unbind-wrong-attrs
  265. */
  266. throw new CKEditorError( 'model-unbind-wrong-attrs: Attributes must be strings.' );
  267. }
  268. unbindAttrs.forEach( attrName => {
  269. const binding = this._boundAttributes[ attrName ];
  270. let toModel, toAttr, toAttrs, toAttrBindings;
  271. binding.to.forEach( to => {
  272. // TODO: ES6 destructuring.
  273. toModel = to[ 0 ];
  274. toAttr = to[ 1 ];
  275. toAttrs = this._boundModels.get( toModel );
  276. toAttrBindings = toAttrs[ toAttr ];
  277. toAttrBindings.delete( binding );
  278. if ( !toAttrBindings.size ) {
  279. delete toAttrs[ toAttr ];
  280. }
  281. if ( !Object.keys( toAttrs ).length ) {
  282. this._boundModels.delete( toModel );
  283. this.stopListening( toModel, 'change' );
  284. }
  285. } );
  286. delete this._boundAttributes[ attrName ];
  287. } );
  288. } else {
  289. this._boundModels.forEach( ( bindings, boundModel ) => {
  290. this.stopListening( boundModel, 'change' );
  291. } );
  292. this._boundModels.clear();
  293. this._boundAttributes = {};
  294. }
  295. }
  296. /**
  297. * A chaining for {@link #bind} providing `.to()` interface.
  298. *
  299. * @protected
  300. * @param {...[Model|String|Function]} args Arguments of the `.to( args )` binding.
  301. */
  302. _bindTo( ...args ) {
  303. const parsedArgs = parseBindToArgs( ...args );
  304. const bindingsKeys = Object.keys( this._bindings );
  305. const numberOfBindings = bindingsKeys.length;
  306. // Eliminate A.bind( 'x' ).to( B, C )
  307. if ( !parsedArgs.callback && parsedArgs.to.length > 1 ) {
  308. /**
  309. * Binding multiple models only possible with callback.
  310. *
  311. * @error model-bind-no-callback
  312. */
  313. throw new CKEditorError( 'model-bind-to-no-callback: Binding multiple models only possible with callback.' ) ;
  314. }
  315. // Eliminate A.bind( 'x', 'y' ).to( B, callback )
  316. if ( numberOfBindings > 1 && parsedArgs.callback ) {
  317. /**
  318. * Cannot bind multiple attributes and use a callback in one binding.
  319. *
  320. * @error model-bind-to-extra-callback
  321. */
  322. throw new CKEditorError( 'model-bind-to-extra-callback: Cannot bind multiple attributes and use a callback in one binding.' ) ;
  323. }
  324. parsedArgs.to.forEach( to => {
  325. // Eliminate A.bind( 'x', 'y' ).to( B, 'a' )
  326. if ( to.attrs.length && to.attrs.length !== numberOfBindings ) {
  327. /**
  328. * The number of attributes must match.
  329. *
  330. * @error model-bind-to-attrs-length
  331. */
  332. throw new CKEditorError( 'model-bind-to-attrs-length: The number of attributes must match.' );
  333. }
  334. // When no to.attrs specified, observing MODEL attributes instead.
  335. if ( !to.attrs.length ) {
  336. to.attrs = this._bindAttrs;
  337. }
  338. // Eliminate A.bind( 'x', 'y' ).to( B, 'a', 'b' ) when B has no 'a'.
  339. if ( !hasAttributes( to.model, to.attrs ) ) {
  340. /*
  341. * Model has no such attribute(s).
  342. *
  343. * @error model-bind-to-missing-attr
  344. */
  345. throw new CKEditorError( 'model-bind-to-missing-attr: Model has no such attribute(s).' );
  346. }
  347. } );
  348. this._to = parsedArgs.to;
  349. // Fill {@link BindChain#_bindings} with callback.
  350. if ( parsedArgs.callback ) {
  351. this._bindings[ bindingsKeys[ 0 ] ].callback = parsedArgs.callback;
  352. }
  353. attachBindToListeners( this._model, this._to );
  354. // Update model._boundAttributes and model._boundModels.
  355. updateBindToBound( this );
  356. // Set initial values of bound attributes.
  357. this._bindAttrs.forEach( attrName => {
  358. updateBoundModelAttr( this._model, attrName );
  359. } );
  360. }
  361. }
  362. /**
  363. * Check if the {@link Model} has given `attrs`.
  364. *
  365. * @private
  366. * @param {Model} model Model to be checked.
  367. * @param {Array} arr An array of `String`.
  368. * @returns {Boolean}
  369. */
  370. function hasAttributes( model, attrs ) {
  371. return attrs.every( a => a in model._attributes );
  372. }
  373. /**
  374. * Check if all entries of the array are of `String` type.
  375. *
  376. * @private
  377. * @param {Array} arr An array to be checked.
  378. * @returns {Boolean}
  379. */
  380. function isStringArray( arr ) {
  381. return arr.every( a => typeof a == 'string' );
  382. }
  383. /**
  384. * Parses and validates {@link Model#bind}`.to( args )` arguments and returns
  385. * an object with a parsed structure. For example
  386. *
  387. * A.bind( 'x' ).to( B, 'a', C, 'b', call );
  388. *
  389. * becomes
  390. *
  391. * {
  392. * to: [
  393. * { model: B, attrs: [ 'a' ] },
  394. * { model: C, attrs: [ 'b' ] },
  395. * ],
  396. * callback: call
  397. * }
  398. *
  399. * @private
  400. * @param {...*} args Arguments of {@link Model#bind}`.to( args )`.
  401. * @returns {Object}
  402. */
  403. function parseBindToArgs( ...args ) {
  404. // Eliminate A.bind( 'x' ).to()
  405. if ( !args.length ) {
  406. /**
  407. * Invalid argument syntax in `to()`.
  408. *
  409. * @error model-bind-to-parse-error
  410. */
  411. throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  412. }
  413. const parsed = { to: [] };
  414. let lastModel;
  415. args.forEach( a => {
  416. // Callback has already been defined.
  417. // Eliminate A.bind( 'x' ).to( B, 'a', callback, C )
  418. if ( parsed.callback ) {
  419. throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  420. } else if ( a instanceof Model ) {
  421. parsed.to.push( ( lastModel = { model: a, attrs: [] } ) );
  422. } else if ( typeof a == 'string' ) {
  423. lastModel.attrs.push( a );
  424. } else if ( typeof a == 'function' ) {
  425. parsed.callback = a;
  426. }
  427. // Eliminate A.bind( 'x' ).to( null, new Date(), etc. )
  428. else {
  429. throw new CKEditorError( 'model-bind-to-parse-error: Invalid argument syntax in `to()`.' );
  430. }
  431. } );
  432. return parsed;
  433. }
  434. /**
  435. * Synchronizes {@link Model#_boundModels} with {@link Binding}.
  436. *
  437. * @private
  438. * @param {Binding} binding A binding to store in {@link Model#_boundModels}.
  439. * @param {Model} toModel A model, which is a new component of `binding`.
  440. * @param {String} toAttrName A name of `toModel`'s attribute, a new component of the `binding`.
  441. */
  442. function updateBoundModels( model, binding, toModel, toAttrName ) {
  443. const bindingsToModel = model._boundModels.get( toModel );
  444. const bindings = bindingsToModel || {};
  445. if ( !bindings[ toAttrName ] ) {
  446. bindings[ toAttrName ] = new Set();
  447. }
  448. // Pass the binding to a corresponding Set in `model._boundModels`.
  449. bindings[ toAttrName ].add( binding );
  450. if ( !bindingsToModel ) {
  451. model._boundModels.set( toModel, bindings );
  452. }
  453. }
  454. /**
  455. * Synchronizes {@link Model#_boundAttributes} and {@link Model#_boundModels}
  456. * with {@link BindChain}.
  457. *
  458. * Assuming the following binding being created
  459. *
  460. * A.bind( 'a', 'b' ).to( B, 'x', 'y' );
  461. *
  462. * the following bindings were initialized by {@link Model#bind} in {@link BindChain#_bindings}:
  463. *
  464. * {
  465. * a: { model: A, attr: 'a', to: [] },
  466. * b: { model: A, attr: 'b', to: [] },
  467. * }
  468. *
  469. * Iterate over all bindings in this chain and fill their `to` properties with
  470. * corresponding to( ... ) arguments (components of the binding), so
  471. *
  472. * {
  473. * a: { model: A, attr: 'a', to: [ B, 'x' ] },
  474. * b: { model: A, attr: 'b', to: [ B, 'y' ] },
  475. * }
  476. *
  477. * Then update the structure of {@link Model#_boundModels} with updated
  478. * binding, so it becomes:
  479. *
  480. * Map( {
  481. * B: {
  482. * x: Set( [
  483. * { model: A, attr: 'a', to: [ [ B, 'x' ] ] }
  484. * ] ),
  485. * y: Set( [
  486. * { model: A, attr: 'b', to: [ [ B, 'y' ] ] },
  487. * ] )
  488. * }
  489. * } )
  490. *
  491. * @private
  492. * @param {BindChain} chain The binding initialized by {@link Model#bind}.
  493. */
  494. function updateBindToBound( chain ) {
  495. let binding, toAttr;
  496. for ( let attrName in chain._bindings ) {
  497. binding = chain._bindings[ attrName ];
  498. // Note: For a binding without a callback, this will run only once
  499. // like in A.bind( 'x', 'y' ).to( B, 'a', 'b' )
  500. // TODO: ES6 destructuring.
  501. chain._to.forEach( to => {
  502. toAttr = to.attrs[ binding.callback ? 0 : chain._bindAttrs.indexOf( attrName ) ];
  503. binding.to.push( [ to.model, toAttr ] );
  504. updateBoundModels( chain._model, binding, to.model, toAttr );
  505. } );
  506. }
  507. }
  508. /**
  509. * Updates an attribute of a {@link Model} with a value
  510. * determined by an entry in {@link Model#_boundAttributes}.
  511. *
  512. * @private
  513. * @param {Model} model A model which attribute is to be updated.
  514. * @param {String} attrName An attribute to be updated.
  515. */
  516. function updateBoundModelAttr( model, attrName ) {
  517. const binding = model._boundAttributes[ attrName ];
  518. let attrValue;
  519. // When a binding with callback is created like
  520. //
  521. // A.bind( 'a' ).to( B, 'b', C, 'c', callback );
  522. //
  523. // collect B.b and C.c, then pass them to callback to set A.a.
  524. if ( binding.callback ) {
  525. attrValue = binding.callback.apply( model, binding.to.map( to => to[ 0 ][ to[ 1 ] ] ) );
  526. } else {
  527. attrValue = binding.to[ 0 ];
  528. attrValue = attrValue[ 0 ][ attrValue[ 1 ] ];
  529. }
  530. // TODO: Needs update after https://github.com/ckeditor/ckeditor5-core/issues/132.
  531. if ( model.hasOwnProperty( attrName ) ) {
  532. model[ attrName ] = attrValue;
  533. } else {
  534. model.set( attrName, attrValue );
  535. }
  536. }
  537. /**
  538. * Starts listening to changes in {@link BindChain._to} models to update
  539. * {@link BindChain._model} {@link BindChain._bindAttrs}. Also sets the
  540. * initial state of {@link BindChain._model}.
  541. *
  542. * @private
  543. * @param {BindChain} chain The chain initialized by {@link Model#bind}.
  544. */
  545. function attachBindToListeners( model, toBindings ) {
  546. toBindings.forEach( to => {
  547. const boundModels = model._boundModels;
  548. let bindings;
  549. // If there's already a chain between the models (`model` listens to
  550. // `to.model`), there's no need to create another `change` event listener.
  551. if ( !boundModels.get( to.model ) ) {
  552. model.listenTo( to.model, 'change', ( evt, attrName ) => {
  553. bindings = boundModels.get( to.model )[ attrName ];
  554. // Note: to.model will fire for any attribute change, react
  555. // to changes of attributes which are bound only.
  556. if ( bindings ) {
  557. bindings.forEach( binding => {
  558. updateBoundModelAttr( model, binding.attr );
  559. } );
  560. }
  561. } );
  562. }
  563. } );
  564. }
  565. utilsObject.extend( Model.prototype, EmitterMixin );
  566. /**
  567. * Fired when an attribute changed value.
  568. *
  569. * @event change
  570. * @param {String} name The attribute name.
  571. * @param {*} value The new attribute value.
  572. * @param {*} oldValue The previous attribute value.
  573. */
  574. /**
  575. * Fired when an specific attribute changed value.
  576. *
  577. * @event change:{attribute}
  578. * @param {*} value The new attribute value.
  579. * @param {*} oldValue The previous attribute value.
  580. */