8
0

model.js 18 KB

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