Your IP : 18.222.121.231
BX.ui.chainedSelectors = function(opts, nf){
this.parentConstruct(BX.ui.chainedSelectors, opts);
/*
events:
parent:
*
BX.ui.chainedSelectors:
control-change
*/
BX.merge(this, {
opts: { // default options
source: '/somewhere.php', // url that will be used to obtain select options from
paginatedRequest: false,
// behaviour
autoSelectWhenSingle: true,
knownBundles: {}, // tree levels that already known
selectedItem: false, // initially selected path in a tree
initialBundlesIncomplete: false, // treat knownBundles as incomplete and try to re-get from server at the first suitable moment
bundlesIncomplete: {}, // lists exactly which bundles in knownBundles are incomplete
rootNodeValue: 0,
ignoreUnSelectable: false,
adapterName: 'combobox',
messages: {
nothingFound: 'Sorry, nothing found',
notSelected: '-- Not selected',
error: 'Error occured'
},
bindEvents: {
init: function(){ // after all we do this
this.setInitialValue();
this.vars.allowHideErrors = true;
}
}
},
vars: { // significant variables
stack: [], // sequence of controls
cache: { // tree level cache
links: {}, // relations of parent-children kind
nodes: {}, // node data
incomplete: {} // array of indicators of incomplete links (links that should be refreshed and completed before passing to control)
},
/*
keys in each node:
DISPLAY, VALUE, IS_PARENT, CAN_CHOOSE
*/
value: false, // currently selected value
eventLock: false,
allowHideErrors: false
},
sys: {
code: 'chainedselectors'
}
});
this.handleInitStack(nf, BX.ui.chainedSelectors, opts);
}
BX.extend(BX.ui.chainedSelectors, BX.ui.networkIOWidget);
// the following functions can be overrided with inheritance
BX.merge(BX.ui.chainedSelectors.prototype, {
// member of stack of initializers, must be defined even if does nothing
init: function(){
var so = this.opts,
sc = this.ctrls,
sv = this.vars;
sc.targetInput = this.getControl('target');
sc.errorMessage = this.getControl('error', true);
//sc.loader = new BX.ui.loader({scope: this.getControl('loader'), timeout: 500});
sc.pool = this.getControl('pool');
if(typeof sc.pool == 'undefined')
sc.pool = sc.scope;
if(typeof so.knownBundles == 'object'){
this.fillCache(so.knownBundles);
if(so.initialBundlesIncomplete){
for(var k in so.knownBundles)
if(so.knownBundles.hasOwnProperty(k))
sv.cache.incomplete[k] = true;
}else if(typeof so.bundlesIncomplete != 'undefined'){
for(var k in so.knownBundles)
{
if(so.knownBundles.hasOwnProperty(k))
if(typeof so.bundlesIncomplete[k] != 'undefined')
sv.cache.incomplete[k] = true;
}
}
}
this.pushFuncStack('buildUpDOM', BX.ui.chainedSelectors);
this.pushFuncStack('bindEvents', BX.ui.chainedSelectors);
},
buildUpDOM: function(){},
bindEvents: function(){
var ctx = this,
so = this.opts,
sv = this.vars,
sc = this.ctrls;
this.bindEvent('control-change', this.controlChangeActions);
BX.bind(sc.targetInput, 'change', function(){
if(sv.eventLock)
return;
ctx.setValue(this.value);
});
},
////////// PUBLIC: free to use outside
// todo
addItems2Cache: function(){
},
// todo
clearCache: function(){
},
// todo
focus: function(){
},
// todo
checkDisabled: function(){
},
// todo
disable: function(){
},
// todo
enable: function(){
},
setValue: function(value){
var sv = this.vars;
// same value
if(sv.value != false && typeof value != 'undefined' && value == sv.value)
return;
if(value == null || value == false || typeof value == 'undefined' || value.toString().length == 0){ // deselect
this.displayRoute([]);
this.setValueVariable('');
this.setTargetValue('');
this.fireEvent('after-clear-selection');
return;
}
// set
this.fireEvent('before-set-value', [value]);
var d = new BX.deferred();
var ctx = this;
d.done(BX.proxy(function(route){
this.displayRoute(route);
sv.value = value;
this.setTargetValue(this.checkCanSelectItem(value) ? value : this.getLastValidValue());
}, this));
d.fail(function(type){
if(type == 'notfound'){
ctx.displayRoute([]);
ctx.setValueVariable('');
ctx.setTargetValue('');
ctx.showError({errors: [ctx.opts.messages.nothingFound], type: 'server-logic', options: {}});
}
});
this.hideError();
this.getRouteToNode(value, d);
},
getValue: function(){
return this.vars.value === false ? '' : this.vars.value;
},
clearSelected: function(){
this.setValue('');
},
getNodeByValue: function(value){
return this.vars.cache.nodes[value];
},
// todo
setTabIndex: function(index){
},
setTargetInputName: function(newName){
this.ctrls.targetInput.setAttribute('name', newName);
},
// todo:
cancelRequest: function(){
},
// specific
getStackSize: function(){
return this.vars.stack.length;
},
getAdapterAtPosition: function(pos){
if(pos < 0 || pos >= this.vars.stack.length)
return null;
return this.vars.stack[pos].control;
},
// low-level, use with caution
setTargetInputValue: function(value){
this.vars.eventLock = true;
this.ctrls.targetInput.value = value;
BX.fireEvent(this.ctrls.targetInput, 'change');
this.vars.eventLock = false;
},
// todo
setFakeInputValue: function(display){
},
setValueVariable: function(value){
this.vars.value = value;
},
////////// PRIVATE: forbidden to use outside (for compatibility reasons)
checkCanSelectItem: function(itemId){
if(this.opts.ignoreUnSelectable)
return true;
var nodes = this.vars.cache.nodes;
if(typeof nodes[itemId] == 'undefined')
return false;
if(typeof nodes[itemId].IS_UNCHOOSABLE == 'undefined')
return true;
return !nodes[itemId].IS_UNCHOOSABLE;
},
controlChangeActions: function(stackIndex, value){
var ctx = this,
so = this.opts,
sv = this.vars,
sc = this.ctrls;
this.hideError();
////////////////
// todo: replace code below with a single call of ctx.setValue() with no intention to drop entire stack, but just append missing items
if(value.length == 0){
ctx.truncateStack(stackIndex);
sv.value = ctx.getLastValidValue();
ctx.setTargetValue(sv.value);
}else{
var node = sv.cache.nodes[value];
if(typeof node == 'undefined')
throw new Error('Selected node not found in the cache');
// node found
ctx.truncateStack(stackIndex);
// links will be downloaded on-demand
if(typeof sv.cache.links[value] != 'undefined' || node.IS_PARENT)
ctx.appendControl(value);
if(ctx.checkCanSelectItem(value)){
sv.value = value;
ctx.setTargetValue(value);
}
/*
if(typeof sv.cache.links[value] != 'undefined'){
// links exist
ctx.appendControl(value);
ctx.setNextValidValue(node, value);
}else{
if(node.IS_PARENT){
var d = new BX.deferred();
d.done(function(){
ctx.appendControl(value);
ctx.setNextValidValue(node, value);
});
d.fail(function(){
ctx.showError({errors: [so.messages.nothingFound], type: 'server-logic', options: {}});
});
ctx.getBundleForNode(value, d);
}else
ctx.setTargetValue(value);
}
*/
}
},
// for changing targerInput value, use this function only
setTargetValue: function(value){
this.setTargetInputValue(value);
this.fireEvent('after-select-item', [value]);
},
// try to get route from cache, if any
getRouteToNodeFromCache: function(value){
var route = [],
sv = this.vars;
if(typeof sv.cache.nodes[value] == 'undefined')
return route;
var node = sv.cache.nodes[value];
route.unshift(node.VALUE);
var i = 0;
var rootNodeFound = false;
var limit = BX.util.getObjectLength(sv.cache.nodes);
while(i < limit){
var parent = node.PARENT_VALUE;
if(parent == this.opts.rootNodeValue)
rootNodeFound = true;
route.unshift(parent);
if(typeof parent == 'undefined' || parent == this.opts.rootNodeValue/*'0'*/ || parent == '0' /*also allowed as root node*/){
break;
}else{
if(typeof sv.cache.nodes[parent] == 'undefined')
throw new Error('Tree integrity compromised');
node = sv.cache.nodes[parent];
}
i++;
}
return route;
},
setInitialValue: function(){
var initalValue = false;
if(this.opts.selectedItem !== false)
initalValue = this.opts.selectedItem;
else if(this.ctrls.targetInput.value.length > 0)
initalValue = this.ctrls.targetInput.value;
this.setValue(initalValue);
},
// get route for nodeId and resolve deferred with it
getRouteToNode: function(nodeId, d){
var sv = this.vars,
ctx = this;
if(typeof nodeId != 'undefined' && nodeId !== false && nodeId.toString().length > 0){
var route = this.getRouteToNodeFromCache(nodeId);
if(route.length == 0){ // || (sv.cache.nodes[nodeId].IS_PARENT && typeof sv.cache.links[nodeId] == 'undefined')){
// no way existed or item is parent without children downloaded
// download route, then try again
ctx.downloadBundle({
request: {VALUE: nodeId}, // get only route
callbacks: {
onLoad: function(data){
// mark absent as incomplete, kz we do not know if there are really more items of that level or not
for(var k in data)
{
if(data.hasOwnProperty(k))
if(typeof sv.cache.links[k] == 'undefined')
sv.cache.incomplete[k] = true;
}
ctx.fillCache(data, true);
var route = ctx.getRouteToNodeFromCache(nodeId); // trying to re-get
if(route.length == 0)
d.reject('notfound');
else
d.resolve(route);
},
onError: function(){ // this will only trigger on internal error, not server-logic error
d.reject('internal');
}
},
options: {} // accessible in refineRequest\refineResponce and showError
});
}else
d.resolve(route);
}else
d.resolve([]);
},
// get bundle for nodeId and resolve deferred with it
getBundleForNode: function(nodeId, d){
var sv = this.vars,
ctx = this;
if(typeof nodeId != 'undefined' && nodeId !== false && nodeId.toString().length > 0){
if(typeof this.vars.cache.links[nodeId] == 'undefined' || this.vars.cache.incomplete[nodeId] === true){
// no way existed or item is parent without children downloaded
// download bundle, then try again
ctx.downloadBundle({
request: {PARENT_VALUE: nodeId}, // get children
callbacks: {
onLoad: function(data){
ctx.fillCache(data);
if(typeof this.vars.cache.links[nodeId] == 'undefined'){ // still not found
d.reject();
}else{
delete(sv.cache.incomplete[nodeId]);
d.resolve(this.vars.cache.links[nodeId]);
}
},
onError: function(){
d.reject();
}
},
options: {} // accessible in refineRequest\refineResponce and showError
});
}else
d.resolve(this.vars.cache.links[nodeId]);
}else
d.resolve([]);
},
// print route that have been found
displayRoute: function(route){
var sv = this.vars;
this.truncateStack(-1); // drop entire stack
if(route.length == 0)
this.appendControl(this.opts.rootNodeValue/*0*/);
else{
route = BX.clone(route);
//route.unshift(this.opts.rootNodeValue/*'0'*/);
for(var k = 0; k < route.length; k++){
var nodeId = route[k];
if(typeof sv.cache.links[nodeId] != 'undefined' || sv.cache.nodes[nodeId].IS_PARENT){
this.appendControl(nodeId, route[k+1]);
/*
// disabling control where is only one option
if(typeof sv.cache.nodes[nodeId] != 'undefined' && sv.cache.nodes[nodeId].IS_PARENT && sv.cache.links[nodeId].length == 1 && sv.cache.incomplete[nodeId] !== true){
var control = this.getLastControl().control;
BX.adjust(control, {
attrs: {disabled: 'disabled'}
});
}
*/
}
}
}
},
appendControl: function(parentNode, selectedNode){
var params = {
parent: this,
messages: this.opts.messages
};
if(typeof selectedNode != 'undefined')
params.selectedItem = selectedNode;
params.parentNode = parentNode;
if(typeof this.vars.cache.links[parentNode] != 'undefined'){
params.knownItems = [];
for(var k in this.vars.cache.links[parentNode])
if(this.vars.cache.links[parentNode].hasOwnProperty(k))
params.knownItems.push(this.vars.cache.nodes[this.vars.cache.links[parentNode][k]]);
}
var control = new BX.ui.chainedSelectors.adapters[this.opts.adapterName](params);
control.setIndex(this.vars.stack.length);
this.vars.stack.push({control: control});
},
setNextValidValue: function(node, value){
var sv = this.vars,
so = this.opts,
sc = this.ctrls;
var hasLinks = typeof sv.cache.links[value] != 'undefined';
var linkCnt = sv.cache.links[value].length;
if(so.autoSelectWhenSingle && node.IS_PARENT && hasLinks && linkCnt == 1){
// do one step farther, user is no needed for clicking
var control = this.getLastControl().control;
/*
BX.adjust(control, {
attrs: {disabled: 'disabled'}
});
*/
control.value = sv.cache.links[value][0];
BX.fireEvent(control, 'change');
}else{
this.setTargetValue(node.IS_UNCHOOSABLE ? this.getLastValidValue() : value);
}
},
getLastValidValue: function(){
var value = undefined,
sc = this.ctrls,
sv = this.vars;
for(var k = sv.stack.length - 1; k >= 0 ; k--){
value = sv.stack[k].control.getValue();
if(typeof value != 'undefined' && (typeof sv.cache.nodes[value] != 'undefined') && (typeof sv.cache.nodes[value].IS_UNCHOOSABLE == 'undefined'))
return value;
}
return '';
},
getLastControl: function(){
return this.vars.stack[this.vars.stack.length - 1];
},
truncateStack: function(pos){
if(pos < -1)
return;
var len = this.vars.stack.length;
for(var k = pos + 1; k < len; k++){
this.vars.stack[k].control.remove();
}
this.vars.stack.splice(pos + 1, len - (pos + 1));
},
getSelectorValue: function(value){
return value;
},
fillCache: function(levels, isIncompleteData){
var sv = this.vars,
so = this.opts;
for(var parent in levels)
{
if(!levels.hasOwnProperty(parent))
continue;
// overwrite if not set
if(typeof sv.cache.links[parent] == 'undefined' || sv.cache.incomplete[parent] === true){
// if not set or complete data passed - recreate
if(typeof sv.cache.links[parent] == 'undefined' || !isIncompleteData)
sv.cache.links[parent] = [];
for(var k in levels[parent])
{
if(!levels[parent].hasOwnProperty(k))
continue;
sv.cache.links[parent].push(levels[parent][k].VALUE);
levels[parent][k].PARENT_VALUE = parent;
this.addItem2Cache(levels[parent][k]);
}
}
}
},
addItem2Cache: function(item){
this.vars.cache.nodes[item.VALUE] = item;
},
getCurrentItem: function(){
return this.vars.cache.nodes[this.vars.value];
},
setTargetInputName: function(newName){
this.ctrls.targetInput.setAttribute('name', newName);
},
showError: function(parameters){
this.setCSSState('error', this.ctrls.scope);
if(BX.type.isElementNode(this.ctrls.errorMessage)){
this.ctrls.errorMessage.innerHTML = BX.util.htmlspecialchars(parameters.errors.join(', '));
BX.show(this.ctrls.errorMessage);
}
BX.debug(parameters);
},
hideError: function(){
if(this.vars.allowHideErrors)
{
this.dropCSSState('error', this.ctrls.scope);
if(BX.type.isElementNode(this.ctrls.errorMessage))
BX.hide(this.ctrls.errorMessage);
}
}
/* Behaviour functions below */
});
BX.ui.chainedSelectors.adapters = {};
// this represents one (single) control that can be used as selector
BX.ui.chainedSelectors.adapters.combobox = function(options){
this.opts = options;
this.control = null;
this.scope = null;
this.index = null;
this.place = function(){
var ctx = this;
var nodes = this.opts.parent.createNodesByTemplate('selector-scope', {}, true);
if(nodes == null)
return;
this.scope = nodes[0];
BX.append(this.scope, this.opts.parent.ctrls.pool);
options.scope = this.scope;
options.arrowScrollAdditional = 5;
options.focusOnMouseSelect = false;
this.opts.parent.fireEvent('before-control-placed', [this]);
this.control = new BX.ui.combobox(options);
this.control.vars.outSideClickScope = this.scope; // a little makeup-related spike to make combobox dropdown feel better
this.control.bindEvent('item-list-discover', BX.proxy(function(d){ // when control doesnt know about nodes, it asks for it
d.stopRace(); // drop auto-reject by timeout in deferred
var dBundle = new BX.deferred();
dBundle.done(BX.proxy(function(bundle){
// convert here from links to required format
// could be slow here
var knownItems = [];
for(var k in bundle)
if(bundle.hasOwnProperty(k))
knownItems.push(this.opts.parent.vars.cache.nodes[bundle[k]]);
this.opts.parent.fireEvent('before-control-item-discover-done', [knownItems, this]);
d.resolve({items: knownItems});
}, this));
this.opts.parent.getBundleForNode(this.opts.parentNode, dBundle);
}, this));
// transfer events from inner widget to outer... bad solution...
this.control.bindEvent('before-display-page', function(){
ctx.opts.parent.fireEvent('control-before-display-page', [ctx]);
});
this.control.bindEvent('after-select-item', BX.proxy(function(value){
this.opts.parent.fireEvent('control-change', [this.index, value]);
}, this));
this.control.bindEvent('after-deselect-item', BX.proxy(function(){
this.opts.parent.fireEvent('control-change', [this.index, '']);
}, this));
if(this.opts.parent.vars.cache.incomplete[this.opts.parentNode])
this.control.clearCache();
this.opts.parent.fireEvent('after-control-placed', [this]);
};
this.remove = function(){
if(this.control != null)
{
this.control.remove();
}
this.control = null;
this.opts = null;
BX.remove(this.scope);
this.scope = null;
};
this.setIndex = function(index){
this.index = index;
};
this.getIndex = function(){
return this.index;
};
this.getValue = function(){
return this.control.getValue();
};
this.getParentValue = function(){
return this.opts.parentNode;
};
this.getControl = function(){
return this.control;
};
// a little hacky method, avoid to use it
// its purpose is to force control to display information we need without any internal logic evaluation
this.setValuePair = function(value, display){
this.control.setTargetInputValue(value);
this.control.setFakeInputValue(display);
this.control.setValueVariable(value);
};
this.place();
return this;
}