'use strict';
var Errors = require('br/Errors');
/**
* @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>
*/
function PresentationNode() {
}
/**
* 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
*/
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) {
if (!PresentationNode.isPrivateKey(sKey)) {
var vItem = oNode[sKey];
if (this._isPresenterChild(sKey, vItem)) {
if (vItem instanceof Property) {
var oProperty = vItem;
if ((!sPropertyName || (sKey == sPropertyName)) && (!vValue || (oProperty.getValue() == vValue))) {
pProperties.push(oProperty);
}
}
} else {
continue;
}
}
}
}
var Properties = require('br/presenter/property/Properties');
return new 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
*/
PresentationNode.prototype.nodes = function(sNodeName, vProperties) {
var Nodes = require('br/presenter/node/Nodes');
sNodeName = (sNodeName && (sNodeName != '*')) ? sNodeName : null;
var mProperties = this._convertToMap(vProperties);
var pNodes = [];
this._getNodes(sNodeName, mProperties, pNodes);
return new 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
*/
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>.
*/
PresentationNode.prototype.removeAllListeners = function() {
this.removeChildListeners();
};
/**
* Removes all listeners attached to the properties contained by this <code>PresentationNode</code>, and any nodes it contains.
*/
PresentationNode.prototype.removeChildListeners = function() {
this.properties().removeAllListeners();
};
// *********************** Private Methods ***********************
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
*/
PresentationNode.prototype._$setPath = function(sPath, oPresenterComponent) {
this.m_sPath = sPath;
for (var sChildToBeSet in this) {
if (!PresentationNode.isPrivateKey(sChildToBeSet)) {
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
*/
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 Errors.IllegalStateError(msg);
}
};
PresentationNode.prototype._isPresenterChild = function(sChildToBeSet, oChildToBeSet) {
return (oChildToBeSet && oChildToBeSet._$setPath);
};
/**
* @private
*/
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
*/
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
*/
PresentationNode.prototype._getNodes = function(sNodeName, mProperties, pNodes) {
for (var sKey in this) {
if (!PresentationNode.isPrivateKey(sKey)) {
var vItem = this[sKey];
if (!this._isPresenterChild(sKey, vItem)) {
continue;
}
if (vItem instanceof 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.
*/
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);
};
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
*/
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 Property)) {
return false;
} else if ((sPropertyValue != '*') && (sPropertyValue != oProperty.getValue())) {
return false;
}
}
return true;
};
PresentationNode.isPrivateKey = function(sKey) {
return (sKey.substr(0, 2) === 'm_' || sKey.charAt(0) === '_');
}
module.exports = PresentationNode;
var Property = require('br/presenter/property/Property');