Settings Navigation Repositioned

This commit is contained in:
2026-03-06 22:02:23 -04:00
parent af75ea54a6
commit 8ce1c36bb9
10 changed files with 321 additions and 195 deletions

View File

@@ -153,90 +153,7 @@
}
// ═══════════════════════════════════════════════════════════════════
// 6. SMOOTH HORIZONTAL DRAG — trending discussions
// Prevents link clicks during drag, smooth momentum scrolling
// ═══════════════════════════════════════════════════════════════════
$$(".cl-topics__scroll").forEach(function (scrollEl) {
var isDown = false;
var startX = 0;
var scrollStart = 0;
var moved = false;
var velX = 0;
var lastX = 0;
var lastTime = 0;
var momentumId = null;
function stopMomentum() {
if (momentumId) {
cancelAnimationFrame(momentumId);
momentumId = null;
}
}
function doMomentum() {
if (Math.abs(velX) < 0.5) return;
scrollEl.scrollLeft -= velX;
velX *= 0.92;
momentumId = requestAnimationFrame(doMomentum);
}
scrollEl.addEventListener("mousedown", function (ev) {
stopMomentum();
isDown = true;
moved = false;
startX = ev.pageX;
scrollStart = scrollEl.scrollLeft;
lastX = ev.pageX;
lastTime = Date.now();
velX = 0;
scrollEl.style.scrollSnapType = "none";
scrollEl.style.userSelect = "none";
});
window.addEventListener("mousemove", function (ev) {
if (!isDown) return;
var dx = ev.pageX - startX;
if (Math.abs(dx) > 3) moved = true;
var now = Date.now();
var dt = now - lastTime;
if (dt > 0) {
velX = (ev.pageX - lastX) / dt * 16;
}
lastX = ev.pageX;
lastTime = now;
scrollEl.scrollLeft = scrollStart - dx;
});
function endDrag() {
if (!isDown) return;
isDown = false;
scrollEl.style.userSelect = "";
if (Math.abs(velX) > 1) {
doMomentum();
}
// Re-enable snap after a tick
setTimeout(function () {
scrollEl.style.scrollSnapType = "";
}, 100);
}
window.addEventListener("mouseup", endDrag);
scrollEl.addEventListener("mouseleave", endDrag);
// Prevent link navigation if we dragged
scrollEl.addEventListener("click", function (ev) {
if (moved) {
ev.preventDefault();
ev.stopPropagation();
moved = false;
}
}, true);
// Touch support — native scrolling works, no extra handling needed
});
// ═══════════════════════════════════════════════════════════════════
// 7. APP BADGE DETECTION
// 6. APP BADGE DETECTION
// ═══════════════════════════════════════════════════════════════════
var ua = navigator.userAgent || "";
if (/iPhone|iPad|iPod/.test(ua)) $$(".cl-app-badge--ios").forEach(function (e) { e.classList.add("highlighted"); });

View File

@@ -33,7 +33,7 @@ const TABS = [
id: "stats",
label: "Stats",
settings: new Set([
"stats_title", "stat_icon_color",
"stats_title", "stat_icon_color", "stat_icon_bg_color", "stat_icon_shape", "stat_counter_color",
"stat_members_label", "stat_topics_label", "stat_posts_label",
"stat_likes_label", "stat_chats_label",
"stats_bg_dark", "stats_bg_light", "stats_min_height", "stats_border_style"
@@ -70,7 +70,7 @@ const TABS = [
id: "groups",
label: "Spaces",
settings: new Set([
"groups_enabled", "groups_title", "groups_count",
"groups_enabled", "groups_title", "groups_count", "groups_selected", "groups_card_bg_color",
"groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style"
])
},
@@ -94,46 +94,73 @@ const TABS = [
"footer_description", "footer_text", "footer_links",
"footer_bg_dark", "footer_bg_light", "footer_border_style"
])
},
{
id: "css",
label: "Custom CSS",
settings: new Set(["custom_css"])
}
];
let currentTab = "general";
let filterActive = false;
let isActive = false;
let recheckTimer = null;
function getContainer() {
return (
document.querySelector(".admin-plugin-config-area") ||
document.querySelector(".admin-detail")
);
}
function applyTabFilter() {
const container = getContainer();
if (!container) return;
function applyTabFilter(container) {
const tab = TABS.find((t) => t.id === currentTab);
if (!tab) return;
container.querySelectorAll(".row.setting[data-setting]").forEach((row) => {
const name = row.getAttribute("data-setting");
row.style.display = tab.settings.has(name) ? "" : "none";
row.classList.toggle(
"cl-tab-hidden",
!filterActive && !tab.settings.has(name)
);
});
const tabBar = container.querySelector(".cl-admin-tabs");
if (tabBar) {
tabBar.classList.toggle("filter-active", filterActive);
}
}
function findFilterInput(container) {
for (const input of container.querySelectorAll("input")) {
if (input.closest(".row.setting") || input.closest(".cl-admin-tabs")) {
continue;
}
const t = (input.type || "text").toLowerCase();
if (t === "text" || t === "search") return input;
}
return null;
}
function buildTabsUI() {
const container =
document.querySelector(".admin-plugin-config-area") ||
document.querySelector(".admin-detail");
const container = getContainer();
if (!container) return false;
// Already injected?
if (container.querySelector(".cl-admin-tabs")) return true;
// Already injected — just re-apply filter
if (container.querySelector(".cl-admin-tabs")) {
applyTabFilter();
return true;
}
const allRows = container.querySelectorAll(".row.setting[data-setting]");
if (allRows.length < 5) return false;
// Verify our settings are present
const firstTab = TABS[0];
// Verify our plugin settings are present
const hasOurs = Array.from(allRows).some((row) =>
firstTab.settings.has(row.getAttribute("data-setting"))
TABS[0].settings.has(row.getAttribute("data-setting"))
);
if (!hasOurs) return false;
// Create tab bar
// Build tab bar
const tabBar = document.createElement("div");
tabBar.className = "cl-admin-tabs";
@@ -141,30 +168,85 @@ function buildTabsUI() {
const btn = document.createElement("button");
btn.className = "cl-admin-tab" + (tab.id === currentTab ? " active" : "");
btn.textContent = tab.label;
btn.setAttribute("data-tab", tab.id);
btn.dataset.tab = tab.id;
btn.addEventListener("click", () => {
currentTab = tab.id;
filterActive = false;
// Clear Discourse filter input so it doesn't conflict
const fi = findFilterInput(container);
if (fi && fi.value) {
fi.value = "";
fi.dispatchEvent(new Event("input", { bubbles: true }));
}
tabBar
.querySelectorAll(".cl-admin-tab")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
applyTabFilter(container);
applyTabFilter();
});
tabBar.appendChild(btn);
});
// Insert tab bar before the first setting row
const settingsParent = allRows[0].parentNode;
settingsParent.insertBefore(tabBar, allRows[0]);
// ── Insertion strategy: place tabs as high as possible ──
let inserted = false;
// Strategy 1: Top of .admin-plugin-config-area__content (above filter bar)
const contentArea = container.querySelector(
".admin-plugin-config-area__content"
);
if (contentArea) {
const form = contentArea.querySelector("form");
const target = form || contentArea;
target.insertBefore(tabBar, target.firstChild);
inserted = true;
}
// Strategy 2: Before the filter controls
if (!inserted) {
const filterArea = container.querySelector(
".admin-site-settings-filter-controls, .setting-filter"
);
if (filterArea) {
filterArea.parentNode.insertBefore(tabBar, filterArea);
inserted = true;
}
}
// Strategy 3: Before the first setting row (fallback)
if (!inserted) {
allRows[0].parentNode.insertBefore(tabBar, allRows[0]);
}
// Add class to disable separator borders
container.classList.add("cl-tabs-active");
// Apply initial filter
applyTabFilter(container);
applyTabFilter();
return true;
}
// ── Global filter detection via event delegation ──
// This survives DOM re-renders because it's on document, not on a specific input
document.addEventListener(
"input",
(e) => {
if (!isActive) return;
const t = e.target;
if (!t || !t.closest) return;
if (t.closest(".row.setting") || t.closest(".cl-admin-tabs")) return;
const container = getContainer();
if (!container || !container.contains(t)) return;
const hasText = t.value.trim().length > 0;
if (hasText !== filterActive) {
filterActive = hasText;
applyTabFilter();
}
},
true
);
export default {
name: "community-landing-admin-tabs",
@@ -175,6 +257,10 @@ export default {
url.includes("community-landing") ||
url.includes("community_landing")
) {
isActive = true;
filterActive = false;
// Initial injection with retries
let attempts = 0;
const tryInject = () => {
if (buildTabsUI() || attempts > 15) return;
@@ -182,6 +268,28 @@ export default {
setTimeout(tryInject, 200);
};
tryInject();
// Periodic re-check: re-injects tab bar if Discourse re-renders the DOM
if (!recheckTimer) {
recheckTimer = setInterval(() => {
if (!isActive) {
clearInterval(recheckTimer);
recheckTimer = null;
return;
}
const c = getContainer();
if (c && !c.querySelector(".cl-admin-tabs")) {
buildTabsUI();
}
}, 500);
}
} else {
// Left plugin settings page — clean up
isActive = false;
if (recheckTimer) {
clearInterval(recheckTimer);
recheckTimer = null;
}
}
});
});