"use strict";
/**
* @module gallery-treeble
*/
/**********************************************************************
* <p>Hierarchical data source.</p>
*
* <p>TreebleDataSource converts a tree of DataSources into a flat list of
* visible items. The merged list must be paginated if the number of child
* nodes might be very large. To turn on this feature, set
* paginateChildren:true.</p>
*
* <p>The tree must be immutable. The total number of items available from
* each DataSource must remain constant. (The one exception to this rule
* is that filtering and sorting are allowed. This is done by detecting
* that the request parameters have changed.)</p>
*
* @namespace DataSource
* @class Treeble
* @extends DataSource.Local
* @constructor
* @param config {Object}
*/
function TreebleDataSource()
{
TreebleDataSource.superclass.constructor.apply(this, arguments);
}
TreebleDataSource.NAME = "treebleDataSource";
TreebleDataSource.ATTRS =
{
/**
* <p>The root datasource.</p>
*
* <p>You <em>must</em> directly set a `treeble_config` object on this
* datasource. (You cannot use `set('treeble_config',...)`.) By
* setting it on each datasource, we allow hetrogeneous datasources to
* be displayed in a single tree. `treeble_config` can contain the
* following configuration:</p>
*
* <dl>
* <dt>generateRequest</dt>
* <dd>(required) The function to convert the initial request into
* a request usable by the actual DataSource. This function takes
* two arguments: state (sort,dir,startIndex,resultCount) and path
* (an array of node indices telling how to reach the node).
* </dd>
* <dt>requestCfg</dt>
* <dd>(optional) Configuration object passed as `cfg` to `sendRequest`.</dd>
* <dt>schemaPluginConfig</dt>
* <dd>(required) Object to pass to `plug` to install a schema.</dd>
* <dt>cachePluginConfig</dt>
* <dd>(optional) Object to pass to `plug` to install a cache.</dd>
* <dt>childNodesKey</dt>
* <dd>(semi-optional) The name of the key inside a node which contains
* the data used to construct the DataSource for retrieving the children.
* This config is only required if you provide a custom parser.</dd>
* <dt>nodeOpenKey</dt>
* <dd>(optional) The name of the key inside a node which contains
* the initial open state of the node. If it is true, the node will
* automatically be opened the first time it is shown. (After that,
* it will remember the state set by the user.)</dd>
* <dt>startIndexExpr</dt>
* <dd>(optional) OGNL expression telling how to extract the startIndex
* from the received data, e.g., `.meta.startIndex`. If it is not
* provided, startIndex is always assumed to be zero.</dd>
* <dt>totalRecordsExpr</dt>
* <dd>(semi-optional) OGNL expression telling how to extract the total number
* of records from the received data, e.g., `.meta.totalRecords`.
* If this is not provided, `totalRecordsReturnExpr` must be
* specified.</dd>
* <dt>totalRecordsReturnExpr</dt>
* <dd>(semi-optional) OGNL expression telling where in the response to store
* the total number of records, e.g., `.meta.totalRecords`.
* This is only appropriate for DataSources that always return the
* entire data set. If this is not provided, `totalRecordsExpr` must
* be specified. If both are provided, `totalRecordsExpr` takes
* priority.</dd>
* </dl>
*
* @attribute root
* @type {DataSource}
* @writeonce
*/
root:
{
writeOnce: true
},
/**
* Pass `true` to paginate the result after merging child nodes into
* the list. The default (`false`) is to paginate only root nodes, so
* all children are visible.
*
* @attribute paginateChildren
* @type {boolean}
* @default false
* @writeonce
*/
paginateChildren:
{
value: false,
validator: Y.Lang.isBoolean,
writeOnce: true
},
/**
* The key in each record that stores an identifier which is unique
* across the entire tree. If this is not specified, then all nodes
* will close when the data is sorted.
*
* @attribute uniqueIdKey
* @type {String}
*/
uniqueIdKey:
{
validator: Y.Lang.isString
}
};
/**
* @event toggled
* @description Fires after an element is opened or closed.
* @param path {Array} the path to the toggled element
* @param open {Boolean} the new state of the element
*/
/*
Each element in this._open contains information about an openable,
top-level node and is the root of a tree of open (or previously opened)
items. Each node in a tree contains the following data:
index: {Number} sorting key; the index of the node
open: null if never opened, true if open, false otherwise
ds: {DataSource} source for child nodes
childTotal: {Number} total number of child nodes
children: {Array} (recursive) child nodes which are or have been opened
parent: {Object} parent item
id: {String} the unique id, if uniqueIdKey has been set
Each level is sorted by index to allow simple traversal in display
order.
*/
function populateOpen(
/* object */ parent,
/* array */ open,
/* object */ req)
{
var data = req.data,
start_index = req.start,
child_nodes_key = req.ds.treeble_config.childNodesKey,
node_open_key = req.ds.treeble_config.nodeOpenKey;
for (var j=0; j<open.length; j++)
{
if (open[j].index >= start_index)
{
break;
}
}
var unique_id_key = this.get('uniqueIdKey');
var result = true;
for (var k=0; k<data.length; k++)
{
var i = start_index + k;
var ds = data[k][ child_nodes_key ];
if (!ds)
{
continue;
}
while (j < open.length && open[j].index < i)
{
open.splice(j, 1);
result = false;
if (unique_id_key)
{
delete this._open_cache[ data[k][ unique_id_key ] ];
}
}
if (j >= open.length || open[j].index > i)
{
var item =
{
index: i,
open: null,
ds: ds,
children: [],
childTotal: 0,
parent: parent
};
var cached_item = null;
if (unique_id_key)
{
item.id = data[k][ unique_id_key ].toString();
cached_item = this._open_cache[ data[k][ unique_id_key ] ];
if (cached_item)
{
item.open = cached_item.open;
item.childTotal = cached_item.childTotal;
this._redo = this._redo || item.open;
}
this._open_cache[ data[k][ unique_id_key ] ] = item;
}
if (!cached_item && node_open_key && data[k][ node_open_key ])
{
this._toggle.push(req.path.concat(i));
}
open.splice(j, 0, item);
}
j++;
}
return result;
}
// TODO: worth switching to binary search?
function searchOpen(
/* array */ list,
/* int */ node_index)
{
for (var i=0; i<list.length; i++)
{
if (list[i].index == node_index)
{
return list[i];
}
}
return false;
}
function getNode(
/* array */ path)
{
var list = this._open;
for (var i=0; i<path.length; i++)
{
var node = searchOpen(list, path[i]);
if (!node)
{
return false;
}
list = node.children;
}
return node;
}
function countVisibleNodes(
// not sent by initiator
/* array */ open)
{
var total = 0;
if (!open)
{
open = this._open;
total = this._topNodeTotal;
}
if (this.get('paginateChildren'))
{
for (var i=0; i<open.length; i++)
{
var node = open[i];
if (node.open)
{
total += node.childTotal;
total += countVisibleNodes.call(this, node.children);
}
}
}
return total;
}
function requestTree(flush_toggle)
{
if (!flush_toggle)
{
var save_toggle = this._toggle.slice(0);
}
this._cancelAllRequests();
if (!flush_toggle)
{
this._toggle = save_toggle;
}
this._redo = false;
this._generating_requests = true;
var req = this._callback.request;
if (this.get('paginateChildren'))
{
this._slices = getVisibleSlicesPgAll(req.startIndex, req.resultCount,
this.get('root'), this._open);
}
else
{
this._slices = getVisibleSlicesPgTop(req.startIndex, req.resultCount,
this.get('root'), this._open);
}
requestSlices.call(this, req);
this._generating_requests = false;
checkFinished.call(this);
}
function getVisibleSlicesPgTop(
/* int */ skip,
/* int */ show,
/* DataSource */ ds,
/* array */ open,
// not sent by initiator
/* array */ path)
{
open = open.concat(
{
index: -1,
open: true,
childTotal: 0,
children: null
});
if (!path)
{
path = [];
}
var slices = [],
send = false;
var m = 0, prev = -1, presend = false;
for (var i=0; i<open.length; i++)
{
var node = open[i];
if (!node.open)
{
continue;
}
var delta = node.index - prev;
if (m + delta >= skip + show ||
node.index == -1)
{
slices.push(
{
ds: ds,
path: path.slice(0),
start: send ? m : skip,
end: skip + show - 1
});
if (m + delta == skip + show && node.childTotal > 0)
{
slices = slices.concat(
getVisibleSlicesPgTop(0, node.childTotal, node.ds,
node.children, path.concat(node.index)));
}
return slices;
}
else if (!send && m + delta == skip)
{
presend = true;
}
else if (m + delta > skip)
{
slices.push(
{
ds: ds,
path: path.slice(0),
start: send ? prev + 1 : skip,
end: m + delta - 1
});
send = true;
}
m += delta;
if (send && node.childTotal > 0)
{
slices = slices.concat(
getVisibleSlicesPgTop(0, node.childTotal, node.ds,
node.children, path.concat(node.index)));
}
prev = node.index;
send = send || presend;
}
}
function getVisibleSlicesPgAll(
/* int */ skip,
/* int */ show,
/* DataSource */ root_ds,
/* array */ open,
// not sent by initiator
/* array */ path,
/* node */ parent,
/* int */ pre,
/* bool */ send,
/* array */ slices)
{
if (!parent)
{
path = [];
parent = null;
pre = 0;
send = false;
slices = [];
}
var ds = parent ? parent.ds : root_ds;
open = open.concat(
{
index: parent ? parent.childTotal : -1,
open: true,
childTotal: 0,
children: null
});
var n = 0, m = 0, prev = -1;
for (var i=0; i<open.length; i++)
{
var node = open[i];
if (!node.open)
{
continue;
}
var delta = node.index - prev;
if (node.children === null)
{
delta--; // last item is off the end
}
if (pre + n + delta >= skip + show ||
node.index == -1)
{
slices.push(
{
ds: ds,
path: path.slice(0),
start: m + (send ? 0 : skip - pre - n),
end: m + (skip + show - 1 - pre - n)
});
return slices;
}
else if (!send && pre + n + delta == skip)
{
send = true;
}
else if (pre + n + delta > skip)
{
slices.push(
{
ds: ds,
path: path.slice(0),
start: m + (send ? 0 : skip - pre - n),
end: m + delta - 1
});
send = true;
}
n += delta;
m += delta;
if (node.childTotal > 0)
{
var info = getVisibleSlicesPgAll(skip, show, root_ds, node.children,
path.concat(node.index),
node, pre+n, send, slices);
if (Y.Lang.isArray(info))
{
return info;
}
else
{
n += info.count;
send = info.send;
}
}
prev = node.index;
}
// only reached when parent != null
var info =
{
count: n,
send: send
};
return info;
}
function requestSlices(
/* object */ request)
{
for (var i=0; i<this._slices.length; i++)
{
var slice = this._slices[i];
var ds = slice.ds;
var req = findRequest.call(this, ds);
if (req)
{
if (Y.Console)
{
if (req.end+1 < slice.start)
{
Y.error('TreebleDataSource found discontinuous range');
}
if (req.path.length != slice.path.length)
{
Y.error('TreebleDataSource found path length mismatch');
}
else
{
for (var j=0; j<slice.path.length; j++)
{
if (req.path[j] != slice.path[j])
{
Y.error('TreebleDataSource found path mismatch');
break;
}
}
}
}
req.end = slice.end;
}
else
{
this._req.push(
{
ds: ds,
path: slice.path,
start: slice.start,
end: slice.end
});
}
}
request = Y.clone(request, true);
for (var i=0; i<this._req.length; i++)
{
var req = this._req[i];
request.startIndex = req.start;
request.resultCount = req.end - req.start + 1;
req.txId = req.ds.sendRequest(
{
request: req.ds.treeble_config.generateRequest(request, req.path),
cfg: req.ds.treeble_config.requestCfg,
callback:
{
success: Y.rbind(treeSuccess, this, i),
failure: Y.rbind(treeFailure, this, i)
}
});
}
}
function findRequest(
/* DataSource */ ds)
{
for (var i=0; i<this._req.length; i++)
{
var req = this._req[i];
if (ds == req.ds)
{
return req;
}
}
return null;
}
function treeSuccess(e, req_index)
{
if (!e.response || e.error ||
!Y.Lang.isArray(e.response.results))
{
treeFailure.apply(this, arguments);
return;
}
var req = searchTxId(this._req, e.tId, req_index);
if (!req)
{
return; // cancelled request
}
if (!this._topResponse && req.ds == this.get('root'))
{
this._topResponse = e.response;
}
req.txId = null;
req.resp = e.response;
req.error = false;
var data_start_index = 0;
if (req.ds.treeble_config.startIndexExpr)
{
data_start_index = Y.Object.evalGet(req.resp, req.ds.treeble_config.startIndexExpr);
}
var slice_start_index = req.start - data_start_index;
req.data = e.response.results.slice(slice_start_index, req.end - data_start_index + 1);
setNodeInfo.call(this, req.data, req.start, req.path, req.ds);
var parent = (req.path.length > 0 ? getNode.call(this, req.path) : null);
var open = (parent !== null ? parent.children : this._open);
if (!populateOpen.call(this, parent, open, req))
{
treeFailure.apply(this, arguments);
return;
}
if (!parent && req.ds.treeble_config.totalRecordsExpr)
{
this._topNodeTotal = Y.Object.evalGet(e.response, req.ds.treeble_config.totalRecordsExpr);
}
else if (!parent && req.ds.treeble_config.totalRecordsReturnExpr)
{
this._topNodeTotal = e.response.results.length;
}
checkFinished.call(this);
}
function treeFailure(e, req_index)
{
var req = searchTxId(this._req, e.tId, req_index);
if (!req)
{
return; // cancelled request
}
this._cancelAllRequests();
this._callback.error = e.error;
this._callback.response = e.response;
this.fire('response', this._callback);
}
function setNodeInfo(
/* array */ list,
/* int */ offset,
/* array */ path,
/* datasource */ ds)
{
var unique_id_key = this.get('uniqueIdKey'),
node_open_key = ds.treeble_config.nodeOpenKey,
set_open = unique_id_key && node_open_key && this._open_ids.length > 0;
var depth = path.length;
for (var i=0; i<list.length; i++)
{
list[i]._yui_node_depth = depth;
list[i]._yui_node_path = path.concat(offset+i);
list[i]._yui_node_ds = ds;
if (set_open)
{
var k = list[i][ unique_id_key ],
j = this._open_ids.indexOf(k.toString());
if (j >= 0)
{
list[i][ node_open_key ] = true;
this._open_ids.splice(j, 1);
}
}
}
}
function searchTxId(
/* array */ req,
/* int */ id,
/* int */ fallback_index)
{
for (var i=0; i<req.length; i++)
{
if (req[i].txId === id)
{
return req[i];
}
}
// synch response arrives before setting txId
if (fallback_index < req.length &&
Y.Lang.isUndefined(req[ fallback_index ].txId))
{
return req[ fallback_index ];
}
return null;
}
function checkFinished()
{
if (this._generating_requests)
{
return;
}
var count = this._req.length;
for (var i=0; i<count; i++)
{
if (!this._req[i].resp)
{
return;
}
}
if (this._redo)
{
Y.Lang.later(0, this, requestTree);
return;
}
else if (this._toggle.length > 0)
{
var t = this._toggle.shift();
this.toggle(t, Y.clone(this._callback.request, true),
{
fn: function()
{
Y.Lang.later(0, this, requestTree);
},
scope: this
});
return;
}
var response = { meta:{} };
Y.mix(response, this._topResponse, true);
response.results = [];
response = Y.clone(response, true);
count = this._slices.length;
for (i=0; i<count; i++)
{
var slice = this._slices[i];
var req = findRequest.call(this, slice.ds);
if (!req)
{
Y.error('Failed to find request for a slice');
continue;
}
var j = slice.start - req.start;
var data = req.data.slice(j, j + slice.end - slice.start + 1);
response.results = response.results.concat(data);
}
var root_ds = this.get('root');
if (root_ds.treeble_config.totalRecordsExpr)
{
Y.Object.evalSet(response, root_ds.treeble_config.totalRecordsExpr, countVisibleNodes.call(this));
}
else if (root_ds.treeble_config.totalRecordsReturnExpr)
{
Y.Object.evalSet(response, root_ds.treeble_config.totalRecordsReturnExpr, countVisibleNodes.call(this));
}
this._callback.response = response;
this.fire('response', this._callback);
}
function toggleSuccess(e, node, completion, path)
{
if (node.ds.treeble_config.totalRecordsExpr)
{
node.childTotal = Y.Object.evalGet(e.response, node.ds.treeble_config.totalRecordsExpr);
}
else if (node.ds.treeble_config.totalRecordsReturnExpr)
{
node.childTotal = e.response.results.length;
}
node.open = true;
node.children = [];
complete(completion);
this.fire('toggled',
{
path: path,
open: node.open
});
}
function toggleFailure(e, node, completion, path)
{
node.childTotal = 0;
node.open = true;
node.children = [];
complete(completion);
this.fire('toggled',
{
path: path,
open: node.open
});
}
function complete(f)
{
if (Y.Lang.isFunction(f))
{
f();
}
else if (f && f.fn)
{
f.fn.apply(f.scope || window, Y.Lang.isUndefined(f.args) ? [] : f.args);
}
}
function compareRequests(r1, r2)
{
var k1 = Y.Object.keys(r1),
k2 = Y.Object.keys(r2);
if (k1.length != k2.length)
{
return false;
}
for (var i=0; i<k1.length; i++)
{
var k = k1[i];
if (k != 'startIndex' && k != 'resultCount' && r1[k] !== r2[k])
{
return false;
}
}
return true;
}
Y.extend(TreebleDataSource, Y.DataSource.Local,
{
initializer: function(config)
{
if (!config.root)
{
Y.error('TreebleDataSource requires DataSource');
}
if (!config.root.treeble_config.childNodesKey)
{
var fields = config.root.schema.get('schema').resultFields;
if (!fields || !Y.Lang.isArray(fields))
{
Y.error('TreebleDataSource root DataSource requires schema.resultFields because treeble_config.childNodesKey was not specified.');
}
for (var i=0; i<fields.length; i++)
{
if (Y.Lang.isObject(fields[i]) && fields[i].parser == 'treebledatasource')
{
config.root.treeble_config.childNodesKey = fields[i].key;
break;
}
}
if (!config.root.treeble_config.childNodesKey)
{
Y.error('TreebleDataSource requires treeble_config.childNodesKey configuration to be set on root DataSource');
}
}
if (!config.root.treeble_config.generateRequest)
{
Y.error('TreebleDataSource requires treeble_config.generateRequest configuration to be set on root DataSource');
}
if (!config.root.treeble_config.totalRecordsExpr && !config.root.treeble_config.totalRecordsReturnExpr)
{
Y.error('TreebleDataSource requires either treeble_config.totalRecordsExpr or treeble_config.totalRecordsReturnExpr configuration to be set on root DataSource');
}
this._open = [];
this._open_cache = {};
this._open_ids = [];
this._toggle = [];
this._req = [];
},
/**
* @method isOpen
* @param path {Array} Path to node
* @return {boolean} true if the node is open
*/
isOpen: function(path)
{
var list = this._open;
for (var i=0; i<path.length; i++)
{
var node = searchOpen(list, path[i]);
if (!node || !node.open)
{
return false;
}
list = node.children;
}
return true;
},
/**
* Toggle the specified node between open and closed. When a node is
* opened for the first time, this requires a request to the
* DataSource. Any code that assumes the node has been opened must be
* passed in as a completion function.
*
* @method toggle
* @param path {Array} Path to the node
* @param request {Object} {sort,dir,startIndex,resultCount}
* @param completion {Function|Object} Function to call when the operation completes. Can be object: {fn,scope,args}
* @return {boolean} false if the path to the node has not yet been fully explored or is not openable, true otherwise
*/
toggle: function(path, request, completion)
{
var node = getNode.call(this, path);
if (!node)
{
return false;
}
if (node.open === null)
{
request.startIndex = 0;
request.resultCount = 0;
node.ds.sendRequest(
{
request: node.ds.treeble_config.generateRequest(request, path),
cfg: node.ds.treeble_config.requestCfg,
callback:
{
success: Y.rbind(toggleSuccess, this, node, completion, path),
failure: Y.rbind(toggleFailure, this, node, completion, path)
}
});
}
else
{
node.open = !node.open;
complete(completion);
this.fire('toggled',
{
path: path,
open: node.open
});
}
return true;
},
/**
* @method getOpenNodeIds
* @return {Array} id's of open nodes
*/
getOpenNodeIds: function()
{
if (!this.get('uniqueIdKey'))
{
return [];
}
function collectOpenIds(open)
{
return Y.reduce(open, [], function(list, node)
{
if (node.open)
{
list.push(node.id);
list = list.concat(collectOpenIds(node.children));
}
return list;
});
}
return collectOpenIds(this._open);
},
/**
* @method setOpenNodeIds
* @param {Array} id's of nodes that should be opened
*/
setOpenNodeIds: function(ids)
{
if (Y.Lang.isArray(ids) &&
this.get('uniqueIdKey') && this.get('root').treeble_config.nodeOpenKey)
{
this._open_ids = ids;
}
},
/**
* @method flushCache
* @param path {Array} Path to node
*/
flushCache: function(path, send_request)
{
var node = getNode.call(this, path);
if (node && node.ds)
{
if (node.ds.cache)
{
node.ds.cache.flush();
}
var was_open = node.open;
node.open = null;
if (was_open)
{
this.toggle(path, {}, send_request);
}
}
},
_defRequestFn: function(e)
{
// wipe out all state if the request parameters change
if (this._callback && !compareRequests(this._callback.request, e.request))
{
this._open = [];
}
this._callback = e;
requestTree.call(this, true);
},
_cancelAllRequests: function()
{
this._req = [];
this._toggle = [];
delete this._topResponse;
}
});
Y.TreebleDataSource = TreebleDataSource;
Y.namespace('DataSource').Treeble = TreebleDataSource;