Last active
December 27, 2015 03:09
-
-
Save tkh44/7257222 to your computer and use it in GitHub Desktop.
Small AutoComplete Library for doing live searches using Backbone collections and JST templates. It uses a basic localStorage cache to try to reduce requests. We use hammer.js for the 'tap' event since we use this is mostly for mobile.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;(function($, window, document, undefined) { | |
| var pluginName = 'AutoComplete', | |
| defaults = { | |
| list: '.auto-complete-list', | |
| cancel: '.auto-complete-cancel', | |
| itemSelector: '.auto-complete-item', | |
| dataId: 'auto-val-set', | |
| valueAttr: 'name', | |
| valueId: 'id', | |
| collection: new Backbone.Collection(), | |
| /** | |
| * Function to build out our data sent in the $.ajax request | |
| * @returns {Object} | |
| */ | |
| buildDataFn: $.noop, | |
| // Name of the JST template to be appended | |
| resultItemTemplate: 'autocomplete-item', | |
| // Function to be called when there are no results | |
| emptyFn: $.noop | |
| }, | |
| ls = window.localStorage; | |
| function AutoComplete(element, options) { | |
| this.options = _.defaults(options, defaults); | |
| this.$input = $(element); | |
| this.$list = $(options.list); | |
| this.$cancel = $(options.cancel); | |
| this.$otherInputs = this.$input.closest('form').find('input, select, textarea').not(this.$input); | |
| this._name = this.options.name || this.$input.attr('name') || ''; | |
| this._previousSearch = ''; | |
| this._ttl = ''; | |
| this.init(); | |
| } | |
| AutoComplete.prototype = { | |
| /** | |
| * Entry Point | |
| */ | |
| init: function() { | |
| var functions = _.functions(this); | |
| functions.unshift(this); | |
| _.bindAll.apply(_, functions); | |
| this.setupEvents(); | |
| }, | |
| /** | |
| * Setup and attach event handlers | |
| */ | |
| setupEvents: function() { | |
| this.$input.on('keyup', this.liveSearch); | |
| this.$input.on('focus blur', this.setupLiveSearch); | |
| this.$otherInputs.on('focus', this.setupLiveSearch); | |
| this.$cancel.on('tap', this.resetAutoComplete); | |
| this.$list.delegate(this.options.itemSelector, 'tap', this.fillInputValue); | |
| }, | |
| /** | |
| * Handle events | |
| * Show/Hide the auto complete list and cancel button | |
| * @param e | |
| */ | |
| setupLiveSearch: function(e) { | |
| // If we are focusing out to another form element hide the autocomplete list | |
| if (this.$otherInputs.is(':focus') || (e.type === "blur" && !this.$input.is(':focus'))) { | |
| this.hideAutoCompleteList(); | |
| } else { | |
| // We let the webview do its focus/unfocus thing before we mess with the DOM | |
| // This seems to help mitigate ui jerks | |
| this.updateListStyle(this.$input, this.$list); | |
| this.$list.show(); | |
| } | |
| }, | |
| /** | |
| * Fill the input when a list item is tapped | |
| * @param e | |
| */ | |
| fillInputValue: function(e) { | |
| var $target = $(e.currentTarget); | |
| this.$input | |
| .val($target.data(this.options.valueAttr)) | |
| .data(this.options.dataId, this.$input.data(this.options.valueId)); | |
| this.$list.hide(); | |
| }, | |
| /** | |
| * Reset AutoComplete back to base state | |
| */ | |
| resetAutoComplete: function() { | |
| this.$input.val('').data(this.options.dataId, ''); | |
| this.$cancel.hide(); | |
| this.hideAutoCompleteList(); | |
| }, | |
| /** | |
| * Hide the AutoComplete list | |
| */ | |
| hideAutoCompleteList: function() { | |
| // Reset the collection so we don't have stale data | |
| this.$list | |
| .hide() | |
| .empty(); | |
| }, | |
| /** | |
| * Set the max-height of the AutoComplete list so that it stays scrollable | |
| * and viewable on screen | |
| * | |
| * @param $input | |
| * @param $list | |
| */ | |
| updateListStyle: function($input, $list) { | |
| var scrollTop = $input.offset().top, | |
| windowHeight = window.innerHeight; | |
| $list.css({ | |
| 'max-height': (windowHeight - (scrollTop + $input.outerHeight(true))) + 'px' | |
| }); | |
| }, | |
| liveSearch: _.debounce(function() { | |
| var self = this, | |
| $input = this.$input, | |
| $list = this.$list, | |
| $cancel = this.$cancel, | |
| value = $.trim($input.val()).toLowerCase(), | |
| listCollection = this.options.collection, | |
| data; | |
| // Prevent search firing after debounce if the input field is cleared (backspace, 'x' button) | |
| if (value.length < 1) { | |
| $list.hide(); | |
| $cancel.hide(); | |
| return; | |
| } | |
| /** | |
| * Success callback for our live search call | |
| * We have to pull some shenanigans to keep collection in order as defined by its | |
| * comparator function | |
| * | |
| * @param collection | |
| * @param res | |
| * @param options | |
| */ | |
| function successCallback(collection, res, options) { | |
| buildList(res, options.previousModels); | |
| } | |
| function errorCallback(col, res, options) { | |
| console.log('error'); | |
| $input.removeClass('input-loading'); | |
| self.$cancel.show(); | |
| } | |
| function buildList(list, previousModels) { | |
| if (previousModels) { | |
| listCollection.remove(previousModels); | |
| _.each(previousModels, self.autoCompleteRemove); | |
| } else { | |
| listCollection.reset([]); | |
| self.$list.empty(); | |
| } | |
| if (list.length) { | |
| listCollection.add(list); | |
| listCollection.each(self.autoCompleteAppend); | |
| } else { | |
| self.showEmptySearch(); | |
| } | |
| self._previousSearch = value; | |
| self.setObject(self.prefix('query'), list); | |
| $input.removeClass('input-loading'); | |
| self.$cancel.show(); | |
| } | |
| function searchRooms() { | |
| data = self.options.buildDataFn(); | |
| listCollection.fetch({ | |
| data: data, | |
| reset: true, | |
| success: successCallback, | |
| error: errorCallback | |
| }); | |
| } | |
| self.$cancel.hide(); | |
| $input.addClass('input-loading'); | |
| if (!this.isExpired() && self.keyExists(value)) { | |
| buildList(this.getObject(self.prefix('query'))); | |
| } else { | |
| searchRooms(); | |
| } | |
| }, 500), | |
| /** | |
| * Remove and item from the list using the data-id attribute | |
| * @param item | |
| */ | |
| autoCompleteRemove: function(item) { | |
| var id = item.get('id'); | |
| $('[data-id=' + id + ']', this.$list).remove(); | |
| }, | |
| /** | |
| * Append and item from the list using the data-id attribute | |
| * @param item | |
| */ | |
| autoCompleteAppend: function(item) { | |
| var $list = this.$list, | |
| template = this.options.resultItemTemplate, | |
| html = JST[template](item.toJSON()); | |
| $list | |
| .append(html) | |
| .show(); | |
| }, | |
| /** | |
| * Callback function to be called when there is no search results | |
| */ | |
| showEmptySearch: function() { | |
| console.log('no search results'); | |
| if (this.options.emptyFn && _.isFunction(this.options.emptyFn)) { | |
| this.options.emptyFn.apply(this); | |
| } | |
| }, | |
| /************************************* | |
| * Local Storage utilities for cache | |
| *************************************/ | |
| /** | |
| * prefix local storage key with our namespace | |
| * @param key | |
| * @returns {string} | |
| */ | |
| prefix: function(key) { | |
| return "__" + this._name + "__" + key; | |
| }, | |
| /** | |
| * Get the current time in milliseconds | |
| * @returns {number} | |
| */ | |
| now: function() { | |
| return new Date().getTime(); | |
| }, | |
| /** | |
| * Check and see if our value is older than 24 hours | |
| * @returns {boolean} | |
| */ | |
| isExpired: function() { | |
| var ttl = ls.getItem(this.prefix('ttl')); | |
| return _.isFinite(ttl) && this.now() > ttl ? true : false; | |
| }, | |
| keyExists: function(value) { | |
| return (value === this._previousSearch.toLowerCase()) | |
| && (this.getObject(this.prefix('query')) !== null); | |
| }, | |
| /** | |
| * Thin wrapper to allow storage of objects in local storage | |
| * **Value must be object with strings as keys!** | |
| * @param key | |
| * @param value | |
| */ | |
| setObject: function(key, value) { | |
| ls.setItem(this.prefix('ttl'), this.now() + 24 * 60 * 60 * 1e3 ); // 24hours | |
| ls.setItem(key, JSON.stringify(value)); | |
| }, | |
| /** | |
| * Thin wrapper to allow retrieval of objects(json) from local storage | |
| * @param key | |
| * @returns {object} | |
| */ | |
| getObject: function(key) { | |
| var value = ls.getItem(key); | |
| return value && JSON.parse(value); | |
| } | |
| }; | |
| /** | |
| * jQuery plugin definition | |
| * @param options | |
| * @returns {*} | |
| */ | |
| $.fn[pluginName] = function(options) { | |
| return this.each(function() { | |
| if (!$.data(this, 'plugin_' + pluginName)) { | |
| $.data(this, 'plugin_' + pluginName, new AutoComplete(this, options)); | |
| } | |
| }); | |
| } | |
| })(jQuery, window, document); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment