mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
refactor: further trim PR #458 debloat across patches, notices, Select, music, archive, search
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { SettingsState } from "@/types/storage";
|
||||
import { settingsState } from "../listeners/SettingsState";
|
||||
import { applyMenuItemVisibility } from "../menuItemVisibility";
|
||||
import { insertKeyAfterInOrder } from "@/seqta/utils/sidebarMenuIcons";
|
||||
import { ensureAnalyticsMenuOrder } from "@/seqta/utils/sidebarMenuIcons";
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import Sortable from "sortablejs";
|
||||
|
||||
@@ -30,20 +30,10 @@ function syncDefaultMenuOrder(menu: HTMLElement) {
|
||||
for (let i = 0; i < childnodes.length; i++) {
|
||||
const key = (childnodes[i] as HTMLElement).dataset.key;
|
||||
if (key && settingsState.defaultmenuorder.indexOf(key) === -1) {
|
||||
if (key === "analytics") {
|
||||
settingsState.defaultmenuorder = insertKeyAfterInOrder(
|
||||
settingsState.defaultmenuorder,
|
||||
key,
|
||||
"courses",
|
||||
);
|
||||
} else {
|
||||
settingsState.defaultmenuorder = [
|
||||
...settingsState.defaultmenuorder,
|
||||
key,
|
||||
];
|
||||
}
|
||||
settingsState.defaultmenuorder = [...settingsState.defaultmenuorder, key];
|
||||
}
|
||||
}
|
||||
ensureAnalyticsMenuOrder();
|
||||
}
|
||||
|
||||
function mergeMenuItemsFromDom(
|
||||
@@ -242,7 +232,6 @@ export function OpenMenuOptions() {
|
||||
if (sortable) {
|
||||
saveNewOrder(sortable);
|
||||
}
|
||||
applyMenuItemVisibility();
|
||||
closeAll();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { settingsState } from "./listeners/SettingsState";
|
||||
import { verboseInfo } from "@/utils/verboseLog";
|
||||
|
||||
const STYLE_ID = "bsplus-menuitem-visibility";
|
||||
|
||||
/** Whether a sidebar key is hidden via Edit Sidebar toggles. */
|
||||
export function isMenuItemHidden(key: string): boolean {
|
||||
const entry = (settingsState.menuitems as Record<string, { toggle?: boolean }>)?.[key];
|
||||
return entry != null && entry.toggle === false;
|
||||
}
|
||||
|
||||
/** Apply hide rules from `menuitems` (re-runnable after edit / storage sync). */
|
||||
export function applyMenuItemVisibility(): void {
|
||||
if (document.querySelector(".editmenuoption-container")) return;
|
||||
@@ -18,7 +11,6 @@ export function applyMenuItemVisibility(): void {
|
||||
for (const [menuItem, config] of Object.entries(settingsState.menuitems ?? {})) {
|
||||
if (config && !config.toggle) {
|
||||
css += `li[data-key=${menuItem}],section[data-key=${menuItem}]{display:var(--menuHidden) !important;transition:1s;}`;
|
||||
verboseInfo(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,109 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { noticeMatchesLabelFilter } from "@/seqta/utils/notices/noticeLabelFilters";
|
||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||
|
||||
const PLACEHOLDER_RE = /\[\[[\w]+[:][\w]+[\]\]]+/g;
|
||||
const SPRING_OPEN = { type: "spring" as const, stiffness: 280, damping: 24, duration: 0.5 };
|
||||
const SPRING_CLOSE = { type: "spring" as const, stiffness: 400, damping: 35, duration: 0.35 };
|
||||
|
||||
const colourStr = (colour?: string) => colour || "#8e8e8e";
|
||||
|
||||
function stripPlaceholders(html: string): string {
|
||||
return html.replace(PLACEHOLDER_RE, "");
|
||||
}
|
||||
|
||||
function noticePreview(contents: string): string {
|
||||
const text = stripPlaceholders(contents)
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return text.substring(0, 150) + (contents.length > 150 ? "..." : "");
|
||||
}
|
||||
|
||||
function noticeBody(contents: string): string {
|
||||
return stripPlaceholders(contents).replace(/ +/, " ");
|
||||
}
|
||||
|
||||
type NoticeCardOpts = {
|
||||
wrapperClass?: string;
|
||||
wrapperStyle?: string;
|
||||
hideClose?: boolean;
|
||||
};
|
||||
|
||||
function noticeCardHtml(
|
||||
notice: { title: string; staff: string; label_title?: string },
|
||||
colour: string | undefined,
|
||||
body: string,
|
||||
opts: NoticeCardOpts = {},
|
||||
): string {
|
||||
const c = colourStr(colour);
|
||||
const closeBtn = opts.hideClose
|
||||
? '<button class="notice-close-btn" style="opacity: 0; pointer-events: none;">×</button>'
|
||||
: '<button class="notice-close-btn">×</button>';
|
||||
return `<div class="notice-unified-content ${opts.wrapperClass ?? "notice-card-state"}" style="--colour: ${c}; ${opts.wrapperStyle ?? ""}">
|
||||
<div class="notice-header">
|
||||
<div class="notice-badge-row">
|
||||
<span class="notice-badge" style="background: linear-gradient(135deg, ${c}, ${c}dd); color: white;">${notice.label_title || "General"}</span>
|
||||
<span class="notice-staff">${notice.staff}</span>
|
||||
</div>
|
||||
${closeBtn}
|
||||
</div>
|
||||
<h2 class="notice-content-title">${notice.title}</h2>
|
||||
<div class="notice-content-body">${body}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function modalTargetSize(sourceWidth: number, contentHeight: number) {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const scrollY = Math.round(window.scrollY);
|
||||
const width = Math.round(Math.min(Math.max(sourceWidth, 800), vw - 40));
|
||||
const height = Math.round(Math.min(Math.max(contentHeight + 32, 200), vh * 0.9));
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
left: Math.round((vw - width) / 2),
|
||||
top: Math.round((vh - height) / 2) + scrollY,
|
||||
scrollX: Math.round(window.scrollX),
|
||||
scrollY,
|
||||
};
|
||||
}
|
||||
|
||||
function measureNoticeHeight(
|
||||
notice: { title: string; staff: string; label_title?: string },
|
||||
body: string,
|
||||
targetWidth: number,
|
||||
): number {
|
||||
const measure = document.createElement("div");
|
||||
measure.style.cssText = `position:absolute;left:-9999px;width:${targetWidth}px;visibility:hidden`;
|
||||
measure.innerHTML = noticeCardHtml(notice, undefined, body, {
|
||||
wrapperClass: "notice-modal-state",
|
||||
wrapperStyle:
|
||||
"position:relative;width:100%;padding:16px;border:1px solid rgba(255,255,255,0.1)",
|
||||
});
|
||||
document.body.appendChild(measure);
|
||||
const height = measure.firstElementChild!.getBoundingClientRect().height;
|
||||
measure.remove();
|
||||
return height;
|
||||
}
|
||||
|
||||
function showSourceElement(el: HTMLElement) {
|
||||
el.style.opacity = "1";
|
||||
el.style.transform = "";
|
||||
}
|
||||
|
||||
function elementScale(el: HTMLElement) {
|
||||
const transform = getComputedStyle(el).transform;
|
||||
if (!transform || transform === "none") return { x: 1, y: 1 };
|
||||
const match = transform.match(/matrix.*\((.+)\)/);
|
||||
if (!match) return { x: 1, y: 1 };
|
||||
const values = match[1].split(", ");
|
||||
return { x: parseFloat(values[0]), y: parseFloat(values[3]) };
|
||||
}
|
||||
|
||||
export function processNoticeColor(colour: unknown): string | undefined {
|
||||
if (typeof colour !== "string") return undefined;
|
||||
const rgb = GetThresholdOfColor(colour);
|
||||
if (rgb < 100 && settingsState.DarkMode) {
|
||||
return undefined;
|
||||
}
|
||||
if (rgb < 100 && settingsState.DarkMode) return undefined;
|
||||
return colour;
|
||||
}
|
||||
|
||||
@@ -29,35 +126,13 @@ export function appendNoticeEmptyState(container: HTMLElement, message: string)
|
||||
}
|
||||
|
||||
function createNoticeElement(notice: any, colour: string | undefined): Node {
|
||||
const textPreview =
|
||||
notice.contents
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.substring(0, 150) + (notice.contents.length > 150 ? "..." : "");
|
||||
|
||||
const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const htmlContent = `
|
||||
<div class="notice-unified-content notice-card-state" data-notice-id="${noticeId}" style="--colour: ${colour || "#8e8e8e"}; position: relative; background: var(--background-primary); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);">
|
||||
<div class="notice-header">
|
||||
<div class="notice-badge-row">
|
||||
<span class="notice-badge" style="background: linear-gradient(135deg, ${colour || "#8e8e8e"}, ${colour || "#8e8e8e"}dd); color: white;">
|
||||
${notice.label_title || "General"}
|
||||
</span>
|
||||
<span class="notice-staff">${notice.staff}</span>
|
||||
</div>
|
||||
<button class="notice-close-btn" style="opacity: 0; pointer-events: none;">×</button>
|
||||
</div>
|
||||
<h2 class="notice-content-title">${notice.title}</h2>
|
||||
<div class="notice-content-body">${textPreview}</div>
|
||||
</div>`;
|
||||
|
||||
const htmlContent = noticeCardHtml(notice, colour, noticePreview(notice.contents), {
|
||||
wrapperStyle:
|
||||
"position: relative; background: var(--background-primary); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);",
|
||||
hideClose: true,
|
||||
});
|
||||
const element = stringToHTML(htmlContent).firstChild as HTMLElement;
|
||||
element.addEventListener("click", () =>
|
||||
openNoticeModal(notice, colour, element),
|
||||
);
|
||||
element.addEventListener("click", () => openNoticeModal(notice, colour, element));
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -66,10 +141,7 @@ export function openNoticeModal(
|
||||
colour: string | undefined,
|
||||
sourceElement: HTMLElement,
|
||||
) {
|
||||
const cleanContent = notice.contents
|
||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||
.replace(/ +/, " ");
|
||||
|
||||
const cleanContent = noticeBody(notice.contents);
|
||||
document.getElementById("notice-modal")?.remove();
|
||||
|
||||
const sourceRect = sourceElement.getBoundingClientRect();
|
||||
@@ -80,84 +152,37 @@ export function openNoticeModal(
|
||||
let sourceWidth = sourceRect.width;
|
||||
let sourceHeight = sourceRect.height;
|
||||
|
||||
const modalHtml = `
|
||||
<div id="notice-modal" class="notice-modal-overlay" style="opacity: 0;">
|
||||
<div class="notice-modal-transition" style="
|
||||
position: fixed;
|
||||
left: ${sourceLeft + scrollX}px;
|
||||
top: ${sourceTop + scrollY}px;
|
||||
width: ${sourceWidth}px;
|
||||
height: ${sourceHeight}px;
|
||||
transform-origin: center;
|
||||
z-index: 10001;
|
||||
">
|
||||
const modalHtml = `<div id="notice-modal" class="notice-modal-overlay" style="opacity: 0;">
|
||||
<div class="notice-modal-transition" style="position:fixed;left:${sourceLeft + scrollX}px;top:${sourceTop + scrollY}px;width:${sourceWidth}px;height:${sourceHeight}px;transform-origin:center;z-index:10001;">
|
||||
<div class="notice-modal-content notice-transitioning">
|
||||
<div class="notice-unified-content notice-card-state">
|
||||
<div class="notice-header">
|
||||
<div class="notice-badge-row">
|
||||
<span class="notice-badge" style="background: linear-gradient(135deg, ${colour || "#8e8e8e"}, ${colour || "#8e8e8e"}dd); color: white;">
|
||||
${notice.label_title || "General"}
|
||||
</span>
|
||||
<span class="notice-staff">${notice.staff}</span>
|
||||
</div>
|
||||
<button class="notice-close-btn">×</button>
|
||||
</div>
|
||||
<h2 class="notice-content-title">${notice.title}</h2>
|
||||
<div class="notice-content-body">${cleanContent}</div>
|
||||
</div>
|
||||
${noticeCardHtml(notice, colour, cleanContent)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const modal = stringToHTML(modalHtml).firstChild as HTMLElement;
|
||||
const transitionContainer = modal.querySelector(
|
||||
".notice-modal-transition",
|
||||
) as HTMLElement;
|
||||
const unifiedContent = modal.querySelector(
|
||||
".notice-unified-content",
|
||||
) as HTMLElement;
|
||||
const transitionContainer = modal.querySelector(".notice-modal-transition") as HTMLElement;
|
||||
const unifiedContent = modal.querySelector(".notice-unified-content") as HTMLElement;
|
||||
const closeBtn = modal.querySelector(".notice-close-btn") as HTMLElement;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
sourceElement.setAttribute("data-transitioning", "true");
|
||||
sourceElement.style.opacity = "0";
|
||||
sourceElement.style.transform = "scale(0.95)";
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
let targetWidth = Math.round(
|
||||
Math.min(Math.max(sourceWidth, 800), viewportWidth - 40),
|
||||
const initialWidth = Math.round(
|
||||
Math.min(Math.max(sourceWidth, 800), window.innerWidth - 40),
|
||||
);
|
||||
const measuredHeight = measureNoticeHeight(notice, cleanContent, initialWidth);
|
||||
let { width: targetWidth, height: targetHeight, left: targetLeft, top: targetTop } =
|
||||
modalTargetSize(sourceWidth, measuredHeight);
|
||||
|
||||
const tempMeasureDiv = document.createElement("div");
|
||||
tempMeasureDiv.style.position = "absolute";
|
||||
tempMeasureDiv.style.left = "-9999px";
|
||||
tempMeasureDiv.style.width = targetWidth + "px";
|
||||
tempMeasureDiv.style.visibility = "hidden";
|
||||
tempMeasureDiv.innerHTML = `
|
||||
<div class="notice-unified-content notice-modal-state" style="position: relative; width: 100%; padding: 16px; border: 1px solid rgba(255, 255, 255, 0.1);">
|
||||
<div class="notice-header">
|
||||
<div class="notice-badge-row">
|
||||
<span class="notice-badge">${notice.label_title || "General"}</span>
|
||||
<span class="notice-staff">${notice.staff}</span>
|
||||
</div>
|
||||
<button class="notice-close-btn">×</button>
|
||||
</div>
|
||||
<h2 class="notice-content-title">${notice.title}</h2>
|
||||
<div class="notice-content-body">${cleanContent}</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(tempMeasureDiv);
|
||||
const measuredHeight =
|
||||
tempMeasureDiv.firstElementChild!.getBoundingClientRect().height;
|
||||
document.body.removeChild(tempMeasureDiv);
|
||||
|
||||
let targetHeight = Math.round(
|
||||
Math.min(Math.max(measuredHeight + 32, 200), viewportHeight * 0.9),
|
||||
);
|
||||
let targetLeft = Math.round((viewportWidth - targetWidth) / 2);
|
||||
let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY;
|
||||
const applyTargetLayout = () => {
|
||||
transitionContainer.style.left = `${Math.round(targetLeft + scrollX)}px`;
|
||||
transitionContainer.style.top = `${Math.round(targetTop)}px`;
|
||||
transitionContainer.style.width = `${Math.round(targetWidth)}px`;
|
||||
transitionContainer.style.height = `${Math.round(targetHeight)}px`;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
@@ -165,8 +190,7 @@ export function openNoticeModal(
|
||||
|
||||
if (!settingsState.animations) {
|
||||
modal.remove();
|
||||
sourceElement.style.opacity = "1";
|
||||
sourceElement.style.transform = "";
|
||||
showSourceElement(sourceElement);
|
||||
sourceElement.removeAttribute("data-transitioning");
|
||||
return;
|
||||
}
|
||||
@@ -179,146 +203,65 @@ export function openNoticeModal(
|
||||
},
|
||||
{ duration: 0.2 },
|
||||
);
|
||||
|
||||
animate(
|
||||
transitionContainer,
|
||||
{ opacity: [1, 0] },
|
||||
{ duration: 0.2, delay: 0.3 },
|
||||
);
|
||||
|
||||
sourceElement.style.opacity = "1";
|
||||
sourceElement.style.transform = "";
|
||||
|
||||
animate(transitionContainer, { opacity: [1, 0] }, { duration: 0.2, delay: 0.3 });
|
||||
showSourceElement(sourceElement);
|
||||
modal.style.pointerEvents = "none";
|
||||
|
||||
animate(
|
||||
transitionContainer,
|
||||
{
|
||||
left: [targetLeft + scrollX, sourceLeft + scrollX],
|
||||
top: [targetTop, sourceTop + scrollY],
|
||||
width: [targetWidth, sourceWidth],
|
||||
height: [targetHeight, sourceHeight],
|
||||
scale: [1, 1],
|
||||
},
|
||||
{
|
||||
duration: 0.35,
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 35,
|
||||
},
|
||||
).finished.then(async () => {
|
||||
animate(transitionContainer, {
|
||||
left: [targetLeft + scrollX, sourceLeft + scrollX],
|
||||
top: [targetTop, sourceTop + scrollY],
|
||||
width: [targetWidth, sourceWidth],
|
||||
height: [targetHeight, sourceHeight],
|
||||
}, SPRING_CLOSE).finished.then(() => {
|
||||
modal.remove();
|
||||
sourceElement.removeAttribute("data-transitioning");
|
||||
});
|
||||
};
|
||||
|
||||
closeBtn?.addEventListener("click", closeModal);
|
||||
modal?.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
closeBtn.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
closeModal();
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
}
|
||||
if (e.key === "Escape") closeModal();
|
||||
};
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
|
||||
const handleResize = () => {
|
||||
const newSourceRect = sourceElement.getBoundingClientRect();
|
||||
const newScrollY = Math.round(window.scrollY);
|
||||
const newScrollX = Math.round(window.scrollX);
|
||||
const rect = sourceElement.getBoundingClientRect();
|
||||
scrollY = Math.round(window.scrollY);
|
||||
scrollX = Math.round(window.scrollX);
|
||||
const scale = elementScale(sourceElement);
|
||||
sourceWidth = rect.width / scale.x;
|
||||
sourceHeight = rect.height / scale.y;
|
||||
sourceLeft = rect.left - (sourceWidth - rect.width) / 2;
|
||||
sourceTop = rect.top - (sourceHeight - rect.height) / 2;
|
||||
|
||||
const computedStyle = getComputedStyle(sourceElement);
|
||||
const transform = computedStyle.transform;
|
||||
let scaleX = 1,
|
||||
scaleY = 1;
|
||||
|
||||
if (transform && transform !== "none") {
|
||||
const matrix = transform.match(/matrix.*\((.+)\)/);
|
||||
if (matrix) {
|
||||
const values = matrix[1].split(", ");
|
||||
scaleX = parseFloat(values[0]);
|
||||
scaleY = parseFloat(values[3]);
|
||||
}
|
||||
}
|
||||
|
||||
const newSourceWidth = newSourceRect.width / scaleX;
|
||||
const newSourceHeight = newSourceRect.height / scaleY;
|
||||
|
||||
const deltaX = (newSourceWidth - newSourceRect.width) / 2;
|
||||
const deltaY = (newSourceHeight - newSourceRect.height) / 2;
|
||||
|
||||
const newSourceLeft = newSourceRect.left - deltaX;
|
||||
const newSourceTop = newSourceRect.top - deltaY;
|
||||
|
||||
const newViewportWidth = window.innerWidth;
|
||||
const newViewportHeight = window.innerHeight;
|
||||
const newTargetWidth = Math.round(
|
||||
Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40),
|
||||
const next = modalTargetSize(
|
||||
sourceWidth,
|
||||
unifiedContent.getBoundingClientRect().height,
|
||||
);
|
||||
const currentHeight = unifiedContent.getBoundingClientRect().height;
|
||||
const newTargetHeight = Math.round(
|
||||
Math.min(Math.max(currentHeight + 32, 200), newViewportHeight * 0.9),
|
||||
);
|
||||
const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2);
|
||||
const newTargetTop =
|
||||
Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY;
|
||||
|
||||
transitionContainer.style.left =
|
||||
Math.round(newTargetLeft + newScrollX) + "px";
|
||||
transitionContainer.style.top = Math.round(newTargetTop) + "px";
|
||||
transitionContainer.style.width = Math.round(newTargetWidth) + "px";
|
||||
transitionContainer.style.height = Math.round(newTargetHeight) + "px";
|
||||
|
||||
sourceLeft = newSourceLeft;
|
||||
sourceTop = newSourceTop;
|
||||
sourceWidth = newSourceWidth;
|
||||
sourceHeight = newSourceHeight;
|
||||
targetLeft = newTargetLeft;
|
||||
targetTop = newTargetTop;
|
||||
targetWidth = newTargetWidth;
|
||||
targetHeight = newTargetHeight;
|
||||
scrollY = newScrollY;
|
||||
scrollX = newScrollX;
|
||||
targetLeft = next.left;
|
||||
targetTop = next.top;
|
||||
targetWidth = next.width;
|
||||
targetHeight = next.height;
|
||||
applyTargetLayout();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
unifiedContent.classList.replace("notice-card-state", "notice-modal-state");
|
||||
if (settingsState.animations) {
|
||||
animate(modal, { opacity: [0, 1] }, { duration: 0.2 });
|
||||
|
||||
animate(
|
||||
transitionContainer,
|
||||
{
|
||||
left: [sourceLeft + scrollX, targetLeft + scrollX],
|
||||
top: [sourceTop + scrollY, targetTop],
|
||||
width: [sourceWidth, targetWidth],
|
||||
height: [sourceHeight, targetHeight],
|
||||
scale: [1, 1],
|
||||
},
|
||||
{
|
||||
duration: 0.5,
|
||||
type: "spring",
|
||||
stiffness: 280,
|
||||
damping: 24,
|
||||
},
|
||||
);
|
||||
|
||||
unifiedContent.classList.remove("notice-card-state");
|
||||
unifiedContent.classList.add("notice-modal-state");
|
||||
animate(transitionContainer, {
|
||||
left: [sourceLeft + scrollX, targetLeft + scrollX],
|
||||
top: [sourceTop + scrollY, targetTop],
|
||||
width: [sourceWidth, targetWidth],
|
||||
height: [sourceHeight, targetHeight],
|
||||
}, SPRING_OPEN);
|
||||
} else {
|
||||
modal.style.opacity = "1";
|
||||
transitionContainer.style.left = Math.round(targetLeft + scrollX) + "px";
|
||||
transitionContainer.style.top = Math.round(targetTop) + "px";
|
||||
transitionContainer.style.width = Math.round(targetWidth) + "px";
|
||||
transitionContainer.style.height = Math.round(targetHeight) + "px";
|
||||
unifiedContent.classList.remove("notice-card-state");
|
||||
unifiedContent.classList.add("notice-modal-state");
|
||||
applyTargetLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,22 +284,19 @@ export function renderNoticesIntoContainer(
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
notices.forEach((notice: any) => {
|
||||
const shouldInclude =
|
||||
settingsState.mockNotices || noticeMatchesLabelFilter(notice, labelTokens);
|
||||
|
||||
if (shouldInclude) {
|
||||
const colour = processNoticeColor(notice.colour);
|
||||
fragment.appendChild(createNoticeElement(notice, colour));
|
||||
for (const notice of notices) {
|
||||
if (
|
||||
settingsState.mockNotices ||
|
||||
noticeMatchesLabelFilter(notice, labelTokens)
|
||||
) {
|
||||
fragment.appendChild(createNoticeElement(notice, processNoticeColor(notice.colour)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fragment.childNodes.length === 0) {
|
||||
if (!fragment.childNodes.length) {
|
||||
appendNoticeEmptyState(noticeContainer, emptyMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
noticeContainer.appendChild(fragment);
|
||||
}
|
||||
|
||||
@@ -372,8 +312,9 @@ export async function fetchNoticesForDate(
|
||||
container.innerHTML = "";
|
||||
}
|
||||
|
||||
let data: { payload?: unknown };
|
||||
try {
|
||||
const data = settingsState.mockNotices
|
||||
data = settingsState.mockNotices
|
||||
? getMockNotices()
|
||||
: await (
|
||||
await fetch(noticesUrl, {
|
||||
@@ -383,11 +324,10 @@ export async function fetchNoticesForDate(
|
||||
body: JSON.stringify({ date }),
|
||||
})
|
||||
).json();
|
||||
|
||||
renderNoticesIntoContainer(containerId, data, labelTokens);
|
||||
} catch {
|
||||
renderNoticesIntoContainer(containerId, { payload: [] }, labelTokens);
|
||||
data = { payload: [] };
|
||||
}
|
||||
renderNoticesIntoContainer(containerId, data, labelTokens);
|
||||
}
|
||||
|
||||
export type SetupNoticesSectionOptions = {
|
||||
@@ -405,9 +345,7 @@ export function setupNoticesSection(options: SetupNoticesSectionOptions): () =>
|
||||
? (document.querySelector(options.dateInput) as HTMLInputElement | null)
|
||||
: options.dateInput;
|
||||
|
||||
if (dateControl) {
|
||||
dateControl.value = options.initialDate;
|
||||
}
|
||||
if (dateControl) dateControl.value = options.initialDate;
|
||||
|
||||
const debouncedInputChange = debounce((e: Event) => {
|
||||
void fetchNoticesForDate(
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
type PrefEntry = { name?: string; value?: unknown };
|
||||
|
||||
const NOTICES_FILTER = "notices.filters";
|
||||
const JSON_HEADERS = { "Content-Type": "application/json; charset=utf-8" };
|
||||
|
||||
/** Parse `notices.filters` pref (space-separated label IDs). */
|
||||
export function parseNoticesFilterPref(prefsPayload: unknown): string[] {
|
||||
if (!Array.isArray(prefsPayload)) return [];
|
||||
const values = (prefsPayload as PrefEntry[])
|
||||
.filter((item) => item?.name === "notices.filters")
|
||||
.map((item) => item?.value)
|
||||
.filter((v): v is string => typeof v === "string");
|
||||
if (values.length === 0) return [];
|
||||
return String(values[0]).split(" ").filter(Boolean);
|
||||
const raw = (prefsPayload as PrefEntry[]).find((item) => item?.name === NOTICES_FILTER)?.value;
|
||||
return typeof raw === "string" ? raw.split(" ").filter(Boolean) : [];
|
||||
}
|
||||
|
||||
/** Label IDs from `load/notices` with `{ mode: "labels" }`. */
|
||||
@@ -16,13 +15,12 @@ export async function fetchNoticeLabelIds(noticesUrl: string): Promise<string[]>
|
||||
try {
|
||||
const res = await fetch(noticesUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
headers: JSON_HEADERS,
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ mode: "labels" }),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const json = (await res.json()) as { payload?: Array<{ id?: number }> };
|
||||
const payload = json?.payload;
|
||||
const payload = ((await res.json()) as { payload?: Array<{ id?: number }> })?.payload;
|
||||
if (!Array.isArray(payload)) return [];
|
||||
return payload
|
||||
.map((entry) => entry?.id)
|
||||
@@ -39,26 +37,16 @@ export async function resolveNoticeFilterTokens(
|
||||
noticesUrl: string,
|
||||
): Promise<string[]> {
|
||||
const fromPref = parseNoticesFilterPref(prefsPayload);
|
||||
if (fromPref.length > 0) return fromPref;
|
||||
return await fetchNoticeLabelIds(noticesUrl);
|
||||
return fromPref.length > 0 ? fromPref : await fetchNoticeLabelIds(noticesUrl);
|
||||
}
|
||||
|
||||
export function normalizeNoticeLabelId(label: unknown): string | null {
|
||||
if (typeof label === "number" && !Number.isNaN(label)) {
|
||||
return String(label);
|
||||
}
|
||||
if (typeof label === "string" && label.trim()) {
|
||||
return label.trim();
|
||||
}
|
||||
if (label && typeof label === "object") {
|
||||
const obj = label as Record<string, unknown>;
|
||||
if (typeof obj.id === "number" && !Number.isNaN(obj.id)) {
|
||||
return String(obj.id);
|
||||
}
|
||||
if (typeof obj.id === "string" && obj.id.trim()) {
|
||||
return obj.id.trim();
|
||||
}
|
||||
}
|
||||
if (typeof label === "number" && !Number.isNaN(label)) return String(label);
|
||||
if (typeof label === "string" && label.trim()) return label.trim();
|
||||
const id =
|
||||
label && typeof label === "object" ? (label as { id?: unknown }).id : undefined;
|
||||
if (typeof id === "number" && !Number.isNaN(id)) return String(id);
|
||||
if (typeof id === "string" && id.trim()) return id.trim();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -68,6 +56,6 @@ export function noticeMatchesLabelFilter(
|
||||
): boolean {
|
||||
if (filterTokens.length === 0) return true;
|
||||
const id = normalizeNoticeLabelId(notice?.label);
|
||||
if (id !== null && filterTokens.includes(id)) return true;
|
||||
return filterTokens.includes(JSON.stringify(notice?.label));
|
||||
return (id !== null && filterTokens.includes(id)) ||
|
||||
filterTokens.includes(JSON.stringify(notice?.label));
|
||||
}
|
||||
|
||||
@@ -8,44 +8,36 @@ import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||
import { verboseInfo } from "@/utils/verboseLog";
|
||||
|
||||
const PAGE_PATCH_LOADER_ID = "bsplus-seqta-menu-colour-patch-loader";
|
||||
let colorisRecoveryAttached = false;
|
||||
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function dismissStaleModaliserContainers(): number {
|
||||
let removed = 0;
|
||||
for (const container of document.querySelectorAll(".modaliser-container")) {
|
||||
const modal = container.querySelector(".modaliser");
|
||||
if (!modal?.childElementCount || !container.classList.contains("visible")) {
|
||||
container.remove();
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
export function dismissStaleColourSlidePanes(
|
||||
forceColourChooser = false,
|
||||
): number {
|
||||
let removed = 0;
|
||||
function dismissStaleColourDialogs(forceColourChooser = false): {
|
||||
slideRemoved: number;
|
||||
modalRemoved: number;
|
||||
} {
|
||||
let slideRemoved = 0;
|
||||
for (const pane of document.querySelectorAll(".uiSlidePane")) {
|
||||
if (pane.querySelector(".pane.colourChooser")) {
|
||||
pane.remove();
|
||||
removed++;
|
||||
slideRemoved++;
|
||||
continue;
|
||||
}
|
||||
if (!forceColourChooser && pane.classList.contains("shown")) continue;
|
||||
if (!pane.classList.contains("shown")) {
|
||||
pane.remove();
|
||||
removed++;
|
||||
slideRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
let modalRemoved = 0;
|
||||
for (const container of document.querySelectorAll(".modaliser-container")) {
|
||||
const modal = container.querySelector(".modaliser");
|
||||
if (!modal?.childElementCount || !container.classList.contains("visible")) {
|
||||
container.remove();
|
||||
modalRemoved++;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
export function dismissStaleColourDialogs(forceColourChooser = false): {
|
||||
slideRemoved: number;
|
||||
modalRemoved: number;
|
||||
} {
|
||||
const slideRemoved = dismissStaleColourSlidePanes(forceColourChooser);
|
||||
const modalRemoved = dismissStaleModaliserContainers();
|
||||
document.body.classList.remove("clr-open");
|
||||
document.documentElement.classList.remove("clr-open");
|
||||
return { slideRemoved, modalRemoved };
|
||||
@@ -69,8 +61,7 @@ function setClrPickerState(reset: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Hide colour-picker / modal layers that intercept clicks after a colour save. */
|
||||
export function dismissTimetableUiBlockers(): {
|
||||
function dismissTimetableUiBlockers(): {
|
||||
slideRemoved: number;
|
||||
modalRemoved: number;
|
||||
} {
|
||||
@@ -79,14 +70,10 @@ export function dismissTimetableUiBlockers(): {
|
||||
return dismissStaleColourDialogs();
|
||||
}
|
||||
|
||||
/** Clear inline styles that can prevent Coloris from reopening. */
|
||||
export function prepareColorisPickerOpen(): void {
|
||||
function prepareColorisPickerOpen(): void {
|
||||
setClrPickerState(true);
|
||||
}
|
||||
|
||||
let colorisRecoveryAttached = false;
|
||||
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function attachTimetableColorisRecovery(): void {
|
||||
if (colorisRecoveryAttached) return;
|
||||
colorisRecoveryAttached = true;
|
||||
|
||||
@@ -7,62 +7,54 @@
|
||||
var MENU_UPDATE_COLOURS = "menu.update.colours";
|
||||
var SUBJECT_PREFIX = "timetable.subject.colour.";
|
||||
var TUTOR_PREFIX = "timetable.tutor.";
|
||||
var CLEANUP_FOLLOWUP_MS = 300;
|
||||
var cleanupFollowup = null;
|
||||
|
||||
function isTesStyling() {
|
||||
var el = document.getElementById("logo-style");
|
||||
return el && el.textContent.indexOf("tesSeqta") !== -1;
|
||||
}
|
||||
|
||||
function dismissModalisers() {
|
||||
var n = 0;
|
||||
var containers = document.querySelectorAll(".modaliser-container");
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var c = containers[i];
|
||||
var m = c.querySelector(".modaliser");
|
||||
if (!m || !m.childElementCount || !c.classList.contains("visible")) {
|
||||
c.remove();
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function dismissSlidePanes(forceColour) {
|
||||
var n = 0;
|
||||
function dismissStaleDialogs(forceColour) {
|
||||
var slide = 0;
|
||||
var panes = document.querySelectorAll(".uiSlidePane");
|
||||
for (var i = 0; i < panes.length; i++) {
|
||||
var p = panes[i];
|
||||
if (p.querySelector(".pane.colourChooser")) {
|
||||
p.remove();
|
||||
n++;
|
||||
slide++;
|
||||
continue;
|
||||
}
|
||||
if (!forceColour && p.classList.contains("shown")) continue;
|
||||
if (!p.classList.contains("shown")) {
|
||||
p.remove();
|
||||
n++;
|
||||
slide++;
|
||||
}
|
||||
}
|
||||
|
||||
var modal = 0;
|
||||
var containers = document.querySelectorAll(".modaliser-container");
|
||||
for (var j = 0; j < containers.length; j++) {
|
||||
var c = containers[j];
|
||||
var m = c.querySelector(".modaliser");
|
||||
if (!m || !m.childElementCount || !c.classList.contains("visible")) {
|
||||
c.remove();
|
||||
modal++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function dismissStaleDialogs(forceColour) {
|
||||
var slide = dismissSlidePanes(forceColour);
|
||||
var modal = dismissModalisers();
|
||||
document.body.classList.remove("clr-open");
|
||||
document.documentElement.classList.remove("clr-open");
|
||||
return { slideRemoved: slide, modalRemoved: modal };
|
||||
}
|
||||
|
||||
function scheduleCleanup() {
|
||||
var delays = [0, 100, 300, 600];
|
||||
for (var i = 0; i < delays.length; i++) {
|
||||
(function (d) {
|
||||
setTimeout(function () {
|
||||
dismissStaleDialogs(true);
|
||||
}, d);
|
||||
})(delays[i]);
|
||||
}
|
||||
dismissStaleDialogs(true);
|
||||
if (cleanupFollowup) clearTimeout(cleanupFollowup);
|
||||
cleanupFollowup = setTimeout(function () {
|
||||
cleanupFollowup = null;
|
||||
dismissStaleDialogs(true);
|
||||
}, CLEANUP_FOLLOWUP_MS);
|
||||
}
|
||||
|
||||
function applyMenuColours() {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
var THEME_STYLE_ID = "custom-theme";
|
||||
var PREVIEW_STYLE_ID = "custom-theme-preview";
|
||||
var urlCache = {};
|
||||
var cssState = { custom: "", preview: "" };
|
||||
var headObserver = null;
|
||||
|
||||
function log(event, detail) {
|
||||
@@ -77,8 +76,6 @@
|
||||
|
||||
function clearAll() {
|
||||
releaseCachedUrls();
|
||||
cssState.custom = "";
|
||||
cssState.preview = "";
|
||||
setStyleText(IMAGES_STYLE_ID, "");
|
||||
document.getElementById(THEME_STYLE_ID)?.remove();
|
||||
document.getElementById(PREVIEW_STYLE_ID)?.remove();
|
||||
@@ -128,18 +125,15 @@
|
||||
if (payload.images !== undefined) applyThemeImages(payload.images);
|
||||
|
||||
if (payload.customCss !== undefined) {
|
||||
cssState.custom = payload.customCss || "";
|
||||
setStyleText(THEME_STYLE_ID, cssState.custom, true);
|
||||
setStyleText(THEME_STYLE_ID, payload.customCss || "", true);
|
||||
log("custom css applied");
|
||||
}
|
||||
|
||||
if (payload.previewCss !== undefined) {
|
||||
cssState.preview = payload.previewCss || "";
|
||||
setStyleText(PREVIEW_STYLE_ID, cssState.preview);
|
||||
setStyleText(PREVIEW_STYLE_ID, payload.previewCss || "");
|
||||
}
|
||||
|
||||
if (payload.clearPreview) {
|
||||
cssState.preview = "";
|
||||
setStyleText(PREVIEW_STYLE_ID, "");
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user