Skip to content

Commit 1690730

Browse files
committed
Change code to use bootstrap-multiselect
Signed-off-by: yubiuser <github@yubiuser.dev>
1 parent f00c3d4 commit 1690730

5 files changed

Lines changed: 158 additions & 60 deletions

File tree

scripts/js/groups-clients.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function initTable() {
189189

190190
// Select assigned groups
191191
selectEl.val(data.groups);
192-
// Initialize Tom Select
192+
// Initialize group multi-select
193193
const ts = utils.createGroupSelect(selectEl, {
194194
onChange() {
195195
// enable Apply button
@@ -198,6 +198,8 @@ function initTable() {
198198
.addClass("btn-success")
199199
.prop("disabled", false)
200200
.on("click", () => {
201+
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
202+
ts.close();
201203
editClient.call(selectEl);
202204
});
203205
}
@@ -213,7 +215,7 @@ function initTable() {
213215
},
214216
});
215217
$(ts.dropdown)
216-
.find(".ts-actions-box")
218+
.find(".multiselect-actions-box")
217219
.append(
218220
'<button type="button" id=btn_apply_' +
219221
dataId +

scripts/js/groups-domains.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function initTable() {
233233

234234
// Select assigned groups
235235
selectEl.val(data.groups);
236-
// Initialize Tom Select
236+
// Initialize group multi-select
237237
const applyBtn = "#btn_apply_" + dataId;
238238
const ts = utils.createGroupSelect(selectEl, {
239239
onChange() {
@@ -244,6 +244,8 @@ function initTable() {
244244
.addClass("btn-success")
245245
.prop("disabled", false)
246246
.on("click", () => {
247+
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
248+
ts.close();
247249
editDomain.call(selectEl);
248250
});
249251
}
@@ -261,7 +263,7 @@ function initTable() {
261263
},
262264
});
263265
$(ts.dropdown)
264-
.find(".ts-actions-box")
266+
.find(".multiselect-actions-box")
265267
.append(
266268
'<button type="button" id=btn_apply_' +
267269
dataId +

scripts/js/groups-lists.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ function initTable() {
285285

286286
// Select assigned groups
287287
selectEl.val(data.groups);
288-
// Initialize Tom Select
288+
// Initialize group multi-select
289289
const ts = utils.createGroupSelect(selectEl, {
290290
onChange() {
291291
// enable Apply button
@@ -294,6 +294,8 @@ function initTable() {
294294
.addClass("btn-success")
295295
.prop("disabled", false)
296296
.on("click", () => {
297+
$(applyBtn).removeClass("btn-success").prop("disabled", true).off("click");
298+
ts.close();
297299
editList.call(selectEl);
298300
});
299301
}
@@ -309,7 +311,7 @@ function initTable() {
309311
},
310312
});
311313
$(ts.dropdown)
312-
.find(".ts-actions-box")
314+
.find(".multiselect-actions-box")
313315
.append(
314316
'<button type="button" id=btn_apply_' +
315317
dataId +

scripts/js/utils.js

Lines changed: 127 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* This file is copyright under the latest version of the EUPL.
66
* Please see LICENSE file for your rights under this license. */
77

8-
/* global moment:false, apiFailure: false, updateFtlInfo: false, NProgress:false, WaitMe:false, TomSelect: false */
8+
/* global moment:false, apiFailure: false, updateFtlInfo: false, NProgress:false, WaitMe:false */
99

1010
"use strict";
1111

@@ -358,49 +358,140 @@ function validateHostnameStrict(name) {
358358
}
359359

360360
/**
361-
* Create a Tom Select multi-select out of a <select multiple> element, with
362-
* "Select all"/"Select none" actions injected into the dropdown (bootstrap-
363-
* select used to provide this via its actionsBox option).
361+
* Create a bootstrap-multiselect out of a <select multiple> element, with
362+
* "Select all"/"Select none" actions injected above the options.
364363
* @param {HTMLElement|jQuery} selectEl - The <select multiple> element (or a jQuery wrapper around it)
365-
* @param {object} [options] - Extra options merged into the Tom Select config
366-
* @returns {TomSelect} The created Tom Select instance
364+
* @param {object} [options] - Extra options merged into the bootstrap-multiselect config
365+
* @returns {object} A small wrapper exposing compatibility helpers used by callers
367366
*/
368367
function createGroupSelect(selectEl, options = {}) {
369-
const el = selectEl instanceof HTMLElement ? selectEl : selectEl[0];
370-
const allValues = [...el.options].map(option => option.value);
371-
372-
const ts = new TomSelect(el, {
373-
plugins: ["remove_button"],
374-
create: false,
375-
placeholder: "none selected",
376-
// Tom Select keeps the placeholder visible on multi-selects by default;
377-
// hide it once at least one group is selected so the "none selected"
378-
// hint does not sit below the selected item chips.
379-
hidePlaceholder: true,
380-
// Render the dropdown into <body> instead of nesting it in the table
381-
// cell, since ancestors like .table-responsive/.card clip overflow and
382-
// would otherwise cut it off (bootstrap-select used container: "body"
383-
// for the same reason).
384-
dropdownParent: "body",
385-
...options,
368+
const select = selectEl instanceof HTMLElement ? $(selectEl) : selectEl;
369+
const allValues = select.find("option").map((_, option) => option.value).get();
370+
const { onChange, onDropdownClose, onInitialized, ...multiselectOptions } = options;
371+
372+
select.multiselect({
373+
nonSelectedText: "none selected",
374+
buttonContainer: '<div class="btn-group w-100" />',
375+
buttonWidth: "100%",
376+
onChange(option, checked) {
377+
if (typeof onChange === "function") {
378+
onChange(option, checked);
379+
}
380+
},
381+
onDropdownHidden(event) {
382+
if (typeof onDropdownClose === "function") {
383+
onDropdownClose(event);
384+
}
385+
},
386+
onInitialized(initializedSelect, initializedContainer) {
387+
if (typeof onInitialized === "function") {
388+
onInitialized(initializedSelect, initializedContainer);
389+
}
390+
},
391+
...multiselectOptions,
386392
});
387393

388-
const actionsBox = document.createElement("div");
389-
actionsBox.className = "ts-actions-box";
390-
actionsBox.innerHTML =
391-
'<div class="btn-group btn-group-sm">' +
392-
'<button type="button" class="btn btn-secondary btn-sm select-all">All</button>' +
393-
'<button type="button" class="btn btn-secondary btn-sm select-none">None</button>' +
394-
"</div>";
395-
actionsBox.querySelector(".select-all").addEventListener("click", () => {
396-
ts.setValue(allValues);
394+
const multiselect = select.data("multiselect");
395+
const container = multiselect ? multiselect.$container : $();
396+
const button = multiselect ? multiselect.$button : $();
397+
const dropdown = multiselect ? multiselect.$popupContainer : $();
398+
const dropdownParent = dropdown.parent();
399+
const updateDropdownPosition = () => {
400+
if (button.length === 0 || dropdown.length === 0) {
401+
return;
402+
}
403+
404+
const rect = button[0].getBoundingClientRect();
405+
dropdown[0].style.setProperty("--multiselect-trigger-left", `${rect.left}px`);
406+
dropdown[0].style.setProperty("--multiselect-trigger-bottom", `${rect.bottom}px`);
407+
dropdown[0].style.setProperty("--multiselect-trigger-width", `${rect.width}px`);
408+
};
409+
410+
const cleanupDropdown = () => {
411+
$(window).off("resize", updateDropdownPosition).off("scroll", updateDropdownPosition);
412+
413+
if (dropdown.length === 0 || dropdownParent.length === 0) {
414+
return;
415+
}
416+
417+
dropdown.removeClass("multiselect-floating-menu show");
418+
dropdown[0].style.removeProperty("--multiselect-trigger-left");
419+
dropdown[0].style.removeProperty("--multiselect-trigger-bottom");
420+
dropdown[0].style.removeProperty("--multiselect-trigger-width");
421+
dropdown.appendTo(dropdownParent);
422+
};
423+
424+
container
425+
.on("shown.bs.dropdown.multiselectPortal", () => {
426+
if (dropdown.length === 0 || dropdownParent.length === 0) {
427+
return;
428+
}
429+
430+
dropdown.detach().appendTo(document.body);
431+
dropdown.addClass("multiselect-floating-menu");
432+
updateDropdownPosition();
433+
$(window)
434+
.on("resize", updateDropdownPosition)
435+
.on("scroll", updateDropdownPosition);
436+
})
437+
.on("hidden.bs.dropdown.multiselectPortal", () => {
438+
cleanupDropdown();
439+
});
440+
441+
const actionsItem = $(
442+
'<div class="multiselect-actions">' +
443+
'<div class="multiselect-actions-box">' +
444+
'<div class="btn-group btn-group-sm">' +
445+
'<button type="button" class="btn btn-secondary btn-sm select-all">All</button>' +
446+
'<button type="button" class="btn btn-secondary btn-sm select-none">None</button>' +
447+
"</div>" +
448+
"</div>" +
449+
"</div>"
450+
);
451+
452+
actionsItem.find(".select-all").on("click", event => {
453+
event.preventDefault();
454+
select.multiselect("select", allValues, true);
397455
});
398-
actionsBox.querySelector(".select-none").addEventListener("click", () => {
399-
ts.clear();
456+
457+
actionsItem.find(".select-none").on("click", event => {
458+
event.preventDefault();
459+
const selectedValues = select.find("option:selected").map((_, option) => option.value).get();
460+
select.multiselect("deselect", selectedValues, true);
400461
});
401-
ts.dropdown.prepend(actionsBox);
402462

403-
return ts;
463+
dropdown.prepend(actionsItem);
464+
465+
return {
466+
setValue(values) {
467+
const selectedValues = select.find("option:selected").map((_, option) => option.value).get();
468+
469+
if (selectedValues.length > 0) {
470+
select.multiselect("deselect", selectedValues);
471+
}
472+
473+
if (Array.isArray(values) && values.length > 0) {
474+
select.multiselect("select", values);
475+
}
476+
477+
select.multiselect("updateButtonText");
478+
},
479+
clear() {
480+
const selectedValues = select.find("option:selected").map((_, option) => option.value).get();
481+
482+
if (selectedValues.length > 0) {
483+
select.multiselect("deselect", selectedValues);
484+
}
485+
486+
select.multiselect("updateButtonText");
487+
},
488+
close() {
489+
button.removeClass("show").attr("aria-expanded", "false");
490+
container.removeClass("show");
491+
cleanupDropdown();
492+
},
493+
dropdown,
494+
};
404495
}
405496

406497
const backupStorage = {};

style/pi-hole.css

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -555,26 +555,40 @@ tfoot.add-new-item > tr > th {
555555
--bs-form-select-bg-img: url("../../style/icons/form-select-arrow-dark.svg");
556556
}
557557

558-
/* Tom Select "Select all / Select none" actions, injected above the
559-
options list to replace bootstrap-select's actionsBox feature */
560-
.ts-actions-box {
558+
.multiselect-actions-box {
561559
display: flex;
562560
flex-wrap: wrap;
563561
gap: 0.25em;
564562
padding: 0.5em;
565563
border-bottom: 1px solid var(--bs-border-color);
566564
}
567565

568-
.ts-actions-box .btn {
566+
.multiselect-actions-box .btn {
569567
flex: 1 0 auto;
570568
}
571569

572-
.ts-actions-box .btn-group {
570+
.multiselect-actions-box .btn-group {
573571
display: flex;
574572
gap: 2px;
575573
width: 100%;
576574
}
577575

576+
.multiselect-floating-menu {
577+
position: fixed;
578+
inset: auto auto auto auto;
579+
top: var(--multiselect-trigger-bottom);
580+
left: var(--multiselect-trigger-left);
581+
min-width: var(--multiselect-trigger-width);
582+
max-width: calc(100vw - var(--multiselect-trigger-left) - 16px);
583+
z-index: 1055;
584+
}
585+
586+
.multiselect-container .dropdown-item.active,
587+
.multiselect-container .dropdown-item:active {
588+
color: var(--bs-dropdown-link-active-color);
589+
background-color: var(--bs-dropdown-link-active-bg);
590+
}
591+
578592
.sidebar-menu .nav-link p {
579593
width: 100%;
580594
}
@@ -1765,19 +1779,6 @@ td.dnssec i {
17651779
text-align: center;
17661780
}
17671781

1768-
/* Tom Select general styles */
1769-
.ts-control,
1770-
.ts-control input,
1771-
.ts-dropdown {
1772-
font-size: 12px;
1773-
}
1774-
1775-
/* Tom Select - Dark theme colors */
1776-
/* Dropdown menu background and border alignment */
1777-
[data-bs-theme="dark"] .ts-dropdown {
1778-
color: var(--bs-dropdown-link-color);
1779-
}
1780-
17811782
/* Dropdown individual option items */
17821783
[data-bs-theme="dark"] .ts-dropdown .option {
17831784
color: var(--bs-dropdown-link-color);

0 commit comments

Comments
 (0)