451 lines
12 KiB
JavaScript
451 lines
12 KiB
JavaScript
|
(function($) {
|
||
|
'use strict';
|
||
|
|
||
|
let _defaults = {
|
||
|
data: {}, // Autocomplete data set
|
||
|
limit: Infinity, // Limit of results the autocomplete shows
|
||
|
onAutocomplete: null, // Callback for when autocompleted
|
||
|
minLength: 1, // Min characters before autocomplete starts
|
||
|
sortFunction: function(a, b, inputString) {
|
||
|
// Sort function for sorting autocomplete results
|
||
|
return a.indexOf(inputString) - b.indexOf(inputString);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @class
|
||
|
*
|
||
|
*/
|
||
|
class Autocomplete extends Component {
|
||
|
/**
|
||
|
* Construct Autocomplete instance
|
||
|
* @constructor
|
||
|
* @param {Element} el
|
||
|
* @param {Object} options
|
||
|
*/
|
||
|
constructor(el, options) {
|
||
|
super(Autocomplete, el, options);
|
||
|
|
||
|
this.el.M_Autocomplete = this;
|
||
|
|
||
|
/**
|
||
|
* Options for the autocomplete
|
||
|
* @member Autocomplete#options
|
||
|
* @prop {Number} duration
|
||
|
* @prop {Number} dist
|
||
|
* @prop {number} shift
|
||
|
* @prop {number} padding
|
||
|
* @prop {Boolean} fullWidth
|
||
|
* @prop {Boolean} indicators
|
||
|
* @prop {Boolean} noWrap
|
||
|
* @prop {Function} onCycleTo
|
||
|
*/
|
||
|
this.options = $.extend({}, Autocomplete.defaults, options);
|
||
|
|
||
|
// Setup
|
||
|
this.isOpen = false;
|
||
|
this.count = 0;
|
||
|
this.activeIndex = -1;
|
||
|
this.oldVal;
|
||
|
this.$inputField = this.$el.closest('.input-field');
|
||
|
this.$active = $();
|
||
|
this._mousedown = false;
|
||
|
this._setupDropdown();
|
||
|
|
||
|
this._setupEventHandlers();
|
||
|
}
|
||
|
|
||
|
static get defaults() {
|
||
|
return _defaults;
|
||
|
}
|
||
|
|
||
|
static init(els, options) {
|
||
|
return super.init(this, els, options);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get Instance
|
||
|
*/
|
||
|
static getInstance(el) {
|
||
|
let domElem = !!el.jquery ? el[0] : el;
|
||
|
return domElem.M_Autocomplete;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Teardown component
|
||
|
*/
|
||
|
destroy() {
|
||
|
this._removeEventHandlers();
|
||
|
this._removeDropdown();
|
||
|
this.el.M_Autocomplete = undefined;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Setup Event Handlers
|
||
|
*/
|
||
|
_setupEventHandlers() {
|
||
|
this._handleInputBlurBound = this._handleInputBlur.bind(this);
|
||
|
this._handleInputKeyupAndFocusBound = this._handleInputKeyupAndFocus.bind(this);
|
||
|
this._handleInputKeydownBound = this._handleInputKeydown.bind(this);
|
||
|
this._handleInputClickBound = this._handleInputClick.bind(this);
|
||
|
this._handleContainerMousedownAndTouchstartBound = this._handleContainerMousedownAndTouchstart.bind(
|
||
|
this
|
||
|
);
|
||
|
this._handleContainerMouseupAndTouchendBound = this._handleContainerMouseupAndTouchend.bind(
|
||
|
this
|
||
|
);
|
||
|
|
||
|
this.el.addEventListener('blur', this._handleInputBlurBound);
|
||
|
this.el.addEventListener('keyup', this._handleInputKeyupAndFocusBound);
|
||
|
this.el.addEventListener('focus', this._handleInputKeyupAndFocusBound);
|
||
|
this.el.addEventListener('keydown', this._handleInputKeydownBound);
|
||
|
this.el.addEventListener('click', this._handleInputClickBound);
|
||
|
this.container.addEventListener(
|
||
|
'mousedown',
|
||
|
this._handleContainerMousedownAndTouchstartBound
|
||
|
);
|
||
|
this.container.addEventListener('mouseup', this._handleContainerMouseupAndTouchendBound);
|
||
|
|
||
|
if (typeof window.ontouchstart !== 'undefined') {
|
||
|
this.container.addEventListener(
|
||
|
'touchstart',
|
||
|
this._handleContainerMousedownAndTouchstartBound
|
||
|
);
|
||
|
this.container.addEventListener('touchend', this._handleContainerMouseupAndTouchendBound);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove Event Handlers
|
||
|
*/
|
||
|
_removeEventHandlers() {
|
||
|
this.el.removeEventListener('blur', this._handleInputBlurBound);
|
||
|
this.el.removeEventListener('keyup', this._handleInputKeyupAndFocusBound);
|
||
|
this.el.removeEventListener('focus', this._handleInputKeyupAndFocusBound);
|
||
|
this.el.removeEventListener('keydown', this._handleInputKeydownBound);
|
||
|
this.el.removeEventListener('click', this._handleInputClickBound);
|
||
|
this.container.removeEventListener(
|
||
|
'mousedown',
|
||
|
this._handleContainerMousedownAndTouchstartBound
|
||
|
);
|
||
|
this.container.removeEventListener('mouseup', this._handleContainerMouseupAndTouchendBound);
|
||
|
|
||
|
if (typeof window.ontouchstart !== 'undefined') {
|
||
|
this.container.removeEventListener(
|
||
|
'touchstart',
|
||
|
this._handleContainerMousedownAndTouchstartBound
|
||
|
);
|
||
|
this.container.removeEventListener(
|
||
|
'touchend',
|
||
|
this._handleContainerMouseupAndTouchendBound
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Setup dropdown
|
||
|
*/
|
||
|
_setupDropdown() {
|
||
|
this.container = document.createElement('ul');
|
||
|
this.container.id = `autocomplete-options-${M.guid()}`;
|
||
|
$(this.container).addClass('autocomplete-content dropdown-content');
|
||
|
this.$inputField.append(this.container);
|
||
|
this.el.setAttribute('data-target', this.container.id);
|
||
|
|
||
|
this.dropdown = M.Dropdown.init(this.el, {
|
||
|
autoFocus: false,
|
||
|
closeOnClick: false,
|
||
|
coverTrigger: false,
|
||
|
onItemClick: (itemEl) => {
|
||
|
this.selectOption($(itemEl));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Sketchy removal of dropdown click handler
|
||
|
this.el.removeEventListener('click', this.dropdown._handleClickBound);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove dropdown
|
||
|
*/
|
||
|
_removeDropdown() {
|
||
|
this.container.parentNode.removeChild(this.container);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Input Blur
|
||
|
*/
|
||
|
_handleInputBlur() {
|
||
|
if (!this._mousedown) {
|
||
|
this.close();
|
||
|
this._resetAutocomplete();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Input Keyup and Focus
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleInputKeyupAndFocus(e) {
|
||
|
if (e.type === 'keyup') {
|
||
|
Autocomplete._keydown = false;
|
||
|
}
|
||
|
|
||
|
this.count = 0;
|
||
|
let val = this.el.value.toLowerCase();
|
||
|
|
||
|
// Don't capture enter or arrow key usage.
|
||
|
if (e.keyCode === 13 || e.keyCode === 38 || e.keyCode === 40) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Check if the input isn't empty
|
||
|
// Check if focus triggered by tab
|
||
|
if (this.oldVal !== val && (M.tabPressed || e.type !== 'focus')) {
|
||
|
this.open();
|
||
|
}
|
||
|
|
||
|
// Update oldVal
|
||
|
this.oldVal = val;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Input Keydown
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleInputKeydown(e) {
|
||
|
Autocomplete._keydown = true;
|
||
|
|
||
|
// Arrow keys and enter key usage
|
||
|
let keyCode = e.keyCode,
|
||
|
liElement,
|
||
|
numItems = $(this.container).children('li').length;
|
||
|
|
||
|
// select element on Enter
|
||
|
if (keyCode === M.keys.ENTER && this.activeIndex >= 0) {
|
||
|
liElement = $(this.container)
|
||
|
.children('li')
|
||
|
.eq(this.activeIndex);
|
||
|
if (liElement.length) {
|
||
|
this.selectOption(liElement);
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Capture up and down key
|
||
|
if (keyCode === M.keys.ARROW_UP || keyCode === M.keys.ARROW_DOWN) {
|
||
|
e.preventDefault();
|
||
|
|
||
|
if (keyCode === M.keys.ARROW_UP && this.activeIndex > 0) {
|
||
|
this.activeIndex--;
|
||
|
}
|
||
|
|
||
|
if (keyCode === M.keys.ARROW_DOWN && this.activeIndex < numItems - 1) {
|
||
|
this.activeIndex++;
|
||
|
}
|
||
|
|
||
|
this.$active.removeClass('active');
|
||
|
if (this.activeIndex >= 0) {
|
||
|
this.$active = $(this.container)
|
||
|
.children('li')
|
||
|
.eq(this.activeIndex);
|
||
|
this.$active.addClass('active');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Input Click
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleInputClick(e) {
|
||
|
this.open();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Container Mousedown and Touchstart
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleContainerMousedownAndTouchstart(e) {
|
||
|
this._mousedown = true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Container Mouseup and Touchend
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleContainerMouseupAndTouchend(e) {
|
||
|
this._mousedown = false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Highlight partial match
|
||
|
*/
|
||
|
_highlight(string, $el) {
|
||
|
let img = $el.find('img');
|
||
|
let matchStart = $el
|
||
|
.text()
|
||
|
.toLowerCase()
|
||
|
.indexOf('' + string.toLowerCase() + ''),
|
||
|
matchEnd = matchStart + string.length - 1,
|
||
|
beforeMatch = $el.text().slice(0, matchStart),
|
||
|
matchText = $el.text().slice(matchStart, matchEnd + 1),
|
||
|
afterMatch = $el.text().slice(matchEnd + 1);
|
||
|
$el.html(
|
||
|
`<span>${beforeMatch}<span class='highlight'>${matchText}</span>${afterMatch}</span>`
|
||
|
);
|
||
|
if (img.length) {
|
||
|
$el.prepend(img);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reset current element position
|
||
|
*/
|
||
|
_resetCurrentElement() {
|
||
|
this.activeIndex = -1;
|
||
|
this.$active.removeClass('active');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Reset autocomplete elements
|
||
|
*/
|
||
|
_resetAutocomplete() {
|
||
|
$(this.container).empty();
|
||
|
this._resetCurrentElement();
|
||
|
this.oldVal = null;
|
||
|
this.isOpen = false;
|
||
|
this._mousedown = false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Select autocomplete option
|
||
|
* @param {Element} el Autocomplete option list item element
|
||
|
*/
|
||
|
selectOption(el) {
|
||
|
let text = el.text().trim();
|
||
|
this.el.value = text;
|
||
|
this.$el.trigger('change');
|
||
|
this._resetAutocomplete();
|
||
|
this.close();
|
||
|
|
||
|
// Handle onAutocomplete callback.
|
||
|
if (typeof this.options.onAutocomplete === 'function') {
|
||
|
this.options.onAutocomplete.call(this, text);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render dropdown content
|
||
|
* @param {Object} data data set
|
||
|
* @param {String} val current input value
|
||
|
*/
|
||
|
_renderDropdown(data, val) {
|
||
|
this._resetAutocomplete();
|
||
|
|
||
|
let matchingData = [];
|
||
|
|
||
|
// Gather all matching data
|
||
|
for (let key in data) {
|
||
|
if (data.hasOwnProperty(key) && key.toLowerCase().indexOf(val) !== -1) {
|
||
|
// Break if past limit
|
||
|
if (this.count >= this.options.limit) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
let entry = {
|
||
|
data: data[key],
|
||
|
key: key
|
||
|
};
|
||
|
matchingData.push(entry);
|
||
|
|
||
|
this.count++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Sort
|
||
|
if (this.options.sortFunction) {
|
||
|
let sortFunctionBound = (a, b) => {
|
||
|
return this.options.sortFunction(
|
||
|
a.key.toLowerCase(),
|
||
|
b.key.toLowerCase(),
|
||
|
val.toLowerCase()
|
||
|
);
|
||
|
};
|
||
|
matchingData.sort(sortFunctionBound);
|
||
|
}
|
||
|
|
||
|
// Render
|
||
|
for (let i = 0; i < matchingData.length; i++) {
|
||
|
let entry = matchingData[i];
|
||
|
let $autocompleteOption = $('<li></li>');
|
||
|
if (!!entry.data) {
|
||
|
$autocompleteOption.append(
|
||
|
`<img src="${entry.data}" class="right circle"><span>${entry.key}</span>`
|
||
|
);
|
||
|
} else {
|
||
|
$autocompleteOption.append('<span>' + entry.key + '</span>');
|
||
|
}
|
||
|
|
||
|
$(this.container).append($autocompleteOption);
|
||
|
this._highlight(val, $autocompleteOption);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Open Autocomplete Dropdown
|
||
|
*/
|
||
|
open() {
|
||
|
let val = this.el.value.toLowerCase();
|
||
|
|
||
|
this._resetAutocomplete();
|
||
|
|
||
|
if (val.length >= this.options.minLength) {
|
||
|
this.isOpen = true;
|
||
|
this._renderDropdown(this.options.data, val);
|
||
|
}
|
||
|
|
||
|
// Open dropdown
|
||
|
if (!this.dropdown.isOpen) {
|
||
|
this.dropdown.open();
|
||
|
} else {
|
||
|
// Recalculate dropdown when its already open
|
||
|
this.dropdown.recalculateDimensions();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Close Autocomplete Dropdown
|
||
|
*/
|
||
|
close() {
|
||
|
this.dropdown.close();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Update Data
|
||
|
* @param {Object} data
|
||
|
*/
|
||
|
updateData(data) {
|
||
|
let val = this.el.value.toLowerCase();
|
||
|
this.options.data = data;
|
||
|
|
||
|
if (this.isOpen) {
|
||
|
this._renderDropdown(data, val);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @static
|
||
|
* @memberof Autocomplete
|
||
|
*/
|
||
|
Autocomplete._keydown = false;
|
||
|
|
||
|
M.Autocomplete = Autocomplete;
|
||
|
|
||
|
if (M.jQueryLoaded) {
|
||
|
M.initializeJqueryWrapper(Autocomplete, 'autocomplete', 'M_Autocomplete');
|
||
|
}
|
||
|
})(cash);
|