| // Copyright 2006 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview An implementation of DataNode for wrapping JS data. |
| * |
| */ |
| |
| |
| goog.provide('goog.ds.JsDataSource'); |
| goog.provide('goog.ds.JsPropertyDataSource'); |
| |
| goog.require('goog.ds.BaseDataNode'); |
| goog.require('goog.ds.BasicNodeList'); |
| goog.require('goog.ds.DataManager'); |
| goog.require('goog.ds.DataNode'); |
| goog.require('goog.ds.EmptyNodeList'); |
| goog.require('goog.ds.LoadState'); |
| |
| |
| /** |
| * Data source whose backing is JavaScript data |
| * |
| * Names that are reserved for system use and shouldn't be used for data node |
| * names: eval, toSource, toString, unwatch, valueOf, watch. Behavior is |
| * undefined if these names are used. |
| * |
| * @param {Object} root The root JS node. |
| * @param {string} dataName The name of this node relative to the parent node. |
| * @param {Object=} opt_parent Optional parent of this JsDataSource. |
| * |
| * implements goog.ds.DataNode. |
| * @constructor |
| * @extends {goog.ds.DataNode} |
| */ |
| // TODO(arv): Use interfaces when available. |
| goog.ds.JsDataSource = function(root, dataName, opt_parent) { |
| this.parent_ = opt_parent; |
| this.dataName_ = dataName; |
| this.setRoot(root); |
| }; |
| |
| |
| /** |
| * The root JS object. Can be null. |
| * @type {*} |
| * @protected |
| * @suppress {underscore|visibility} |
| */ |
| goog.ds.JsDataSource.prototype.root_; |
| |
| |
| /** |
| * Sets the root JS object |
| * @param {Object} root The root JS object. Can be null. |
| * |
| * @protected |
| */ |
| goog.ds.JsDataSource.prototype.setRoot = function(root) { |
| this.root_ = root; |
| this.childNodeList_ = null; |
| }; |
| |
| |
| /** |
| * Set this data source to use list semantics. List data sources: |
| * - Are assumed to have child nodes of all of the same type of data |
| * - Fire data changes on the root node of the list whenever children |
| * are added or removed |
| * @param {?boolean} isList True to use list semantics. |
| * @private |
| */ |
| goog.ds.JsDataSource.prototype.setIsList_ = function(isList) { |
| this.isList_ = isList; |
| }; |
| |
| |
| /** @override */ |
| goog.ds.JsDataSource.prototype.get = function() { |
| return !goog.isObject(this.root_) ? this.root_ : this.getChildNodes(); |
| }; |
| |
| |
| /** |
| * Set the value of the node |
| * @param {*} value The new value of the node. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.set = function(value) { |
| if (value && goog.isObject(this.root_)) { |
| throw Error('Can\'t set group nodes to new values yet'); |
| } |
| |
| if (this.parent_) { |
| this.parent_.root_[this.dataName_] = value; |
| } |
| this.root_ = value; |
| this.childNodeList_ = null; |
| |
| goog.ds.DataManager.getInstance().fireDataChange(this.getDataPath()); |
| }; |
| |
| |
| /** |
| * TODO(user) revisit lazy creation. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.getChildNodes = function(opt_selector) { |
| if (!this.root_) { |
| return new goog.ds.EmptyNodeList(); |
| } |
| |
| if (!opt_selector || opt_selector == goog.ds.STR_ALL_CHILDREN_SELECTOR) { |
| this.createChildNodes_(false); |
| return this.childNodeList_; |
| } else if (opt_selector.indexOf(goog.ds.STR_WILDCARD) == -1) { |
| if (this.root_[opt_selector] != null) { |
| return new goog.ds.BasicNodeList([this.getChildNode(opt_selector)]); |
| } else { |
| return new goog.ds.EmptyNodeList(); |
| } |
| } else { |
| throw Error('Selector not supported yet (' + opt_selector + ')'); |
| } |
| |
| }; |
| |
| |
| /** |
| * Creates the DataNodeList with the child nodes for this element. |
| * Allows for only building list as needed. |
| * |
| * @param {boolean=} opt_force Whether to force recreating child nodes, |
| * defaults to false. |
| * @private |
| */ |
| goog.ds.JsDataSource.prototype.createChildNodes_ = function(opt_force) { |
| if (this.childNodeList_ && !opt_force) { |
| return; |
| } |
| |
| if (!goog.isObject(this.root_)) { |
| this.childNodeList_ = new goog.ds.EmptyNodeList(); |
| return; |
| } |
| |
| var childNodeList = new goog.ds.BasicNodeList(); |
| var newNode; |
| if (goog.isArray(this.root_)) { |
| var len = this.root_.length; |
| for (var i = 0; i < len; i++) { |
| // "id" is reserved node name that will map to a named child node |
| // TODO(user) Configurable logic for choosing id node |
| var node = this.root_[i]; |
| var id = node.id; |
| var name = id != null ? String(id) : '[' + i + ']'; |
| newNode = new goog.ds.JsDataSource(node, name, this); |
| childNodeList.add(newNode); |
| } |
| } else { |
| for (var name in this.root_) { |
| var obj = this.root_[name]; |
| // If the node is already a datasource, then add it. |
| if (obj.getDataName) { |
| childNodeList.add(obj); |
| } else if (!goog.isFunction(obj)) { |
| newNode = new goog.ds.JsDataSource(obj, name, this); |
| childNodeList.add(newNode); |
| } |
| } |
| } |
| this.childNodeList_ = childNodeList; |
| }; |
| |
| |
| /** |
| * Gets a named child node of the current node |
| * @param {string} name The node name. |
| * @param {boolean=} opt_canCreate If true, can create child node. |
| * @return {goog.ds.DataNode} The child node, or null if no node of |
| * this name exists. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.getChildNode = function(name, opt_canCreate) { |
| if (!this.root_) { |
| return null; |
| } |
| var node = /** @type {goog.ds.DataNode} */ (this.getChildNodes().get(name)); |
| if (!node && opt_canCreate) { |
| var newObj = {}; |
| if (goog.isArray(this.root_)) { |
| newObj['id'] = name; |
| this.root_.push(newObj); |
| } else { |
| this.root_[name] = newObj; |
| } |
| node = new goog.ds.JsDataSource(newObj, name, this); |
| if (this.childNodeList_) { |
| this.childNodeList_.add(node); |
| } |
| } |
| return node; |
| }; |
| |
| |
| /** |
| * Gets the value of a child node |
| * @param {string} name The node name. |
| * @return {Object} The value of the node, or null if no value or the child |
| * node doesn't exist. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.getChildNodeValue = function(name) { |
| if (this.childNodeList_) { |
| var node = this.getChildNodes().get(name); |
| return node ? node.get() : null; |
| } else if (this.root_) { |
| return this.root_[name]; |
| } else { |
| return null; |
| } |
| }; |
| |
| |
| /** |
| * Sets a named child node of the current node. |
| * If value is null, removes the child node. |
| * @param {string} name The node name. |
| * @param {Object} value The value to set, can be DataNode, object, |
| * property, or null. |
| * @return {Object} The child node, if set. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.setChildNode = function(name, value) { |
| var removedPath = null; |
| var node = null; |
| var addedNode = false; |
| |
| // Set node to the DataNode to add - if the value isn't already a DataNode, |
| // creates a JsDataSource or JsPropertyDataSource wrapper |
| if (value != null) { |
| if (value.getDataName) { |
| // The value is a DataNode. We must update its parent. |
| node = value; |
| node.parent_ = this; |
| } else { |
| if (goog.isArray(value) || goog.isObject(value)) { |
| node = new goog.ds.JsDataSource(value, name, this); |
| } else { |
| node = new goog.ds.JsPropertyDataSource( |
| /** @type {goog.ds.DataNode} */ (this.root_), name, this); |
| } |
| } |
| } |
| |
| // This logic will get cleaner once we can remove the backing array / object |
| // and just rely on the childNodeList_. This is needed until dependent code |
| // is cleaned up. |
| // TODO(user) Remove backing array / object and just use childNodeList_ |
| |
| if (goog.isArray(this.root_)) { |
| // To remove by name, need to create a map of the child nodes by ID |
| this.createChildNodes_(); |
| var index = this.childNodeList_.indexOf(name); |
| if (value == null) { |
| // Remove the node |
| var nodeToRemove = this.childNodeList_.get(name); |
| if (nodeToRemove) { |
| removedPath = nodeToRemove.getDataPath(); |
| } |
| this.root_.splice(index, 1); |
| } else { |
| // Add the node |
| if (index) { |
| this.root_[index] = value; |
| } else { |
| this.root_.push(value); |
| } |
| } |
| if (index == null) { |
| addedNode = true; |
| } |
| this.childNodeList_.setNode(name, /** @type {goog.ds.DataNode} */ (node)); |
| } else if (goog.isObject(this.root_)) { |
| if (value == null) { |
| // Remove the node |
| this.createChildNodes_(); |
| var nodeToRemove = this.childNodeList_.get(name); |
| if (nodeToRemove) { |
| removedPath = nodeToRemove.getDataPath(); |
| } |
| delete this.root_[name]; |
| } else { |
| // Add the node |
| if (!this.root_[name]) { |
| addedNode = true; |
| } |
| this.root_[name] = value; |
| } |
| // Only need to update childNodeList_ if has been created already |
| if (this.childNodeList_) { |
| this.childNodeList_.setNode(name, /** @type {goog.ds.DataNode} */ (node)); |
| } |
| } |
| |
| // Fire the event that the node changed |
| var dm = goog.ds.DataManager.getInstance(); |
| if (node) { |
| dm.fireDataChange(node.getDataPath()); |
| if (addedNode && this.isList()) { |
| dm.fireDataChange(this.getDataPath()); |
| dm.fireDataChange(this.getDataPath() + '/count()'); |
| } |
| } else if (removedPath) { |
| dm.fireDataChange(removedPath); |
| if (this.isList()) { |
| dm.fireDataChange(this.getDataPath()); |
| dm.fireDataChange(this.getDataPath() + '/count()'); |
| } |
| } |
| return node; |
| }; |
| |
| |
| /** |
| * Get the name of the node relative to the parent node |
| * @return {string} The name of the node. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.getDataName = function() { |
| return this.dataName_; |
| }; |
| |
| |
| /** |
| * Setthe name of the node relative to the parent node |
| * @param {string} dataName The name of the node. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.setDataName = function(dataName) { |
| this.dataName_ = dataName; |
| }; |
| |
| |
| /** |
| * Gets the a qualified data path to this node |
| * @return {string} The data path. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.getDataPath = function() { |
| var parentPath = ''; |
| if (this.parent_) { |
| parentPath = this.parent_.getDataPath() + goog.ds.STR_PATH_SEPARATOR; |
| } |
| |
| return parentPath + this.dataName_; |
| }; |
| |
| |
| /** |
| * Load or reload the backing data for this node |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.load = function() { |
| // Nothing to do |
| }; |
| |
| |
| /** |
| * Gets the state of the backing data for this node |
| * TODO(user) Discuss null value handling |
| * @return {goog.ds.LoadState} The state. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.getLoadState = function() { |
| return (this.root_ == null) ? goog.ds.LoadState.NOT_LOADED : |
| goog.ds.LoadState.LOADED; |
| }; |
| |
| |
| /** |
| * Whether the value of this node is a homogeneous list of data |
| * @return {boolean} True if a list. |
| * @override |
| */ |
| goog.ds.JsDataSource.prototype.isList = function() { |
| return this.isList_ != null ? this.isList_ : goog.isArray(this.root_); |
| }; |
| |
| |
| |
| /** |
| * Data source for JavaScript properties that arent objects. Contains reference |
| * to parent object so that you can set the vaule |
| * |
| * @param {goog.ds.DataNode} parent Parent object. |
| * @param {string} dataName Name of this property. |
| * @param {goog.ds.DataNode=} opt_parentDataNode The parent data node. If |
| * omitted, assumes that the parent object is the parent data node. |
| * |
| * @constructor |
| * @extends {goog.ds.BaseDataNode} |
| * @final |
| */ |
| goog.ds.JsPropertyDataSource = function(parent, dataName, opt_parentDataNode) { |
| goog.ds.BaseDataNode.call(this); |
| this.dataName_ = dataName; |
| this.parent_ = parent; |
| this.parentDataNode_ = opt_parentDataNode || this.parent_; |
| }; |
| goog.inherits(goog.ds.JsPropertyDataSource, goog.ds.BaseDataNode); |
| |
| |
| /** |
| * Get the value of the node |
| * @return {Object} The value of the node, or null if no value. |
| */ |
| goog.ds.JsPropertyDataSource.prototype.get = function() { |
| return this.parent_[this.dataName_]; |
| }; |
| |
| |
| /** |
| * Set the value of the node |
| * @param {Object} value The new value of the node. |
| * @override |
| */ |
| goog.ds.JsPropertyDataSource.prototype.set = function(value) { |
| var oldValue = this.parent_[this.dataName_]; |
| this.parent_[this.dataName_] = value; |
| |
| if (oldValue != value) { |
| goog.ds.DataManager.getInstance().fireDataChange(this.getDataPath()); |
| } |
| }; |
| |
| |
| /** |
| * Get the name of the node relative to the parent node |
| * @return {string} The name of the node. |
| * @override |
| */ |
| goog.ds.JsPropertyDataSource.prototype.getDataName = function() { |
| return this.dataName_; |
| }; |
| |
| |
| /** @override */ |
| goog.ds.JsPropertyDataSource.prototype.getParent = function() { |
| return this.parentDataNode_; |
| }; |