@endereco/streetautocomplete
Version:
Assist the input of street. When user enters first characters of a street it shows a dropdown with street variants. User can select a variant by clicking on it or by navigating to it with keyboard arrows and pressing enter. When selected a variant is copi
482 lines (419 loc) • 16.6 kB
JavaScript
/**
* Endereco SDK.
*
* @author Ilja Weber <ilja@endereco.de>
* @copyright 2019 mobilemojo – Apps & eCommerce UG (haftungsbeschränkt) & Co. KG
* {@link https://endereco.de}
*/
function StreetAutocomplete(config) {
var $self = this;
/**
* Combine object, IE 11 compatible.
*/
this.mergeObjects = function(objects) {
return objects.reduce(function (r, o) {
Object.keys(o).forEach(function (k) {
r[k] = o[k];
});
return r;
}, {})
};
this.requestBody = {
"jsonrpc": "2.0",
"id": 1,
"method": "streetAutocomplete",
"params": {
"street": "",
"postCode": "",
"cityName": "",
"country": "de",
"language": "de"
}
}
this.defaultConfig = {
'useWatcher': true,
'referer': 'not_set',
'tid': 'not_set'
};
this.fieldsAreSet = false;
this.dirty = false;
this.originalInput;
this.blockInput = false;
this.lastInputError = true;
this.config = $self.mergeObjects([this.defaultConfig, config]);
this.connector = new XMLHttpRequest();
this.createEvent = function(eventName) {
var event;
if(typeof(Event) === 'function') {
event = new Event(eventName);
}else{
event = document.createEvent('Event');
event.initEvent(eventName, true, true);
}
return event;
};
/**
* Helper function to update existing config, overwriting existing fields.
*
* @param newConfig
*/
this.updateConfig = function(newConfig) {
$self.config = $self.mergeObjects([$self.config, newConfig]);
};
/**
* Checks if fields are set.
*/
this.checkIfFieldsAreSet = function() {
var areFieldsSet = false;
if((null !== document.querySelector($self.config.inputSelector))) {
areFieldsSet = true;
}
if (!$self.fieldsAreSet && areFieldsSet) {
$self.dirty = true;
$self.fieldsAreSet = true;
} else if($self.fieldsAreSet && !areFieldsSet) {
$self.fieldsAreSet = false;
}
}
/**
* Get predictions for the provided input.
*/
this.getPredictions = function() {
$self.activeElementIndex = -1;
$self.originalInput = '';
return new Promise( function(resolve, reject) {
var countryCode = 'de';
var countryElement;
// On data receive
$self.connector.onreadystatechange = function() {
var $data = {};
if(4 === $self.connector.readyState) {
if ($self.connector.responseText && '' !== $self.connector.responseText) {
$data = JSON.parse($self.connector.responseText);
if ($data.result) {
$self.lastInputError = false;
resolve($data);
} else {
$self.lastInputError = true;
reject($data);
}
} else {
$self.lastInputError = true;
reject($data);
}
}
};
// Set request values.
if ($self.inputElement) {
$self.requestBody.params.street = $self.inputElement.value.trim();
}
// Set post code.
if (undefined !== $self.config.secondaryInputSelectors.postCode && '' !== document.querySelector($self.config.secondaryInputSelectors.postCode).value.trim()) {
$self.requestBody.params.postCode = document.querySelector($self.config.secondaryInputSelectors.postCode).value.trim();
}
// Set city name.
if (undefined !== $self.config.secondaryInputSelectors.cityName && '' !== document.querySelector($self.config.secondaryInputSelectors.cityName).value.trim()) {
$self.requestBody.params.cityName = document.querySelector($self.config.secondaryInputSelectors.cityName).value.trim();
}
// Set country.
countryElement = document.querySelector($self.config.secondaryInputSelectors.country);
if ((undefined !== countryElement) && (null !== countryElement)) {
countryCode = countryElement.options[countryElement.selectedIndex].getAttribute('data-code');
if ('' === countryCode) {
countryCode = countryElement.options[countryElement.selectedIndex].value;
}
if ('' === countryCode) {
countryCode = 'de';
}
$self.requestBody.params.country = countryCode;
}
// Set language
if (undefined !== $self.config.language && '' !== $self.config.language) {
$self.requestBody.params.language = $self.config.language;
}
/**
* Backward compatibility for referer
* If not set, it will use the browser url.
*/
if ('not_set' === $self.config.referer) {
$self.config.referer = window.location.href;
}
$self.connector.open('POST', $self.config.endpoint, true);
$self.connector.setRequestHeader("Content-type", "application/json");
$self.connector.setRequestHeader("X-Auth-Key", $self.config.apiKey);
$self.connector.setRequestHeader("X-Transaction-Id", $self.config.tid);
$self.connector.setRequestHeader("X-Transaction-Referer", $self.config.referer);
$self.connector.send(JSON.stringify($self.requestBody));
});
}
/**
* Renders predictions in a dropdown.
*/
this.renderDropdown = function() {
var ul;
var li;
var street;
var input;
if ('' === $self.originalInput) {
input = $self.inputElement.value.trim();
$self.originalInput = input;
} else {
input = $self.originalInput;
}
var direction = getComputedStyle($self.inputElement).direction;
var counter = 0;
var regEx;
var replaceMask;
var event;
var selectedStreet;
$self.removeDropdown();
if (0 === $self.predictions.length) {
return;
}
ul = document.createElement('ul');
if ('rtl' === direction) {
ul.style.textAlign = 'right';
} else {
ul.style.textAlign = 'left';
}
ul.style.zIndex = '9001';
ul.style.borderRadius = '4px';
ul.style.backgroundColor = '#fff';
ul.style.border = '1px solid #dedede';
ul.style.listStyle = 'none';
ul.style.padding = '4px 4px';
ul.style.margin = 0;
ul.style.position = 'absolute';
ul.style.top = 4 + $self.inputElement.offsetTop + $self.inputElement.offsetHeight + 'px';
if ('rtl' === direction) {
ul.style.right = $self.inputElement.offsetParent.offsetWidth - $self.inputElement.offsetLeft - $self.inputElement.offsetWidth + 'px';
} else {
ul.style.left = $self.inputElement.offsetLeft + 'px';
}
ul.setAttribute('class', 'endereco-dropdown')
$self.dropdown = ul;
$self.inputElement.parentNode.insertBefore(ul, $self.inputElement.nextSibling);
ul.addEventListener('mouseout', function() {
$self.inputElement.value = $self.originalInput;
});
// Iterate through list and create new elements
$self.predictions.forEach( function(element) {
li = document.createElement('li');
li.style.cursor = 'pointer';
li.style.color = '#000';
li.style.padding = '2px 4px';
li.style.margin = '0';
li.setAttribute('data-index', counter);
if (counter === $self.activeElementIndex) {
li.style.backgroundColor = 'rgba(0, 137, 167, 0.25)';
} else {
li.style.backgroundColor = 'transparent';
}
li.addEventListener('mouseover', function() {
this.style.backgroundColor = 'rgba(0, 137, 167, 0.25)';
$self.blockInput = true;
});
li.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
$self.blockInput = false;
});
regEx = new RegExp('(' + input + ')', 'ig');
replaceMask = '<mark style="background-color: transparent; padding: 0; margin: 0; font-weight: 700; color: ' + $self.config.colors.secondaryColor + '">$1</mark>';
street = element.street.replace(regEx, replaceMask);
li.innerHTML = street;
li.setAttribute('data-street', element.street);
// Register event
li.addEventListener('mouseover', function(mEvent) {
mEvent.preventDefault();
selectedStreet = this.getAttribute('data-street');
$self.inputElement.value = selectedStreet;
$self.activeElementIndex = this.getAttribute('data-index') * 1;
});
li.addEventListener( 'mousedown', function(mEvent) {
mEvent.preventDefault();
event = $self.createEvent('endereco.valid');
$self.inputElement.dispatchEvent(event);
$self.saveOriginal();
$self.removeDropdown();
});
$self.dropdown.appendChild(li);
counter++;
});
};
/**
* Validate fields.
*/
this.validate = function() {
var input = $self.inputElement.value.trim();
var event;
var includes = false;
if ($self.lastInputError) {
return;
}
$self.predictions.forEach( function(prediction) {
if (input === prediction.street) {
includes = true;
}
});
if (includes) {
event = $self.createEvent('endereco.valid');
$self.inputElement.dispatchEvent(event);
} else if('' === input) {
event = $self.createEvent('endereco.clean');
$self.inputElement.dispatchEvent(event);
} else {
event = $self.createEvent('endereco.check');
$self.inputElement.dispatchEvent(event);
}
};
/**
* Removes dropdown from DOM.
*/
this.removeDropdown = function() {
if (null !== $self.dropdown && undefined !== $self.dropdown ) {
$self.dropdown.parentElement.removeChild($self.dropdown);
$self.dropdown = undefined;
}
$self.blockInput = false;
};
/**
* Init postCodeAutocomplete.
*/
this.init = function() {
try {
$self.inputElement = document.querySelector($self.config.inputSelector);
$self.dropdown = undefined;
$self.dropdownDraw = true;
$self.predictions = [];
$self.activeElementIndex = -1;
$self.saveOriginal();
} catch(e) {
console.log('Could not initiate StreetAutocomplete because of error.', e);
}
// Disable browser autocomplete
if ($self.isChrome()) {
$self.inputElement.setAttribute('autocomplete', 'autocomplete_' + Math.random().toString(36).substring(2) + Date.now());
} else {
$self.inputElement.setAttribute('autocomplete', 'off' );
}
// Register events
$self.inputElement.addEventListener('input', function() {
var $this = this;
var acCall = $self.getPredictions();
$self.originalInput = this.value;
acCall.then( function($data) {
$self.predictions = $data.result.predictions;
if ($this === document.activeElement) {
$self.renderDropdown();
}
if ($data.cmd && $data.cmd.use_tid) {
$self.config.tid = $data.cmd.use_tid;
if (0 < $self.config.serviceGroup.length) {
$self.config.serviceGroup.forEach( function(serviceObject) {
serviceObject.updateConfig({'tid': $data.cmd.use_tid});
})
}
}
}, function($data){console.log('Rejected with data:', $data)});
});
$self.inputElement.addEventListener('focus', function() {
if ('' === this.value && 'not_set' !== $self.config.tid) {
return;
}
var acCall = $self.getPredictions();
$self.saveOriginal();
acCall.then( function($data) {
$self.predictions = $data.result.predictions;
$self.validate();
if ($data.cmd && $data.cmd.use_tid) {
$self.config.tid = $data.cmd.use_tid;
if (0 < $self.config.serviceGroup.length) {
$self.config.serviceGroup.forEach( function(serviceObject) {
serviceObject.updateConfig({'tid': $data.cmd.use_tid});
})
}
}
}, function($data){console.log('Rejected with data:', $data)});
});
// Register blur event
$self.inputElement.addEventListener('blur', function() {
$self.removeDropdown();
$self.restoreOriginal();
$self.validate();
});
// Register mouse navigation
$self.inputElement.addEventListener('keydown', function(mEvent) {
var event;
if ('ArrowUp' === mEvent.key || 'Up' === mEvent.key) {
mEvent.preventDefault();
if (0 === $self.activeElementIndex) {
$self.activeElementIndex = -1;
$self.inputElement.value = $self.originalInput;
}
if (0 < $self.activeElementIndex) {
$self.activeElementIndex--;
// Prefill selection to input
if (0 < $self.predictions.length) {
$self.inputElement.value = $self.predictions[$self.activeElementIndex].street;
}
}
$self.renderDropdown();
}
if ('ArrowDown' === mEvent.key || 'Down' === mEvent.key) {
mEvent.preventDefault();
if ($self.activeElementIndex < ($self.predictions.length-1)) {
$self.activeElementIndex++;
}
// Prefill selection to input
if (0 < $self.predictions.length) {
$self.inputElement.value = $self.predictions[$self.activeElementIndex].street;
}
$self.renderDropdown();
}
if ('Enter' === mEvent.key || 'Enter' === mEvent.key) {
mEvent.preventDefault();
// If only one prediction.
if (1 === $self.predictions.length) {
// Prefill selection to input
$self.inputElement.value = $self.predictions[0].street;
}
// Then.
event = $self.createEvent('endereco.valid');
$self.inputElement.dispatchEvent(event);
$self.saveOriginal();
$self.removeDropdown();
}
if ($self.blockInput) {
mEvent.preventDefault();
return;
}
});
$self.dirty = false;
console.log('StreetAutocomplete initiated.');
}
/**
* Resotre original values.
*/
this.restoreOriginal = function() {
$self.inputElement.value = $self.originalInput;
}
/**
* Save original state.
*/
this.saveOriginal = function() {
$self.originalInput = $self.inputElement.value;
}
// Check if the browser is chrome
this.isChrome = function() {
return /chrom(e|ium)/.test( navigator.userAgent.toLowerCase( ) );
}
// Service loop.
setInterval( function() {
if ($self.config.useWatcher) {
$self.checkIfFieldsAreSet();
}
if ($self.dirty) {
$self.init();
}
}, 300);
}