diff --git a/.eslintrc.js b/.eslintrc.js
index 89eac82..d3a16b0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,7 +4,6 @@ module.exports = {
commonjs: true,
es6: true,
node: true,
- jquery: true,
},
extends: [
'airbnb-base',
@@ -29,8 +28,8 @@ module.exports = {
indent: [
'error',
'tab',
- {
- SwitchCase: 1
+ {
+ SwitchCase: 1,
},
],
'no-tabs': 0,
diff --git a/gulp/publish-frontend.mjs b/gulp/publish-frontend.mjs
index faca59a..580da30 100644
--- a/gulp/publish-frontend.mjs
+++ b/gulp/publish-frontend.mjs
@@ -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',
@@ -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;
diff --git a/gulp/update-vendor.mjs b/gulp/update-vendor.mjs
index 8bcc1e3..18b952e 100644
--- a/gulp/update-vendor.mjs
+++ b/gulp/update-vendor.mjs
@@ -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',
];
diff --git a/package-lock.json b/package-lock.json
index 5a2f9f7..02afa72 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,8 +27,6 @@
"gulp-s3-upload": "^1.7.3",
"gulp-sass": "^5.1.0",
"gulp-terser": "^2.0.0",
- "jquery": "^3.6.0",
- "jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"sass": "^1.54.0",
@@ -6494,19 +6492,6 @@
"node": ">= 0.6.0"
}
},
- "node_modules/jquery": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
- "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/jquery-touchswipe": {
- "version": "1.6.19",
- "resolved": "https://registry.npmjs.org/jquery-touchswipe/-/jquery-touchswipe-1.6.19.tgz",
- "integrity": "sha512-b0BGje9reNRU3u6ksAK9QqnX7yBRgLNe/wYG7DOfyDlhBlYjayIT8bSOHmcuvptIDW/ubM9CTW/mnZf9Rohuow==",
- "dev": true
- },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
diff --git a/package.json b/package.json
index 6e9eba0..fa0d3d8 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -48,4 +46,4 @@
"express": "^4.17.1",
"ejs": "^3.1.5"
}
-}
+}
\ No newline at end of file
diff --git a/server/scripts/index.mjs b/server/scripts/index.mjs
index 0376b47..7db31f8 100644
--- a/server/scripts/index.mjs
+++ b/server/scripts/index.mjs
@@ -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();
@@ -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',
@@ -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;
};
@@ -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,
diff --git a/server/scripts/modules/autocomplete.mjs b/server/scripts/modules/autocomplete.mjs
new file mode 100644
index 0000000..8d42887
--- /dev/null
+++ b/server/scripts/modules/autocomplete.mjs
@@ -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'), '$1')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/<(\/?strong)>/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 = `
${this.options.noSuggestionNotice}
`;
+ this.showSuggestions();
+ }
+}
+
+export default AutoComplete;
diff --git a/server/scripts/vendor/auto/jquery.js b/server/scripts/vendor/auto/jquery.js
deleted file mode 100644
index 1a86433..0000000
--- a/server/scripts/vendor/auto/jquery.js
+++ /dev/null
@@ -1,10716 +0,0 @@
-/*!
- * jQuery JavaScript Library v3.7.1
- * https://jquery.com/
- *
- * Copyright OpenJS Foundation and other contributors
- * Released under the MIT license
- * https://jquery.org/license
- *
- * Date: 2023-08-28T13:37Z
- */
-( function( global, factory ) {
-
- "use strict";
-
- if ( typeof module === "object" && typeof module.exports === "object" ) {
-
- // For CommonJS and CommonJS-like environments where a proper `window`
- // is present, execute the factory and get jQuery.
- // For environments that do not have a `window` with a `document`
- // (such as Node.js), expose a factory as module.exports.
- // This accentuates the need for the creation of a real `window`.
- // e.g. var jQuery = require("jquery")(window);
- // See ticket trac-14549 for more info.
- module.exports = global.document ?
- factory( global, true ) :
- function( w ) {
- if ( !w.document ) {
- throw new Error( "jQuery requires a window with a document" );
- }
- return factory( w );
- };
- } else {
- factory( global );
- }
-
-// Pass this if window is not defined yet
-} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
-
-// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
-// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
-// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
-// enough that all such attempts are guarded in a try block.
-"use strict";
-
-var arr = [];
-
-var getProto = Object.getPrototypeOf;
-
-var slice = arr.slice;
-
-var flat = arr.flat ? function( array ) {
- return arr.flat.call( array );
-} : function( array ) {
- return arr.concat.apply( [], array );
-};
-
-
-var push = arr.push;
-
-var indexOf = arr.indexOf;
-
-var class2type = {};
-
-var toString = class2type.toString;
-
-var hasOwn = class2type.hasOwnProperty;
-
-var fnToString = hasOwn.toString;
-
-var ObjectFunctionString = fnToString.call( Object );
-
-var support = {};
-
-var isFunction = function isFunction( obj ) {
-
- // Support: Chrome <=57, Firefox <=52
- // In some browsers, typeof returns "function" for HTML