1angular.module('oi.select', []); 2angular.module('oi.select') 3 4.provider('oiSelect', function() { 5 return { 6 options: { 7 debounce: 500, 8 searchFilter: 'oiSelectCloseIcon', 9 dropdownFilter: 'oiSelectHighlight', 10 listFilter: 'oiSelectAscSort', 11 groupFilter: 'oiSelectGroup', 12 editItem: false, 13 newItem: false, 14 closeList: true, 15 saveTrigger: 'enter tab blur' 16 }, 17 version: { 18 full: '0.2.20', 19 major: 0, 20 minor: 2, 21 dot: 20 22 }, 23 $get: function() { 24 return { 25 options: this.options, 26 version: this.version 27 }; 28 } 29 }; 30}) 31 32.factory('oiSelectEscape', function() { 33 var rEscapableCharacters = /[-\/\\^$*+?.()|[\]{}]/g; // cache escape + match String 34 var sEscapeMatch = '\\$&'; 35 36 return function(string) { 37 return String(string).replace(rEscapableCharacters, sEscapeMatch); 38 }; 39}) 40 41.factory('oiSelectEditItem', function() { 42 return function(removedItem, lastQuery, getLabel, itemIsCorrected) { 43 return itemIsCorrected ? '' : getLabel(removedItem); 44 }; 45}) 46 47.factory('oiUtils', ['$document', '$timeout', function($document, $timeout) { 48 /** 49 * Check to see if a DOM element is a descendant of another DOM element. 50 * 51 * @param {DOM element} container 52 * @param {DOM element} contained 53 * @param {string} class name of element in container 54 * @returns {boolean} 55 */ 56 function contains(container, contained, className) { 57 var current = contained; 58 59 while (current && current.ownerDocument && current.nodeType !== 11) { 60 if (className) { 61 if (current === container) { 62 return false; 63 } 64 if (current.className.indexOf(className) >= 0) { //current.classList.contains(className) doesn't work in IE9 65 return true; 66 } 67 } else { 68 if (current === container) { 69 return true; 70 } 71 } 72 current = current.parentNode; 73 } 74 75 return false; 76 } 77 78 /** 79 * Simulate focus/blur events of the inner input element to the outer element 80 * 81 * @param {element} outer element 82 * @param {element} inner input element 83 * @returns {function} deregistration function for listeners. 84 */ 85 function bindFocusBlur(element, inputElement) { 86 var isFocused, isMousedown, isBlur; 87 88 $document[0].addEventListener('click', clickHandler, true); 89 element[0].addEventListener('mousedown', mousedownHandler, true); 90 element[0].addEventListener('blur', blurHandler, true); 91 inputElement.on('focus', focusHandler); 92 93 function blurHandler(event) { 94 if (event && event.target.nodeName !== 'INPUT') return; //for IE 95 96 isBlur = false; 97 98 if (isMousedown) { 99 isBlur = true; 100 return; 101 } 102 103 $timeout(function () { 104 element.triggerHandler('blur'); //conflict with current live cycle (case: multiple=none + tab) 105 }); 106 } 107 108 function focusHandler() { 109 if (!isFocused) { 110 isFocused = true; 111 112 $timeout(function () { 113 element.triggerHandler('focus'); //conflict with current live cycle (case: multiple=none + tab) 114 }); 115 } 116 } 117 118 function mousedownHandler() { 119 isMousedown = true; 120 } 121 122 function clickHandler(event) { 123 isMousedown = false; 124 125 var activeElement = event.target; 126 var isSelectElement = contains(element[0], activeElement); 127 128 if (isBlur && !isSelectElement) { 129 blurHandler(); 130 } 131 132 if (isSelectElement && activeElement.nodeName !== 'INPUT') { 133 $timeout(function () { 134 inputElement[0].focus(); 135 }); 136 } 137 138 if (!isSelectElement && isFocused) { 139 isFocused = false;
140 } 141 } 142 143 return function () { 144 $document[0].removeEventListener('click', clickHandler); 145 element[0].removeEventListener('mousedown', mousedownHandler, true); 146 element[0].removeEventListener('blur', blurHandler, true); 147 inputElement.off('focus', focusHandler); 148 } 149 } 150 151 /** 152 * Sets the selected item in the dropdown menu 153 * of available options. 154 * 155 * @param {object} list 156 * @param {object} item 157 */ 158 function scrollActiveOption(list, item) { 159 var y, height_menu, height_item, scroll, scroll_top, scroll_bottom; 160 161 if (item) { 162 height_menu = list.offsetHeight; 163 height_item = getWidthOrHeight(item, 'height', 'margin'); //outerHeight(true); 164 scroll = list.scrollTop || 0; 165 y = getOffset(item).top - getOffset(list).top + scroll; 166 scroll_top = y; 167 scroll_bottom = y - height_menu + height_item; 168 169 //TODO Make animation 170 if (y + height_item > height_menu + scroll) { 171 list.scrollTop = scroll_bottom; 172 } else if (y < scroll) { 173 list.scrollTop = scroll_top; 174 } 175 } 176 } 177 178 // Used for matching numbers 179 var core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source; 180 var rnumnonpx = new RegExp("^(" + core_pnum + ")(?!px)[a-z%]+$", "i"); 181 182 function augmentWidthOrHeight(elem, name, extra, isBorderBox, styles) { 183 var i = extra === (isBorderBox ? 'border' : 'content') ? 184 // If we already have the right measurement, avoid augmentation 185 4 : 186 // Otherwise initialize for horizontal or vertical properties 187 name === 'width' ? 1 : 0, 188 189 val = 0, 190 cssExpand = ['Top', 'Right', 'Bottom', 'Left']; 191 192 //TODO Use angular.element.css instead of getStyleValue after https://github.com/caitp/angular.js/commit/92bbb5e225253ebddd38ef5735d66ffef76b6a14 will be applied 193 function getStyleValue(name) { 194 return parseFloat(styles[name]); 195 } 196 197 for (; i < 4; i += 2) { 198 // both box models exclude margin, so add it if we want it 199 if (extra === 'margin') { 200 val += getStyleValue(extra + cssExpand[i]); 201 } 202 203 if (isBorderBox) { 204 // border-box includes padding, so remove it if we want content 205 if (extra === 'content') { 206 val -= getStyleValue('padding' + cssExpand[i]); 207 } 208 209 // at this point, extra isn't border nor margin, so remove border 210 if (extra !== 'margin') { 211 val -= getStyleValue('border' + cssExpand[i] + 'Width'); 212 } 213 } else { 214 val += getStyleValue('padding' + cssExpand[i]); 215 216 // at this point, extra isn't content nor padding, so add border 217 if (extra !== 'padding') { 218 val += getStyleValue('border' + cssExpand[i] + 'Width'); 219 } 220 } 221 } 222 223 return val; 224 } 225 226 function getOffset(elem) { 227 var docElem, win, 228 box = elem.getBoundingClientRect(), 229 doc = elem && elem.ownerDocument; 230 231 if (!doc) { 232 return; 233 } 234 235 docElem = doc.documentElement; 236 win = getWindow(doc); 237 238 return { 239 top: box.top + win.pageYOffset - docElem.clientTop, 240 left: box.left + win.pageXOffset - docElem.clientLeft 241 }; 242 } 243 244 function getWindow(elem) { 245 return elem != null && elem === elem.window ? elem : elem.nodeType === 9 && elem.defaultView; 246 } 247 248 function getWidthOrHeight(elem, name, extra) { 249 250 // Start with offset property, which is equivalent to the border-box value 251 var valueIsBorderBox = true, 252 val = name === 'width' ? elem.offsetWidth : elem.offsetHeight, 253 styles = window.getComputedStyle(elem, null), 254 255 //TODO Make isBorderBox after https://github.com/caitp/angular.js/commit/92bbb5e225253ebddd38ef5735d66ffef76b6a14 will be applied 256 isBorderBox = false; //jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; 257 258 // some non-html elements return undefined for offsetWidth, so check for null/undefined 259 // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 260 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 261 if (val <= 0 || val == null) { 262 // Fall back to computed then uncomputed css if necessary 263 val = styles[name]; 264 265 if (val < 0 || val == null) { 266 val = elem.style[name]; 267 } 268 269 // Computed unit is not pixels. Stop here and return. 270 if (rnumnonpx.test(val)) { 271 return val; 272 } 273 274 // we need the check for style in case a browser which returns unreliable values 275 // for getComputedStyle silently falls back to the reliable elem.style 276 //valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); 277 278 // Normalize "", auto, and prepare for extra 279 val = parseFloat(val) || 0; 280 } 281 282 // use the active box-sizing model to add/subtract irrelevant styles 283 return val + augmentWidthOrHeight(elem, name, extra || ( isBorderBox ? "border" : "content" ), valueIsBorderBox, styles); 284 } 285 286 function groupsIsEmpty(groups) { 287 for (var k in groups) { 288 if (groups.hasOwnProperty(k) && groups[k].length) { 289 return false;
290 } 291 } 292 return true; 293 } 294 295 function objToArr(obj) { 296 var arr = []; 297 298 angular.forEach(obj, function (value, key) { 299 if (key.toString().charAt(0) !== '$') { 300 arr.push(value); 301 } 302 }); 303 304 return arr; 305 } 306 307 //lodash _.intersection + filter + invert 308 function intersection(xArr, yArr, xFilter, yFilter, invert) { 309 var i, j, n, filteredX, filteredY, out = invert ? [].concat(xArr) : []; 310 311 for (i = 0, n = xArr.length; i < xArr.length; i++) { 312 filteredX = xFilter ? xFilter(xArr[i]) : xArr[i]; 313 314 for (j = 0; j < yArr.length; j++) { 315 filteredY = yFilter ? yFilter(yArr[j]) : yArr[j]; 316 317 if (angular.equals(filteredX, filteredY, xArr, yArr, i, j)) { 318 invert ? out.splice(i + out.length - n, 1) : out.push(yArr[j]); 319 break; 320 } 321 } 322 } 323 return out; 324 } 325 326 function getValue(valueName, item, scope, getter) { 327 var locals = {}; 328 329 //'name.subname' -> {name: {subname: list}}' 330 valueName.split('.').reduce(function (previousValue, currentItem, index, arr) { 331 return previousValue[currentItem] = index < arr.length - 1 ? {} : item; 332 }, locals); 333 334 return getter(scope, locals); 335 } 336 337 return { 338 contains: contains, 339 bindFocusBlur: bindFocusBlur, 340 scrollActiveOption: scrollActiveOption, 341 groupsIsEmpty: groupsIsEmpty, 342 objToArr: objToArr, 343 getValue: getValue, 344 intersection: intersection 345 } 346}]); 347angular.module('oi.select') 348 349.directive('oiSelect', ['$document', '$q', '$timeout', '$parse', '$interpolate', '$injector', '$filter', '$animate', 'oiUtils', 'oiSelect', function($document, $q, $timeout, $parse, $interpolate, $injector, $filter, $animate, oiUtils, oiSelect) { 350 var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; 351 var VALUES_REGEXP = /([^\(\)\s\|\s]*)\s*(\(.*\))?\s*(\|?\s*.+)?/; 352 353 return { 354 restrict: 'AE', 355 templateUrl: 'src/template.html', 356 require: 'ngModel', 357 scope: {}, 358 compile: function (element, attrs) { 359 var optionsExp = attrs.oiOptions, 360 match = optionsExp ? optionsExp.match(NG_OPTIONS_REGEXP) : ['', 'i', '', '', '', 'i', '', '', '']; 361 362 if (!match) { 363 throw new Error("Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'"); 364 } 365 366 var selectAsName = / as /.test(match[0]) && match[1], //item.modelValue 367 displayName = match[2] || match[1], //item.label 368 valueName = match[5] || match[7], //item 369 groupByName = match[3] || '', //item.groupName 370 disableWhenName = match[4] || '', //item.disableWhenName 371 trackByName = match[9] || displayName, //item.id 372 valueMatches = match[8].match(VALUES_REGEXP); //collection 373 374 var valuesName = valueMatches[1], //collection 375 filteredValuesName = valuesName + (valueMatches[3] || ''), //collection | filter 376 valuesFnName = valuesName + (valueMatches[2] || ''); //collection() 377 378 var selectAsFn = selectAsName && $parse(selectAsName), 379 displayFn = $parse(displayName), 380 groupByFn = $parse(groupByName), 381 disableWhenFn = $parse(disableWhenName), 382 filteredValuesFn = $parse(filteredValuesName), 383 valuesFn = $parse(valuesFnName), 384 trackByFn = $parse(trackByName); 385 386 var multiplePlaceholderFn = $interpolate(attrs.multiplePlaceholder || ''), 387 placeholderFn = $interpolate(attrs.placeholder || ''), 388 optionsFn = $parse(attrs.oiSelectOptions), 389 isOldAngular = angular.version.major <= 1 && angular.version.minor <= 3; 390 391 var keyUpDownWerePressed, 392 matchesWereReset, 393 timeoutPromise, 394 lastQuery, 395 removedItem, 396 multiple, 397 multipleLimit, 398 newItemFn; 399 400 return function(scope, element, attrs, ctrl) { 401 // Override the standard $isEmpty because an empty array means the input is empty. 402 ctrl.$isEmpty = function(value) { return !exists(value) }; 403 404 var inputElement = element.find('input'), 405 listElement = angular.element(element[0].querySelector('.select-dropdown')), 406 placeholder = placeholderFn(scope), 407 multiplePlaceholder = multiplePlaceholderFn(scope), 408 elementOptions = optionsFn(scope.$parent) || {}, 409 options = angular.extend({cleanModel: elementOptions.newItem === 'prompt'}, oiSelect.options, elementOptions), 410 editItem = options.editItem, 411 editItemIsCorrected = editItem === 'correct', 412 waitTime = 0; 413 414 if (editItem === true || editItem === 'correct') { 415 editItem = 'oiSelectEditItem'; 416 } 417 var editItemFn = editItem ? $injector.get(editItem) : angular.noop, 418 removeItemFn = $parse(options.removeItemFn); 419 420 match = options.searchFilter.split(':'); 421 var searchFilter = $filter(match[0]), 422 searchFilterOptionsFn = $parse(match[1]); 423 424 match = options.dropdownFilter.split(':'); 425 var dropdownFilter = $filter(match[0]), 426 dropdownFilterOptionsFn = $parse(match[1]); 427 428 match = options.listFilter.split(':'); 429 var listFilter = $filter(match[0]), 430 listFilterOptionsFn = $parse(match[1]); 431 432 match = options.groupFilter.split(':'); 433 var groupFilter = $filter(match[0]), 434 groupFilterOptionsFn = $parse(match[1]); 435 436 if (options.newItemFn) { 437 newItemFn = $parse(options.newItemFn); 438 439 } else { 440 newItemFn = function(scope, locals) { 441 return (optionsFn(locals) || {}).newItemModel || locals.$query; 442 }; 443 } 444 445 if (options.cleanModel && (!editItem || editItemIsCorrected)) { 446 element.addClass('cleanMode'); 447 } 448 449 var unbindFocusBlur = oiUtils.bindFocusBlur(element, inputElement); 450 451 if (angular.isDefined(attrs.autofocus)) { 452 $timeout(function() { 453 inputElement[0].focus(); 454 }); 455 } 456 457 if (angular.isDefined(attrs.readonly)) { 458 inputElement.attr('readonly', true) 459 } 460 461 if (angular.isDefined(attrs.tabindex)) { 462 inputElement.attr('tabindex', attrs.tabindex); 463 element[0].removeAttribute('tabindex'); 464 } 465 466 if (options.maxlength) { 467 inputElement.attr('maxlength', options.maxlength); 468 } 469 470 attrs.$observe('disabled', function(value) { 471 inputElement.prop('disabled', value); 472 473 //hide empty string with input 474 if (multiple && ctrl.$modelValue && ctrl.$modelValue.length) { 475 scope.inputHide = value; 476 } 477 }); 478 479 scope.$on('$destroy', unbindFocusBlur); 480 481 scope.$parent.$watch(attrs.multipleLimit, function(value) { 482 multipleLimit = Number(value) || Infinity; 483 }); 484 485 scope.$parent.$watch(attrs.multiple, function(multipleValue) { 486 multiple = multipleValue === undefined ? angular.isDefined(attrs.multiple) : multipleValue; 487 488 element[multiple ? 'addClass' : 'removeClass']('multiple'); 489 }); 490 491 function valueChangedManually() { //case: clean model; prompt + editItem: 'correct'; initial value = defined/undefined 492 if (editItemIsCorrected) { 493 element.removeClass('cleanMode'); 494 } 495 editItemIsCorrected = false;
496 } 497 498 scope.$parent.$watch(attrs.ngModel, function(value, oldValue) { 499 var output = compact(value), 500 promise = $q.when(output); 501 502 modifyPlaceholder(); 503 504 if (exists(oldValue) && value !== oldValue) { 505 valueChangedManually(); 506 } 507 508 if (!multiple) { 509 restoreInput(); 510 } 511 512 if (selectAsFn && exists(value)) { 513 promise = getMatches(null, value) 514 .then(function(collection) { 515 return oiUtils.intersection(output, collection, null, selectAs); 516 }); 517 timeoutPromise = null; //`resetMatches` should not cancel the `promise` 518 } 519 520 if (multiple && attrs.disabled && !exists(value)) { //case: multiple, disabled=true + remove all items 521 scope.inputHide = false;
522 } 523 524 promise.then(function(collection) { 525 scope.output = collection; 526 527 if (collection.length !== output.length) { 528 scope.removeItem(collection.length); //if newItem was not created 529 } 530 }); 531 }); 532 533 scope.$watch('query', function(inputValue, oldValue) { 534 if (saveOn(inputValue.slice(0, -1), inputValue.slice(-1))) { 535 return; 536 } 537 538 //We don't get matches if nothing added into matches list 539 if (inputValue !== oldValue && (!scope.oldQuery || inputValue) && !matchesWereReset) { 540 listElement[0].scrollTop = 0; 541 542 if (inputValue) { 543 getMatches(inputValue); 544 scope.oldQuery = null; 545 } else if (multiple) { 546 resetMatches(); 547 matchesWereReset = true; 548 } 549 } 550 matchesWereReset = false; 551 }); 552 553 scope.$watch('groups', function(groups) { 554 if (oiUtils.groupsIsEmpty(groups)) { 555 scope.isOpen = false; 556 557 } else if (!scope.isOpen && !attrs.disabled) { 558 scope.isOpen = true; 559 scope.isFocused = true; 560 } 561 }); 562 563 scope.$watch('isFocused', function(isFocused) { 564 $animate[isFocused ? 'addClass' : 'removeClass'](element, 'focused', !isOldAngular && { 565 tempClasses: 'focused-animate' 566 }); 567 }); 568 569 scope.$watch('isOpen', function(isOpen) { 570 $animate[isOpen ? 'addClass' : 'removeClass'](element, 'open', !isOldAngular && { 571 tempClasses: 'open-animate' 572 }); 573 }); 574 575 scope.$watch('showLoader', function(isLoading) { 576 $animate[isLoading ? 'addClass' : 'removeClass'](element, 'loading', !isOldAngular && { 577 tempClasses: 'loading-animate' 578 }); 579 }); 580 581 scope.addItem = function addItem(option) { 582 lastQuery = scope.query; 583 584 //duplicate 585 if (multiple && oiUtils.intersection(scope.output, [option], trackBy, trackBy).length) return; 586 587 //limit is reached 588 if (scope.output.length >= multipleLimit) { 589 blinkClass('limited'); 590 591 return; 592 } 593 594 var optionGroup = scope.groups[getGroupName(option)] = scope.groups[getGroupName(option)] || []; 595 var modelOption = selectAsFn ? selectAs(option) : option; 596 597 optionGroup.splice(optionGroup.indexOf(option), 1); 598 599 if (multiple) { 600 ctrl.$setViewValue(angular.isArray(ctrl.$modelValue) ? ctrl.$modelValue.concat(modelOption) : [modelOption]); 601 602 } else { 603 ctrl.$setViewValue(modelOption); 604 restoreInput(); 605 } 606 607 if (oiUtils.groupsIsEmpty(scope.groups)) { 608 scope.groups = {}; //it is necessary for groups watcher 609 } 610 611 if (!multiple && !options.closeList) { 612 resetMatches({query: true}); 613 } 614 615 valueChangedManually(); 616 617 scope.oldQuery = scope.oldQuery || scope.query; 618 scope.query = ''; 619 scope.backspaceFocus = false; 620 }; 621 622 scope.removeItem = function removeItem(position) { 623 if (attrs.disabled || multiple && position < 0) return; 624 625 removedItem = multiple ? ctrl.$modelValue[position] : ctrl.$modelValue; 626 627 $q.when(removeItemFn(scope.$parent, {$item: removedItem})) 628 .then(function() { 629 if (!multiple && !scope.inputHide) return; 630 631 if (multiple) { 632 ctrl.$modelValue.splice(position, 1); 633 ctrl.$setViewValue([].concat(ctrl.$modelValue)); 634 635 } else { 636 cleanInput(); 637 638 if (options.cleanModel) { 639 ctrl.$setViewValue(undefined); 640 } 641 } 642 643 if (multiple || !scope.backspaceFocus) { 644 scope.query = editItemFn(removedItem, lastQuery, getLabel, editItemIsCorrected, element) || ''; 645 } 646 647 if (multiple && options.closeList) { 648 resetMatches({query: true}); 649 } 650 }) 651 }; 652 653 scope.setSelection = function(index) { 654 if (!keyUpDownWerePressed && scope.selectorPosition !== index) { 655 setOption(listElement, index); 656 } else { 657 keyUpDownWerePressed = false;
658 } 659 }; 660 661 scope.keyUp = function keyUp(event) { //scope.query is actual 662 switch (event.keyCode) { 663 case 8: /* backspace */ 664 if (!scope.query.length && (!multiple || !scope.output.length)) { 665 resetMatches(); 666 } 667 } 668 }; 669 670 scope.keyDown = function keyDown(event) { 671 var top = 0, 672 bottom = scope.order.length - 1; 673 674 switch (event.keyCode) { 675 case 38: /* up */ 676 scope.selectorPosition = angular.isNumber(scope.selectorPosition) ? scope.selectorPosition : top; 677 setOption(listElement, scope.selectorPosition === top ? bottom : scope.selectorPosition - 1); 678 keyUpDownWerePressed = true; 679 break; 680 681 case 40: /* down */ 682 scope.selectorPosition = angular.isNumber(scope.selectorPosition) ? scope.selectorPosition : top - 1; 683 setOption(listElement, scope.selectorPosition === bottom ? top : scope.selectorPosition + 1); 684 keyUpDownWerePressed = true; 685 if (!scope.query.length && !scope.isOpen) { 686 getMatches(); 687 } 688 if (scope.inputHide) { 689 cleanInput(); 690 } 691 692 break; 693 694 case 37: /* left */ 695 case 39: /* right */ 696 break; 697 698 case 9: /* tab */ 699 saveOn('tab'); 700 break; 701 702 case 13: /* enter */ 703 saveOn('enter'); 704 event.preventDefault(); // Prevent the event from bubbling up as it might otherwise cause a form submission 705 break; 706 707 case 32: /* space */ 708 saveOn('space'); 709 break; 710 711 case 27: /* esc */ 712 if (!multiple) { 713 restoreInput(); 714 715 if (options.cleanModel) { 716 ctrl.$setViewValue(removedItem); 717 } 718 } 719 resetMatches(); 720 break; 721 722 case 8: /* backspace */ 723 if (!scope.query.length) { 724 if (!multiple || editItem) { 725 scope.backspaceFocus = true; 726 } 727 if (scope.backspaceFocus && scope.output && (!multiple || scope.output.length)) { //prevent restoring last deleted option 728 scope.removeItem(scope.output.length - 1); 729 730 if (editItem) { 731 event.preventDefault(); 732 } 733 break; 734 } 735 scope.backspaceFocus = !scope.backspaceFocus; 736 break; 737 } 738 default: /* any key */ 739 if (scope.inputHide) { 740 cleanInput(); 741 } 742 scope.backspaceFocus = false; 743 return false; //preventDefaults 744 } 745 }; 746 747 scope.getSearchLabel = function(item) { 748 var label = getLabel(item); 749 750 return searchFilter(label, scope.oldQuery || scope.query, item, searchFilterOptionsFn(scope.$parent), element); 751 }; 752 753 scope.getDropdownLabel = function(item) { 754 var label = getLabel(item); 755 756 return dropdownFilter(label, scope.oldQuery || scope.query, item, dropdownFilterOptionsFn(scope.$parent), element); 757 }; 758 759 scope.getGroupLabel = function(group, items) { 760 return groupFilter(group, scope.oldQuery || scope.query, items, groupFilterOptionsFn(scope.$parent), element); 761 }; 762 763 scope.getDisableWhen = getDisableWhen; 764 765 766 resetMatches(); 767 768 element[0].addEventListener('click', click, true); //triggered before add or delete item event 769 element.on('focus', focus); 770 element.on('blur', blur); 771 772 function blinkClass(name, delay) {
773 delay = delay || 150; 774 775 element.addClass(name); 776 777 $timeout(function() { 778 element.removeClass(name); 779 }, delay); 780 } 781 782 function cleanInput() { 783 scope.listItemHide = true; 784 scope.inputHide = false; 785 } 786 787 function restoreInput() { 788 var modelExists = exists(ctrl.$modelValue); 789 scope.listItemHide = !modelExists; 790 scope.inputHide = modelExists; 791 } 792 793 function click(event) { 794 //option is disabled 795 if (oiUtils.contains(element[0], event.target, 'disabled')) return; 796 797 //limit is reached 798 if (scope.output.length >= multipleLimit && oiUtils.contains(element[0], event.target, 'select-dropdown')) return; 799 800 if (scope.inputHide) { 801 scope.removeItem(0); //because click on border (not on chosen item) doesn't remove chosen element 802 } 803 804 if (scope.isOpen && options.closeList && (event.target.nodeName !== 'INPUT' || !scope.query.length)) { //do not reset if you are editing the query 805 resetMatches({query: options.editItem && !editItemIsCorrected}); 806 scope.$evalAsync(); 807 } else { 808 getMatches(scope.query); 809 } 810 } 811 812 function focus(event) { 813 if (scope.isFocused) return; 814 815 scope.isFocused = true; 816 817 if (attrs.disabled) return; 818 819 scope.backspaceFocus = false; 820 } 821 822 823 function blur(event) { 824 scope.isFocused = false; 825 826 if (!multiple) { 827 restoreInput(); 828 } 829 830 if (!saveOn('blur')) { 831 resetMatches(); 832 } 833 scope.$evalAsync(); 834 } 835 836 function saveOn(query, triggerName) { 837 if (!triggerName) { 838 triggerName = query; 839 query = scope.query; 840 } 841 842 var isTriggered = options.saveTrigger.split(' ').indexOf(triggerName) + 1, 843 isNewItem = options.newItem && query, 844 selectedOrder = triggerName !== 'blur' ? scope.order[scope.selectorPosition] : null, //do not save selected element in dropdown list on blur 845 itemPromise; 846 847 if (isTriggered && (isNewItem || selectedOrder)) { 848 scope.showLoader = true; 849 itemPromise = $q.when(selectedOrder || newItemFn(scope.$parent, {$query: query})); 850 851 itemPromise 852 .then(function(data) { 853 if (data === undefined) { 854 return $q.reject(); 855 } 856 857 scope.addItem(data); 858 859 var bottom = scope.order.length - 1; 860 861 if (scope.selectorPosition === bottom) { 862 setOption(listElement, 0); //TODO optimise when list will be closed 863 } 864 options.newItemFn && !selectedOrder || $timeout(angular.noop); //TODO $applyAsync work since Angular 1.3 865 resetMatches(); 866 }) 867 .catch(function() { 868 blinkClass('invalid-item'); 869 scope.showLoader = false;
870 }); 871 872 return true; 873 } 874 } 875 876 function modifyPlaceholder() { 877 var currentPlaceholder = multiple && exists(ctrl.$modelValue) ? multiplePlaceholder : placeholder; 878 inputElement.attr('placeholder', currentPlaceholder); 879 } 880 881 function trackBy(item) { 882 return oiUtils.getValue(valueName, item, scope.$parent, trackByFn); 883 } 884 885 function selectAs(item) { 886 return oiUtils.getValue(valueName, item, scope.$parent, selectAsFn); 887 } 888 889 function getLabel(item) { 890 return oiUtils.getValue(valueName, item, scope.$parent, displayFn); 891 } 892 893 function getDisableWhen(item) { 894 return oiUtils.getValue(valueName, item, scope.$parent, disableWhenFn); 895 } 896 897 function getGroupName(option) { 898 return oiUtils.getValue(valueName, option, scope.$parent, groupByFn) || ''; 899 } 900 901 function filter(list) { 902 return oiUtils.getValue(valuesName, list, scope.$parent, filteredValuesFn); 903 } 904 905 function compact(value) { 906 value = value instanceof Array ? value : value ? [value]: []; 907 908 return value.filter(function(item) { 909 return item && (item instanceof Array && item.length || selectAsFn || getLabel(item)); 910 }); 911 } 912 913 function exists(value) { 914 return !!compact(value).length; 915 } 916 917 function getMatches(query, selectedAs) { 918 if (timeoutPromise && waitTime) { 919 $timeout.cancel(timeoutPromise); //cancel previous timeout 920 } 921 922 timeoutPromise = $timeout(function() { 923 var values = valuesFn(scope.$parent, {$query: query, $selectedAs: selectedAs}) || ''; 924 925 scope.selectorPosition = options.newItem === 'prompt' ? false : 0; 926 927 if (!query && !selectedAs) { 928 scope.oldQuery = null; 929 } 930 931 if (values.$promise && !values.$resolved || angular.isFunction(values.then)) { 932 waitTime = options.debounce; 933 } 934 935 scope.showLoader = true; 936 937 return $q.when(values.$promise || values) 938 .then(function(values) { 939 scope.groups = {}; 940 941 if (values && !selectedAs) { 942 var outputValues = multiple ? scope.output : []; 943 var filteredList = listFilter(oiUtils.objToArr(values), query, getLabel, listFilterOptionsFn(scope.$parent), element); 944 var withoutIntersection = oiUtils.intersection(filteredList, outputValues, trackBy, trackBy, true); 945 var filteredOutput = filter(withoutIntersection); 946 947 scope.groups = group(filteredOutput); 948 } 949 updateGroupPos(); 950 951 return values; 952 }) 953 .finally(function(){ 954 scope.showLoader = false;
955 956 if (options.closeList && !options.cleanModel) { //case: prompt 957 $timeout(function() { 958 setOption(listElement, 0); 959 }); 960 } 961 }); 962 }, waitTime); 963 964 return timeoutPromise; 965 } 966 967 function updateGroupPos() { 968 var i, key, value, collectionKeys = [], groupCount = 0; 969 970 scope.order = []; 971 scope.groupPos = {}; 972 973 for (key in scope.groups) { 974 if (scope.groups.hasOwnProperty(key) && key.charAt(0) != '$') { 975 collectionKeys.push(key); 976 } 977 } 978 979 if (isOldAngular) { 980 collectionKeys.sort(); //TODO: Think of a way which does not depend on the order in which Angular displays objects by ngRepeat 981 } 982 983 for (i = 0; i < collectionKeys.length; i++) { 984 key = collectionKeys[i]; 985 value = scope.groups[key]; 986 987 scope.order = scope.order.concat(value); 988 scope.groupPos[key] = groupCount; 989 groupCount += value.length 990 } 991 } 992 993 function resetMatches(options) { 994 options = options || {}; 995 996 scope.oldQuery = null; 997 scope.backspaceFocus = false; // clears focus on any chosen item for del 998 scope.groups = {}; 999 scope.order = []; 1000 scope.showLoader = false;
1001 scope.isOpen = false; 1002 waitTime = 0; 1003 1004 if (!options.query) { 1005 scope.query = ''; 1006 } 1007 1008 if (timeoutPromise) { 1009 $timeout.cancel(timeoutPromise);//cancel previous timeout 1010 } 1011 } 1012 1013 function setOption(listElement, position) { 1014 scope.selectorPosition = position; 1015 oiUtils.scrollActiveOption(listElement[0], listElement.find('li')[position]); 1016 } 1017 1018 function group(input) { 1019 var optionGroups = {'':[]}, 1020 optionGroupName, 1021 optionGroup; 1022 1023 for (var i = 0; i < input.length; i++) { 1024 optionGroupName = getGroupName(input[i]); 1025 1026 if (!(optionGroup = optionGroups[optionGroupName])) { 1027 optionGroup = optionGroups[optionGroupName] = []; 1028 } 1029 optionGroup.push(input[i]); 1030 } 1031 1032 return optionGroups; 1033 } 1034 } 1035 } 1036 } 1037}]); 1038 1039angular.module('oi.select') 1040 1041.filter('oiSelectGroup', ['$sce', function($sce) { 1042 return function(label) { 1043 return $sce.trustAsHtml(label); 1044 }; 1045}]) 1046 1047.filter('oiSelectCloseIcon', ['$sce', function($sce) { 1048 return function(label) { 1049 var closeIcon = '<span class="close select-search-list-item_selection-remove">Ã</span>'; 1050 1051 return $sce.trustAsHtml(label + closeIcon); 1052 }; 1053}]) 1054 1055.filter('oiSelectHighlight', ['$sce', 'oiSelectEscape', function($sce, oiSelectEscape) { 1056 return function(label, query) { 1057 var html; 1058 1059 if (query.length > 0 || angular.isNumber(query)) { 1060 label = label.toString(); 1061 query = oiSelectEscape(query.toString()); 1062 1063 html = label.replace(new RegExp(query, 'gi'), '<strong>$&</strong>'); 1064 } else { 1065 html = label; 1066 } 1067 1068 return $sce.trustAsHtml(html); 1069 }; 1070}]) 1071 1072.filter('oiSelectAscSort', ['oiSelectEscape', function(oiSelectEscape) { 1073 function ascSort(input, query, getLabel, options) { 1074 var i, j, isFound, output, output1 = [], output2 = [], output3 = [], output4 = []; 1075 1076 if (query) { 1077 query = oiSelectEscape(String(query)); 1078 1079 for (i = 0, isFound = false; i < input.length; i++) { 1080 isFound = getLabel(input[i]).match(new RegExp(query, "i")); 1081 1082 if (!isFound && options && (options.length || options.fields)) { 1083 for (j = 0; j < options.length; j++) { 1084 if (isFound) break; 1085 1086 isFound = String(input[i][options[j]]).match(new RegExp(query, "i")); 1087 } 1088 } 1089 1090 if (isFound) { 1091 output1.push(input[i]); 1092 } 1093 } 1094 for (i = 0; i < output1.length; i++) { 1095 if (getLabel(output1[i]).match(new RegExp('^' + query, "i"))) { 1096 output2.push(output1[i]); 1097 } else { 1098 output3.push(output1[i]); 1099 } 1100 } 1101 output = output2.concat(output3); 1102 1103 if (options && (options === true || options.all)) { 1104 inputLabel: for (i = 0; i < input.length; i++) { 1105 for (j = 0; j < output.length; j++) { 1106 if (input[i] === output[j]) { 1107 continue inputLabel; 1108 } 1109 } 1110 output4.push(input[i]); 1111 } 1112 output = output.concat(output4); 1113 } 1114 } else { 1115 output = [].concat(input); 1116 } 1117 1118 return output; 1119 } 1120 1121 return ascSort; 1122}]) 1123 1124.filter('none', function() { 1125 return function(input) { 1126 return input; 1127 }; 1128}); 1129angular.module("oi.select").run(["$templateCache", function($templateCache) {$templateCache.put("src/template.html","<div class=select-search><ul class=select-search-list><li class=\"btn btn-default btn-xs select-search-list-item select-search-list-item_selection\" ng-hide=listItemHide ng-repeat=\"item in output track by $index\" ng-class=\"{focused: backspaceFocus && $last}\" ng-click=removeItem($index) ng-bind-html=getSearchLabel(item)></li><li class=\"select-search-list-item select-search-list-item_input\" ng-class=\"{\'select-search-list-item_hide\': inputHide}\"><input autocomplete=off ng-model=query ng-keyup=keyUp($event) ng-keydown=keyDown($event)></li><li class=\"select-search-list-item select-search-list-item_loader\" ng-show=showLoader></li></ul></div><div class=select-dropdown ng-show=isOpen><ul ng-if=isOpen class=select-dropdown-optgroup ng-repeat=\"(group, options) in groups\"><div class=select-dropdown-optgroup-header ng-if=\"group && options.length\" ng-bind-html=\"getGroupLabel(group, options)\"></div><li class=select-dropdown-optgroup-option ng-init=\"isDisabled = getDisableWhen(option)\" ng-repeat=\"option in options\" ng-class=\"{\'active\': selectorPosition === groupPos[group] + $index, \'disabled\': isDisabled, \'ungroup\': !group}\" ng-click=\"isDisabled || addItem(option)\" ng-mouseenter=\"setSelection(groupPos[group] + $index)\" ng-bind-html=getDropdownLabel(option)></li></ul></div>");}]);
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.