%PDF- %PDF-
Direktori : /home/bitrix/www/bitrix/js/sale/ |
Current File : /home/bitrix/www/bitrix/js/sale/core_ui_chainedselectors.js |
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; }