Source: br-presenter/src/br/presenter/node/PresentationNode.js

/**
 * @module br/presenter/node/PresentationNode
 */

/**
 * Constructs a new instance of <code>PresentationNode</code>.
 * 
 * @class
 * @alias module:br/presenter/node/PresentationNode
 * 
 * @classdesc
 * Base class of all complex objects (nodes) within a presentation model.
 * 
 * <p>A {@link module:br/presenter/PresentationModel} is a tree of <code>PresentationNode</code>
 * instances, with instances of {@link module:br/presenter/property/Property} and <code>Function</code>
 * forming the leafs of the tree. Objects that do not extend <code>PresentationNode</code>
 * are not considered to be part of the presentation model, and are not accessible within the
 * view.</p>
 * 
 * <p>When a {@link module:br/presenter/PresentationModel} is created the <code>_$setPath</code> method is called
 * which throws an exception if the model does not adhere to a tree structure. It also creates a "path" label
 * on each node which identifies the node in standard object notation from the root node.</p>
 * 
 * <p>The structure is not strictly a tree because nodes are allowed to hold references back up to their
 * direct ancestors. When any of the (recursive) search functions that find descendant nodes are called these
 * "back links" are ignored, preventing infinite recursion.</p>
 */
br.presenter.node.PresentationNode = function()
{
};

/**
 * Returns all nested properties matching the search criteria reachable from this node.
 *
 * <p>Care is taken not to search up the tree in cyclic presentation models (where
 * some of the presentation nodes have back references to presentation nodes higher
 * up in the tree).</p>
 *
 * @param {String} sPropertyName The name of properties to match.
 * @param {Object} vValue The value of properties to match.
 * @type br.presenter.property.Properties
 *
 * @see #nodes
 */
br.presenter.node.PresentationNode.prototype.properties = function(sPropertyName, vValue)
{
	var pNodes = this.nodes().getNodesArray();
	// we need to get properties for the current node
	pNodes.push(this);

	var pProperties = [];

	for(var i = 0, l = pNodes.length; i < l; ++i)
	{
		var oNode = pNodes[i];

		for(var sKey in oNode)
		{
			var vItem = oNode[sKey];
			if(this._isPresenterChild(sKey, vItem)){
				if(vItem instanceof br.presenter.property.Property)
				{
					var oProperty = vItem;

					if((!sPropertyName || (sKey == sPropertyName)) && (!vValue || (oProperty.getValue() == vValue)))
					{
						pProperties.push(oProperty);
					}
				}
			}else{
				continue;
			}
		}
	}

	return new br.presenter.property.Properties(pProperties);
};

/**
 * Returns all nested nodes matching the search criteria reachable from this node.
 *
 * <p>Care is taken not to search up the tree in cyclic presentation models (where
 * some of the presentation nodes have back references to presentation nodes higher
 * up in the tree).</p>
 *
 * @param {String} sNodeName The name of nodes to match.
 * @param {Object} vProperties Only nodes having this array or map of properties will be matched.
 * @type br.presenter.node.Nodes
 *
 * @see #properties
 */
br.presenter.node.PresentationNode.prototype.nodes = function(sNodeName, vProperties)
{
	sNodeName = (sNodeName && (sNodeName != "*")) ? sNodeName : null;
	var mProperties = this._convertToMap(vProperties);
	var pNodes = [];
	this._getNodes(sNodeName, mProperties, pNodes);
	return new br.presenter.node.Nodes(pNodes);
};

/**
 * Returns the path that would be required to bind this node from the view.
 *
 * <p>This method is used internally, but might also be useful in allowing the dynamic
 * construction of views for arbitrary presentation models.</p>
 *
 * @type String
 */
br.presenter.node.PresentationNode.prototype.getPath = function()
{
	return this.m_sPath;
};



/**
 * @deprecated This method has been replaced by #removeChildListeners which recurses the node tree.
 * Removes all listeners attached to the properties contained by this <code>PresentationNode</code>.
 */
br.presenter.node.PresentationNode.prototype.removeAllListeners = function()
{
	this.removeChildListeners();
};

/**
 * Removes all listeners attached to the properties contained by this <code>PresentationNode</code>, and any nodes it contains.
 */
br.presenter.node.PresentationNode.prototype.removeChildListeners = function()
{
	this.properties().removeAllListeners();
};

// *********************** Private Methods ***********************

br.presenter.node.PresentationNode.prototype._convertToMap = function(vProperties)
{
	var mProperties;

	if(vProperties instanceof Array)
	{
		mProperties = {};

		for(var i = 0, l = vProperties.length; i < l; ++i)
		{
			mProperties[vProperties[i]] = "*";
		}
	}
	else
	{
		mProperties = vProperties || {};
	}

	return mProperties;
};



/**
 * @private
 */
br.presenter.node.PresentationNode.prototype._$setPath = function(sPath, oPresenterComponent)
{
	this.m_sPath = sPath;

	for(var sChildToBeSet in this)
	{
		var oChildToBeSet = this[sChildToBeSet];

		if(this._isPresenterChild(sChildToBeSet, oChildToBeSet) )
		{
			var sCurrentPath = oChildToBeSet.getPath();
			var sChildPath = sPath + '.' + sChildToBeSet;

			if(sCurrentPath === undefined)
			{
				oChildToBeSet._$setPath(sChildPath, oPresenterComponent);
			}
			else if(sCurrentPath !== sChildPath)
			{
				this._checkAncestor(sCurrentPath, sChildPath);
			}
		}
	}

	this.__oPresenterComponent = oPresenterComponent;
};

/*
 * PN's form a tree but we want to allow a child node to hold a reference to an ancestor node.
 * The methods that recurse the PN structure will ignore such links thus avoiding infinite recursion.
 * We recognize an ancestor node because its path must be a prefix of its childrens paths
 */
br.presenter.node.PresentationNode.prototype._checkAncestor = function(sOtherPath, sChildPath)
{
	if(sOtherPath === ""){ // the toplevel - PresentationModel
		return;
	}

	if(sChildPath.indexOf(sOtherPath) != 0){
		var msg = "OtherPath: '" + sOtherPath + "  'ChildPath:'" + sChildPath + "' are both references to the same instance in PresentationNode.";
		throw new br.Errors.IllegalStateError(msg);
	}
}

br.presenter.node.PresentationNode.prototype._isPresenterChild = function(sChildToBeSet, oChildToBeSet)
{
	return (oChildToBeSet  && oChildToBeSet._$setPath);
};

/**
 * @private
 */
br.presenter.node.PresentationNode.prototype._$clearPropertiesPath = function()
{
	var pProperties = this.properties();
	for(var i = 0; i < pProperties.m_pProperties.length; i++){
		pProperties.m_pProperties[i]._$setPath(undefined);
	}
};

/**
 * @private
 */
br.presenter.node.PresentationNode.prototype._$clearNodePaths = function()
{
	// It's possible for nodes to be newly created and then passed into
	// a NodeList before the nodes have had their path set.  This causes
	// problems here.
	// For now, we don't clear children of nodes that don't have their
	// path set.  This is probably wrong and will need to be fixed properly.
	if (this.m_sPath === undefined) return;

	var pNodes = this.nodes().getNodesArray();
	this._$clearPropertiesPath();
	for(var i = 0; i < pNodes.length; i++)
	{
		pNodes[i]._$clearNodePaths();
	}
	this.m_sPath = undefined;
};

/**
 * @private
 */
br.presenter.node.PresentationNode.prototype._getNodes = function(sNodeName, mProperties, pNodes)
{
	for(var sKey in this)
	{
		var vItem = this[sKey];
		if(!this._isPresenterChild(sKey, vItem)){
			continue;
		}

		if(vItem instanceof br.presenter.node.PresentationNode){

			if(this._isUpwardReference(this, vItem)){
				continue;
			}

			var oPresentationNode = vItem;
			if(this._containsNode(pNodes, oPresentationNode)){
				continue;
			}

			if(this._nodeMatchesQuery(oPresentationNode, sKey, sNodeName, mProperties)){
				pNodes.push(oPresentationNode);
			}

			oPresentationNode._getNodes(sNodeName, mProperties, pNodes);
		}
	}
};

/*
 * We know that the only duplicate references to nodes are ancestor nodes.[enforced by _$setPath()]
 * These must have shorter paths than any children.
 */
br.presenter.node.PresentationNode.prototype._isUpwardReference = function(oParentNode, oChildNode)
{
	var sChildPath = oChildNode.getPath();
	var sParentPath = oParentNode.getPath();
	if(sChildPath === undefined && sParentPath === undefined){
		return false;
	}
	// This is a temporary thing to make the tests pass.
	if (sChildPath === undefined || sParentPath === undefined) {
		return false;
	}
	return (sChildPath.length < sParentPath.length);
};


br.presenter.node.PresentationNode.prototype._containsNode = function(pNodes, oNode)
{
	for (var i = 0, end = pNodes.length; i < end ; i++){
		if(pNodes[i] === oNode){
			return true;
		}
	}
	return false;
}

/**
 * @private
 */
br.presenter.node.PresentationNode.prototype._nodeMatchesQuery = function(oPresentationNode, sActualNodeName, sNodeName, mProperties)
{
	if((sNodeName) && (sNodeName != sActualNodeName))
	{
		return false;
	}

	for(var sProperty in mProperties)
	{
		var sPropertyValue = mProperties[sProperty];
		var oProperty = oPresentationNode[sProperty];

		if(!(oProperty instanceof br.presenter.property.Property))
		{
			return false;
		}
		else if((sPropertyValue != "*") && (sPropertyValue != oProperty.getValue()))
		{
			return false;
		}
	}

	return true;
};