source : gazAutoComplete.js

(function (angular) {
    'use strict';
    /**
     * @memberof spApp
     * @ngdoc directive
     * @name gazAutoComplete
     * @description
     *   Gazetteer autocomplete
     */
    angular.module('gaz-auto-complete-directive', ['gaz-auto-complete-service']).directive('gazAutoComplete',
        ['$timeout', 'GazAutoCompleteService', function ($timeout, GazAutoCompleteService) {
            return {
                scope: {
                    _userobjects: '=',
                    _custom: '&onCustom',
                },
                link: function (scope, iElement, iAttrs) {
                    var gaz_selected = false;
                    var limit = 10;
                    var q = {};
                    var historicResults = [];
                    var hasMore = true;
                    var currentPos = 0; // Remember add more button position to let dropdown list scroll down to view
                    var a = iElement.autocomplete({
                        minLength: 2,
                        delay: 500,
                        source: function (searchTerm, response) {
                            // only search term if search is triggered by input box, like 'vic'
                            // q=vic&start=0, if triggered by 'Add more'
                            var urlParams = new URLSearchParams(searchTerm.term);
                            if (urlParams.has('q') && urlParams.has('start')) {
                                q.q = urlParams.get('q');
                                q.start = urlParams.get('start');
                                q.limit = limit;
                                q.include = urlParams.get('include');
                            } else {
                                q.start = 0;
                                q.q = searchTerm.term;
                                q.limit = limit;
                                q.include = undefined;
                            }
                            GazAutoCompleteService.search(q).then(function (data) {
                                response(
                                    $.map(data, function (item) {
                                        if (item.fid !== $SH.userObjectsField || scope._userobjects) {
                                            return {
                                                label: item.name,
                                                info: item.description ? item.description + " (" + item.fieldname + ")" : "(" + item.fieldname + ")",
                                                fieldname: item.fieldname,
                                                fid: item.fid,
                                                value: item
                                            }
                                        }else {
                                            return null
                                        }
                                    })
                                )
                            });
                        },
                        response: function (event, ui) {
                            var data = ui.content;
                            if (data.length < limit)
                                hasMore = false;
                            else
                                hasMore = true;
                            if (q.start > 0) {
                                //reverse history records and then push them at FRONT
                                historicResults.reverse().forEach(function (item) {
                                    data.unshift(item)
                                })
                            }
                            //update historic result
                            historicResults = data.slice();
                            //All related fields will be attached with result if query does not limit search on field
                            if (q.include) {
                                // Get distinct field and fidx
                                var fids = data.map(function (item) {
                                    return item.fid;
                                })
                                var fields = data.filter(function (value, index, final) {
                                    return fids.indexOf(value.fid) == index;
                                }).map(function (item) {
                                    return {label: item.fieldname, fid: item.fid, isField: true}
                                });
                            } else {
                                var fields = _.map(JSON.parse(data[0].value.fields), function (field) {
                                    var fs = field.split('|');
                                    return {label: fs[1], fid: fs[0], isField: true}
                                });
                            };
                            fields.forEach(function (item) {
                                data.unshift(item);
                            });
                        },
                        select: function (event, ui) {
                            //click on category to show all fields or part
                            gaz_selected = false;
                            if (ui.item.isCategory) {
                                if (ui.item.isExpanded) {
                                    $(event.currentTarget).first().find("li[isField]").hide();
                                    $(event.currentTarget).first().find("li[expandedFieldFilter]").hide()
                                    $(event.currentTarget).first().find("li[expandedFieldFilter=false]").show()
                                } else {
                                    $(event.currentTarget).first().find("li[isField]").show();
                                    $(event.currentTarget).first().find("li[expandedFieldFilter]").show()
                                    $(event.currentTarget).first().find("li[expandedFieldFilter=false]").hide()
                                }
                                return false;
                            } else if (ui.item.isField) {  //check on filter checkbox
                                // click on radio button or label or outside label
                                event.stopPropagation();
                                event.preventDefault();
                                //check q.include, if it is same as ui.item.fid,
                                //it clicks on a checked item  -> uncheck it
                                if (q.include === ui.item.fid)
                                    var term = "q=" + q.q + "&start=0";
                                else
                                    var term = "q=" + q.q + "&start=0&include=" + ui.item.fid;
                                //Have to put the trigger method into setTimeout make  _renderItem working
                                setTimeout(function () {
                                    $('#gazAutoComplete').autocomplete('search', term);
                                }, 0);
                                return false;
                            } else if (ui.item.showMore) {
                                q.start = parseInt(q.start) + limit;
                                var term = 'q=' + q.q + "&start=" + q.start
                                if (q.include)
                                    term += '&include=' + q.include;
                                event.stopPropagation();
                                event.preventDefault();
                                //Have to put the trigger method into setTimeout make  _renderItem working
                                setTimeout(function () {
                                    $('#gazAutoComplete').autocomplete('search', term);
                                }, 0);
                                return false;
                            } else {
                                scope._custom()(ui.item.value.pid);
                                scope.label = ui.item.label;
                                gaz_selected = true;
                                $timeout(function () {
                                    iElement.val(scope.label);
                                }, 0)
                            }
                        },
                        close: function (event, ui) {
                            // This function fires after select: and after autocomplete has already "closed" everything.  This is why event.preventDefault() won't work.
                            // ** ui is an empty object here so we have to use our own variable to check if the selected item is "selectable" or not..
                            if (!gaz_selected && event.currentTarget) {
                                // We need to undo what autocomplete has already done..
                                $('#' + event.currentTarget.id).show(); // Keep the selection window open
                                // ta-da!  To the end user, nothing changes when clicking on an item that was not selectable.
                            }
                        },
                        open: function (event, ui) {
                            $('.ui-autocomplete').css('height', 'auto');
                            $('.ui-autocomplete').css('width', 'auto');
                            //Get some values needed to determine whether the widget is on
                            //the screen
                            var $input = $(event.target),
                                inputTop = $input.offset().top,
                                inputHeight = $input.height(),
                                autocompleteHeight = $('.ui-autocomplete').height(),
                                windowHeight = $(window).height();
                            //The widget has left the screen if the input's height plus it's offset from the top of
                            //the screen, plus the height of the autocomplete are greater than the height of the
                            //window.
                            if ((inputHeight + inputTop + autocompleteHeight) > windowHeight) {
                                //Set the new height of the autocomplete to the height of the window, minus the
                                //height of the input and the offset of the input from the top of the screen.  The
                                //20 is simply there to give some spacing between the bottom of the screen and the
                                //bottom of the autocomplete widget.
                                $('.ui-autocomplete')
                                    .css('max-height', (windowHeight - inputHeight - inputTop - 20) + 'px');
                                $('.ui-autocomplete').css('overflow-y', 'auto');
                            }
                        }
                    })
                    a.data("ui-autocomplete")._renderItem = function (ul, item) {
                        if (item.isField) {
                            if (item.fid === q.include) // field has been checked
                                var html = "<label><span class='glyphicon glyphicon-check'></span>" + item.label + "</label>";
                            else
                                var html = "<label><span class='glyphicon glyphicon-unchecked'></span>" + item.label + "</label>";
                            return $("<li class='autocomplete-item' isField >")
                                .append($("<a>").append(html))
                            //.appendTo(ul);  //Let renderMenu to control
                        } else if (item.isCategory) {
                            if (item.isExpanded)
                                return $("<li class='autocomplete-item' style='text-align:center' expandedFieldFilter=true>")
                                    .append($("<a>").append("<label>" + item.label + "</label> <span class='glyphicon glyphicon-menu-up'></span>"))
                            else
                                return $("<li class='autocomplete-item' style='text-align:center' expandedFieldFilter=false>")
                                    .append($("<a>").append("<label>" + item.label + "</label> <span class='glyphicon glyphicon-menu-down'></span>"))
                        }
                        else if (item.showMore) {
                            return $("<li class='autocomplete-item' style='text-align:center'  showMore>")
                                .append($("<a>").append(" <label>" + item.label + "</label>  <span class='glyphicon glyphicon-menu-down'></span></a>"))
                        }
                        else {
                            return $("<li field=" + item.fid + " class='autocomplete-item'>")
                                .append($("<a>").append(item.label + "<br><i>" + item.info + "</i>"))
                            //.appendTo(ul);
                        }
                    };
                    a.data("ui-autocomplete")._renderMenu = function (ul, items) {
                        var that = this;
                        var isExpanded = true;
                        var numOfFilters = items.filter(function (item) {
                            return item.isField
                        }).length;
                        if (q.include) { // One field is checked, expand field files
                            isExpanded = true;
                        }
                        if (numOfFilters > 1)
                            if (isExpanded) {
                                ul.append(that._renderItemData(ul, {
                                    label: 'Show ' + numOfFilters + ' filters',
                                    isCategory: true,
                                    isExpanded: false
                                }).hide())
                                ul.append(that._renderItemData(ul, {
                                    label: 'Hide filters',
                                    isCategory: true,
                                    isExpanded: true
                                }).show())
                            } else {
                                ul.append(that._renderItemData(ul, {
                                    label: 'Show ' + numOfFilters + ' filters',
                                    isCategory: true,
                                    isExpanded: false
                                }).show())
                                ul.append(that._renderItemData(ul, {
                                    label: 'Hide filters',
                                    isCategory: true,
                                    isExpanded: true
                                }).hide())
                            }
                        $.each(items, function (index, item) {
                            if (item.isField) {
                                if (isExpanded)
                                    ul.append(that._renderItemData(ul, item));
                                else
                                    ul.append(that._renderItemData(ul, item).hide());
                            } else
                                ul.append(that._renderItemData(ul, item));
                        });
                        if (hasMore)
                            ul.append(that._renderItemData(ul, {label: 'Show more', showMore: true}))
                    }
                }
            };
        }])
}(angular));