Each variation will be presented in the following sections.
This utility offers a type-ahead function that can be applied to input elements.
It generates a list with keywords fetched from a server.
Veams >= v5.0.0
- Veams Framework.veams install vu type-ahead
bower install veams-utility-type-ahead --save
String
} - Modifier classes.Object
} - Options object which gets stringified.Number
} - Height of suggestion list.The module gives you the possibility to override default options:
String
} [‘type-ahead__current-value’] - Current value class.String
} [[data-js-item=“type-ahead__input”]] - Element selector for input.Number
} [2] - Minimal length of input value before suggestions will be shown.Number
} [0] - Minimal time that must have passed before new request is sent to server.String
} [[data-js-item=“suggestion-item”]] - Element selector for suggestion item.String
} [[data-js-item=“type-ahead__list”]] - Element selector for type-ahead list.String
} [‘SUGGESTIONS__A11Y’] - Template name for suggestion (list item).String
} [‘SUGGESTIONS__OPTIONS’] - Template name for option.{
"variations": {
"default": {
"docs": {
"variationName": "Default"
},
"settings": {
"typeAheadClasses": false,
"id": "type-ahead",
"height": "400px",
"type": "text",
"attributes": [
{
"attrKey": "data-js-item",
"attrValue": "type-ahead__input"
},
{
"attrKey": "autocomplete",
"attrValue": "off"
}
],
"autocomplete": "off",
"typeAheadJsOptions": {
"serviceOptions": {
"url": "/ajax/example-ajax.json",
"method": "get",
"type": "json",
"contentType": "application/json",
"X-CSRFToken":"475438ff641b168e1f7e1a06c0fea6eb"
}
}
},
"content": {}
}
}
}
<div class="u-type-ahead{{#if settings.typeAheadClasses}} {{settings.typeAheadClasses}}{{/if}}"
data-js-module="type-ahead"{{#if settings.typeAheadJsOptions}}
data-js-options='{{stringify settings.typeAheadJsOptions}}'{{/if}}>
{{! WrapWith START: }}
{{#wrapWith "c-form"}}
{{> c-form__input }}
{{/wrapWith}}
{{! WrapWith END: }}
<ul class="type-ahead__list" role="listbox" data-js-item="type-ahead__list" data-js-height="{{ settings.height }}"></ul>
</div>
/* ===================================================
utility: type-ahead
=================================================== */
/* ---------------------------------------------------
Global Variables
--------------------------------------------------- */
$type-ahead-animation-duration-std: 200ms;
$type-ahead-animation-easing-std: ease-in-out;
.u-type-ahead {
width: 100%;
text-align: center;
datalist {
display: none;
}
.type-ahead__list {
position: absolute;
bottom: 0;
left: 0;
z-index: 100;
width: 100%;
background-color: #fff;
transform: translate3d(0, 90%, 0);
padding: 0;
margin: 0;
height: auto !important; // TO OVERWRITE JS TOGGLER BEHAVIOUR - OTHERWISE JS-LOGIC HAS TO BE REFACTORED - TODO
/*
MODIFIERS
----------------------- */
&.is-open {
box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .1);
padding-top: 4px;
padding-bottom: 4px;
@include bp($bp-desktop-l) {
padding-top: 8px;
padding-bottom: 8px;
}
&:empty {
display: none;
}
}
}
.type-ahead__suggestion-item {
width: 100%;
display: block;
}
.type-ahead__suggestion-link {
display: block;
text-decoration: none;
padding: 10px 0;
font-size: 1.8rem;
line-height: 22px;
font-family: $font-family;
color: #000;
transition: color $type-ahead-animation-duration-std, background-color $type-ahead-animation-duration-std;
@include bp($bp-tablet-p) {
padding: 18px 0;
font-size: 3rem;
line-height: 3rem;
}
@include bp($bp-tablet-l) {
}
@include bp($bp-desktop-m) {
}
@include bp($bp-desktop-l) {
font-size: 3.4rem;
line-height: 3.4rem;
padding: 20px 0;
}
&:hover,
&:focus,
&.a11y-focus-key {
background-color: #f2f7f9;
color: #055d8d;
outline: 0;
em {
color: #055d8d !important;
}
}
}
.type-ahead__current-value {
color: #b1b1b1;
}
}
/**
* This module offers type-ahead functionality for input fields.
*
* @module TypeAhead
* @version v3.1.4
*
* @author José Medina
*/
// Imports
import {Veams} from 'app';
import VeamsComponent from 'veams/lib/common/component';
import AjaxService from 'veams/lib/services/http';
import Toggler from '../../../components/toggler/js/toggler';
// Variables
const $ = Veams.$;
class TypeAhead extends VeamsComponent {
/**
* Constructor for our class
*
* @see module.js
*
* @param {Object} obj - Object which is passed to our class
* @param {Object} obj.el - element which will be saved in this.el
* @param {Object} obj.options - options which will be passed in as JSON object
*/
constructor(obj) {
let options = {
currentValueClass: "type-ahead__current-value",
inputSel: '[data-js-item="type-ahead__input"]',
minLength: 2,
minLoadTime: 0,
suggestionItemSel: '[data-js-item="suggestion-item"]',
suggestionListSel: '[data-js-item="type-ahead__list"]',
templates: {
tplSuggestionsA11y: "SUGGESTIONS__A11Y",
tplSuggestionsOpt: "SUGGESTIONS__OPTIONS"
}
};
super(obj, options);
}
/**
* Get module information
*/
static get info() {
return {
version: '3.1.4',
vc: true,
mod: false // set to true if source was modified in project
};
}
/**
* Subscribe handling
*/
get subscribe() {
return {}
}
/**
* Initialize the view
*
*/
initialize() {
this.$suggestionList = this.$el.find(this.options.suggestionListSel);
this.$input = this.$el.find(this.options.inputSel);
this.currentValue = this.$input.val();
this.suggestionList = new Toggler({
el: this.$suggestionList[0],
namespace: 'Toggler',
appInstance: Veams
});
this.suggestionList.close();
this.service = new AjaxService(this.options.serviceOptions);
this.service.requestDidOpen = (request, obj) => {
if (this.options.serviceOptions.contentType) {
request.setRequestHeader("Content-type", this.options.serviceOptions.contentType);
}
request.setRequestHeader("X-CSRFToken", this.options.serviceOptions["X-CSRFToken"]);
}
}
/**
* Bind Vent events for the resize events and the load more button
*/
bindEvents() {
this.$input.on(Veams.EVENTS.keyup, this.onKeyup.bind(this));
$(window).on(Veams.EVENTS.keydown, this.onWindowKeydown.bind(this));
this.$input.on(Veams.EVENTS.blur, this.onItemBlur.bind(this));
}
/**
* Set focus when needed
*
* @param {Object} obj - CTA event object
*/
setFocus(obj) {
if (obj.isActive) {
setTimeout(() => {
this.$input[0].focus();
}, 1000);
}
}
/**
* Handle window key down event to control the module with arrow keys, enter and escape
*
* @param {Object} event - Event object
*/
onWindowKeydown(event) {
if (this.el.contains(event.target)) {
let getFocusableElements = () => {
return Array.prototype.slice.call($("a, input", this.$el));
};
let focusableEls, index;
switch (event.keyCode) {
case 38:
event.preventDefault();
focusableEls = getFocusableElements();
index = focusableEls.indexOf(document.activeElement);
if (index > -1 && index - 1 >= 0) {
focusableEls[index - 1].focus();
}
break;
case 40:
event.preventDefault();
focusableEls = getFocusableElements();
index = focusableEls.indexOf(document.activeElement);
if (index > -1 && index + 1 < focusableEls.length) {
focusableEls[index + 1].focus();
}
break;
case 27:
this.closeSuggestionList();
break;
case 13:
this.triggerChosenSuggestion(event.target);
this.closeSuggestionList();
break;
}
}
}
/**
* Handle input key up event to control the module with arrow keys, enter and escape
*
* @param {Object} event - Event object
*/
onKeyup(event) {
switch (event.keyCode) {
case 38:
case 40:
case 27:
case 13:
event.preventDefault();
break;
default:
this.currentValue = this.$input.val();
this.render(this.generateTimeOfRequest());
}
}
/**
* Bind filter items events as soon as render occurs
*
*/
bindItemEvents() {
this.$suggestionList.find(this.options.suggestionItemSel)
.on(Veams.EVENTS.touchstart + ' ' + Veams.EVENTS.click, this.onChosenSuggestion.bind(this))
.on(Veams.EVENTS.blur, this.onItemBlur.bind(this))
.on(Veams.EVENTS.focus, this.onItemFocus.bind(this))
.on(Veams.EVENTS.mouseenter, this.onItemMouseEnter.bind(this))
.on(Veams.EVENTS.mouseleave, this.onItemMouseLeave.bind(this));
}
/**
* Handle touch and click event
*
* @param {Object} event - Event object
* @param {object} [currentTarget] - Target to which listener was attached (used in VeamsQuery).
*/
onChosenSuggestion(event, currentTarget) {
let target = currentTarget || event.currentTarget;
event.preventDefault();
this.triggerChosenSuggestion(target);
this.closeSuggestionList();
}
/**
* Handle item focus event
*
* @param {Object} event - Event object
*/
onItemFocus(event) {
this.$input.val($(event.target).closest(this.options.suggestionItemSel).attr('data-value'));
}
/**
* Handle item mouse enter event
*
* @param {Object} event - Event object
*/
onItemMouseEnter(event) {
this.onItemFocus.apply(this, arguments);
}
/**
* Handle item mouse leave event
*/
onItemMouseLeave() {
this.$input.val(this.currentValue);
}
/**
* Handle item blur event
*
* @param {Object} event - Event object
*/
onItemBlur(event) {
this.$input.val(this.currentValue);
if (!!event.relatedTarget && !this.$suggestionList[0].contains(event.relatedTarget) &&
event.relatedTarget !== this.$input[0]) {
this.closeSuggestionList();
}
}
/**
* Helper function that looks for the current input value and transmit it
*
* @param {Object} target - DOM Node
*/
triggerChosenSuggestion(target) {
this.currentValue = $(target).attr('data-value') || $(target).val();
this.$input.val(this.currentValue);
this.$el.trigger(Veams.EVENTS.typeAhead.suggestionChoosen, {
search: this.currentValue
});
}
/**
* Render class
*
* Perform and server request to get the suggestions to a given value
*
* @param {Number} timeOfRequest - Time of request
*/
render(timeOfRequest) {
if (this.currentValue.length < this.options.minLength || !this.$el[0].contains(document.activeElement)) {
this.closeSuggestionList();
return false;
}
return new Promise((resolve, reject) => {
this.service[this.options.serviceOptions.method]({
data: JSON.stringify({}),
url: this.options.serviceOptions.url + "?type-ahead=" + this.currentValue
}).then((data) => {
setTimeout(() => {
if (timeOfRequest !== this.timeOfRequest) {
return false;
}
this.$suggestionList.html(Veams.templater.render(this.options.templates.tplSuggestionsA11y,
this.formatResponseDataHelper(data)));
this.bindItemEvents();
this.suggestionList.open();
resolve();
}, Math.max(this.options.minLoadTime - (Date.now() - timeOfRequest), 0));
}, (error) => {
reject();
throw new Error("Typeahead render() :: could not resolve server request :: ", error);
});
});
}
/**
* Cache the current timestamp to figure out which was the last request
*
* If 2 delta time between request is less than minLoadTime, they will be rejected
* and the one with the latest timestamp will be sent
*
* @return {Number} - Time of request
*/
generateTimeOfRequest() {
this.timeOfRequest = Date.now();
return this.timeOfRequest;
}
/**
* Data formatter that prepares the data for the template to render
*
* @param {Object} data - Data to be prepared for rendering
* @return {Object} - Prepared data (ready for rendering)
*/
formatResponseDataHelper(data) {
data.suggestions = data.suggestions.map((item) => {
item.suggestionText = this.getSuggestionTextHelper(item.name);
item.searchVal = this.currentValue;
return item;
});
return data;
}
/**
* Data formatter that prepares the data for the template to render
*
* @param {String} name - Item name
* @return {String} - Modified data
*/
getSuggestionTextHelper(name) {
let currentValueCaseInsensitive = new RegExp(this.currentValue, "ig");
return name.replace(currentValueCaseInsensitive, '<em class="' + this.options.currentValueClass + '">$&</em>');
}
/**
* Close the suggestion list and empty the list
*
*/
closeSuggestionList() {
this.suggestionList.close();
this.$suggestionList.empty();
}
}
export default TypeAhead;
<div class="u-type-ahead" data-js-module="type-ahead" data-js-options='{"serviceOptions":{"url":"/ajax/example-ajax.json","method":"get","type":"json","contentType":"application/json","X-CSRFToken":"475438ff641b168e1f7e1a06c0fea6eb"}}'>
<form class="c-form--default
" action="//" method="" data-css="c-form">
<div class="form__input">
<div class="form__input-wrapper">
<input id="type-ahead" name="type-ahead" type="text" data-js-item="type-ahead__input" autocomplete="off" class="form__input-text" />
</div>
</div>
</form>
<ul class="type-ahead__list" role="listbox" data-js-item="type-ahead__list" data-js-height="400px"></ul>
</div>