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.