;(function ($) {
$.fn.autofill = function (options = {}) {
const PLUGIN_VERSION = 0.3
const defaults = {
debug: false,
autofillSelection: true,
itemsLimit: 5,
values: [],
datasetURL: "",
datasetMethod: "GET",
datasetPostData: {},
datasetHeaders: {},
datasetFormatting: null,
minCharacters: 3,
minDelay: 250,
onLoading: null,
onUpdate: null,
onSelect: null,
onEmpty: null,
onError: null,
darkMode: false,
fullWidth: true,
}
const settings = $.extend(defaults, options)
if (settings.debug) {
console.log("[AUTOFILL] PLUGIN_VERSION", PLUGIN_VERSION)
}
return this.each(function () {
/*
* ELEMENTS
*/
const elem = $(this)
.attr("autocomplete", "off")
.addClass("autofill-input")
const container = $(`
`)
const list = $(
``
)
container.insertAfter(elem)
elem.appendTo(container) //.attr("data-bs-toggle", "dropdown")
list.appendTo(container)
/*
* DATA HOLDERS
*/
const suggest = new Map()
const found = new Set()
var previousValue = ""
/*
* LOCAL SETTINGS FROM THE ELEMENT ITSELF
+ Being able to rewrite the global settings of the selector is just an extra
*/
settings.datasetURL = elem.data("autofill-dataseturl")
? elem.data("autofill-dataseturl").toString()
: settings.datasetURL.toString()
settings.datasetMethod = elem.data("autofill-datasetmethod")
? elem.data("autofill-datasetmethod").toString()
: settings.datasetMethod.toString()
settings.itemsLimit =
(elem.data("autofill-itemslimit") &&
NaN !== parseInt(elem.data("autofill-itemslimit")) &&
parseInt(elem.data("autofill-itemslimit")).toString() ===
elem.data("autofill-itemslimit").toString() &&
0 < parseInt(elem.data("autofill-itemslimit"))
? parseInt(elem.data("autofill-itemslimit"))
: 5) ||
parseInt(settings.itemsLimit) ||
5
settings.autofillSelection =
undefined !== elem.data("autofill-autofillselection")
? Boolean(elem.data("autofill-autofillselection"))
: settings.autofillSelection
settings.values =
undefined !== elem.data("autofill-values")
? elem.data("autofill-values").split("|")
: settings.values
elem.__autofillUpdate = function () {
list.removeClass("show").empty()
if (
!elem.val().trim() ||
elem.val().trim().length < settings.minCharacters
) {
return null
}
if (found.size) {
found.forEach((v) => {
const item = $(
`${v.id.replace(
new RegExp(`(${elem.val()})`, "gmi"),
"$1"
)}`
)
item.on("click", function (e) {
e.preventDefault()
e.stopImmediatePropagation()
list.removeClass("show")
if (settings.autofillSelection) {
elem.val(v.id)
} else {
elem.val("")
}
elem.trigger("focus").trigger(
"autofill-selected",
v
)
if (
typeof function () {} ===
typeof settings.onSelect
) {
settings.onSelect(v, item)
}
})
list.append(item)
})
} else {
const item = $(
`${$.fn.autofill.lang.emptyTable}`
)
item.on("click", function (e) {
e.preventDefault()
list.removeClass("show")
elem.trigger("focus")
})
list.append(item)
elem.trigger("autofill-empty", list)
if (typeof function () {} === typeof settings.onEmpty) {
settings.onEmpty(item)
}
}
elem.trigger("autofill-update", list)
if (typeof function () {} === typeof settings.onUpdate) {
settings.onUpdate(list)
}
list.addClass("show")
}
elem.__timers = {}
if (settings.values.length) {
$.each(settings.values, function (i, v) {
suggest.set(v, v)
})
}
elem.on("keyup", function (e) {
if ("ArrowUp" == e.key || "ArrowDown" == e.key) {
if ("ArrowUp" == e.key) {
if (
!list.find("a.active").length ||
list.find(":first-child > a.active").length
) {
list.find("a.active").removeClass("active")
list.find(":last-child > a").addClass("active")
} else {
list.find("a.active")
.addClass("active-previous")
.parent()
.prev()
.find("a")
.addClass("active")
list.find(".active-previous").removeClass(
"active active-previous"
)
}
} else if ("ArrowDown" == e.key) {
if (
!list.find("a.active").length ||
list.find(":last-child > a.active").length
) {
list.find("a.active").removeClass("active")
list.find(":first-child > a").addClass("active")
} else {
list.find("a.active")
.addClass("active-previous")
.parent()
.next()
.find("a")
.addClass("active")
list.find(".active-previous").removeClass(
"active active-previous"
)
}
}
return null
} else if (
("Enter" == e.key || "Return" == e.key) &&
list.find("a.active").length
) {
list.find("a.active").trigger("click")
return null
} else if ("Esc" == e.key || "Escape" == e.key) {
list.removeClass("show")
return null
}
const text = elem.val()
list.removeClass("show")
try {
clearInterval(elem.__timers)
} catch (tErr) {}
if (
!text.trim() ||
text.trim().length < settings.minCharacters
) {
return null
}
if (previousValue.trim() == text.trim()) {
list.addClass("show")
return null
}
previousValue = text.trim()
found.clear()
if (settings.datasetURL) {
// TBA
found.clear()
elem.trigger("autofill-loading")
elem.__timers = setTimeout(() => {
const datasetPostData = $.extend(
{},
typeof function () {} ===
typeof settings.datasetPostData
? settings.datasetPostData()
: settings.datasetPostData,
{ q: text },
settings.debug ? { __v: PLUGIN_VERSION } : {}
)
if (settings.debug) {
console.log(
"[AUTOFILL] datasetPostData:",
datasetPostData
)
}
$.ajax({
url: settings.datasetURL,
method: settings.datasetMethod,
data: datasetPostData,
dataType: "JSON",
headers: $.extend(
{
Accept: "application/json",
},
settings.datasetHeaders
),
cache: false,
beforeSend: () => {
if (settings.debug) {
console.log("[AUTOFILL] AJAX:", {
url: settings.datasetURL,
method: settings.datasetMethod,
data: datasetPostData,
dataType: "JSON",
headers: $.extend(
{
Accept: "application/json",
},
settings.datasetHeaders
),
cache: false,
})
}
},
})
.then((data) => {
if (settings.debug) {
console.log("[AUTOFILL] AJAX result:", data)
}
if (
typeof function () {} ===
typeof settings.datasetFormatting
) {
data = settings.datasetFormatting(data)
}
$.each(data, (i, v) => {
found.add({
id: i,
object: v,
})
})
elem.__autofillUpdate()
})
.catch((a, b, c) => {
if (settings.debug) {
console.log(
"[AUTOFILL] AJAX error:",
a,
b,
c
)
}
console.error(a)
console.error(b)
console.error(c)
elem.trigger("autofill-error", a, b, c)
if (
typeof function () {} ===
typeof settings.onError
) {
settings.onError(a, b, c)
}
})
}, settings.minDelay)
} else {
suggest.forEach((v) => {
if (settings.itemsLimit <= found.size) {
return null
}
if (
v.toLowerCase() == text.toLowerCase() ||
v.toLowerCase().includes(text.toLowerCase())
) {
found.add({
id: v,
object: v,
})
}
})
if (settings.debug) {
console.log("[AUTOFILL] about to suggest:", found)
}
elem.__autofillUpdate()
}
})
/*
* LOADING EVENT
- Triggered during AJAX calls
*/
elem.on("autofill-loading", function () {
const item = $(
`${$.fn.autofill.lang.processing}`
)
item.on("click", function (e) {
e.preventDefault()
list.removeClass("show")
elem.trigger("focus")
})
list.empty().append(item).addClass("show")
if (typeof function () {} === typeof settings.onLoading) {
settings.onLoading(item)
}
})
})
}
$.fn.autofill.lang = {
emptyTable: "Nothing to suggest...",
processing: "Processing...",
}
})(jQuery)