Skip to content

Commit

Permalink
non-jquery autocomplete, needs more keyboard integration
Browse files Browse the repository at this point in the history
  • Loading branch information
netbymatt committed Oct 22, 2024
1 parent e2d7a96 commit c7eb56f
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 10,906 deletions.
5 changes: 2 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module.exports = {
commonjs: true,
es6: true,
node: true,
jquery: true,
},
extends: [
'airbnb-base',
Expand All @@ -29,8 +28,8 @@ module.exports = {
indent: [
'error',
'tab',
{
SwitchCase: 1
{
SwitchCase: 1,
},
],
'no-tabs': 0,
Expand Down
4 changes: 1 addition & 3 deletions gulp/publish-frontend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ const compressJsData = () => src(jsSourcesData)
.pipe(dest(RESOURCES_PATH));

const jsVendorSources = [
'server/scripts/vendor/auto/jquery.js',
'server/scripts/vendor/jquery.autocomplete.min.js',
'server/scripts/vendor/auto/nosleep.js',
'server/scripts/vendor/auto/swiped-events.js',
'server/scripts/vendor/auto/suncalc.js',
Expand Down Expand Up @@ -173,6 +171,6 @@ const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVend

// upload_images could be in parallel with upload, but _images logs a lot and has little changes
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);

export default publishFrontend;
1 change: 0 additions & 1 deletion gulp/update-vendor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const vendorFiles = [
'./node_modules/luxon/build/es6/luxon.js',
'./node_modules/luxon/build/es6/luxon.js.map',
'./node_modules/nosleep.js/dist/NoSleep.js',
'./node_modules/jquery/dist/jquery.js',
'./node_modules/suncalc/suncalc.js',
'./node_modules/swiped-events/src/swiped-events.js',
];
Expand Down
15 changes: 0 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
"homepage": "https://github.com/netbymatt/ws4kp#readme",
"devDependencies": {
"del": "^7.1.0",
"jquery": "^3.6.0",
"jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"suncalc": "^1.8.0",
Expand All @@ -48,4 +46,4 @@
"express": "^4.17.1",
"ejs": "^3.1.5"
}
}
}
13 changes: 5 additions & 8 deletions server/scripts/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { round2 } from './modules/utils/units.mjs';
import { parseQueryString } from './modules/share.mjs';
import settings from './modules/settings.mjs';
import AutoComplete from './modules/autocomplete.mjs';

document.addEventListener('DOMContentLoaded', () => {
init();
Expand Down Expand Up @@ -56,7 +57,7 @@ const init = () => {
document.addEventListener('keydown', documentKeydown);
document.addEventListener('touchmove', (e) => { if (document.fullscreenElement) e.preventDefault(); });

$(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete({
const autoComplete = new AutoComplete(document.querySelector(TXT_ADDRESS_SELECTOR), {
serviceUrl: 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/suggest',
deferRequestBy: 300,
paramName: 'text',
Expand All @@ -76,13 +77,12 @@ const init = () => {
minChars: 3,
showNoSuggestionNotice: true,
noSuggestionNotice: 'No results found. Please try a different search string.',
onSelect(suggestion) { autocompleteOnSelect(suggestion, this); },
onSelect(suggestion) { autocompleteOnSelect(suggestion); },
width: 490,
});

const formSubmit = () => {
const ac = $(TXT_ADDRESS_SELECTOR).devbridgeAutocomplete();
if (ac.suggestions[0]) $(ac.suggestionsContainer.children[0]).trigger('click');
if (autoComplete.suggestions[0]) autoComplete.suggestionsContainer.children[0].trigger('click');
return false;
};

Expand Down Expand Up @@ -133,10 +133,7 @@ const init = () => {
document.querySelector('#container').addEventListener('swiped-right', () => swipeCallBack('right'));
};

const autocompleteOnSelect = async (suggestion, elem) => {
// Do not auto get the same city twice.
if (elem.previousSuggestionValue === suggestion.value) return;

const autocompleteOnSelect = async (suggestion) => {
const data = await json('https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/find', {
data: {
text: suggestion.value,
Expand Down
229 changes: 229 additions & 0 deletions server/scripts/modules/autocomplete.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/* eslint-disable default-case */
import { json } from './utils/fetch.mjs';

const KEYS = {
ESC: 27,
TAB: 9,
RETURN: 13,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
};

const DEFAULT_OPTIONS = {
autoSelectFirst: false,
serviceUrl: null,
lookup: null,
onSelect: null,
onHint: null,
width: 'auto',
minChars: 1,
maxHeight: 300,
deferRequestBy: 0,
params: {},
delimiter: null,
zIndex: 9999,
type: 'GET',
noCache: false,
preserveInput: false,
containerClass: 'autocomplete-suggestions',
tabDisabled: false,
dataType: 'text',
currentRequest: null,
triggerSelectOnValidInput: true,
preventBadQueries: true,
paramName: 'query',
transformResult: (a) => a,
showNoSuggestionNotice: false,
noSuggestionNotice: 'No results',
orientation: 'bottom',
forceFixPosition: false,
};

const escapeRegExChars = (string) => string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');

const formatResult = (suggestion, search) => {
// Do not replace anything if the current value is empty
if (!search) {
return suggestion;
}

const pattern = `(${escapeRegExChars(search)})`;

return suggestion
.replace(new RegExp(pattern, 'gi'), '<strong>$1</strong>')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/&lt;(\/?strong)&gt;/g, '<$1>');
};

class AutoComplete {
constructor(elem, options) {
this.options = { ...DEFAULT_OPTIONS, ...options };
this.elem = elem;
this.selectedItem = -1;
this.onChangeTimeout = null;
this.currentValue = '';
this.suggestions = [];
this.cachedResponses = {};

// create and add the results container
const results = document.createElement('div');
results.style.display = 'none';
results.classList.add(this.options.containerClass);
results.style.width = (typeof this.options.width === 'string') ? this.options.width : `${this.options.width}px`;
results.style.zIndex = this.options.zIndex;
results.style.maxHeight = `${this.options.maxHeight}px`;
results.style.overflowX = 'hidden';
results.addEventListener('mouseover', (e) => this.mouseOver(e));
results.addEventListener('mouseout', (e) => this.mouseOut(e));
results.addEventListener('click', (e) => this.click(e));

this.results = results;
this.elem.after(results);

// add handlers for typing text
this.elem.addEventListener('keyup', (e) => this.keyUp(e));
}

mouseOver(e) {
// suggestion line
if (e.target?.classList?.contains('suggestion')) {
e.target.classList.add('selected');
this.selectedItem = parseInt(e.target.dataset.item, 10);
}
}

mouseOut(e) {
// suggestion line
if (e.target?.classList?.contains('suggestion')) {
e.target.classList.remove('selected');
this.selectedItem = -1;
}
}

click(e) {
// suggestion line
if (e.target?.classList?.contains('suggestion')) {
// get the entire suggestion
const suggestion = this.suggestions[parseInt(e.target.dataset.item, 10)];
this.options.onSelect(suggestion);
this.elem.value = suggestion.value;
this.hideSuggestions();
}
}

hideSuggestions() {
this.results.style.display = 'none';
}

showSuggestions() {
this.results.style.removeProperty('display');
}

clearSuggestions() {
this.results.innerHTML = '';
}

keyUp(e) {
// ignore some keys
switch (e.which) {
case KEYS.UP:
case KEYS.DOWN:
return;
}

clearTimeout(this.onChangeTimeout);

if (this.currentValue !== this.elem.value) {
if (this.options.deferRequestBy > 0) {
// defer lookup during rapid key presses
this.onChangeTimeout = setTimeout(() => {
this.onValueChange();
}, this.options.deferRequestBy);
}
}
}

onValueChange() {
clearTimeout(this.onValueChange);

// confirm value actually changed
if (this.currentValue === this.elem.value) return;
// store new value
this.currentValue = this.elem.value;

// clear the selected index
this.selectedItem = -1;
this.results.querySelectorAll('div').forEach((elem) => elem.classList.remove('selected'));

// if less than minimum don't query api
if (this.currentValue.length < this.options.minChars) {
this.hideSuggestions();
return;
}

this.getSuggestions(this.currentValue);
}

async getSuggestions(search) {
// assemble options
const searchOptions = { ...this.options.params };
searchOptions[this.options.paramName] = search;

// build search url
const url = new URL(this.options.serviceUrl);
Object.entries(searchOptions).forEach(([key, value]) => {
url.searchParams.append(key, value);
});

let result = this.cachedResponses[search];
if (!result) {
// make the request
const resultRaw = await json(url);

// use the provided parser
result = this.options.transformResult(resultRaw);
}

// store suggestions
this.cachedResponses[search] = result.suggestions;
this.suggestions = result.suggestions;

// populate the suggestion area
this.populateSuggestions();
}

populateSuggestions() {
if (this.suggestions.length === 0) {
if (this.options.showNoSuggestionNotice) {
this.noSuggestionNotice();
} else {
this.hideSuggestions();
}
return;
}

// build the list
const suggestionElems = this.suggestions.map((suggested, idx) => {
const elem = document.createElement('div');
elem.classList.add('suggestion');
elem.dataset.item = idx;
elem.innerHTML = (formatResult(suggested.value, this.currentValue));
return elem.outerHTML;
});

this.results.innerHTML = suggestionElems.join('');
this.showSuggestions();
}

noSuggestionNotice() {
this.results.innerHTML = `<div>${this.options.noSuggestionNotice}</div>`;
this.showSuggestions();
}
}

export default AutoComplete;
Loading

0 comments on commit c7eb56f

Please sign in to comment.