Skip to content

Commit 1454732

Browse files
committed
add: better sorting for search
1 parent 8b3f9a7 commit 1454732

File tree

2 files changed

+166
-97
lines changed

2 files changed

+166
-97
lines changed

basxbread/layout/components/forms/widgets.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -359,37 +359,40 @@ def __init__(
359359
style="",
360360
placeholder=_("Type to search"),
361361
onkeyup="""
362-
let searchInputs = event.target.value.toLowerCase().split(' ').filter((term) => term.length > 0)
362+
let query = event.target.value.toLowerCase().split(' ').filter((term) => term.length > 0)
363363
let resultsElem = this.nextElementSibling
364364
resultsElem.innerHTML = ''
365-
if(searchInputs.length == 0 || searchInputs.findIndex((term) => term.length >= 2) == -1) {
365+
if(query.length == 0 || query.findIndex((term) => term.length >= 2) == -1) {
366366
return
367367
}
368368
let searchElem = this
369369
let selectElem = this.nextElementSibling.nextElementSibling;
370+
var resultList = []
370371
for(let opt of selectElem.childNodes) {
371372
let content = opt.innerHTML.toLowerCase()
372-
if(searchInputs.findIndex((term) => content.includes(term)) != -1) {
373-
let entry = $.create("div", {
373+
if(query.findIndex((term) => content.includes(term)) != -1) {
374+
resultList.push($.create("div", {
374375
class: 'hoverable',
375-
style: 'padding: 0.5rem',
376+
style: {padding: "0.5rem"},
376377
value: opt.getAttribute('value'),
377378
contents: [opt.innerText],
378379
onclick: (e) => {
379380
selectElem.value = e.target.getAttribute('value')
380381
searchElem.classList.toggle('hidden')
381382
resultsElem.classList.toggle('hidden')
382383
selectElem.classList.toggle('hidden')
383-
}
384-
})
385-
resultsElem.appendChild(entry)
384+
},
385+
rank: searchMatchRank(query, opt.innerText)
386+
}))
386387
}
387388
}
389+
let sorted = resultList.sort((a, b) => parseFloat(a.getAttribute('rank')) > parseFloat(b.getAttribute('rank')) ? 1 : -1).slice(0, 10)
390+
sorted.forEach(node => resultsElem.appendChild(node))
388391
""",
389392
),
390393
hg.DIV(
391-
_class="bx--select-input hidden",
392-
style="position: absolute; height: auto; min-height: 0.5rem; top: 2.5rem; z-index: 10; box-shadow: gray 0 3px 6px 3px; overflow-y: scroll; overflow-x: hidden",
394+
_class="hidden",
395+
style="position: absolute; height: auto; min-width: 5rem; min-height: 0.5rem; top: 2.5rem; z-index: 10; box-shadow: gray 0 3px 6px 3px; overflow-y: scroll; overflow-x: hidden; margin-left: 1.5rem",
393396
),
394397
hg.SELECT(
395398
hg.Iterator(

basxbread/static/js/main.js

Lines changed: 153 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,197 @@
1-
21
// support onload after page loaded for all elements
3-
document.addEventListener(
4-
"DOMContentLoaded",
5-
() => basxbread_load_elements()
6-
);
2+
document.addEventListener("DOMContentLoaded", () => basxbread_load_elements());
73

84
// same for ajax content
9-
htmx.onLoad(function(content) {
10-
basxbread_load_elements();
5+
htmx.onLoad(function (content) {
6+
basxbread_load_elements();
117
});
128

139
function basxbread_load_elements() {
14-
$$('[onload]:not(body):not(frame):not(iframe):not(img):not(link):not(script):not(style)')._.fire("load")
10+
$$(
11+
"[onload]:not(body):not(frame):not(iframe):not(img):not(link):not(script):not(style)"
12+
)._.fire("load");
1513
}
1614

1715
// make sure to display content from faild ajax requests
18-
htmx.on('htmx:responseError', function(event) {
19-
console.log(event);
20-
event.detail.target.innerHTML = event.detail.xhr.responseText;
16+
htmx.on("htmx:responseError", function (event) {
17+
event.detail.target.innerHTML = event.detail.xhr.responseText;
2118
});
2219

2320
// some helper which are used for the multiselect widget
2421
function updateMultiselect(e) {
25-
let elem = $('.bx--list-box__selection', e);
26-
if (elem) {
27-
elem.firstChild.textContent = $$('fieldset input[type=checkbox][checked]', e).length;
28-
}
22+
let elem = $(".bx--list-box__selection", e);
23+
if (elem) {
24+
elem.firstChild.textContent = $$(
25+
"fieldset input[type=checkbox][checked]",
26+
e
27+
).length;
28+
}
2929
}
3030

3131
function filterOptions(e) {
32-
var searchterm = $('input.bx--text-input', e).value.toLowerCase()
33-
for(i of $$('fieldset .bx--list-box__menu-item', e)) {
34-
if (i.innerText.toLowerCase().includes(searchterm)) {
35-
$(i)._.style({display: "initial"});
36-
} else {
37-
$(i)._.style({display: "none"});
38-
}
32+
var searchterm = $("input.bx--text-input", e).value.toLowerCase();
33+
for (i of $$("fieldset .bx--list-box__menu-item", e)) {
34+
if (i.innerText.toLowerCase().includes(searchterm)) {
35+
$(i)._.style({ display: "initial" });
36+
} else {
37+
$(i)._.style({ display: "none" });
3938
}
39+
}
4040
}
4141

4242
function clearMultiselect(e) {
43-
for(i of $$('fieldset input[type=checkbox][checked]', e)) {
44-
i.parentElement.setAttribute("data-contained-checkbox-state", "false");
45-
i.removeAttribute("checked");
46-
i.removeAttribute("aria-checked");
47-
}
48-
updateMultiselect(e);
43+
for (i of $$("fieldset input[type=checkbox][checked]", e)) {
44+
i.parentElement.setAttribute("data-contained-checkbox-state", "false");
45+
i.removeAttribute("checked");
46+
i.removeAttribute("aria-checked");
47+
}
48+
updateMultiselect(e);
4949
}
5050

5151
// function which make the django inline-formset mechanism work dynamically
5252
function init_formset(form_prefix) {
53-
update_add_button(form_prefix);
53+
update_add_button(form_prefix);
5454
}
5555

5656
function delete_inline_element(checkbox, element) {
57-
checkbox.checked = true;
58-
element.style.display = "none";
57+
checkbox.checked = true;
58+
element.style.display = "none";
5959
}
6060

6161
function update_add_button(form_prefix) {
62-
var formcount = $('#id_' + form_prefix + '-TOTAL_FORMS');
63-
var maxforms = $('#id_' + form_prefix + '-MAX_NUM_FORMS');
64-
var addbutton = $('#add_' + form_prefix + '_button');
65-
if(addbutton) {
66-
addbutton.style.display = "inline-flex";
67-
if(parseInt(formcount.value) >= parseInt(maxforms.value)) {
68-
addbutton.style.display = "none";
69-
}
62+
var formcount = $("#id_" + form_prefix + "-TOTAL_FORMS");
63+
var maxforms = $("#id_" + form_prefix + "-MAX_NUM_FORMS");
64+
var addbutton = $("#add_" + form_prefix + "_button");
65+
if (addbutton) {
66+
addbutton.style.display = "inline-flex";
67+
if (parseInt(formcount.value) >= parseInt(maxforms.value)) {
68+
addbutton.style.display = "none";
7069
}
70+
}
7171
}
7272

7373
function formset_add(form_prefix, list_container) {
74-
let container_elem = $(list_container);
75-
76-
// some magic to add a new form element, copied from template empty_XXX_form
77-
// DOMParser.parseFromString does not work because it create a valid DOM document but we work with DOM elements
78-
let placeholder = document.createElement("DIV");
79-
container_elem.appendChild(placeholder);
80-
var formcount = $('#id_' + form_prefix + '-TOTAL_FORMS')
81-
var newElementStr = $('#empty_' + form_prefix + '_form').innerText.replace(/__prefix__/g, formcount.value)
82-
placeholder.outerHTML = newElementStr;
83-
84-
formcount.value = parseInt(formcount.value) + 1;
85-
update_add_button(form_prefix);
86-
updateMultiselect(container_elem);
87-
88-
basxbread_load_elements();
89-
htmx.process(container_elem);
74+
let container_elem = $(list_container);
75+
76+
// some magic to add a new form element, copied from template empty_XXX_form
77+
// DOMParser.parseFromString does not work because it create a valid DOM document but we work with DOM elements
78+
let placeholder = document.createElement("DIV");
79+
container_elem.appendChild(placeholder);
80+
var formcount = $("#id_" + form_prefix + "-TOTAL_FORMS");
81+
var newElementStr = $("#empty_" + form_prefix + "_form").innerText.replace(
82+
/__prefix__/g,
83+
formcount.value
84+
);
85+
placeholder.outerHTML = newElementStr;
86+
87+
formcount.value = parseInt(formcount.value) + 1;
88+
update_add_button(form_prefix);
89+
updateMultiselect(container_elem);
90+
91+
basxbread_load_elements();
92+
htmx.process(container_elem);
9093
}
9194

9295
// Function which is used to collect checkboxes from a datatable and submit the selected checkboxes to a URL for bulk processing
93-
function submitbulkaction(table, actionurl, method="GET") {
94-
let form = document.createElement("form");
95-
form.method = method;
96-
form.action = actionurl;
97-
98-
// make sure query parameters of the URL are passed as well (necessary for filters)
99-
let url = new URL(actionurl, new URL(document.baseURI).origin);
100-
for (const [key, value] of url.searchParams) {
101-
let input = document.createElement("input");
102-
input.name = key;
103-
input.type = "hidden";
104-
input.value = value;
105-
form.appendChild(input);
106-
}
107-
108-
for(let checkbox of table.querySelectorAll('input[type=checkbox][data-event=select]')) {
109-
form.appendChild(checkbox.cloneNode(true));
110-
}
111-
112-
for(let checkbox of table.querySelectorAll('input[type=checkbox][data-event=select-all]')) {
113-
form.appendChild(checkbox.cloneNode(true));
114-
}
115-
116-
document.body.appendChild(form);
117-
form.submit();
96+
function submitbulkaction(table, actionurl, method = "GET") {
97+
let form = document.createElement("form");
98+
form.method = method;
99+
form.action = actionurl;
100+
101+
// make sure query parameters of the URL are passed as well (necessary for filters)
102+
let url = new URL(actionurl, new URL(document.baseURI).origin);
103+
for (const [key, value] of url.searchParams) {
104+
let input = document.createElement("input");
105+
input.name = key;
106+
input.type = "hidden";
107+
input.value = value;
108+
form.appendChild(input);
109+
}
110+
111+
for (let checkbox of table.querySelectorAll(
112+
"input[type=checkbox][data-event=select]"
113+
)) {
114+
form.appendChild(checkbox.cloneNode(true));
115+
}
116+
117+
for (let checkbox of table.querySelectorAll(
118+
"input[type=checkbox][data-event=select-all]"
119+
)) {
120+
form.appendChild(checkbox.cloneNode(true));
121+
}
122+
123+
document.body.appendChild(form);
124+
form.submit();
118125
}
119126

120127
// helper functions to set and get basxbread-namespaced cookies
121128
function setBasxBreadCookie(key, value) {
122-
document.cookie = "basxbread-" + key + "=" + encodeURIComponent(value) + "; path=/";
129+
document.cookie =
130+
"basxbread-" + key + "=" + encodeURIComponent(value) + "; path=/";
131+
}
132+
133+
function getBasxBreadCookie(key, _default = null) {
134+
var ret = document.cookie
135+
.split("; ")
136+
.find((row) => row.startsWith("basxbread-" + key + "="));
137+
if (!ret) return _default;
138+
ret = ret.split("=")[1];
139+
return ret ? decodeURIComponent(ret) : _default;
140+
}
141+
142+
function getEditDistance(a, b) {
143+
if (a.length == 0) return b.length;
144+
if (b.length == 0) return a.length;
145+
146+
var matrix = [];
147+
148+
// increment along the first column of each row
149+
var i;
150+
for (i = 0; i <= b.length; i++) {
151+
matrix[i] = [i];
152+
}
153+
154+
// increment each column in the first row
155+
var j;
156+
for (j = 0; j <= a.length; j++) {
157+
matrix[0][j] = j;
158+
}
159+
160+
// Fill in the rest of the matrix
161+
for (i = 1; i <= b.length; i++) {
162+
for (j = 1; j <= a.length; j++) {
163+
if (b.charAt(i - 1) == a.charAt(j - 1)) {
164+
matrix[i][j] = matrix[i - 1][j - 1];
165+
} else {
166+
matrix[i][j] = Math.min(
167+
matrix[i - 1][j - 1] + 1, // substitution
168+
Math.min(
169+
matrix[i][j - 1] + 1, // insertion
170+
matrix[i - 1][j] + 1
171+
)
172+
); // deletion
173+
}
174+
}
175+
}
176+
177+
return matrix[b.length][a.length];
123178
}
124179

125-
function getBasxBreadCookie(key, _default=null) {
126-
var ret = document.cookie.split('; ').find(row => row.startsWith("basxbread-" + key + '='))
127-
if(!ret)
128-
return _default;
129-
ret = ret.split('=')[1];
130-
return ret ? decodeURIComponent(ret) : _default;
180+
function searchMatchRank(searchTerms, value) {
181+
var score = 0;
182+
let valueTokens = value.toLowerCase().split(" ");
183+
for (let token of valueTokens) {
184+
var lowestScore = 999999;
185+
for (let term of searchTerms) {
186+
if (token == term) {
187+
lowestScore = 0;
188+
} else if (token.startsWith(term)) {
189+
lowestScore = 1 / term.length;
190+
} else {
191+
lowestScore = Math.min(getEditDistance(token, term), lowestScore);
192+
}
193+
}
194+
score += lowestScore;
195+
}
196+
return score;
131197
}

0 commit comments

Comments
 (0)