source/class/core/property/Multi.js
/*
==================================================================================================
Core - JavaScript Foundation
Copyright 2010-2012 Zynga Inc.
Copyright 2012-2014 Sebastian Werner
==================================================================================================
*/
"use strict";
(function()
{
/*
---------------------------------------------------------------------------
INTERNAL DATA
---------------------------------------------------------------------------
*/
/** {Integer} Number of properties created. For debug proposes. */
var MultiCounter = 0;
/** {Integer} Maps multi property names to global property IDs */
var propertyNameToId = {};
/**
* {Map} Configuration for property fields
*/
var priorityToFieldConfig =
{
// Override
4 : {},
// User (aka Instance-specific value)
3: {},
// Theme, see {core.property.IThemeable}
2: {
get : "getThemedValue"
},
// Inheritance, see {core.property.IInheritable}
1 : {
get : "getInheritedValue"
}
};
/**
* Maps the name of a field to its priority
*
*/
var fieldToPriority =
{
inherited : 1,
theme : 2,
user : 3,
override : 4
};
// Shared variables (constants)
var initKeyPrefix = "$$init-";
var store = "$$data";
// Improve compressibility
var Undefined;
var PropertyUtil = core.property.Util;
/*
---------------------------------------------------------------------------
INTERNALS INHERITANCE
---------------------------------------------------------------------------
*/
/**
* Updates children of a obj {Object} where the given property has been modified
* from the @oldValue {var} to the @newValue {var}. The property configuration
* is made available via @config {Map}.
*/
var changeInheritedHelper = function(obj, newValue, oldValue, config)
{
// TODO: Improved this lookup via $$children
if (!obj._getChildren) {
return;
}
var children = obj._getChildren();
var length = children.length;
if (!length) {
return;
}
var inheritedPriority = fieldToPriority.inherited;
var propertyName=config.name, propertyApply=config.apply, propertyFire=config.fire;
var propertyId = propertyNameToId[propertyName];
var propertyInitKey = initKeyPrefix + propertyName;
var child, childData, childOldPriority, childOldValue, childOldGetter, childNewValue;
var Util = core.property.Util;
for (var i=0, l=children.length; i<l; i++)
{
child = children[i];
// Block child if it does not support the changed property
if (!Util.getPropertyDefinition(child.constructor, propertyName)) {
continue;
}
childData = child[store];
if (!childData) {
childData = child[store] = {};
}
// Quick lookup (higher priority value exist)
childOldPriority = childData[propertyId];
if (childOldPriority !== Undefined && childOldPriority > inheritedPriority) {
continue;
}
//
// Compute child's old value
//
if (childOldPriority === inheritedPriority)
{
childOldValue = oldValue;
}
else if (childOldPriority !== Undefined)
{
childOldGetter = priorityToFieldConfig[childOldPriority].get;
if (childOldGetter) {
childOldValue = child[childOldGetter](propertyName);
} else {
childOldValue = child[propertyId+childOldPriority];
}
}
else
{
childOldValue = child[propertyInitKey];
}
//
// Compute child's new value
//
childNewValue = newValue;
if (childNewValue === Undefined)
{
childNewValue = child[propertyInitKey];
childData[propertyId] = Undefined;
}
else
{
// Remember that we use the inherited value here
childData[propertyId] = inheritedPriority;
}
//
// Publish change
//
if (childNewValue !== childOldValue)
{
// Call apply
if (propertyApply) {
child[propertyApply](childNewValue, childOldValue, propertyName);
}
// Fire event
if (propertyFire)
{
var eventObject = core.property.Event.obtain(propertyFire, childNewValue, childOldValue, propertyName);
context.dispatchEvent(eventObject);
eventObject.release();
}
// Go into recursion
changeInheritedHelper(child, childNewValue, childOldValue, config);
}
}
};
/*
---------------------------------------------------------------------------
CLASS DEFINITION
---------------------------------------------------------------------------
*/
/**
* Multi-level property which support multiple values per property with integrated priorization. The following fields
* are available for properties depending on their configuration:
*
* * `inherited`
* * `theme`
* * `user`
* * `override`
*
* Higher values mean higher priority e.g. user values override themed values. There is an additional value
* which is the init value and is stored property-wide (read: class specific - not instance specific).
*
* Additional configuration flags (compared to simple properties):
*
* * `inheritable`: Whether the property value should be inheritable. If the property does not have a user defined or an init value, the property will try to get the value from the parent of the current object.
* * `themeable`: Whether the property allows a themable value read dynamically from a theming system.
* The object containing this property needs to implement a method `getThemedValue`.
*
* #break(core.property.Debug)
*/
core.Module("core.property.Multi",
{
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
/**
* Adds a new multi-field property with the given @name {String} (Camel-case and no special characters) and configuration (@config {Map}) to the @clazz {Class}.
*
* Please note that you need to define one of "init" or "nullable". Otherwise you might get errors during runtime function calls.
*/
create : function(config)
{
/*
---------------------------------------------------------------------------
INTRO: IDENTICAL BETWEEN SIMPLE AND MULTI
---------------------------------------------------------------------------
*/
// Increase counter
MultiCounter++;
// Generate property ID
// Identically named property might store data on the same field as in this case this is typically on different
// classes. We reserve four slots for storing instance-specific data: inheritance, theme, user and override
var propertyId = propertyNameToId[name];
if (!propertyId)
{
propertyId = propertyNameToId[name] = core.property.Core.ID;
// Number of fields + meta field to store where we store the data
core.property.Core.ID += 5;
}
var name = config.name;
var members = {};
// Shorthands: Better compression/obfuscation/performance
var propertyNullable = config.nullable;
var propertyInit = config.init;
var propertyFire = config.fire;
var propertyApply = config.apply;
var propertyInheritable = config.inheritable;
/*
---------------------------------------------------------------------------
FACTORY METHODS :: SETTER
---------------------------------------------------------------------------
*/
var setter = function(modifyPriority)
{
return function(newValue)
{
var context = this;
if (jasy.Env.isSet("debug")) {
core.property.Debug.checkSetter(context, config, arguments);
}
var data = context[store];
if (!data) {
data = context[store] = {};
}
else
{
// Read old value
var oldPriority = data[propertyId];
if (oldPriority !== Undefined)
{
var oldGetter = priorityToFieldConfig[oldPriority].get;
if (oldGetter) {
var oldValue = context[oldGetter](name);
} else {
var oldValue = data[propertyId+oldPriority];
}
}
}
// context.debug("Save " + name + "[" + modifyPriority + "]=" + newValue);
// Store new value
data[propertyId+modifyPriority] = newValue;
// Ignore lower-priority changes
if (oldPriority === Undefined || oldPriority <= modifyPriority)
{
// Whether the storage field was changed
if (oldPriority !== modifyPriority) {
data[propertyId] = modifyPriority;
}
// Fallback to init value on prototype chain (when supported)
// This is always the value on the current class, not explicitely the class which creates the property.
// This is mainly for supporting init value overrides with "refined" properties
if (oldValue === Undefined && propertyInit !== Undefined) {
oldValue = propertyInit;
}
// this.debug("Value Compare: " + newValue + " !== " + oldValue);
// Whether the value has been modified
if (newValue !== oldValue)
{
// Call apply
if (propertyApply) {
context[propertyApply](newValue, oldValue, config.name);
}
// Fire event
if (propertyFire)
{
var eventObject = core.property.Event.obtain(propertyFire, newValue, oldValue, propertyName);
context.dispatchEvent(eventObject);
eventObject.release();
}
// Inheritance support
if (propertyInheritable) {
changeInheritedHelper(context, newValue, oldValue, config);
}
}
}
return newValue;
};
};
/*
---------------------------------------------------------------------------
FACTORY METHODS :: RESETTER
---------------------------------------------------------------------------
*/
var resetter = function(modifyPriority)
{
return function(value)
{
var context = this;
if (jasy.Env.isSet("debug")) {
core.property.Debug.checkResetter(context, config, arguments);
}
var data = context[store];
// context.debug("Delete " + name + "[" + modifyPriority + "]");
// Only need to react when current field is resetted
var oldPriority = data[propertyId];
if (oldPriority === modifyPriority)
{
// Read old value
var oldValue = data[propertyId+oldPriority];
// We lost the current value, now we need to find the next stored value
var newValue, newGetter;
for (var newPriority=modifyPriority-1; newPriority>0; newPriority--)
{
newGetter = priorityToFieldConfig[newPriority].get;
if (newGetter) {
newValue = context[newGetter] ? context[newGetter](name) : Undefined;
} else {
newValue = data[propertyId+newPriority];
}
if (newValue !== Undefined) {
break;
}
}
// No value has been found
if (newValue === Undefined)
{
newPriority = Undefined;
// Let's try the class-wide init value
if (propertyInit !== Undefined) {
newValue = propertyInit;
}
else if (jasy.Env.isSet("debug"))
{
// Still no value. We warn about that the property is not nullable.
if (!propertyNullable) {
context.error("Missing value for: " + name + " (during reset())");
}
}
}
// Update current field
data[propertyId] = newPriority;
}
// Remove value from store
// This is placed here, because we need to keep the old value first and only want to do this when needed.
// Do not use delete operator for performance reasons: just modifying the value to undefined is enough.
data[propertyId+modifyPriority] = Undefined;
// Only need to react when current field is resetted
if (oldPriority === modifyPriority && oldValue !== newValue)
{
// Call apply
if (propertyApply) {
context[propertyApply](newValue, oldValue, config.name);
}
// Fire event
if (propertyFire)
{
var eventObject = core.property.Event.obtain(propertyFire, newValue, oldValue, propertyName);
context.dispatchEvent(eventObject);
eventObject.release();
}
// Inheritance support
if (propertyInheritable) {
changeInheritedHelper(context, newValue, oldValue, config);
}
}
};
};
/*
---------------------------------------------------------------------------
FACTORY METHODS :: GETTER
---------------------------------------------------------------------------
*/
var getter = function()
{
var context = this;
if (jasy.Env.isSet("debug")) {
core.property.Debug.checkGetter(context, config, arguments);
}
var data = context[store];
var currentPriority = data && data[propertyId];
if (currentPriority === Undefined)
{
// Fallback to init value on prototype chain (when supported)
// This is always the value on the current class, not explicitely the class which creates the property.
// This is mainly for supporting init value overrides with "refined" properties
if (propertyInit !== Undefined) {
return propertyInit;
}
// Alternatively chose null, if possible
if (propertyNullable) {
return null;
}
if (jasy.Env.isSet("debug"))
{
context.error("Missing value for: " + name +
" (during get()). Either define an init value, make the property nullable or define a fallback value.");
}
return;
}
// Special get() support for themable/inheritable properties
var currentGetter = priorityToFieldConfig[currentPriority].get;
if (currentGetter)
{
if (jasy.Env.isSet("debug"))
{
var value = context[currentGetter](name);
if (value === Undefined) {
throw new Error("Ooops. Invalid value at getter: " + name + " in " + context + " via getter: " + currentGetter);
}
return value;
}
else
{
return context[currentGetter](name);
}
}
else
{
return data[propertyId+currentPriority];
}
};
/*
---------------------------------------------------------------------------
FACTORY METHODS :: ATTACH METHODS
---------------------------------------------------------------------------
*/
members.get = getter;
// There are exactly two types of init methods:
// 1. Initializing the value given in the property configuration (calling apply methods, firing events, etc.)
// 2. Initializing the value during instance creation (useful for instance-specific non-shared values)
if (propertyInit !== Undefined)
{
members.init = function()
{
var context = this;
var data = context[store];
if (data)
{
// Check whether there is already another value assigned.
// In this case the whole function could be left early.
var oldPriority = data[propertyId];
if (oldPriority !== Undefined) {
return;
}
}
// Call apply
if (propertyApply) {
context[propertyApply](propertyInit, Undefined, config.name);
}
// Fire event
if (propertyFire)
{
var eventObject = core.property.Event.obtain(propertyFire, propertyInit, Undefined, config.name);
context.dispatchEvent(eventObject);
eventObject.release();
}
// Inheritance support
if (propertyInheritable) {
changeInheritedHelper(context, propertyInit, Undefined, config);
}
};
}
members.set = setter(3);
members.reset = resetter(3);
},
/**
* Returns a value of the @obj {Object} from a specific @field {String}
* (one of "init", "inheritance", "theme", "user" or "override") for the given @propertyName {String} -
* ignoring any priorities.
*/
getSingleValue : function(obj, propertyName, field)
{
var key = propertyNameToId[propertyName] + fieldToPriority[field];
if (jasy.Env.isSet("debug"))
{
if (typeof key != "number" || isNaN(key)) {
throw new Error("Invalid property or field: " + propertyName + ", " + field);
}
}
return obj[store][key];
},
/**
* Imports a list of values. Useful for batch-applying a whole set of properties. Supports
* <code>undefined</code> values to reset properties.
*
* - @obj {Object} Any object
* - @values {Map} Map of properties to apply
* - @oldValues {Map} Map of old property values. Just used for comparision. Required for theme changes. In case of a state change the old value is not available otherwise.
* - @field {String} Storage field to modify
*/
importData : function(obj, values, oldValues, field)
{
// Check existence of data structure
var data = obj[store];
if (!data) {
data = obj[store] = {};
}
// Commonly used variables
var modifyPriority = fieldToPriority[field];
var propertyName, propertyId, newValue, oldValue, oldPriority, propertyInit;
// Import every given property
for (propertyName in values)
{
propertyId = propertyNameToId[propertyName];
if (jasy.Env.isSet("debug"))
{
if (propertyId === undefined) {
throw new Error(obj + ": Invalid property to import: " + propertyName);
}
}
// Ignore if there is a higher priorized value
// Earliest return option: Higher priorized value set
oldPriority = data[propertyId];
if (oldPriority > modifyPriority) {
continue;
}
newValue = values[propertyName];
// If nothing is set at the moment and no new value is given then simply ignore the property for the moment
if (oldPriority === Undefined && newValue === Undefined) {
continue;
}
// Read out old value
if (oldPriority != null)
{
if (oldValues && oldPriority == modifyPriority) {
oldValue = oldValues[propertyName];
}
else
{
var oldGetter = priorityToFieldConfig[oldPriority].get;
if (oldGetter) {
oldValue = obj[oldGetter] ? obj[oldGetter](propertyName) : Undefined;
} else {
oldValue = data[propertyId+oldPriority];
}
}
}
else
{
oldValue = Undefined;
}
// Compare old and new value
// Second earliest return option: New value given and identical to old
if (oldValue === newValue) {
continue;
}
// Reset implementation block
if (newValue === Undefined)
{
// We lost the current value, now we need to find the next stored value
var newValue, newGetter;
for (var newPriority=modifyPriority-1; newPriority>0; newPriority--)
{
newGetter = priorityToFieldConfig[newPriority].get;
if (newGetter) {
newValue = obj[newGetter] ? obj[newGetter](propertyName) : Undefined;
} else {
newValue = data[propertyId+newPriority];
}
if (newValue !== Undefined) {
break;
}
}
// No value has been found
if (newValue === Undefined)
{
newPriority = Undefined;
// Let's try the property-wide init value
if (propertyInit !== Undefined)
{
newValue = propertyInit;
}
else if (jasy.Env.isSet("debug"))
{
// Still no value. We warn about that the property is not nullable.
var config = PropertyUtil.getPropertyDefinition(obj.constructor, propertyName);
if (!config.nullable) {
obj.error("Missing value for: " + propertyName + " (during reset() - from theme system)");
}
}
}
// Be sure that priority is right
data[propertyId] = newPriority;
}
// Set implementation block
else if (oldPriority != modifyPriority)
{
data[propertyId] = modifyPriority;
}
// Call change helper
// Third earlist "return" option, ok, not really a return option, but we at least omit useless change calls
// when values are identical
if (newValue !== oldValue)
{
var config = PropertyUtil.getPropertyDefinition(obj.constructor, propertyName);
// Call apply
if (config.apply) {
obj[config.apply](newValue, oldValue, config.name);
}
// Fire event
if (config.fire)
{
var eventObject = core.property.Event.obtain(config.fire, newValue, oldValue, config.name);
obj.dispatchEvent(eventObject);
eventObject.release();
}
// Inheritance support
if (config.inheritable) {
changeInheritedHelper(obj, newValue, oldValue, config);
}
}
}
},
/*
---------------------------------------------------------------------------
PUBLIC INHERITANCE API
---------------------------------------------------------------------------
*/
/**
* {Map} Returns a list (a map type for faster lookup) of all inheritable properties supported by the given @clazz {core.Class}.
*
* You may choose to access inheritable properties via:
* `obj.__inheritables || core.property.Multi.getInheritableProperties(obj)`
* for better performance.
*/
getInheritableProperties : function(clazz)
{
var result = clazz.__inheritables = {};
// Find all local properties which are inheritable
var props = clazz.$$properties;
if (props)
{
for (var name in props)
{
if (props[name].inheritable) {
result[name] = props[name];
}
}
}
var superClass = clazz.superclass;
if (superClass && superClass !== Object)
{
var remote = superClass.__inheritables || this.getInheritableProperties(superClass);
for (var name in remote) {
result[name] = remote[name];
}
}
return result;
},
/**
* Process an object whenever the parent has changed.
*
* Should be called by the object itself which was modified. Required are both parents, the old and the new one
* to make this work correctly. All given objects need to support the "$$parent" and "$$data" object fields.
*
* This function is quite optimized for reduced additional function calls. The only expensive scenarios are when
* a property is currently inherited or the new parent offers a value which needs to aquired using a get()
* call (e.g. themed or itself inherited). This means it is basically cheap for initial application creation,
* but is more expensive as soon as the application is running and objects are moved around dynamically.
*
* - @obj {Object} The modified object
* - @newParent {Object} The current parent
* - @oldParent {Object} The new parent
*/
moveObject : function(obj, newParent, oldParent)
{
// Fast compare (e.g. both null - should not happen, but still)
if (newParent == oldParent) {
return;
}
// Runtime variables
var inheritedPriority,
clazz, properties, propertyName, propertyId, propertyConfig,
data, oldPriority, oldValue, newValue,
newParentData, newParentPriority, newParentGetter;
// Fill with shared values through processing of all properties
inheritedPriority = fieldToPriority.inherited;
// Cache data field from object
data = obj[store];
if (!data) {
data = obj[store] = {};
}
// Cache data field from new parent
newParentData = newParent ? newParent[store] : Undefined;
// Iterate through all inheritable properties
clazz = obj.constructor;
properties = clazz.__inheritables || this.getInheritableProperties(clazz);
for (propertyName in properties)
{
propertyId = propertyNameToId[propertyName];
//
// READ OLD VALUE
//
oldPriority = data ? data[propertyId] : Undefined;
if (oldPriority === Undefined)
{
// Fallback to class-wide init value
oldValue = properties[propertyName].init;
}
else if (oldPriority == inheritedPriority)
{
// If we have used an inherited value, just ask the old parent for its value
oldValue = oldParent.get(propertyName);
}
else
{
// Higher priority field exists
continue;
}
//
// READ NEW VALUE
//
// Read new parent's value
newValue = Undefined;
if (newParent)
{
newParentPriority = newParentData ? newParentData[propertyId] : Undefined;
if (newParentPriority === Undefined)
{
// try to read old value from init value
parentConstructor = newParent.constructor;
parentProperties = parentConstructor.__inheritables || this.getInheritableProperties(parentConstructor);
newValue = parentProperties[propertyName].init;
}
else
{
// Deal with special getters (value comes from inheritable/themeable)
newParentGetter = priorityToFieldConfig[newParentPriority].get;
if (newParentGetter) {
newValue = newParent[newParentGetter] ? newParent[newParentGetter](propertyName) : Undefined;
} else {
newValue = newParentData[propertyId+newParentPriority];
}
if (newValue === Undefined) {
newValue = newParent[propertyInitKey];
}
}
}
// In cases where we have no new parent or the new parent don't has a value
// itself as well, then we try to use our init value as the new value
if (newValue === Undefined)
{
newValue = obj[propertyInitKey];
if (data[propertyId] !== Undefined) {
data[propertyId] = Undefined;
}
}
else
{
data[propertyId] = inheritedPriority;
}
//
// PERFORM CHANGES
//
// Compare values
if (newValue !== oldValue)
{
// obj.debug("Refresh: " + propertyName + ": " + oldValue + " => " + newValue);
propertyConfig = properties[propertyName];
// Call apply
if (propertyConfig.apply) {
obj[propertyConfig.apply](newValue, oldValue, propertyName);
}
// Fire event
if (propertyConfig.fire)
{
var eventObject = core.property.Event.obtain(propertyConfig.fire, newValue, oldValue, propertyName);
context.dispatchEvent(eventObject);
eventObject.release();
}
// Update children
changeInheritedHelper(obj, newValue, oldValue, propertyConfig);
}
}
}
});
})();