Enhance rendering and QOL in settings related pages in web UI

This commit is contained in:
Théophile Diot 2024-09-11 10:24:09 +02:00
parent 0d133eab98
commit 197460b1bc
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
7 changed files with 250 additions and 174 deletions

View file

@ -34,6 +34,7 @@
--bs-light: #dbdee0;
--bs-dark: #2b2c40;
--bs-gray: rgba(34, 48, 62, 0.5);
--bs-bw-green-rgb: 46, 172, 104;
--bs-primary-rgb: 11, 53, 74;
--bs-secondary-rgb: 53, 114, 142;
--bs-success-rgb: 113, 221, 55;

View file

@ -429,3 +429,16 @@ a.badge:hover {
left: 1.5rem !important;
z-index: 1080;
}
.multiple-highlight {
background-color: rgba(var(--bs-bw-green-rgb), 0.5);
transition:
background-color 2s ease,
opacity 2s ease;
opacity: 1;
}
.multiple-highlight-fade {
background-color: transparent;
opacity: 1; /* You can set this to 0 if you want it to fade out */
}

View file

@ -3,83 +3,120 @@ $(document).ready(() => {
let currentMode = "easy";
let currentType = "all";
let currentKeywords = "";
const $pluginSearch = $("#plugin-search");
const $pluginTypeSelect = $("#plugin-type-select");
const $pluginDropdownMenu = $("#plugins-dropdown-menu");
const pluginDropdownItems = $("#plugins-dropdown-menu li.nav-item");
const updateUrlParams = (params, removeHash = false) => {
// Create a new URL based on the current location (this keeps both the search params and the hash)
const newUrl = new URL(window.location.href);
// Merge existing search parameters with new parameters
const searchParams = new URLSearchParams(newUrl.search);
// Add or update the parameters with the new values passed in the `params` object
Object.keys(params).forEach((key) => {
searchParams.set(key, params[key]);
Object.entries(params).forEach(([key, value]) => {
if (value === null || value === undefined) {
searchParams.delete(key);
} else {
searchParams.set(key, value);
}
});
// Update the search params of the URL
newUrl.search = searchParams.toString();
// Optionally remove the hash from the URL
if (removeHash) {
newUrl.hash = "";
}
// Push the updated URL (this keeps or removes the hash and updates the search params)
history.pushState(params, document.title, newUrl.toString());
};
$("#select-plugin").on("click", () => $("#plugin-search").focus());
const handleModeChange = (targetClass) => {
currentMode = targetClass.substring(1).replace("navs-modes-", "");
// Debounce for search input to avoid triggering the search on every keystroke
let debounceTimer;
$("#plugin-search").on("input", (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
// Prepare params for the URL update
const params = {};
if (currentType !== "all") params.type = currentType;
// If "easy" is selected, remove the "mode" parameter
if (currentMode === "easy") {
params.mode = null; // Set mode to null to remove it from the URL
updateUrlParams(params); // Call the function without the hash (keep it intact)
} else {
// If another mode is selected, update the "mode" parameter
params.mode = currentMode;
updateUrlParams(params); // Keep the mode in the URL
}
};
const handleTabChange = (targetClass) => {
currentPlugin = targetClass.substring(1).replace("navs-plugins-", "");
// Prepare the params for URL (parameters to be updated in the URL)
const params = {};
if (currentType !== "all") params.type = currentType;
if (currentMode !== "easy") params.mode = currentMode;
// If "general" is selected and a hash exists, remove the hash but keep the parameters
if (currentPlugin === "general" && window.location.hash) {
// Call updateUrlParams with `removeHash = true` to remove the hash fragment
updateUrlParams(params, true);
} else {
// Update the URL hash to the current plugin (e.g., #plugin-name)
window.location.hash = currentPlugin;
// Also update the URL parameters (if any exist) while preserving the hash
updateUrlParams(params);
}
};
const debounce = (func, delay) => {
let debounceTimer;
return (...args) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(this, args), delay);
};
};
$("#select-plugin").on("click", () => $pluginSearch.focus());
$("#plugin-search").on(
"input",
debounce((e) => {
const inputValue = e.target.value.toLowerCase();
let visibleItems = 0;
pluginDropdownItems.each(function () {
const item = $(this);
if (currentType !== "all" && item.data("type") !== currentType) return;
const matches =
(currentType === "all" || item.data("type") === currentType) &&
(item.text().toLowerCase().includes(inputValue) ||
item.find("button").data("bs-target").includes(inputValue));
const text = item.text().toLowerCase();
const pluginId = item
.find("button")
.data("bs-target")
.replace("#navs-plugins-", "");
item.toggle(matches);
text.includes(inputValue) || pluginId.includes(inputValue)
? item.show()
: item.hide();
if (matches) {
visibleItems++; // Increment when an item is shown
}
});
// Show "No Item" message if no items match
const noVisibleItems =
pluginDropdownItems.filter(":visible").length === 0;
if (noVisibleItems && $(".no-items").length === 0) {
$("#plugins-dropdown-menu").append(
'<li class="no-items dropdown-item text-muted">No Item</li>',
);
if (visibleItems === 0) {
if ($pluginDropdownMenu.find(".no-items").length === 0) {
$pluginDropdownMenu.append(
'<li class="no-items dropdown-item text-muted">No Item</li>',
);
}
} else {
$(".no-items").remove();
$pluginDropdownMenu.find(".no-items").remove();
}
}, 300); // 300ms delay for debounce
});
}, 50),
);
// Clear search and "No Item" message when the dropdown is closed
$("#select-plugin").on("hidden.bs.dropdown", () => {
$("#plugin-search").val("").trigger("input");
$(".no-items").remove();
});
const handleModeChange = (targetClass) => {
currentMode = targetClass.substring(1).replace("navs-modes-", "");
if (currentMode === "easy") {
updateUrlParams(currentType !== "all" ? { type: currentType } : {});
} else {
updateUrlParams({ mode: currentMode });
}
};
// Attach event listener to handle mode changes when tabs are switched
$('#service-modes-menu button[data-bs-toggle="tab"]').on(
"shown.bs.tab",
(e) => {
@ -87,19 +124,6 @@ $(document).ready(() => {
},
);
const handleTabChange = (targetClass) => {
currentPlugin = targetClass.substring(1).replace("navs-plugins-", "");
if (currentPlugin === "general" && window.location.hash) {
const params = {};
if (currentType !== "all") params.type = currentType;
if (currentMode !== "easy") params.mode = currentMode;
// Call updateUrlParams with `removeHash = true` to remove the hash
updateUrlParams(params, true);
} else {
window.location.hash = currentPlugin;
}
};
$('#plugins-dropdown-menu button[data-bs-toggle="tab"]').on(
"shown.bs.tab",
(e) => {
@ -122,14 +146,15 @@ $(document).ready(() => {
$("#plugin-type-select").on("change", function () {
currentType = $(this).val();
const params = currentType === "all" ? {} : { type: currentType };
const params =
currentType === "all" ? { type: null } : { type: currentType };
updateUrlParams(params);
pluginDropdownItems.each(function () {
$(this).toggle(
currentType === "all" || $(this).data("type") === currentType,
);
const typeMatches =
currentType === "all" || $(this).data("type") === currentType;
$(this).toggle(typeMatches);
});
const currentPane = $('div[id^="navs-plugins-"].active').first();
@ -152,16 +177,32 @@ $(document).ready(() => {
$(".add-multiple").on("click", function () {
const multipleId = $(this).attr("id").replace("add-", "");
const suffix =
parseInt(
$(`#${multipleId}`)
.find(".multiple-container")
.last()
.find(".multiple-collapse")
.attr("id")
.replace(`${multipleId}-`, ""),
10,
) + 1;
// Get all existing suffixes
const existingContainers = $(`#${multipleId}`).find(".multiple-container");
const existingSuffixes = existingContainers
.map(function () {
return parseInt(
$(this)
.find(".multiple-collapse")
.attr("id")
.replace(`${multipleId}-`, ""),
10,
);
})
.get()
.sort((a, b) => a - b); // Sort the suffixes in ascending order
// Find the first missing suffix
let suffix = 0;
for (let i = 0; i < existingSuffixes.length; i++) {
if (existingSuffixes[i] !== i) {
suffix = i;
break;
}
suffix = existingSuffixes.length; // If no gaps, use the next number
}
const cloneId = `${multipleId}-${suffix}`;
// Clone the first .multiple-container and reset input values
@ -170,82 +211,133 @@ $(document).ready(() => {
.first()
.clone();
// Update the IDs and names of the cloned inputs/selects
multipleClone.find("input, select").each(function () {
const type = $(this).attr("type");
const defaultVal = $(this).data("default");
// Helper function to reset inputs/selects
const resetInputField = (element, suffix) => {
const elementType = element.attr("type");
const defaultVal = element.data("default") || "";
// Enable the inputs/selects and update values
$(this).attr("disabled", false);
const newId = $(this).attr("id").replace("-0", `-${suffix}`);
const newName = `${$(this).attr("name")}_${suffix}`;
$(this).attr("id", newId).attr("name", newName);
// Safeguard checks for missing attributes
const originalId = element.attr("id") || "";
const originalLabelledBy = element.attr("aria-labelledby") || "";
// Update the label for the input/select
const settingLabel = $(this).next("label");
settingLabel.attr("for", newId).text(`${settingLabel.text()}_${suffix}`);
// Update IDs and attributes
const newId = originalId.replace("-0", `-${suffix}`);
const newLabelledBy = originalLabelledBy.replace("-0", `-${suffix}`);
const newName = `${element.attr("name") || ""}_${suffix}`;
// Update value to an empty string or default value
if ($(this).is("select")) {
$(this).val(defaultVal);
$(this)
.find("option")
.each(function () {
$(this).prop("selected", false);
});
} else if (type === "checkbox") {
$(this).prop("checked", false);
element
.attr("id", newId)
.attr("aria-labelledby", newLabelledBy)
.attr("name", newName)
.attr("data-original", defaultVal)
.prop("disabled", false);
// Cache label and description elements to avoid multiple traversals
const settingLabel = element.next("label");
const labelText = (settingLabel.text() || "").trim();
const descriptionLabel = settingLabel
.closest(".col-12")
.find("label")
.first();
// Update label attributes safely
const originalLabelId = descriptionLabel.attr("id") || "";
const newLabelId = originalLabelId.replace("-0", `-${suffix}`);
const originalLabelFor = descriptionLabel.attr("for") || "";
const newLabelFor = originalLabelFor.replace("-0", `-${suffix}`);
descriptionLabel.attr("id", newLabelId).attr("for", newLabelFor);
settingLabel.attr("for", newId).text(`${labelText}_${suffix}`);
// Reset the value
if (element.is("select")) {
element.val(defaultVal);
element.find("option").each(function () {
$(this).prop("selected", $(this).val() === defaultVal);
});
} else if (elementType === "checkbox") {
element.prop("checked", defaultVal === "yes");
} else {
$(this).val("");
element.val(defaultVal);
}
};
// Reset input/select fields inside the clone
multipleClone.find("input, select").each(function () {
resetInputField($(this), suffix);
});
// Update the collapse section's ID and remove tooltips
multipleClone.find(".multiple-collapse").attr("id", `${cloneId}`);
multipleClone
.find(".multiple-collapse")
.attr("id", `${cloneId}`)
.find('[data-bs-toggle="tooltip"]:not(.badge)')
.each(function () {
$(this)
.removeAttr("data-bs-toggle")
.removeAttr("data-bs-placement")
.removeAttr("data-bs-original-title");
});
.removeAttr("data-bs-toggle data-bs-placement data-bs-original-title");
// Add the #suffix to h6
// Update the title with the new suffix
const multipleTitle = multipleClone.find("h6");
multipleTitle.text(`${multipleTitle.text()} #${suffix}`);
const titleText = multipleTitle.text().replace(/#\d+$/, ""); // Remove existing suffix if present
multipleTitle.text(`${titleText} #${suffix}`);
// Append the "REMOVE" button
// Remove "add-multiple" button and append the "REMOVE" button
multipleClone.find(".add-multiple").remove();
multipleClone.find(".show-multiple").before(
`<div>
<button id="remove-${cloneId}"
type="button"
class="btn btn-xs btn-text-danger rounded-pill remove-multiple p-0 pe-2">
<i class="bx bx-trash bx-sm"></i>&nbsp;REMOVE
</button>
</div>`,
);
multipleClone.find(".show-multiple").before(`
<div>
<button id="remove-${cloneId}" type="button" class="btn btn-xs btn-text-danger rounded-pill remove-multiple p-0 pe-2">
<i class="bx bx-trash bx-sm"></i>&nbsp;REMOVE
</button>
</div>
`);
// Append the cloned element to the container
$(`#${multipleId}`).append(multipleClone);
// Insert the new element in the correct order based on suffix
let inserted = false;
existingContainers.each(function () {
const containerSuffix = parseInt(
$(this)
.find(".multiple-collapse")
.attr("id")
.replace(`${multipleId}-`, ""),
10,
);
if (containerSuffix > suffix) {
$(this).before(multipleClone); // Insert before the first container with a higher suffix
inserted = true;
return false; // Break the loop
}
});
if (!inserted) {
// If no higher suffix was found, append to the end
$(`#${multipleId}`).append(multipleClone);
}
// Reinitialize Bootstrap tooltips for the newly added clone
multipleClone.find('[data-bs-toggle="tooltip"]').tooltip();
// Update the data-bs-target and aria-controls attributes of the show-multiple button
// Update show-multiple button's target and aria-controls attributes
const showMultiple = multipleClone.find(".show-multiple");
showMultiple
.attr("data-bs-target", `#${cloneId}`)
.attr("aria-controls", cloneId);
if (showMultiple.text().trim() === "SHOW") showMultiple.trigger("click");
// Scroll to the newly added element
multipleClone.focus()[0].scrollIntoView({
behavior: "smooth",
block: "start",
});
// Scroll to the newly added element smoothly
multipleClone
.focus()[0]
.scrollIntoView({ behavior: "smooth", block: "start" });
// Add the highlight class to the newly added clone with initial opacity
multipleClone.addClass("multiple-highlight");
// Use setTimeout to gradually fade out the highlight effect after the element is added
setTimeout(() => {
multipleClone.addClass("multiple-highlight-fade");
}, 500); // Delay slightly increased to ensure the class is applied
// Remove both classes after the transition completes
setTimeout(() => {
multipleClone.removeClass("multiple-highlight multiple-highlight-fade");
}, 5000); // Adjusted to 5 seconds for a slower effect
});
$(document).on("click", ".remove-multiple", function () {
@ -285,52 +377,6 @@ $(document).ready(() => {
$(this).remove(); // Remove the element after collapse
}, 60);
});
// Update all next elements' IDs and names
elementToRemove.nextAll().each(function () {
const nextId = $(this).find(".multiple-collapse").attr("id");
const nextSuffix = parseInt(
nextId.substring(nextId.lastIndexOf("-") + 1),
10,
);
const newSuffix = nextSuffix - 1;
const newId = nextId.replace(`-${nextSuffix}`, `-${newSuffix}`);
// Update the ID of the next element
$(this).find(".multiple-collapse").attr("id", newId);
const multipleTitle = $(this).find("h6");
multipleTitle.text(function () {
return $(this).text().replace(` #${nextSuffix}`, ` #${newSuffix}`);
});
// Update the input/select name and corresponding label
$(this)
.find("input, select")
.each(function () {
const newName = $(this)
.attr("name")
.replace(`_${nextSuffix}`, `_${newSuffix}`);
$(this).attr("name", newName);
// Find the associated label and update its 'for' attribute and text
const settingLabel = $(`label[for="${$(this).attr("id")}"]`);
if (settingLabel.length) {
settingLabel.attr("for", newId).text(function () {
return $(this).text().replace(`_${nextSuffix}`, `_${newSuffix}`);
});
}
});
// Update the data-bs-target and aria-controls of the show-multiple button
const showMultiple = $(this).find(".show-multiple");
showMultiple
.attr("data-bs-target", `#${newId}`)
.attr("aria-controls", newId);
const removeMultiple = $(this).find(".remove-multiple");
removeMultiple.attr("id", `remove-${newId}`);
});
});
$("#save-settings").on("click", function () {

View file

@ -4,7 +4,7 @@
class="form-check-input"
type="checkbox"
role="switch"
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
data-original="{{ setting_value }}"
data-default="{{ setting_default }}"
{% if setting_value == "yes" %}checked{% endif %}

View file

@ -3,7 +3,7 @@
name="{{ setting }}"
type="{{ setting_data['type'] }}"
class="form-control plugin-setting"
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
pattern="{{ setting_data['regex'] }}"
value="{{ setting_value }}"
data-original="{{ setting_value }}"

View file

@ -88,7 +88,8 @@
role="tabpanel"
aria-labelledby="navs-plugins-{{ plugin['id'] }}-tab"
data-type="{{ plugin['type'] }}">
<div class="card-header">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="pt-1">
<h5 class="card-title d-inline border p-2{{ plugin_types[plugin['type']].get('title-class', '') }}">
{{ plugin["name"] }}&nbsp;&nbsp;v{{ plugin["version"] }}&nbsp;&nbsp;{{ plugin_types[plugin["type"]].get('icon', '<img src="' + pro_diamond_url + '"
alt="Pro plugin"
@ -96,6 +97,21 @@
height="15.5px">') |safe }}
</h5>
<p class="card-subtitle text-muted mt-2">{{ plugin["description"] }}</p>
</div>
<div class="d-flex justify-content-center align-items-center">
<a href="{% if plugin['type'] == 'core' %}https://docs.bunkerweb.io/latest/settings/?utm_campaign=self&utm_source=ui#{% if plugin['id'] == 'general' %}global-settings{% else %}{{ plugin['id'] }}{% endif %}{% else %}https://docs.bunkerweb.io/latest/plugins/?utm_campaign=self&utm_source=ui{% endif %}"
class="btn btn-sm btn-outline-primary rounded-pill"
target="_blank"
rel="noopener">
<i class="bx bx-link"></i>&nbsp;More info
</a>
{% if plugin["page"] %}
<a href="{{ url_for('plugins') }}/{{ plugin['id'] }}"
class="btn btn-sm btn-outline-primary rounded-pill ms-2">
<i class="bx bxs-file-html"></i>&nbsp;Custom page
</a>
{% endif %}
</div>
</div>
<div class="card-body row pb-0">
{% for setting, setting_data in filtered_settings.items() if not setting_data.get('multiple', false) and setting not in blacklisted_settings and (not service_endpoint or setting_data['context'] == "multisite") %}
@ -219,8 +235,8 @@
<div class="col-12{% if multiple_settings %} col-md-6{% endif %}{% if settings|length > 2 and not multiple_multiples %} col-lg-4{% endif %} pb-2"
{% if disabled %}data-bs-toggle="tooltip" data-bs-placement="top" title="Disabled by {{ setting_method }}"{% endif %}>
<div class="d-flex justify-content-between align-items-center">
<label id="multiple-label-{{ plugin['id'] }}-{{ setting_data['id'] }}"
for="multiple-setting-{{ plugin['id'] }}-{{ setting_data['id'] }}"
<label id="multiple-label-{{ plugin['id'] }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
for="multiple-setting-{{ plugin['id'] }}-{{ setting_data['id'] }}-{{ setting_suffix }}"
class="form-label fw-semibold text-truncate">
{{ setting_data["label"]|capitalize }}
</label>

View file

@ -2,7 +2,7 @@
<select id="{{ setting_id_prefix }}setting-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
name="{{ setting }}"
class="form-select"
aria-labelledby="label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
aria-labelledby="{{ setting_id_prefix }}label-{{ plugin['id'] }}-{{ setting_data['id'] }}{{ setting_id_suffix }}"
data-original="{{ setting_value }}"
data-default="{{ setting_default }}"
{% if disabled %}disabled{% endif %}>