UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

591 lines (586 loc) 20.5 kB
(function (enyo) { //*@protected // The internal store for bindings so they can be found by id later var _bindingMap = {}, /** Used by the binding's setter for both targets and sources when determing whether or not to force a notification to fire. We cannot easily determine this for types that are passed by reference (e.g., arrays and native JavaScript objects). But for these types it is much clearer in nearly all cases. */ _force = /(string|number|boolean)/; /** For tracking purposes we count bindings. */ enyo.BindingCount = 0; //*@public /** _enyo.Binding_ is a mechanism used to keep properties synchronized. A binding may be used to link two properties on different objects, or even two properties on the same object. Once a binding has been established, it will wait for change notifications; when a notification arrives, the binding will synchronize the value between the two ends. Note that bindings may be one-way (the default) or two-way. Usually, you will not need to create _enyo.Binding_ objects arbitrarily, but will instead rely on the public [BindingSupport API](#enyo/source/kernel/mixins/BindingSupport.js), which is applied to [enyo.Object](#enyo.Object) and so is available on all of its subkinds. */ enyo.kind({ name: "enyo.Binding", kind: null, noDefer: true, /** The _from_ property designates a path in which the property of the source to bind from may be found. If the source is explicitly provided and the path is relative (i.e., it begins with a "."), it is relative to the source; otherwise, it is relative to the owner of the binding. To have a binding be evaluated from the global scope, prefix the path with a "^". If the source and the "^" are used in tandem, the "^" will be ignored and the path will be assumed to be relative to the provided source. */ from: "", /** The _to_ property designates a path in which the property of the target to bind from may be found. If the target is explicitly provided and the path is relative (i.e., it begins with a "."), it is relative to the target; otherwise, it is relative to the owner of the binding. To have a binding be evaluated from the global scope, prefix the path with a "^". If the target and the "^" are used in tandem, the "^" will be ignored and the path will be assumed to be relative to the provided target. */ to: "", /** Set this only to a reference for an object to use as the source for the binding. If this is not a bindable object, the source will be derived from the _from_ property during initialization. */ source: null, /** Set this to false to keep the binding from propagating `undefined` in either direction. For more control over the values that get propagated see [transform](#transform). Defaults to `true`. */ allowUndefined: true, /** Set this only to a reference for an object to use as the target for the binding. If this is not a bindable object, the target will be derived from the _to_ property during initialization. */ target: null, /** Set this to a function or the name of a method on the owner of this binding. A transform method is used to programmatically modify the value being synchronized. The method will be executed with three parameters, the _value_ being synchronized, the _direction_ (a string matching either "source" or "target", as in "going to the source") and a reference to this binding. In cases where the binding should be interrupted and not propagate the synchronization at all, call the _stop()_ method of the passed-in binding reference. */ transform: "", /** If the binding was able to resolve both ends (i.e., its _source_ and _target_ objects), this boolean will be true. Setting this manually will have undesirable effects. */ connected: false, /** Each binding has a unique id that can be used with the global static method _enyo.Binding.find()_ to retrieve a reference to that binding. It can also be used to track registered listeners on objects back to their bindings. */ id: "", /** If a binding is one-way, this flag should be true (the default). If this flag is set to false, the binding will be two-way. */ oneWay: true, /** By default, a binding will attempt to connect to both ends (_source_ and _target_). If this process should be deferred, set this flag to false. */ autoConnect: true, /** By default, a binding will attempt to synchronize its values from its _source_ to its _target_. If this process should be deferred, set this flag to false. */ autoSync: true, /** The _owner_ property is used extensively for various purposes within a binding. One primary purpose is to serve as a root object from which to search for its ends (the _source_ and/or _target_). If the owner created the binding, it will also be responsible for destroying it (automatically). */ owner: null, /** Boolean indicating whether this binding has been destroyed. Do not change this flag arbitrarily. */ destroyed: false, statics: { /** This method may be used to retrieve a binding by its id globally. Simply pass in the known _id_ string. If the id is found, the method will return a reference to the binding; otherwise, it will return _undefined_. */ find: function (id) { return _bindingMap[id]; } }, //*@protected sourceObserver: null, targetObserver: null, sourceConnected: false, targetConnected: false, sourceRegistered: false, targetRegistered: false, registeredSource: null, registeredTarget: null, sourcePath: "", targetPath: "", sourceProp: "", targetProp: "", building: true, //* Used internally to track the original values passed to the binding for rebuilding. originals: null, constructor: function (props) { if (props) { enyo.mixin(this, props); } this.id = enyo.uid("binding"); _bindingMap[this.id] = this; // faster this way than calling mixin or only or any other helper/convenience // method to do the same thing... this.originals = this.originals || {}; this.originals.from = this.from; this.originals.to = this.to; this.originals.source = this.source; this.originals.target = this.target; this.initTransform(); this.refresh(); enyo.BindingCount++; }, isConnected: function () { this.connected = (this.sourceConnected && this.targetConnected); return this.connected; }, registered: function (which, object) { if (which == "source") { this.sourceRegistered = true; this.registeredSource = object; } else if (which == "target") { this.targetRegistered = true; this.registeredTarget = object; } if (this.autoSync && this.isRegistered() && !this.synchronizing) { this.sync(); } }, isRegistered: function () { return !! (this.sourceRegistered && this.targetRegistered); }, syncFromSource: function () { if (!this.synchronizing) { this.synchronizing = true; if (this.isConnected() && this.isRegistered()) { var value = this.getSourceValue(), fn = this.transform; if (fn && typeof fn == "function") { value = fn.call(this.owner || this, value, "source", this); } if (this.allowUndefined || value !== undefined) { this.setTargetValue(value); } } this.synchronizing = false; } }, syncFromTarget: function () { if (!this.oneWay && !this.synchronizing) { this.synchronizing = true; if (this.isConnected() && this.isRegistered()) { var value = this.getTargetValue(), fn = this.transform; if (fn && typeof fn == "function") { value = fn.call(this.owner || this, value, "target", this); } if (this.allowUndefined || value !== undefined) { this.setSourceValue(value); } } this.synchronizing = false; } }, resolve: function () { var source = this.source, target = this.target, from = this.from, to = this.to, tp, sp; // this allows empty bindings to be created if (!from || !to) { return; } // this is the first track and should only be reachable if the binding has not yet been // built or it has been completely reset, on subsequent calls this path will be skipped if (this.building) { if (from[0] != "." && from[0] != "^") { throw "enyo.Binding: from path must begin with `.` or `^`"; } else if (to[0] != "." && to[0] != "^") { throw "enyo.Binding: to path must beging with `.` or `^`"; } // both of the following scenarios should only happen if the to/from are relative // if not they will fail later and this is intended as it is a logical fallacy to // both supply the global flag and a source/target of any sort if (typeof source == "string") { if (source[0] != "." && source[0] != "^") { throw "enyo.Binding: if source is a string it must begin with `.` or `^`"; } from = ((source == "."? "": source) + from); source = null; } if (typeof target == "string") { if (target[0] != "." && target[0] != "^") { throw "enyo.Binding: if target is a string it must begin with `.` or `^`"; } to = ((target == "."? "": target) + to); target = null; } // if we need to, infer the root paths if (!source) { this.source = source = (from[0] == "."? this.owner: enyo.global); } if (!target) { this.target = target = (to[0] == "."? this.owner: enyo.global); } // update our properties that will be used later for actually connecting the pieces sp = from.slice(1).split("."); tp = to .slice(1).split("."); // unfortunately special handling is required for cases where the object requested is // actually a reference to a component in the `$` hash var is = enyo.lastIndexOf(".", from); var it = enyo.lastIndexOf(".", to); this.sourcePath = (from[0] == "^"? sp.slice(0, -1): sp).join("."); this.targetPath = (to[0] == "^"? tp.slice(0,-1): tp).join("."); if (is > -1 && from[is-1] == "$") { // this means that the final property we need to request is something on the // hash directly and not a property of that object this.sourceProp = sp.slice(-2).join("."); } else { this.sourceProp = sp .pop(); } if (it > -1 && from[it-1] == "$") { // same situation as for the source above this.targetProp = tp.slice(-2).join("."); } else { this.targetProp = tp .pop(); } this.building = false; } if (source === enyo.global) { this.source = enyo.getPath(this.sourcePath); this.sourceGlobal = true; } if (target === enyo.global) { // if this is also a one-way binding we have a special need to reduce the path // by one more when retrieving the base target so we can attempt to register an observer // as expected for changes on the last base object in the chain prior to the // final property tp = this.targetPath.split("."); this.targetGlobalPath = tp.slice(0,-1).join("."); this.targetPath = tp.pop(); this.target = enyo.getPath(this.targetGlobalPath); this.targetGlobal = true; } return this; }, connectSource: function () { // the actual source/root object we need to connect to, without this reference // or a starting place, we cannot connect var source = this.source, // the path from the root object leading to the final property (which we will // be registering for) path = this.sourceGlobal? this.sourceProp: this.sourcePath, // the observer we need to register fn = this.sourceObserver, id = this.sourceObserverId || (this.sourceObserverId=enyo.uid("__bindingObserver__")); // if we were searching from a global path but originally didn't find the source it // will be enyo.global and we need to try and find it again if (source === enyo.global) { source = this.resolve().source; } if (source && source.addObserver && !this.sourceConnected) { if (!fn) { fn = this.sourceObserver = enyo.bindSafely(this, this.syncFromSource); fn.binding = this; fn.bindingProp = "source"; } this.sourceConnected = true; this.sourceObserver = source.addObserver(path, fn, null, id); } else { this.sourceConnected = false; } }, connectTarget: function () { // the actual target/root object we need to connect to, without this reference // or a starting place, we cannot connect var target = this.target, // the path from the root object leading to the final property (which we will // be registering for) path = this.targetGlobal? this.targetProp: this.targetPath, // the observer we need to register fn = this.targetObserver, id = this.targetObserverId || (this.targetObserverId=enyo.uid("__bindingObserver__")); if (target === enyo.global) { target = this.resolve().target; } if (target && target.addObserver && !this.targetConnected) { if (!fn && path) { fn = this.targetObserver = enyo.bindSafely(this, this.syncFromTarget); fn.binding = this; fn.bindingProp = "target"; } this.targetConnected = true; if (path) { this.targetObserver = target.addObserver(path, fn, null, id); } else { this.registered("target", this.target); } } else { this.targetConnected = false; } }, disconnectSource: function () { if (this.source && this.sourceConnected) { // the actual source/root object we need to connect to, without this reference // or a starting place, we cannot connect var source = this.source, // the observer we need to register fn = this.sourceObserver, id = this.sourceObserverId, _omap = enyo._observerMap[source.objectObserverId], _e = _omap && _omap[id]; if (_e) { if (source && source.addObserver && this.sourceConnected) { if (fn) { source.removeObserver(_e.observerProp, fn); } } } } this.sourceObserver = null; this.sourceConnected = false; }, disconnectTarget: function () { if (this.target && this.targetConnected) { // the actual target/root object we need to connect to, without this reference // or a starting place, we cannot connect var target = this.target, // the observer we need to register fn = this.targetObserver, id = this.targetObserverId, _omap = enyo._observerMap[target.objectObserverId], _e = _omap && _omap[id]; if (_e) { if (target && target.addObserver && this.targetConnected) { if (fn) { target.removeObserver(_e.observerProp, fn); } } } } this.targetObserver = null; this.targetConnected = false; }, /** Retrieving the values actually requires for the ends to have completely registered. This ensures we don't have invalid values propagated and/or extra work being done too early. */ getSourceValue: function () { var source = this.registeredSource; if (source) { return source.get(this.sourceProp); } }, getTargetValue: function () { var target = this.registeredTarget; if (target) { return target.get(this.targetProp); } }, setSourceValue: function (value) { var source = this.registeredSource; if (source) { if (source.destroyed) { this.destroy(); return; } if (!this.stop) { source.set(this.sourceProp, value, !_force.test(typeof value)); } else { this.stop = false; } } }, setTargetValue: function (value) { var target = this.registeredTarget; if (target) { if (target.destroyed) { this.destroy(); return; } if (!this.stop) { target.set(this.targetProp, value, !_force.test(typeof value)); } else { this.stop = false; } } }, //*@public /** Connects the ends (i.e., the _source_ and _target_) of the binding. While you typically won't need to call this method, it is safe to call even when the ends are already established. Note that if one or both of the ends does become connected and the _autoSync_ flag is true, the ends will automatically be synchronized. Returns a reference to the binding. */ connect: function () { var c = this.isConnected(); if (!c) { this.connecting = true; if (!this.sourceConnected) { this.connectSource(); } if (!this.targetConnected) { this.connectTarget(); } this.connecting = false; this.isConnected(); } if (this.connected && !c && this.autoSync) { this.sync(); } return this; }, /** Synchronizes values from the _source_ to the _target_. This usually will not need to be called manually. Two-way bindings will automatically synchronize from the _target_ end once they are connected. Returns a reference to the binding. */ sync: function () { if (!this.connecting) { this.syncFromSource(); } return this; }, /** Disconnects from the ends (i.e., _source_ and _target_) if a connection exists at either end. This method will most likely not need to be called directly. Returns a reference to the binding. */ disconnect: function () { this.disconnectSource(); this.disconnectTarget(); this.isConnected(); return this; }, /** Refreshes the binding, only rebuilding the parts that are missing. Will synchronize if it is able to connect and the _autoSync_ flag is true. Returns a reference to the binding. */ refresh: function () { this.stop = false; this.resolve(); if (this.autoConnect) { this.connect(); } return this; }, /** Resets all properties to their original state. Returns a reference to the binding. */ reset: function () { this.disconnect(); enyo.mixin(this, this.originals); this.building = true; this.stop = false; this.sourceRegistered = false; this.targetRegistered = false; this.registeredSource = null; this.registeredTarget = null; return this; }, /** Will cause a single propagation attempt to fail. Typically not called outside the scope of a transform. */ stop: function () { this.stop = true; }, /** Rebuilds the entire binding. Will synchronize if it is able to connect and the _autoSync_ flag is true. Returns a reference to the binding. */ rebuild: function () { return this.reset().refresh(); }, /** Releases all of the binding's parts and unregisters its observers. Typically, this method will not need to be called directly unless the binding was created without an owner. */ destroy: function () { this.disconnect(); this.destroyed = true; this.source = null; this.target = null; this.registeredSource = null; this.registeredTarget = null; this.sourceObserver = null; this.targetObserver = null; this.transform = null; this.originals = null; if (this.owner) { this.owner.removeBinding(this); this.owner = null; } delete _bindingMap[this.id]; enyo.BindingCount--; }, //*@protected initTransform: function () { var tf = this.transform, o = this.owner, bo = o? o.bindingTransformOwner: null; if (tf && enyo.isString(tf)) { // test first against the common case which is that it is on the // transform owner or the actual owner if (bo || o) { tf = enyo.getPath.call(bo || o, this.transform); // worst case here is to check if there was a bo and that failed if there is an // owner go ahead and check that too if (!tf && bo && o) { tf = enyo.getPath.call(o, this.transform); } } // only if that fails to we attempt to find the global if (!tf) { tf = enyo.getPath(this.transform); } } this.transform = enyo.isFunction(tf)? tf: null; } }); /** Use this framework property to control the default kind of binding to use for all kinds. It may be overridden at the kind level by setting the _defaultBindingKind_ property to a different kind of binding. */ enyo.defaultBindingKind = enyo.Binding; })(enyo);