PageSourceSearch

https://rekrutacja.pedagogium.pl/pdg-irk-app/content/script/oi.select/select-tpls.js

js pedagogium.pl collected 2026-10-04 13:33:29 UTC 45,366 bytes, 1,129 lines download raw bytes

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.