PageSourceSearch

https://boobl-goom.ru/bitrix/js/sale/core_ui_autocomplete.js?1638199562

js boobl-goom.ru collected 2026-10-01 12:36:01 UTC 33,053 bytes, 1,290 lines download raw bytes

1(function(){
2
3	if(typeof BX.ui != 'object')
4		BX.ui = {};
5
6	//////////////////////////////
7	// generic autocomplete
8	//////////////////////////////
9
10	/*
11	todo: implement handling selectbox in the role of target input
12	todo: take care about the situation when BX.ajax() cannot handle request in time, and user presses enter or somehow else input looses focus
13	todo: make connection between origin and fake inputs, handling: change
14	todo: do ajax call interruption if it cannot handle request in time, but there is another newly typed request came and pending
15	*/
16
17	BX.ui.autoComplete = function(opts, nf){
18
19		this.parentConstruct(BX.ui.autoComplete, opts);
20
21		BX.merge(this, {
22			opts: { // default options
23				source:					'/somewhere.php', // url that will be used to obtain select options from
24				pageSize:				20, // amount of variants to show
25				paginatedRequest:		true, // if true, parameters for server-side paginator will be sent in the request
26
27				// behaviour
28				autoSelectOnBlur:			false, // if true, plugin will select first item in variant list when input looses focus
29				autoSelectOnTab: 			false, // if true, plugin will select first item in variant list when user types tab button on fake input with string being incomplete typed in
30				autoSelectOnlyIfOneVariant: false,
31				selectOnEnter:				true, // item can be selected by pressing Enter on it
32				selectByClick:				true, // item can be selected by clicking on it
33				chooseUsingArrows:			true, // item can be choosable when user presses arrow up\down while input is in focus
34				scrollToVariantOnArrow: 	true,
35
36				closePopupOnOuterClick: true, // if true, popup will be closed when user clicks outside the widget
37
38				focusOnMouseSelect:		true, // if true, plugin will return focus to the fake input after user selects item using mouse. it allows not to lose tabindex order
39				autoSelectIfOneVariant:	false, // if true, and only one variant found for currently typed query, it will be selected without any dropdown shown
40				debounceTime:			500, // time of reaction on input content change. Should not be too small
41				startSearchLen:			2, // minimum string length search will start with
42
43				knownItems:				[], // already known items which go directly to the cache
44				selectedItem:			false, // initially selected item VALUE
45				useCache:				true, // always should be true, behaviour when useCache == false is not implemented
46				usePagingOnScroll:		false, // if true, showing items on pane scroll will be used. Note that you must specify also paneHConstraint option, or have popup height limited in your css. Otherwise you will be wondered with the result :)
47				useCustomScrollPane:	false, // always false, the developing of a custom scroll panel were frozen currently
48				scrollThrottleTimeout:	300, // timeout of reaction on pane scroll. Should not be too small
49				paneHConstraint:		0, // limit pane height with this value, if greather than zero
50
51				// language-dependent messages to display
52				messages: {
53					nothingFound:		'Sorry, nothing found',
54					error:				'Error occured',
55					clearSelection:		'Clear selection'
56				},
57
58				// magic design-related values
59				arrowScrollAdditional:	0,
60				pageUpWardOffset:		0,
61				wrapTagName:			'span',
62				paneHConstraintType:	'max-height', // constraint type of pane height
63				wrapSeparate: 			true,
64
65				bindEvents: {
66					'init': function(){ // after all we do this
67						this.setInitialValue();
68						this.vars.allowHideErrors = true;
69					}
70				}
71			},
72			vars: { // significant variables
73				opened:					false, // whether dropdown is opened or not
74				eventLock:				false, // when we fire 'change' on the target node, we dont want our own callback to be called
75				displayPageMutex:		false,
76				blockingCall:			false,
77				keyboardMutex:			false,
78
79				cache: { // item cache
80					nodes:				{}, // data index, keeps data for each node ever loaded
81					search:				{}, // cache for request: map from query string to a set of items in responce. Here pagenavigation can be implemented without a trouble
82					ceilings:			{}
83				},
84				lastQuery:				null,
85				lastPage:				0,
86				displayedIndex:			[],
87
88				value:					false, // actually, [VALUE,DISPLAY] pair id
89				currentGlow:			false, // item that is currently highlighted by arrow-up\down keys
90				previousGlow:			false, // previous value of currentGlow
91
92				outSideClickScope:		null,
93				forceSelectSingeOnce:	false,
94				allowHideErrors: 		false
95			},
96			ctrls: { // links to controls
97				displayedItems: {},
98				popup: null
99			},
100			sys: {
101				code:					'autocomplete'
102			}
103		});
104
105		this.handleInitStack(nf, BX.ui.autoComplete, opts);
106	};
107	BX.extend(BX.ui.autoComplete, BX.ui.widget);
108
109	// the following functions can be overrided with inheritance
110	BX.merge(BX.ui.autoComplete.prototype, {
111
112		// member of stack of initializers, must be defined even if do nothing
113		init: function(){
114			var ctx = this,
115				so = this.opts,
116				sv = this.vars,
117				sc = this.ctrls;
118
119			// find input
120			var input = this.getControl('input', true);
121			if(input == null)
122				input = sc.scope.querySelector('input[type="text"]');
123			if(input == null)
124				input = sc.scope.querySelector('select');
125			if(input == null)
126				throw new Error('Input control still not found');
127
128			sc.inputs = {
129				origin: input == null ? sc.scope : input
130			};
131
132			sv.loader = new BX.ui.loader({
133				timeout: 500
134			});
135			sv.loader.bindEvent('toggle', BX.proxy(this.whenLoaderToggle, ctx));
136
137			if(sc.inputs.origin.nodeName == 'SELECT'){
138				throw new Error('Sorry, usage of <select> node as a source currently is not implemented');
139				// todo: get knownItems from sc.inputs.origin options (and also so.selectedItem)
140			}
141
142			if(typeof so.knownItems == 'object')
143				this.fillCache(so.knownItems, false);
144
145			this.pushFuncStack('buildUpDOM', BX.ui.autoComplete);
146			this.pushFuncStack('bindEvents', BX.ui.autoComplete);
147		},
148
149		buildUpDOM: function(){
150			var so = this.opts,
151				sc = this.ctrls,
152				sv = this.vars,
153				code = this.sys.code;
154
155			// add container node
156			sc.container = BX.create('div', {
157				props: {
158					className: 'bx-ui-'+code+'-container'
159				},
160				style: {
161					margin: 0,
162					padding: 0,
163					border: 'none',
164					position: 'relative'
165				}
166			});
167
168			//BX.style(sc.container, 'display', BX.style(sc.inputs.origin, 'display'));
169
170			BX.insertAfter(sc.container, sc.inputs.origin);
171
172			// clone input node
173			var pseudoInput = BX.clone(sc.inputs.origin);
174			pseudoInput.removeAttribute('name'); // make it invisible for form
175			BX.adjust(pseudoInput, {
176				props: {
177					className: 'bx-ui-'+code+'-fake'
178				}
179			});
180			sc.container.appendChild(pseudoInput);
181
182			sc.inputs.fake = pseudoInput;
183
184			/*
185			if(sv.value !== false)
186			{
187				console.dir('set from input');
188				//this.selectItem(sv.value);
189				this.setFakeInputValue(sv.cache.nodes[sv.value].DISPLAY);
190			}
191			else
192				this.setFakeInputValue('');
193			*/
194
195			// hide origin input
196			BX.hide(sc.inputs.origin);
197			//sc.inputs.origin.setAttribute('type', 'hidden');
198
199			if(BX.browser.IsIE8()){
200				BX.bind(sc.inputs.fake, 'click', function(e){
201					BX.eventCancelBubble(e);
202				});
203				BX.bind(sc.container, 'click', function(){
204					sc.inputs.fake.focus();
205				});
206			}
207
208			// clear handle
209			sc.clear = this.getControl('clear', true);
210			if(!BX.type.isElementNode(sc.clear)){
211				sc.clear = BX.create('div', {
212					props: {
213						className: 'bx-ui-'+code+'-clear'
214					},
215					attrs: {
216						title: so.messages.clearSelection
217					},
218					style: {
219						position: 'absolute',
220						top: '0px',
221						right: '0px'
222					}
223				});
224
225				sc.container.appendChild(sc.clear);
226			}
227
228			//if(sv.value === false)
229			BX.style(sc.clear, 'display', 'none');
230
231			// insert pane
232			sc.pane = this.getControl('pane', true);
233			if(!BX.type.isElementNode(sc.pane)){
234				sc.pane = BX.create('div', {
235					props: {
236						className: 'bx-ui-'+code+'-pane'
237					},
238					style: {
239						display: 'none',
240						position: 'absolute'
241					}
242				});
243				sc.container.appendChild(sc.pane);
244			}
245			// adjust a bit
246			if(so.usePagingOnScroll){
247				BX.style(sc.pane, 'overflow-y', 'auto');
248				BX.style(sc.pane, 'overflow-x', 'hidden');
249
250				if(so.paneHConstraint > 0 && so.paneHConstraintType != '')
251					BX.style(sc.pane, so.paneHConstraintType, so.paneHConstraint+'px');
252			}
253
254			if (so.usePopup)
255			{
256				BX.style(sc.pane, 'position', 'inherit');
257				BX.style(sc.pane, 'border', 'none');
258				BX.style(sc.pane, 'box-shadow', 'none');
259			}
260
261			// insert variants
262			sc.vars = this.getControl('variants', true);
263			if(!BX.type.isElementNode(sc.vars)){
264				sc.vars = BX.create('div', {
265					props: {
266						className: 'bx-ui-'+code+'-variants'
267					}
268				});
269				sc.pane.appendChild(sc.vars);
270			}
271
272			// nothing found message
273			sc.nothingFound = this.getControl('nothing-found', true);
274			// error message
275			sc.errorMessage = this.getControl('error-message', true);
276		},
277
278		bindEvents: function(){
279
280			var ctx = this,
281				so = this.opts,
282				sv = this.vars,
283				sc = this.ctrls,
284				code = this.sys.code;
285
286			// bind item select
287			if(so.selectByClick){
288
289				BX.bindDelegate(sc.pane, 'click', {
290					className: 'bx-ui-'+code+'-variant'
291				}, function(){
292					var id = BX.data(this, 'bx-'+code+'-item-value');
293					if(typeof id != 'undefined' && typeof sv.cache.nodes[id] != 'undefined'){
294						ctx.selectItem(id);
295						if(so.focusOnMouseSelect)
296							sc.inputs.fake.focus();
297					}
298				});
299			}
300
301			// bind debounced key-type input change
302			BX.bindDebouncedChange(sc.inputs.fake,
303
304				function(val){
305
306					if(val.length >= so.startSearchLen){
307
308						ctx.displayPage({QUERY: val});
309
310					}else
311						ctx.hideDropdown();
312				},
313				function(){
314
315					ctx.fireEvent('before-input-value-modify');
316
317					// dropping selections
318					ctx.deselectItem();
319
320					ctx.fireEvent('after-input-value-modify');
321				},
322				so.debounceTime,
323				sc.inputs.fake
324			);
325
326			// close dropdown by a custom event
327			BX.addCustomEvent(document, 'bx-ui-'+code+'-close-dropdown', function(){
328				ctx.hideDropdown();
329			});
330
331			// outside click should close the dropdown
332			if(so.closePopupOnOuterClick){
333
334				sv.outSideClickScope = sc.container;
335
336				BX.bind(document, 'click', function(e){
337					e = e || window.event;
338
339					if(!BX.isParentForNode(sv.outSideClickScope, e.target || e.srcElement)){
340						ctx.hideDropdown();
341					}
342				});
343			}
344
345			// keyboard events
346			BX.bind(sc.inputs.fake, 'keydown', function(e){
347
348				if(sv.keyboardMutex){
349					//console.dir('kb mutex is on, ignore keyboard');
350					return;
351				}
352
353				var key = e.keyCode || e.which;
354				var displayedLen = sv.displayedIndex.length;
355
356				if(so.chooseUsingArrows){
357
358					// up (38) - down (40)
359					if((key == 38 || key == 40) && sv.opened && displayedLen > 0){
360						var way = key == 38 ? -1 : 1;
361
362						// end were reached, but we can load more
363						if(sv.currentGlow == displayedLen - 1 && ctx.getCanLoadMore()){
364							return;
365						}
366
367						sv.previousGlow = sv.currentGlow;
368						sv.currentGlow += way;
369
370						sv.currentGlow = sv.currentGlow % displayedLen;
371						if(sv.currentGlow < 0)
372							sv.currentGlow = displayedLen + sv.currentGlow;
373
374						ctx.toggleGlow();
375
376						var item = sc.displayedItems[sv.displayedIndex[sv.currentGlow]];
377
378						if(so.scrollToVariantOnArrow){
379
380							// here we determine currently selected item is not in the visible area
381							var pos = BX.pos(item, sc.vars);
382							var itemBottomPos = pos.top+pos.height;
383
384							var a = pos.top;
385							var b = pos.height;
386							var c = sc.pane.clientHeight;
387							var d = sc.pane.scrollTop;
388							var f = so.arrowScrollAdditional;
389
390							if(a + b > c + d){
391								sc.scrollController.scrollTo({dy: a + b - (c + d) + f});
392							}else if(a < d){
393								sc.scrollController.scrollTo({dy: -(d - a + f)});
394							}
395						}
396
397						BX.PreventDefault(e);
398					}
399				}
400
401				// on enter we perform item selection
402				if(key == 13 && so.selectOnEnter){
403					if(sv.currentGlow !== false)
404						ctx.selectItem(sv.displayedIndex[sv.currentGlow]);
405					else if(sv.opened && displayedLen > 0)
406						ctx.selectItem(sv.displayedIndex[0]);
407				}
408
409				// on tab dropdown close and optional select
410				if(key == 9 && sv.opened && so.autoSelectOnTab){
411
412					if((so.autoSelectOnlyIfOneVariant && displayedLen == 1) || (!so.autoSelectOnlyIfOneVariant && displayedLen > 0))
413						ctx.selectItem(sv.displayedIndex[0]);
414					else
415						ctx.hideDropdown();
416				}
417
418				if(key == 13)
419					BX.PreventDefault(e);
420			});
421
422			if(so.autoSelectOnBlur)
423			{
424				BX.bind(sc.inputs.fake, 'blur', function(){
425
426					var displayedLen = sv.displayedIndex.length;
427
428					if(sv.opened && ((so.autoSelectOnlyIfOneVariant && displayedLen == 1) || (!so.autoSelectOnlyIfOneVariant && displayedLen > 0)))
429						ctx.selectItem(sv.displayedIndex[0]);
430				});
431			}
432
433			if(so.autoSelectOnBlur)
434			{
435				BX.bind(sc.inputs.fake, 'blur', function(){
436
437					var displayedLen = sv.displayedIndex.length;
438
439					if(sv.opened && ((so.autoSelectOnlyIfOneVariant && displayedLen == 1) || (!so.autoSelectOnlyIfOneVariant && displayedLen > 0)))
440						ctx.selectItem(sv.displayedIndex[0]);
441				});
442			}
443
444			// when nothing were selected (but there were already an attempt of search), open dropdown if it was closed occasionly by user
445			BX.bind(sc.inputs.fake, 'click', function(){
446				if(!sv.opened && sv.value === false && sv.displayedIndex.length > 0)
447				{
448					ctx.showDropdown();
449				}
450			});
451
452			// clear handle
453			BX.bind(sc.clear, 'click', function(){
454				ctx.clearSelected();
455			});
456
457			if(so.usePagingOnScroll){
458
459				if(so.useCustomScrollPane){
460					sc.scrollController = null;
461					throw new Error('Sorry, custom scroll panel currently is not implemented');
462				}else{
463					sc.scrollController = new BX.ui.scrollPaneNative({
464						scope: sc.pane,
465						eventTimeout: so.scrollThrottleTimeout,
466						controls: {
467							'container': sc.pane
468						}
469					});
470				}
471
472				ctx.vars.addPage = BX.debounce(function(){
473
474					if(!sv.opened)
475						return;
476
477					ctx.blockingCall(); // set mutex
478					ctx.displayNextPage();
479				}, 10);
480
481				sc.scrollController.bindEvent('scroll-to-end', ctx.vars.addPage);
482				sc.scrollController.bindEvent('has-free-space', ctx.vars.addPage);
483
484				// when really add smth, dispatch this signal:
485				this.bindEvent('after-page-display', function(){
486					sc.scrollController.informContentChanged();
487					if(ctx.vars.lastPage == 0)
488						sc.scrollController.dropScrollTop();
489				});
490			}
491
492			// handle onChange event
493			BX.bind(sc.inputs.origin, 'change', function(){
494
495				if(sv.eventLock)
496					return;
497
498				if(this.value == ''){ // this is a plain reset
499					if(sv.value)
500						ctx.clearSelected();
501				}else{
502					ctx.setValue(this.value);
503				}
504			});
505		},
506
507		////////// PUBLIC: free to use outside
508
509		// common
510
511		// todo
512		addItems2Cache: function(){
513		},
514
515		clearCache: function(){
516		},
517
518		// set focus to fake input
519		focus: function(){
520			this.ctrls.inputs.fake.focus();
521		},
522
523		// todo
524		checkDisabled: function(){
525		},
526
527		// todo
528		disable: function(){
529		},
530
531		// todo
532		enable: function(){
533		},
534
535		// function made to have ability to set value externally: when passed empty string, it resets the whole control, otherwise
536		// it searches for the value in the cache. And then, if none is found, sends request to server to obtain missing item
537		setValue: function(value, autoSelect){
538
539			var sv = this.vars,
540				so = this.opts,
541				sc = this.ctrls,
542				ctx = this;
543			
544			this.hideError();
545
546			// clean
547
548			if(value == null || value == false || typeof value == 'undefined' || value.toString().length == 0){ // deselect
549
550				this.resetVariables();
551
552				BX.cleanNode(sc.vars);
553
554				if(BX.type.isElementNode(sc.nothingFound))
555					BX.hide(sc.nothingFound);
556
557				this.fireEvent('after-deselect-item');
558				this.fireEvent('after-clear-selection');
559
560				return;
561			}else if(value == sv.value) // dup
562				return;
563
564			if(autoSelect !== false)
565				sv.forceSelectSingeOnce = true;
566
567			if(typeof sv.cache.nodes[value] == 'undefined'){
568
569				// lazyload it...
570				this.resetNavVariables();
571
572				ctx.downloadBundle({VALUE: value}, function(data){
573
574					ctx.fillCache(data, false); // storing item in the cache
575
576					if(typeof sv.cache.nodes[value] == 'undefined'){ // still not found
577						ctx.showNothingFound();
578					}else{
579
580						//////////////////
581						if(so.autoSelectIfOneVariant || sv.forceSelectSingeOnce)
582							ctx.selectItem(value);
583						else
584							ctx.displayVariants([value]);
585						//////////////////
586					}
587				}, function(){
588					sv.forceSelectSingeOnce = false;
589				});
590
591			}else{
592				if(sv.forceSelectSingeOnce)
593					this.selectItem(value);
594				else
595					this.displayVariants([value]);
596
597				sv.forceSelectSingeOnce = false;
598			}
599		},
600
601		getValue: function(){
602			return this.vars.value === false ? '' : this.vars.value;
603		},
604
605		clearSelected: function(){
606			this.setValue('');
607		},
608
609		getNodeByValue: function(value){
610			return this.vars.cache.nodes[value];
611		},
612
613		setTabIndex: function(index){
614			this.ctrls.inputs.fake.setAttribute('tabindex', index);
615		},
616
617		setTargetInputName: function(newName){
618			this.ctrls.inputs.origin.setAttribute('name', newName);
619		},
620
621		cancelRequest: function(){
622			// completely cancel current pending query
623			this.vars.forceSelectSingeOnce = false;
624
625			// todo
626		},
627
628		// low-level, use with caution
629
630		setTargetInputValue: function(value){
631			this.vars.eventLock = true;
632			this.ctrls.inputs.origin.value = this.getSelectorValue(value);
633			BX.fireEvent(this.ctrls.inputs.origin, 'change');
634			this.vars.eventLock = false;
635		},
636
637		// function sets value of a fake input
638		setFakeInputValue: function(display){
639
640			var sc = this.ctrls;
641
642			BX.data(sc.inputs.fake, 'bx-dc-previous-value', display); // prevent bindDebouncedChange from fire
643			sc.inputs.fake.value = display; // update fake input value
644		},
645
646		setValueVariable: function(value){
647			this.vars.value = value;
648		},
649
650		////////// PRIVATE: forbidden to use outside (for compatibility reasons)
651
652		fillCache: function(items, key){
653
654			var sv = this.vars,
655				so = this.opts;
656
657			if(!items.length)
658				return;
659
660			// first fill items themselves
661			for(var k in items)
662				if(items.hasOwnProperty(k))
663					this.addItem2Cache(items[k]);
664
665			if(typeof key == 'number' && key != 0){
666
667				if(typeof sv.cache.search[key] == 'undefined')
668					sv.cache.search[key] = [];
669
670				for(var k in items)
671					if(items.hasOwnProperty(k))
672						sv.cache.search[key].push(items[k].VALUE);
673			}
674		},
675
676		addItem2Cache: function(item){
677			this.vars.cache.nodes[item.VALUE] = item;
678		},
679
680		setInitialValue: function(){
681
682			var initalValue = false;
683			if(this.opts.selectedItem !== false)
684				initalValue = this.opts.selectedItem;
685			else if(this.ctrls.inputs.origin.value.length > 0)
686				initalValue = this.ctrls.inputs.origin.value;
687
688			if(initalValue !== false)
689				this.setValue(initalValue);
690		},
691
692		getCachedPage: function(query, pageNum){
693
694			var so = this.opts,
695				sv = this.vars;
696
697			var key = this.getCacheKeyForQuery(query);
698
699			if(typeof sv.cache.search[key] != 'object' || !('length' in sv.cache.search[key]))
700				return false;
701
702			var page = sv.cache.search[key].slice((pageNum * so.pageSize), ((pageNum + 1) * so.pageSize));
703
704			if(page.length == 0)
705				return false;
706
707			return page;
708		},
709
710		displayNextPage: function(){
711
712			var sv = this.vars;
713
714			if(!this.opts.usePagingOnScroll || sv.lastQuery == null)
715				return;
716
717			this.displayPage(sv.lastQuery, sv.lastPage + 1);
718		},
719
720		displayPage: function(query, pageNum){
721
722			var so = this.opts,
723				sv = this.vars,
724				sc = this.ctrls,
725				ctx = this;
726
727			if(sv.blockingCall && sv.displayPageMutex) // display is locked with the previous call
728				return;
729
730			query = this.refineQuery(query);
731			var key = this.getCacheKeyForQuery(query);
732
733			if(typeof pageNum == 'undefined')
734				pageNum = 0;
735
736			var ceiling = this.manageCeiling(key);
737			if(ceiling > 0 && ceiling <= pageNum)
738				return; // no more pages, maximum were reached for that query
739
740			//console.dir('lock kb mutex');
741			sv.keyboardMutex = true;
742
743			sv.displayPageMutex = true;
744			sv.blockingCall = false;
745
746			sv.lastQuery = query;
747			sv.lastPage = pageNum;
748
749			if(so.useCache){
750
751				var page = this.getCachedPage(query, pageNum);
752
753				if(page !== false){
754
755					// showing cached page
756					//console.dir('From cache');
757
758					//////////////////
759					if((so.autoSelectIfOneVariant || sv.forceSelectSingeOnce) && page.length == 1 && sv.lastPage == 0)
760						ctx.selectItem(page[0]);
761					else
762						ctx.displayVariants(page, pageNum);
763					//////////////////
764
765					sv.forceSelectSingeOnce =	false;
766					sv.displayPageMutex =		false;
767					sv.keyboardMutex =			false;
768
769				}else{
770
771					// page not found, download
772					//console.dir('Load page');
773
774					ctx.downloadBundle(query, function(data){
775
776						ctx.fillCache(data, key); // storing data in the cache
777						page = this.getCachedPage(query, pageNum);
778
779						if(page == false){
780							if(pageNum == 0)
781								ctx.showNothingFound(); // still not found
782							else{
783								ctx.manageCeiling(key, pageNum); // maximum reached
784							}
785						}else{
786
787							//////////////////
788							if((so.autoSelectIfOneVariant || sv.forceSelectSingeOnce) && page.length == 1 && sv.lastPage == 0)
789								ctx.selectItem(page[0]);
790							else
791								ctx.displayVariants(page, pageNum);
792							//////////////////
793
794							if(!so.paginatedRequest) // we have just downloaded everyting, no further paging needed for
795								ctx.manageCeiling(key, Math.ceil(sv.cache.search[key].length / so.pageSize));
796						}
797
798					}, function(){
799						sv.forceSelectSingeOnce =	false;
800						sv.blockingCall =			false;
801						sv.displayPageMutex =		false;
802						sv.keyboardMutex =			false;
803						//console.dir('UNlock kb mutex');
804					});
805				}
806
807			}else
808				throw new Error('Sorry, useCache = false currently is not implemented');
809		},
810
811		manageCeiling: function(key, pageNum){
812
813			var sv = this.vars;
814
815			if(typeof pageNum == 'undefined'){
816				if(typeof sv.cache.ceilings[key] == 'undefined')
817					return -1;
818
819				return sv.cache.ceilings[key];
820			}
821
822			sv.cache.ceilings[key] = pageNum;
823		},
824
825		getCanLoadMore: function(key){
826
827			var sv = this.vars;
828
829			if(typeof key == 'undefined')
830				key = this.getCacheKeyForQuery(sv.lastQuery);
831
832			if(typeof sv.cache.ceilings[key] == 'undefined')
833				return true;
834
835			return sv.lastPage < sv.cache.ceilings[key];
836		},
837
838		resetVariables: function(){
839			this.deselectItem();
840			this.setFakeInputValue('');
841			this.resetNavVariables();
842		},
843
844		resetNavVariables: function(){
845			this.vars.lastQuery = null;
846			this.vars.lastPage = 0;
847		},
848
849		// this function sets value of an origin input
850		setTargetValue: function(value){
851
852			var so = this.opts,
853				sv = this.vars,
854				wasSelected = this.vars.value !== false;
855
856			sv.value = value == '' ? false : value;
857
858			this.setTargetInputValue(value);
859
860			if(sv.value)
861				this.fireEvent('after-select-item', [sv.value, true]);
862			else if(wasSelected)
863				this.fireEvent('after-deselect-item');
864		},
865
866		blockingCall: function(){
867			this.vars.blockingCall = true;
868		},
869
870		downloadBundle: function(request, onLoad, onComplete, onError){
871
872			var so = this.opts,
873				sv = this.vars,
874				sc = this.ctrls,
875				ctx = this;
876
877			sv.loader.show();
878			BX.ajax({
879
880				url: ctx.opts.source,
881				method: 'post',
882				dataType: 'json',
883				async: true,
884				processData: true,
885				emulateOnload: true,
886				start: true,
887				data: BX.merge(ctx.refineRequest(request), ctx.getNavParams()),
888				//cache: true,
889				onsuccess: function(result){
890
891					//try{
892
893					sv.loader.hide();
894
895					if(result.result){
896
897						result.data = ctx.refineResponce(result.data, request);
898
899						if(typeof result.data == 'undefined')
900							result.data = [];
901
902						onLoad.apply(ctx, [result.data]);
903
904					}else
905						ctx.showError(ctx.opts.messages.error, result.errors);
906
907					onComplete.call(ctx);
908
909					//}catch(e){console.dir(e);}
910
911					if (sc.popup)
912					{
913						sc.popup.adjustPosition();
914					}
915
916				},
917				onfailure: function(e){
918
919					sv.loader.hide();
920					ctx.showError(
921						so.messages.error,
922						false,
923						e
924					);
925
926					onComplete.call(ctx);
927					if(BX.type.isFunction(onError))
928						onError.call(ctx);
929
930					if (sc.popup)
931					{
932						sc.popup.adjustPosition();
933					}
934				}
935
936			});
937		},
938
939		getNavParams: function(){
940			return this.opts.paginatedRequest ? {
941				PAGE_SIZE: this.opts.pageSize,
942				PAGE: this.vars.lastPage
943			} : {};
944		},
945
946		getSelectorValue: function(value){
947			return value;
948		},
949
950		getCacheKeyForQuery: function(query){
951			var str = '';
952			for(var k in query){
953				if(query.hasOwnProperty(k)){
954					str += k.toString().toLowerCase()+':'+query[k].toString().toLowerCase()+'|';
955				}
956			}
957
958			return BX.util.hashCode(str);
959		},
960
961		// cache depends on the value returned by this function
962		refineQuery: function(query){
963			return query;
964		},
965
966		// this function is called just before request send, i.e. it`s look like a 'query proxy'
967		refineRequest: function(query){
968			return query;
969		},
970
971		// responce 'proxy-back'
972		refineResponce: function(responce, request){
973			return responce;
974		},
975
976		// turn everyting off and clear up selection
977		deselectItem: function(){
978			var sv = this.vars,
979				sc = this.ctrls,
980				so = this.opts;
981
982			this.hideError();
983
984			this.whenClearToggle.apply(this, [false]);
985
986			sv.currentGlow = false;
987			sv.previousGlow = false;
988			sv.displayedIndex = [];
989			sc.displayedItems = {};
990
991			// todo: turn off input origin "change" signal processing to avoid collisions
992			this.setTargetValue('');
993		},
994
995		// invokes when user selects value
996		selectItem: function(value){
997
998			var sv = this.vars,
999				sc = this.ctrls,
1000				so = this.opts;
1001
1002			sv.value = value; // set current [VALUE, DISPLAY] pair id
1003			sc.inputs.origin.value = sv.cache.nodes[value].VALUE; // update origin input value
1004
1005			this.setFakeInputValue(this.whenItemSelect.apply(this, [value]));
1006
1007			this.hideDropdown();
1008
1009			this.whenClearToggle.apply(this, [true]);
1010
1011			this.setTargetValue(value);
1012		},
1013
1014		toggleGlow: function(){
1015
1016			var so = this.opts,
1017				sc = this.ctrls,
1018				sv = this.vars;
1019
1020			var item = sc.displayedItems[sv.displayedIndex[sv.currentGlow]];
1021
1022			if(sv.previousGlow !== false)
1023				this.whenItemToggle(false, sc.displayedItems[sv.displayedIndex[sv.previousGlow]], sv.previousGlow); // here we can have event triggering instead
1024
1025			if(sv.currentGlow !== false)
1026				this.whenItemToggle(true, item, sv.currentGlow); // here we can have event triggering instead
1027		},
1028
1029		showDropdown: function(){
1030
1031			if(this.vars.opened)
1032				return;
1033
1034			var flip = !this.vars.opened;
1035			this.vars.opened = true;
1036
1037			if(this.vars.lastPage == 0){
1038				this.whenItemToggle(false, this.ctrls.displayedItems[this.vars.displayedIndex[this.vars.currentGlow]], this.vars.currentGlow);
1039				this.vars.currentGlow = 0;
1040				this.toggleGlow();
1041			}
1042
1043			BX.show(this.ctrls.pane); // read height
1044			var paneHeight = BX.height(this.ctrls.pane);
1045			BX.hide(this.ctrls.pane); // read height end
1046
1047			var input = this.ctrls.inputs.fake;
1048			var inputPos = BX.pos(input);
1049
1050			var spaceUnderItem = BX.scrollTop(window) + BX.height(window) - (Math.ceil(inputPos.top) + inputPos.height);
1051
1052			this.whenDropdownToggle.apply(this, [true, spaceUnderItem - paneHeight < -20, inputPos.height, flip]);
1053		},
1054
1055		hideDropdown: function(){
1056
1057			this.vars.opened = false;
1058			this.whenDropdownToggle.apply(this, [false, false, 0, true]);
1059		},
1060
1061		clearDisplayedVariants: function(){
1062			this.hideDropdown();
1063			this.vars.displayedIndex = [];
1064		},
1065
1066		displayVariants: function(items, pageNum){
1067			var sc = this.ctrls,
1068				sv = this.vars,
1069				so = this.opts,
1070				code = this.sys.code;
1071
1072			//this.hideDropdown(); // todo: here ...
1073			this.hideNothingFound(); // and here ...
1074
1075			if(typeof pageNum == 'undefined' || pageNum == 0){
1076				BX.cleanNode(sc.vars); // and here "deferred" pattern required, kz hideDropdown() might be async in some implementations
1077
1078				sv.displayedIndex = [];
1079				sc.displayedItems = {};
1080			}
1081
1082			var base = sv.displayedIndex.length;
1083
1084			for(var k in items)
1085			{
1086				if(!items.hasOwnProperty(k))
1087					continue;
1088
1089				if(!items[k])
1090					continue;
1091
1092				var domItem = this.whenRenderVariant(items[k], base + parseInt(k))[0];
1093
1094				BX.data(domItem, 'bx-'+code+'-item-value', items[k]);
1095
1096				sc.vars.appendChild(domItem);
1097				this.fireEvent('after-item-append', [domItem]);
1098
1099				sv.displayedIndex.push(items[k]);
1100				sc.displayedItems[items[k]] = domItem;
1101			}
1102			this.showDropdown();
1103
1104			this.fireEvent('after-page-display', [sv.cache.nodes, pageNum]);
1105		},
1106
1107		showNothingFound: function(){
1108
1109			BX.cleanNode(this.ctrls.vars);
1110
1111			if(BX.type.isElementNode(this.ctrls.nothingFound))
1112				this.whenNothingFoundToggle(true);
1113			else{ // show message directly in the dropdown
1114
1115				this.ctrls.vars.appendChild(this.whenRenderNothingFound(this.opts.messages.nothingFound));
1116
1117				this.showDropdown();
1118			}
1119
1120			this.fireEvent('nothing-found'); // this.opts.onNothingFound.apply(this);
1121		},
1122
1123		hideNothingFound: function(){
1124			if(BX.type.isElementNode(this.ctrls.nothingFound))
1125				this.whenNothingFoundToggle(false);
1126		},
1127
1128		// actually, means "hide static error"
1129		hideError: function(){
1130			if(BX.type.isElementNode(this.ctrls.errorMessage) && this.vars.allowHideErrors)
1131				BX.hide(this.ctrls.errorMessage);
1132		},
1133
1134		showError: function(errorLabel, messages, sysDesc){
1135
1136			BX.cleanNode(this.ctrls.vars);
1137
1138			this.ctrls.vars.appendChild(this.whenRenderError(errorLabel));
1139
1140			this.showDropdown();
1141
1142			BX.debug(arguments);
1143		},
1144
1145		refineItemDataForTemplate: function(itemData){
1146
1147			var query = this.vars.lastQuery.QUERY;
1148
1149			if(BX.type.isNotEmptyString(query)){
1150				var chunks = [];
1151				if(this.opts.wrapSeparate)
1152					chunks = query.split(/\s+/);
1153				else
1154					chunks = [query];
1155
1156				itemData['=display_wrapped'] = BX.util.wrapSubstring(itemData.DISPLAY, chunks, this.opts.wrapTagName, true);
1157			}else
1158				itemData['=display_wrapped'] = BX.util.htmlspecialchars(itemData.DISPLAY);
1159
1160			return itemData;
1161		},
1162
1163		getCurrentItem: function(){
1164			return this.vars.cache.nodes[this.vars.value];
1165		},
1166
1167		/* Behaviour functions below */
1168
1169		whenDisplayVariant: function(itemId){
1170			return this.vars.cache.nodes[itemId]['DISPLAY'];
1171		},
1172
1173		whenItemSelect: function(itemId){ // evaluates when user selects a particular item. should return value which we want in our fake input
1174			return this.vars.cache.nodes[itemId]['DISPLAY'];
1175		},
1176
1177		whenLoaderToggle: function(way){
1178			BX[way ? 'addClass' : 'removeClass'](this.ctrls.inputs.fake, 'bx-u
1178i-'+this.sys.code+'-loading');
1179		},
1180
1181		whenDropdownToggle: function(way, upward, inputHeight, flip){
1182			if(way)
1183			{
1184				if(flip)
1185				{
1186					this.whenDecidePaneOrient(upward, inputHeight, flip);
1187				}
1188				if (this.opts.usePopup)
1189				{
1190					this.ctrls.popup = BX.PopupWindowManager.create(
1191						'popup-location-search-list',
1192						this.ctrls.scope.parentNode,
1193						{
1194							className: 'bx-sls',
1195							content: this.ctrls.pane,
1196							bindOptions: { forceBindPosition: true },
1197							zIndex: 1020
1198						}
1199					);
1200					this.ctrls.popup.show();
1201				}
1202				BX.show(this.ctrls.pane);
1203			}
1204			else
1205			{
1206				if (this.opts.usePopup && this.ctrls.popup)
1207				{
1208					this.ctrls.popup.destroy();
1209				}
1210				BX.hide(this.ctrls.pane);
1211			}
1212
1213			this.fireEvent('after-popup-toggled', [way]);
1214		},
1215
1216		whenDecidePaneOrient: function(upward, inputHeight){
1217
1218			var sc = this.ctrls;
1219
1220			if(upward){
1221				var top = BX.style(sc.pane, 'top');
1222				if(top != 'auto'){
1223					BX.data(sc.pane, 'pane-top', top);
1224				}
1225				BX.style(sc.pane, 'top', 'auto');
1226
1227				BX.style(sc.pane, 'bottom', (inputHeight + this.opts.pageUpWardOffset)+'px');
1228			}else{
1229				var top = BX.data(sc.pane, 'pane-top');
1230				if(typeof top != 'undefined')
1231					BX.style(sc.pane, 'top', top);
1232
1233				BX.style(sc.pane, 'bottom', 'auto');
1234			}
1235		},
1236
1237		whenItemToggle: function(way, node, itemId){
1238			BX[way ? 'addClass' : 'removeClass'](node, 'bx-ui-'+this.sys.code+'-variant-active');
1239		},
1240
1241		whenClearToggle: function(way){
1242			BX[way ? 'show' : 'hide'](this.ctrls.clear);
1243		},
1244
1245		whenNothingFoundToggle: function(way){
1246			BX[way ? 'show' : 'hide'](this.ctrls.nothingFound);
1247		},
1248
1249		whenRenderVariant: function(itemId, index){
1250
1251			var itemData = BX.clone(this.vars.cache.nodes[itemId]);
1252			itemData = this.refineItemDataForTemplate(itemData);
1253			itemData.index = index;
1254
1255			this.fireEvent('before-render-variant', [itemData]);
1256
1257			if(typeof this.tmpls['dropdown-item'] == 'string')
1258				return this.createNodesByTemplate('dropdown-item', itemData, true);
1259
1260			return [BX.create('div', {
1261				props: {
1262					className: 'bx-ui-'+this.sys.code+'-variant'
1263				},
1264				text: this.vars.cache.nodes[itemId]['DISPLAY']
1265			})];
1266		},
1267
1268		whenRenderError: function(message){
1269
1270			if(typeof this.tmpls['error'] == 'string')
1271				return this.createNodesByTemplate('error', {message: message}, true)[0];
1272
1273			return BX.create('div', {
1274				props: {
1275					className: 'bx-ui-'+this.sys.code+'-error'
1276				},
1277				text: message
1278			});
1279		},
1280
1281		whenRenderNothingFound: function(message){
1282
1283			if(typeof this.tmpls['nothing-found'] == 'string')
1284				return this.createNodesByTemplate('nothing-found', {message: message}, true)[0];
1285
1286			return this.whenRenderError(message);
1287		}
1288	});
1289
1290})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.