diff --git a/src/all.js b/src/all.js index 77e18e03e6..cb8130d6f3 100644 --- a/src/all.js +++ b/src/all.js @@ -1 +1,3 @@ goog.provide('ol'); + +goog.require('ol.MVCObject'); diff --git a/src/ol/mvcobject.js b/src/ol/mvcobject.js new file mode 100644 index 0000000000..a62738b516 --- /dev/null +++ b/src/ol/mvcobject.js @@ -0,0 +1,267 @@ + +/** + * @fileoverview An implementation of Google Maps' MVCObject. + * @see https://developers.google.com/maps/articles/mvcfun + * @see https://developers.google.com/maps/documentation/javascript/reference + */ + +goog.provide('ol.MVCObject'); + +goog.require('goog.array'); +goog.require('goog.events'); +goog.require('goog.events.EventTarget'); +goog.require('goog.object'); + + +/** + * @typedef {{target: ol.MVCObject, key: string}} + */ +ol.MVCObjectAccessor; + + + +/** + * @constructor + * @extends {goog.events.EventTarget} + */ +ol.MVCObject = function() { + goog.base(this); +}; +goog.inherits(ol.MVCObject, goog.events.EventTarget); + + +/** + * @param {string} str String. + * @return {string} Capitalized string. + */ +ol.MVCObject.capitalize = function(str) { + return str.substr(0, 1).toUpperCase() + str.substr(1); +}; + + +/** + * @param {ol.MVCObject|Object} arg Argument. + * @return {ol.MVCObject} MVCObject. + */ +ol.MVCObject.create = function(arg) { + if (arg instanceof ol.MVCObject) { + return arg; + } else { + var mvcObject = new ol.MVCObject(); + mvcObject.setOptions(arg); + return mvcObject; + } +}; + + +/** + * @private + * @type {Object.} + */ +ol.MVCObject.getterNameCache_ = {}; + + +/** + * @param {string} str String. + * @private + * @return {string} Capitalized string. + */ +ol.MVCObject.getGetterName_ = function(str) { + return ol.MVCObject.getterNameCache_[str] || + (ol.MVCObject.getterNameCache_[str] = + 'get' + ol.MVCObject.capitalize(str)); +}; + + +/** + * @private + * @type {Object.} + */ +ol.MVCObject.setterNameCache_ = {}; + + +/** + * @param {string} str String. + * @private + * @return {string} Capitalized string. + */ +ol.MVCObject.getSetterName_ = function(str) { + return ol.MVCObject.setterNameCache_[str] || + (ol.MVCObject.setterNameCache_[str] = + 'set' + ol.MVCObject.capitalize(str)); +}; + + +/** + * @param {ol.MVCObject} obj Object. + * @return {Object.} Accessors. + */ +ol.MVCObject.getAccessors = function(obj) { + return obj['gm_accessors_'] || (obj['gm_accessors_'] = {}); +}; + + +/** + * @param {ol.MVCObject} obj Object. + * @return {Object.} Listeners. + */ +ol.MVCObject.getListeners = function(obj) { + return obj['gm_bindings_'] || (obj['gm_bindings_'] = {}); +}; + + +/** + * @param {string} key Key. + * @param {ol.MVCObject} target Target. + * @param {string=} opt_targetKey Target key. + * @param {boolean=} opt_noNotify No notify. + */ +ol.MVCObject.prototype.bindTo = + function(key, target, opt_targetKey, opt_noNotify) { + var targetKey = goog.isDef(opt_targetKey) ? opt_targetKey : key; + this.unbind(key); + var eventType = targetKey.toLowerCase() + '_changed'; + var listeners = ol.MVCObject.getListeners(this); + listeners[key] = goog.events.listen(target, eventType, function() { + this.notifyInternal_(key); + }, undefined, this); + var accessors = ol.MVCObject.getAccessors(this); + accessors[key] = {target: target, key: targetKey}; + var noNotify = goog.isDef(opt_noNotify) ? opt_noNotify : false; + if (!noNotify) { + this.notifyInternal_(key); + } +}; + + +/** + * @param {string} key Key. + */ +ol.MVCObject.prototype.changed = function(key) { +}; + + +/** + * @param {string} key Key. + * @return {*} Value. + */ +ol.MVCObject.prototype.get = function(key) { + var accessors = ol.MVCObject.getAccessors(this); + if (goog.object.containsKey(accessors, key)) { + var accessor = accessors[key]; + var target = accessor.target; + var targetKey = accessor.key; + var getterName = ol.MVCObject.getGetterName_(targetKey); + if (target[getterName]) { + return target[getterName](); + } else { + return target.get(targetKey); + } + } else { + return this[key]; + } +}; + + +/** + * @param {string} key Key. + */ +ol.MVCObject.prototype.notify = function(key) { + var accessors = ol.MVCObject.getAccessors(this); + if (goog.object.containsKey(accessors, key)) { + var accessor = accessors[key]; + var target = accessor.target; + var targetKey = accessor.key; + target.notify(targetKey); + } else { + this.notifyInternal_(key); + } +}; + + +/** + * @param {string} key Key. + * @private + */ +ol.MVCObject.prototype.notifyInternal_ = function(key) { + var changedMethodName = key + '_changed'; + if (this[changedMethodName]) { + this[changedMethodName](); + } else { + this.changed(key); + } + var eventType = key.toLowerCase() + '_changed'; + this.dispatchEvent(eventType); +}; + + +/** + * @param {string} key Key. + * @param {*} value Value. + */ +ol.MVCObject.prototype.set = function(key, value) { + var accessors = ol.MVCObject.getAccessors(this); + if (goog.object.containsKey(accessors, key)) { + var accessor = accessors[key]; + var target = accessor.target; + var targetKey = accessor.key; + var setterName = ol.MVCObject.getSetterName_(targetKey); + if (target[setterName]) { + target[setterName](value); + } else { + target.set(targetKey, value); + } + } else { + this[key] = value; + this.notifyInternal_(key); + } +}; + + +/** + * @param {Object.} options Options. + */ +ol.MVCObject.prototype.setOptions = function(options) { + goog.object.forEach(options, function(value, key) { + var setterName = ol.MVCObject.getSetterName_(key); + if (this[setterName]) { + this[setterName](value); + } else { + this.set(key, value); + } + }, this); +}; + + +/** + * @param {Object.} values Values. + */ +ol.MVCObject.prototype.setValues = ol.MVCObject.prototype.setOptions; + + +/** + * @param {string} key Key. + */ +ol.MVCObject.prototype.unbind = function(key) { + var listeners = ol.MVCObject.getListeners(this); + var listener = listeners[key]; + if (listener) { + delete listeners[key]; + goog.events.unlistenByKey(listener); + var value = this.get(key); + var accessors = ol.MVCObject.getAccessors(this); + delete accessors[key]; + this[key] = value; + } +}; + + +/** + */ +ol.MVCObject.prototype.unbindAll = function() { + var listeners = ol.MVCObject.getListeners(this); + var keys = goog.object.getKeys(listeners); + goog.array.forEach(keys, function(key) { + this.unbind(key); + }, this); +}; diff --git a/src/ol/mvcobject_test.js b/src/ol/mvcobject_test.js new file mode 100644 index 0000000000..594987e3d0 --- /dev/null +++ b/src/ol/mvcobject_test.js @@ -0,0 +1,450 @@ +goog.require('goog.testing.jsunit'); +goog.require('ol.MVCObject'); + + +function testModel() { + var m = new ol.MVCObject(); + assertNotNullNorUndefined(m); +} + + +function testGetUndefined() { + var m = new ol.MVCObject(); + assertUndefined(m.get('k')); +} + + +function testGetSetGet() { + var m = new ol.MVCObject(); + assertUndefined(m.get('k')); + m.set('k', 1); + assertEquals(1, m.get('k')); +} + + +function testSetValues() { + var m = new ol.MVCObject(); + m.setValues({ + k1: 1, + k2: 2 + }); + assertEquals(1, m.get('k1')); + assertEquals(2, m.get('k2')); +} + +function testNotifyCallback() { + var m = new ol.MVCObject(); + var callbackCalled; + m.changed = function() { + callbackCalled = true; + }; + m.notify('k'); + assertTrue(callbackCalled); +} + + +function testNotifyKeyCallback() { + var m = new ol.MVCObject(); + var callbackCalled = false; + m.k_changed = function() { + callbackCalled = true; + }; + m.notify('k'); + assertTrue(callbackCalled); +} + + +function testNotifyKeyEvent() { + var m = new ol.MVCObject(); + var eventDispatched = false; + goog.events.listen(m, 'k_changed', function() { + eventDispatched = true; + }); + m.notify('k'); + assertTrue(eventDispatched); +} + + +function testSetNotifyCallback() { + var m = new ol.MVCObject(); + var callbackCalled; + m.changed = function() { + callbackCalled = true; + }; + m.set('k', 1); + assertTrue(callbackCalled); +} + + +function testSetNotifyKeyCallback() { + var m = new ol.MVCObject(); + var callbackCalled = false; + m.k_changed = function(v) { + callbackCalled = true; + }; + m.set('k', 1); + assertTrue(callbackCalled); +} + + +function testBindSetNotifyKeyCallback() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + var callbackCalled = false; + n.k_changed = function(v) { + callbackCalled = true; + }; + n.bindTo('k', m); + m.set('k', 1); + assertTrue(callbackCalled); +} + + +function testSetNotifyKeyEvent() { + var m = new ol.MVCObject(); + var eventDispatched = false; + goog.events.listen(m, 'k_changed', function() { + eventDispatched = true; + }); + m.set('k', 1); + assertTrue(eventDispatched); +} + + +function testSetBind() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + m.set('k', 1); + assertEquals(1, m.get('k')); + assertUndefined(n.get('k')); + n.bindTo('k', m); + assertEquals(1, m.get('k')); + assertEquals(1, n.get('k')); +} + + +function testBindSet() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.bindTo('k', m); + m.set('k', 1); + assertEquals(1, m.get('k')); + assertEquals(1, n.get('k')); +} + + +function testBindSetBackwards() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.bindTo('k', m); + n.set('k', 1); + assertEquals(1, m.get('k')); + assertEquals(1, n.get('k')); +} + + +function testSetBindBackwards() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.set('k', 1); + n.bindTo('k', m); + assertUndefined(m.get('k')); + assertUndefined(n.get('k')); +} + + +function testBindSetUnbind() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.bindTo('k', m); + n.set('k', 1); + assertEquals(1, m.get('k')); + assertEquals(1, n.get('k')); + n.unbind('k'); + assertEquals(1, m.get('k')); + assertEquals(1, n.get('k')); + n.set('k', 2); + assertEquals(1, m.get('k')); + assertEquals(2, n.get('k')); +} + + +function testUnbindAll() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.bindTo('k', m); + n.set('k', 1); + assertEquals(m.get('k'), 1); + assertEquals(n.get('k'), 1); + n.unbindAll(); + assertEquals(m.get('k'), 1); + assertEquals(n.get('k'), 1); + n.set('k', 2); + assertEquals(m.get('k'), 1); + assertEquals(n.get('k'), 2); +} + + +function testBindNotify() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + m.bindTo('k', n); + mCallbackCalled = false; + m.k_changed = function() { + mCallbackCalled = true; + }; + nCallbackCalled = false; + n.k_changed = function() { + nCallbackCalled = true; + }; + n.set('k', 1); + assertTrue(mCallbackCalled); + assertTrue(nCallbackCalled); +} + + +function testBindBackwardsNotify() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.bindTo('k', m); + mCallbackCalled = false; + m.k_changed = function() { + mCallbackCalled = true; + }; + nCallbackCalled = false; + n.k_changed = function() { + nCallbackCalled = true; + }; + n.set('k', 1); + assertTrue(mCallbackCalled); + assertTrue(nCallbackCalled); +} + + +function testBindRename() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + n.bindTo('kn', m, 'km'); + m.set('km', 1); + assertEquals(m.get('km'), 1); + assertEquals(n.get('kn'), 1); +} + + +function testBindRenameCallbacks() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + var kmCallbackCalled = false; + m.km_changed = function() { + kmCallbackCalled = true; + }; + var knCallbackCalled = false; + n.kn_changed = function() { + knCallbackCalled = true; + }; + n.bindTo('kn', m, 'km'); + m.set('km', 1); + assertEquals(m.get('km'), 1); + assertEquals(n.get('kn'), 1); + assertTrue(kmCallbackCalled); + assertTrue(knCallbackCalled); +} + + +function testTransitiveBindForwards() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + var o = new ol.MVCObject(); + n.bindTo('kn', m, 'km'); + o.bindTo('ko', n, 'kn'); + m.set('km', 1); + assertEquals(1, m.get('km')); + assertEquals(1, n.get('kn')); + assertEquals(1, o.get('ko')); +} + + +function testTransitiveBindBackwards() { + var m = new ol.MVCObject(); + var n = new ol.MVCObject(); + var o = new ol.MVCObject(); + n.bindTo('kn', m, 'km'); + o.bindTo('ko', n, 'kn'); + o.set('ko', 1); + assertEquals(1, m.get('km')); + assertEquals(1, n.get('kn')); + assertEquals(1, o.get('ko')); +} + + +function testInheritance() { + var C = function() {}; + C.prototype = new ol.MVCObject(); + var callbackCalled; + C.prototype.k_changed = function() { + callbackCalled = true; + }; + var c = new C(); + c.set('k', 1); + assertEquals(1, c.get('k')); + assertTrue(callbackCalled); +} + + +function testMrideyAccessors() { + // http://blog.mridey.com/2010/03/maps-javascript-api-v3-more-about.html + var a = new ol.MVCObject(); + a.set('level', 2); + assertEquals(2, a.get('level')); + var b = new ol.MVCObject(); + b.setValues({ + level: 2, + index: 3, + description: 'Hello world.' + }); + assertEquals(3, b.get('index')); +} + + +function testMrideyBinding() { + // http://blog.mridey.com/2010/03/maps-javascript-api-v3-more-about.html + var a = new ol.MVCObject(); + a.set('level', 2); + var b = new ol.MVCObject(); + b.bindTo('index', a, 'level'); + assertEquals(2, b.get('index')); + a.set('level', 3); + assertEquals(3, b.get('index')); + b.set('index', 4); + assertEquals(4, a.get('level')); + var c = new ol.MVCObject(); + c.bindTo('zoom', a, 'level'); + assertEquals(4, c.get('zoom')); + b.unbind('index'); + assertEquals(4, b.get('index')); + c.set('zoom', 5); + assertEquals(5, a.get('level')); + assertEquals(4, b.get('index')); +} + + +function testCircularBind() { + var a = new ol.MVCObject(); + var b = new ol.MVCObject(); + a.bindTo('k', b); + assertThrows(function() { + b.bindTo('k', a); + }); +} + + +function testPriority() { + var a = new ol.MVCObject(); + var b = new ol.MVCObject(); + a.set('k', 1); + b.set('k', 2); + a.bindTo('k', b); + assertEquals(2, a.get('k')); + assertEquals(2, b.get('k')); +} + + +function testPriorityUndefined() { + var a = new ol.MVCObject(); + var b = new ol.MVCObject(); + a.set('k', 1); + a.bindTo('k', b); + assertUndefined(a.get('k')); + assertUndefined(b.get('k')); +} + + +function testSetter() { + var a = new ol.MVCObject(); + var x; + var setterCalled; + a.setX = function(value) { + this.x = value; + setterCalled = true; + }; + a.set('x', 1); + assertEquals(1, a.get('x')); + assertUndefined(setterCalled); +} + + +function testSetterBind() { + var a = new ol.MVCObject(); + var x; + var setterCalled; + a.setX = function(value) { + this.x = value; + setterCalled = true; + }; + var b = new ol.MVCObject(); + b.bindTo('x', a); + b.set('x', 1); + assertEquals(1, a.get('x')); + assertEquals(1, b.get('x')); + assertTrue(setterCalled); +} + + +function testGetter() { + var a = new ol.MVCObject(); + var getterCalled; + a.getX = function() { + getterCalled = true; + return 1; + }; + assertUndefined(a.get('x')); + assertUndefined(getterCalled); +} + + +function testGetterBind() { + var a = new ol.MVCObject(); + var getterCalled; + a.getX = function() { + getterCalled = true; + return 1; + }; + var b = new ol.MVCObject(); + b.bindTo('x', a); + assertEquals(1, b.get('x')); + assertTrue(getterCalled); +} + + +function testBindSelf() { + var a = new ol.MVCObject(); + assertThrows(function() { + a.bindTo('k', a); + }); +} + + +function testChangedKey() { + var a = new ol.MVCObject(); + var changedKey; + a.changed = function(key) { + changedKey = key; + }; + a.set('k', 1); + assertEquals('k', changedKey); +} + + +function testCreateFromObject() { + var obj = {k: 1}; + var mvcObject = ol.MVCObject.create(obj); + assertTrue(mvcObject instanceof ol.MVCObject); + assertEquals(1, mvcObject.get('k')); +} + + +function testCreateFromMVCObject() { + var mvcObject1 = new ol.MVCObject(); + var mvcObject2 = ol.MVCObject.create(mvcObject1); + assertTrue(mvcObject2 === mvcObject1); +}