Option name | Type | Description |
---|---|---|
el | Element | |
params | Object |
Typeahead constructor
function Typeahead(el, params) {
if (!el) {
return;
}
this._setParams(this.defaults, true);
this._setParams(params || {});
this._cacheElements(el);
this._maintainFocus(function() {
this._parseParams();
this._bindEventListenerCallbacks();
this._addEventListeners();
});
}
Typeahead.prototype = {
Include common functionality.
_setParams: Base.setParams,
_triggerEvent: Base.triggerEvent,
remove: Base.remove,
Whitelisted parameters which can be set on construction.
_whitelistedParams: ['actionCodes', 'format', 'placeholder', 'matchPlaceholderSize', 'onChange', 'onFocus', 'onBlur', 'onBackspace', 'onEnd'],
Default values for internal properties we will be setting.
These are set on each construction so we don't leak properties
into the prototype chain.
defaults: {
el: null,
inputEl: null,
placeholderEl: null,
placeholder: null,
characters: null,
format: null,
ignoreCodes: [
9, // Tab
16, // Shift
17, // Ctrl
18, // Alt
20, // CAPS
91, // Meta
93, // Alt
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123 // F1-F12
],
actionCodes: {
BACKSPACE: 8,
DELETE: 46,
LEFT: 37,
RIGHT: 39
},
pasteCode: 86, // v
pauseBlurFocus: 0,
matchPlaceholderSize: false,
isFocused: false,
isRunning: false,
onChange: null,
onFocus: null,
onBlur: null,
onBackspace: null,
onEnd: null,
_atEnd: false,
_oldVal: null,
_onInputBound: null,
_onKeydownBound: null,
_onFocusBound: null,
_onBlurBound: null,
_onPlaceholderClickBound: null
},
Option name | Type | Description |
---|---|---|
cursorIndex | Number | |
params | Object |
Run the formatting. @todo: rename this.
run: function(cursorIndex, params) {
params = params || {};
if (this.isRunning) return;
this.isRunning = true;
var oldVal = this.inputEl.value;
var val = '';
var placeholder = '';
var i = 0;
var len = this.format.length;
var skipCount = 0;
var valDone = false;
for (; i < len; i++) {
// Add numbers
if (this.format[i] === '\\d') {
if (this.characters[i - skipCount]) {
val += this.characters[i - skipCount];
}
else {
valDone = true;
}
placeholder += valDone ? this.placeholder[i] : this.characters[i - skipCount];
}
// Placeholder characters
else {
if (!valDone) {
val += this.format[i];
}
placeholder += this.format[i];
skipCount++;
}
}
if (this.isFocused) {
cursorIndex = cursorIndex === undefined ? this._getCaretEnd() : cursorIndex;
}
// If there are no characters, set the cursorIndex to be the last placeholder entry.
if (this.isFocused && !this.characters.length) {
cursorIndex = val.length;
}
// No characters and we shouldn't use just placeholder values
if (!this.characters.length && params.notOnlyPlaceholders) {
val = '';
}
this.inputEl.value = val;
this.placeholderEl.innerHTML = placeholder;
this._updateWidth();
if (this.isFocused) {
this._setCaretPositionTranslated(cursorIndex);
}
if (val !== oldVal) {
this._triggerEvent(this.inputEl, 'input');
}
this.isRunning = false;
if (val !== oldVal) {
(this.onChange || noop)(val, oldVal, this.inputEl);
}
if (!this._atEnd && this.isFocused && this.characters.length === this.maxLength && this._caretIsAtEnd()) {
this._atEnd = true;
(this.onEnd || noop)(this);
} else {
this._atEnd = false;
}
},
Option name | Type | Description |
---|---|---|
character | String | |
start | Number | |
end | Number | |
skipCheck | Boolean |
Add a character to the characters array at a given index.
addCharacterAtIndex: function(character, start, end, skipCheck) {
// Don't add at an index beyond what we can support.
if (this.maxLength && start >= this.maxLength) {
return;
}
if (!skipCheck) {
var re;
// Try to build a regex for this format character.
try {
re = new RegExp(this.format[start]);
} catch (e) {}
if (!re || !re.exec(character)) {
return;
}
}
this.characters.splice(start, end - start, character);
// If we've added at an index that pushes the length beyond what we support,
// remove the trailing characters.
if (this.maxLength && this.characters.length > this.maxLength) {
this.characters.splice(this.maxLength, this.characters.length);
}
this.run(start + 1);
},
Option name | Type | Description |
---|---|---|
character | String |
Add a character at the position of the caret.
addCharacterAtCaret: function(character) {
var pos = this._getCaretStart();
var re;
// If we're beyond the bounds of the format, stop.
if (this.format[pos] === undefined) {
(this.onEnd || noop)(this, character);
return;
}
// Try to build a regex for this format character.
try {
re = new RegExp(this.format[pos]);
} catch (e) {}
// We couldn't build a regex (so it's invalid) or the regex failed (so it's invalid)
if (!re || !re.exec(character)) {
if (this._moveCaret('right')) {
this.addCharacterAtCaret(character);
}
return;
}
this.addCharacterAtIndex(character, this._getCaretStartTranslated(), this._getCaretEndTranslated(), true);
},
Option name | Type | Description |
---|---|---|
index | Number | |
length | Number | Optional |
offset | Number | Optional |
Remove a character from the character array by index.
removeCharacterAtIndex: function(index, length, offset) {
// Don't want a negative splice length or else we start
// removing characters from the end.
if (index + offset < 0) {
return;
}
length = length !== undefined ? length : 1;
this.characters.splice(index + offset, length);
this.run(index + (offset || 1));
},
Option name | Type | Description |
---|---|---|
offset | Number | Optional An offset from the current position. |
Remove the character at the caret.
removeCharacterAtCaret: function(offset) {
var start = this._getCaretStartTranslated();
var end = this._getCaretEndTranslated();
var length = 1;
var tmp;
if (start !== end) {
// If the end is less than the start, the user dragged from right to left.
// Just swap them to make it easier to handle.
if (end < start) {
tmp = start;
start = end;
end = tmp;
}
// The length of characters removed
length = end - start;
// Bump the start position @todo: haven't thought through why this is, but it's needed.
start++;
}
this.removeCharacterAtIndex(start, length, offset);
},
Remove the character in the current range.
removeCharactersInRange: function() {
this.removeCharacterAtIndex(this._getCaretStartTranslated(), this._getCaretEndTranslated());
},
Option name | Type | Description |
---|---|---|
value | String |
Set the value of the typeahead. Maintain the position of the caret.
setValue: function(value) {
this.settingValue = true;
this.pause();
this.characters = (value + '').split('');
this.run();
if (this.isFocused) this._setCaretPosition(this._getCaretStart());
this.resume();
this.settingValue = false;
},
Option name | Type | Description |
---|---|---|
asInt | Boolean | Get the value as a parsed integer. |
return | String, Number |
Get the value of the typeahead.
getValue: function(asInt) {
return asInt && this.inputEl.value ? parseInt(this.inputEl.value, 10) : this.inputEl.value;
},
Option name | Type | Description |
---|---|---|
pos | Number |
Move the caret position.
moveCaret: function(pos) {
this._setCaretPositionTranslated(pos);
},
Move the caret to the end of the input.
moveCaretToEnd: function() {
this.moveCaret(this.characters.length);
},
Move the caret to the start of the input.
moveCaretToStart: function() {
this.moveCaret(0);
},
Pause events.
pause: function() {
this.pauseBlurFocus++;
},
Resume events.
resume: function() {
this.pauseBlurFocus--;
},
Clear the value.
clear: function() {
this.pause();
this.characters = [];
this.run(0, {notOnlyPlaceholders: true});
this.resume();
},
Option name | Type | Description |
---|---|---|
el | Object |
Store a reference to the needed elements.
_cacheElements: function(el) {
this.el = el;
this.inputEl = this.el.querySelector('[type="text"], [type="email"], [type="phone"], textarea') || this._createDefaultInputElement();
this.placeholderEl = this.el.querySelector('.spark-input__placeholder') || this._createDefaultPlaceholderElement();
},
Parse parameters from the elements.
_parseParams: function() {
// Store the value characters
this.characters = this._parseCharacters(this.inputEl.value);
// Store format
this.format = this._parseFormat(this.format ? this.format : this.inputEl.getAttribute('data-typeahead-format'));
// Store the original placeholder
this.placeholder = this.placeholder ? this.placeholder : this.inputEl.getAttribute('placeholder').split('');
// Get the total number of characters we can have
this.maxLength = this._getCharactersAllowedCount(this.format);
},
Option name | Type | Description |
---|---|---|
format | String | |
return | Array |
Parse the format string into an array.
_parseFormat: function(format) {
var i = 0;
var len = format.length;
var arr = [];
var lastWasEscape = false;
for (; i < len; i++) {
if (format[i] === '\\' && !lastWasEscape) {
lastWasEscape = true;
} else {
arr.push((lastWasEscape ? '\\' : '') + format[i]);
lastWasEscape = false;
}
}
return arr;
},
Option name | Type | Description |
---|---|---|
characters | String | |
return | Array |
Parse the characters string into an array, ignoring characters which don't
match the format requirements.
_parseCharacters: function(characters) {
var chars = characters.split('');
var i = 0;
var len = characters.length;
var regexes = [];
var arr = [];
for (; i < len; i++) {
// Try to build a regex for this format character.
try {
// Make sure this format starts with an escape character. @todo: this is pretty limiting, but
// it's necessary or else '-' or '+' matches properly.
regexes[i] = this.format[i][0] === '\\' ? new RegExp(this.format[i]) : null;
} catch (e) {}
// If we were able to create a regex and our char passes, add it to the array
// of characters to return.
if (regexes[i] && regexes[i].exec(chars[i])) {
arr.push(chars[i]);
}
}
return arr;
},
Create the default input element.
_createDefaultInputElement: function() {
var el = document.createElement('input');
el.className = 'spark-input__field';
el.setAttribute('data-typeahead', '');
el.setAttribute('type', 'tel');
this.el.appendChild(el);
return el;
},
Create the default input element.
_createDefaultPlaceholderElement: function() {
var el = document.createElement('span');
el.className = 'spark-input__placeholder';
this.el.appendChild(el);
return el;
},
Option name | Type | Description |
---|---|---|
format | Array | |
return | Number |
Get the maximum number of characters allowed.
_getCharactersAllowedCount: function(format) {
var i = 0;
var len = format.length;
var allowed = 0;
for (; i < len; i++) {
if (format[i] === '\\d') {
allowed++;
}
}
return allowed;
},
Create bound versions of event listener callbacks and store them.
Otherwise we can't unbind from these events later because the
function signatures won't match.
_bindEventListenerCallbacks: function() {
this._onKeydownBound = this._onKeydown.bind(this);
this._onInputBound = this._onInput.bind(this);
this._onFocusBound = this._onFocus.bind(this);
this._onBlurBound = this._onBlur.bind(this);
this._onPlaceholderClickBound = this._onPlaceholderClick.bind(this);
},
Add event listeners to keypress and keydown.
_addEventListeners: function() {
this.inputEl.addEventListener('keydown', this._onKeydownBound, false);
this.inputEl.addEventListener('input', this._onInputBound, false);
this.inputEl.addEventListener('focus', this._onFocusBound, false);
this.placeholderEl.addEventListener('click', this._onPlaceholderClickBound, false);
},
Add event listeners to keypress and keydown.
_removeEventListeners: function() {
this.inputEl.removeEventListener('keydown', this._onKeydownBound);
this.inputEl.removeEventListener('focus', this._onFocusBound);
this.placeholderEl.removeEventListener('click', this._onPlaceholderClickBound);
},
Get the position of the caret in the element.
_getCaretStart: function() {
return this._maintainFocus(function() {
var caretPosition;
// IE support
if (document.selection) {
this.inputEl.focus();
var sel = document.selection.createRange();
sel.moveStart('character', -this.inputEl.value.length);
caretPosition = sel.text.length;
} else if (this.inputEl.selectionStart || this.inputEl.selectionStart === 0) {
caretPosition = this.inputEl.selectionStart;
}
return caretPosition;
});
},
Get the end position of the caret in the element.
_getCaretEnd: function() {
return this._maintainFocus(function() {
var caretPosition;
// IE support - @todo: this doesn't work in IE
if (document.selection) {
this.inputEl.focus();
var sel = document.selection.createRange();
sel.moveStart('character', -this.inputEl.value.length);
caretPosition = sel.text.length;
} else if (this.inputEl.selectionEnd || this.inputEl.selectionEnd === 0) {
caretPosition = this.inputEl.selectionEnd;
}
return caretPosition;
});
},
Is the caret at the end of the input?
_caretIsAtEnd: function() {
return this._getCaretStart() === this.maxLength;
},
Set the position of the caret in the element.
_setCaretPosition: function(pos) {
return this._maintainFocus(function() {
// IE support
if (document.selection) {
this.inputEl.focus();
var sel = document.selection.createRange();
sel.moveStart('character', -this.inputEl.value.length);
sel.moveStart('character', pos);
sel.moveEnd('character', 0);
sel.select();
} else if (this.inputEl.selectionStart || this.inputEl.selectionStart === 0) {
this.inputEl.selectionStart = pos;
this.inputEl.selectionEnd = pos;
}
});
},
Option name | Type | Description |
---|---|---|
pos | Number | |
return | Number |
Get the position of the caret translated to the corresponding index in the
characters array. This means ignoring format characters.
_getCaretPositionTranslated: function(pos) {
var i = 0;
var skipCount = 0;
for (; i < pos; i++) {
// Count non-numbers as a skip. @todo: this needs to work with more than numbers.
if (this.format[i] !== '\\d') {
skipCount++;
}
}
return pos - skipCount;
},
Get the starting position of the caret translated.
_getCaretStartTranslated: function() {
return this._getCaretPositionTranslated(this._getCaretStart());
},
Get the ending position of the caret translated.
_getCaretEndTranslated: function() {
return this._getCaretPositionTranslated(this._getCaretEnd());
},
Option name | Type | Description |
---|---|---|
pos | Number |
Set the position of the caret translated to the corresponding index in the
characters array. This means ignoring format characters.
_setCaretPositionTranslated: function(pos) {
var i = 0;
var skipCount = 0;
for (; i < pos + skipCount; i++) {
// Count non-numbers as a skip. @todo: this needs to work with more than numbers.
if (this.format[i] !== undefined && this.format[i] !== '\\d') {
skipCount++;
}
}
this._setCaretPosition(pos + skipCount);
},
Option name | Type | Description |
---|---|---|
direction | String | The direction of the movement |
return | Boolean | Was the caret actually moved? |
Move the caret position
_moveCaret: function(direction) {
var curPos = this._getCaretStart();
if (direction === 'left') {
this._setCaretPosition(curPos - 1);
} else if (direction === 'right') {
this._setCaretPosition(curPos + 1);
}
return curPos !== this._getCaretStart();
},
Empty the input when we only have placeholders.
_emptyWhenOnlyPlaceholders: function() {
if (!this.characters.length) {
this.clear();
}
},
Option name | Type | Description |
---|---|---|
callback | Function |
Run a callback function that may change the focus of the document, but
make sure focus goes back to where it needs to be. Also, set the state
so that blur/focus events don't fire from this instance.
_maintainFocus: function(callback) {
this.pause();
var originalActiveElement = document.activeElement;
//For IE
if(!originalActiveElement) {
originalActiveElement = document.body;
}
var output = (callback || noop).call(this);
// If we didn't have focus, go back to focusing on the original
if (originalActiveElement !== this.inputEl) {
this.inputEl.blur();
originalActiveElement.focus();
}
this.resume();
return output;
},
Update the width of the typeahead. If we should be matching the width
of the placeholder, do so. Otherwise, take no action.
_updateWidth: function() {
if (this.matchPlaceholderSize) {
this.placeholderEl.style.width = 'auto';
// Add 2px to account for caret width in IE... @todo: better possible fix?
this.inputEl.style.width = 'auto';
this.inputEl.style.width = this.placeholderEl.offsetWidth + 2 + 'px';
this.placeholderEl.style.width = '';
}
},
Option name | Type | Description |
---|---|---|
e | Object |
Listen for delete and arrows.
_onKeydown: function(e) {
var code = e.keyCode || e.which;
if (code === this.pasteCode && (e.metaKey || e.ctrlKey)) {
return;
}
if (code === this.actionCodes.BACKSPACE) {
this.removeCharacterAtCaret(-1);
this._onBackspace();
e.preventDefault();
} else if (code === this.actionCodes.DELETE) {
this.removeCharacterAtCaret(0);
e.preventDefault();
} else if (code === this.actionCodes.LEFT) {
if (!this._getCaretStart()) {
(this.onBackspace || noop)();
}
} else if (code === this.actionCodes.RIGHT) {
if (this._getCaretStart() === this.characters.length) {
(this.onEnd || noop)();
}
} else {
if (this.ignoreCodes.indexOf(code) === -1) {
e.preventDefault();
this.addCharacterAtCaret(String.fromCharCode(code));
}
}
},
Option name | Type | Description |
---|---|---|
e | Object |
When the input event fires, validate. This happens
with a copy+paste.
_onInput: function(e) {
e.preventDefault();
this.characters = this._parseCharacters(this.inputEl.value);
this.run();
},
Option name | Type | Description |
---|---|---|
e | Object |
When we focus, run the formatting.
_onFocus: function() {
window.removeEventListener('blur', this._onBlurBound, false);
window.addEventListener('blur', this._onBlurBound, false);
this.inputEl.removeEventListener('blur', this._onBlurBound, false);
this.inputEl.addEventListener('blur', this._onBlurBound, false);
if (this.isFocused || this.pauseBlurFocus || this.isRunning) return;
this.run();
(this.onFocus || noop)(this.getValue());
this.isFocused = true;
this._oldVal = this.inputEl.value;
},
Option name | Type | Description |
---|---|---|
e | Object |
When we blur, if we have no characters, remove the placeholders.
_onBlur: function() {
window.removeEventListener('blur', this._onBlurBound);
this.inputEl.removeEventListener('blur', this._onBlurBound);
this.isFocused = false;
if (this.pauseBlurFocus || this.isRunning) return;
this._emptyWhenOnlyPlaceholders();
// preventDefault will not dispatch change event
// manually dispatch change event
if(this._oldVal !== this.inputEl.value) {
this._triggerEvent(this.inputEl, 'change');
}
(this.onBlur || noop)(this.getValue());
},
Option name | Type | Description |
---|---|---|
e | Object |
When the placeholder receives a click event, focus on the input. This happens in IE10 for some
reason that I cannot fully fathom, but it has something to do with the explicit width being
set on an empty element.
_onPlaceholderClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.inputEl.focus();
},
Option name | Type | Description |
---|---|---|
e | Object |
When we backspace, if we have no characters left let listeners know.
_onBackspace: function() {
if (!this._getCaretStart())(this.onBackspace || noop)();
}
};
Base.exportjQuery(Typeahead, 'Typeahead');
return Typeahead;
}));