chore: reduce complexity

This commit is contained in:
2026-06-26 19:41:27 +09:30
parent a3bdb0fe09
commit ca4cbea0f0
6 changed files with 484 additions and 419 deletions
+39 -35
View File
@@ -513,20 +513,10 @@ function isTrustedMessage(event) {
return event.source === window && event.origin === window.location.origin; return event.source === window && event.origin === window.location.origin;
} }
window.addEventListener("message", (event) => { function executeReactFiberAction(fiberInstance, action, payload) {
if (!isTrustedMessage(event)) return;
if (event.data.type === "reactFiberRequest") {
const { selector, action, payload, debug, messageId } = event.data;
const fiberInstance = ReactFiber.find(selector, {
debug,
});
let response;
switch (action) { switch (action) {
case "getState": case "getState":
response = fiberInstance.getState(payload.key); return fiberInstance.getState(payload.key);
break;
case "setState": case "setState":
if ( if (
payload.updateObject && payload.updateObject &&
@@ -537,51 +527,54 @@ window.addEventListener("message", (event) => {
} else { } else {
console.warn("[pageState] setState rejected: only plain objects are allowed"); console.warn("[pageState] setState rejected: only plain objects are allowed");
} }
response = {}; return {};
break;
case "getProp": case "getProp":
response = fiberInstance.getProp(payload.propName); return fiberInstance.getProp(payload.propName);
break;
case "setProp": case "setProp":
fiberInstance.setProp(payload.propName, payload.value); fiberInstance.setProp(payload.propName, payload.value);
response = {}; return {};
break;
case "forceUpdate": case "forceUpdate":
fiberInstance.forceUpdate(); fiberInstance.forceUpdate();
response = {}; return {};
break;
default: default:
console.warn(`[pageState] Unknown action: ${action}`); console.warn(`[pageState] Unknown action: ${action}`);
response = null; return null;
}
} }
function sanitizeResponseForPostMessage(response) {
if (response !== null && typeof response === "object") { if (response !== null && typeof response === "object") {
response = makeSerializable(response); response = makeSerializable(response);
} }
// Final safety check before postMessage
try { try {
deepFunctionCheck(response); deepFunctionCheck(response);
} catch (functionError) { } catch (functionError) {
console.warn("[pageState] Function detected in response, cleaning:", functionError.message); console.warn("[pageState] Function detected in response, cleaning:", functionError.message);
response = `[Cleaned Response - Function found at: ${functionError.message}]`; return `[Cleaned Response - Function found at: ${functionError.message}]`;
} }
// Additional structured clone test
try { try {
// Test if the object can be cloned (same algorithm as postMessage) if (typeof structuredClone === "function") {
if (typeof structuredClone === 'function') {
structuredClone(response); structuredClone(response);
} else { } else {
// Fallback for older browsers - try JSON round-trip
JSON.parse(JSON.stringify(response)); JSON.parse(JSON.stringify(response));
} }
} catch (cloneError) { } catch (cloneError) {
console.warn("[pageState] Response not cloneable, fallback:", cloneError.message); console.warn("[pageState] Response not cloneable, fallback:", cloneError.message);
response = `[Uncloneable Response: ${cloneError.message}]`; return `[Uncloneable Response: ${cloneError.message}]`;
} }
return response;
}
function handleReactFiberRequest(data) {
const { selector, action, payload, debug, messageId } = data;
const fiberInstance = ReactFiber.find(selector, { debug });
const response = sanitizeResponseForPostMessage(
executeReactFiberAction(fiberInstance, action, payload),
);
window.postMessage( window.postMessage(
{ {
type: "reactFiberResponse", type: "reactFiberResponse",
@@ -590,11 +583,11 @@ window.addEventListener("message", (event) => {
}, },
window.location.origin, window.location.origin,
); );
} else if (event.data.type === "triggerKeyboardEvent") { }
// Handle keyboard event triggering from content script
const { key, code, altKey, ctrlKey, metaKey, shiftKey, keyCode } = event.data;
const keyboardEvent = new KeyboardEvent('keydown', { function handleTriggerKeyboardEvent(data) {
const { key, code, altKey, ctrlKey, metaKey, shiftKey, keyCode } = data;
const keyboardEvent = new KeyboardEvent("keydown", {
key, key,
code, code,
keyCode: keyCode || 0, keyCode: keyCode || 0,
@@ -604,9 +597,20 @@ window.addEventListener("message", (event) => {
metaKey: metaKey || false, metaKey: metaKey || false,
shiftKey: shiftKey || false, shiftKey: shiftKey || false,
bubbles: true, bubbles: true,
cancelable: true cancelable: true,
}); });
document.dispatchEvent(keyboardEvent); document.dispatchEvent(keyboardEvent);
} }
window.addEventListener("message", (event) => {
if (!isTrustedMessage(event)) return;
if (event.data.type === "reactFiberRequest") {
handleReactFiberRequest(event.data);
return;
}
if (event.data.type === "triggerKeyboardEvent") {
handleTriggerKeyboardEvent(event.data);
}
}); });
@@ -1040,7 +1040,15 @@ function resolveTabSetClasses(): Record<string, string> {
return resolved; return resolved;
} }
async function buildWeightingsTabContent(api: any, sheet: HTMLElement) { interface WeightingTabContext {
assessmentID?: string;
autoWeight?: number;
override?: number | string;
weightingUnavailable: boolean;
statusNote: string;
}
async function resolveWeightingTabContext(api: any): Promise<WeightingTabContext> {
const selectedItem = document.querySelector( const selectedItem = document.querySelector(
"[class*='AssessmentItem__AssessmentItem___'][class*='selected___']", "[class*='AssessmentItem__AssessmentItem___'][class*='selected___']",
) as HTMLElement | null; ) as HTMLElement | null;
@@ -1056,14 +1064,11 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
? (api.storage.weightings?.[assessmentID] as WeightingEntry | undefined) ? (api.storage.weightings?.[assessmentID] as WeightingEntry | undefined)
: undefined; : undefined;
const rawWeight = entry?.weight; const rawWeight = entry?.weight;
const weightingUnavailable = rawWeight === "N/A"; const weightingUnavailable = rawWeight === "N/A";
const autoWeight = const autoWeight =
rawWeight && rawWeight !== "processing" && rawWeight !== "N/A" rawWeight && rawWeight !== "processing" && rawWeight !== "N/A"
? rawWeight ? rawWeight
: undefined; : undefined;
const override = assessmentID const override = assessmentID
? api.storage.weightingOverrides?.[assessmentID] ? api.storage.weightingOverrides?.[assessmentID]
: undefined; : undefined;
@@ -1076,6 +1081,22 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
? "No weighting was found in the marksheet. Set one manually." ? "No weighting was found in the marksheet. Set one manually."
: "Overrides the auto-detected value."; : "Overrides the auto-detected value.";
return {
assessmentID,
autoWeight,
override,
weightingUnavailable,
statusNote,
};
}
function renderWeightingTabHtml(
sheet: HTMLElement,
context: WeightingTabContext,
) {
const { assessmentID, autoWeight, override, weightingUnavailable, statusNote } =
context;
sheet.innerHTML = ` sheet.innerHTML = `
<style> <style>
#betterseqta-weight-override::placeholder { #betterseqta-weight-override::placeholder {
@@ -1120,9 +1141,13 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
${!assessmentID ? `<p style="font-size:12px;color:rgba(255,80,80,0.8);margin-top:8px">Assessment not yet indexed — try refreshing.</p>` : ""} ${!assessmentID ? `<p style="font-size:12px;color:rgba(255,80,80,0.8);margin-top:8px">Assessment not yet indexed — try refreshing.</p>` : ""}
</div> </div>
`; `;
}
if (!assessmentID) return; function attachWeightingInputHandlers(
sheet: HTMLElement,
api: any,
assessmentID: string,
) {
const input = sheet.querySelector( const input = sheet.querySelector(
"#betterseqta-weight-override", "#betterseqta-weight-override",
) as HTMLInputElement; ) as HTMLInputElement;
@@ -1132,20 +1157,17 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
const save = () => { const save = () => {
const raw = input.value.trim(); const raw = input.value.trim();
if (raw === "") {
const result = saveWeightingOverride(api, assessmentID, "");
if (!result.ok) return;
input.style.borderColor = "rgba(128,128,128,0.3)";
} else {
const result = saveWeightingOverride(api, assessmentID, raw); const result = saveWeightingOverride(api, assessmentID, raw);
if (!result.ok) { if (!result.ok) {
if (raw !== "") {
input.style.borderColor = "rgba(255,80,80,0.6)"; input.style.borderColor = "rgba(255,80,80,0.6)";
statusEl.textContent = result.error ?? "Invalid. Must be 0 or greater"; statusEl.textContent = result.error ?? "Invalid. Must be 0 or greater";
statusEl.style.color = "rgba(255,80,80,0.8)"; statusEl.style.color = "rgba(255,80,80,0.8)";
}
return; return;
} }
input.style.borderColor = "rgba(128,128,128,0.3)"; input.style.borderColor = "rgba(128,128,128,0.3)";
}
statusEl.textContent = "Saved"; statusEl.textContent = "Saved";
statusEl.style.color = ""; statusEl.style.color = "";
setTimeout(() => (statusEl.textContent = ""), 2000); setTimeout(() => (statusEl.textContent = ""), 2000);
@@ -1165,6 +1187,13 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
}); });
} }
async function buildWeightingsTabContent(api: any, sheet: HTMLElement) {
const context = await resolveWeightingTabContext(api);
renderWeightingTabHtml(sheet, context);
if (!context.assessmentID) return;
attachWeightingInputHandlers(sheet, api, context.assessmentID);
}
export function injectWeightingsTab(api: any) { export function injectWeightingsTab(api: any) {
const tabList = document.querySelector( const tabList = document.querySelector(
'[class*="TabSet__tabs___"]', '[class*="TabSet__tabs___"]',
@@ -128,6 +128,58 @@ export async function mountSearchBar(
} }
}; };
const resetIdleProgressUi = () => {
clearDoneFlashTimer();
progressBarWrapper.classList.remove("is-active", "is-rough-complete");
searchAnchor.classList.remove("is-indexing");
searchButton.classList.remove("is-indexing");
progressText.classList.remove(
"is-active",
"is-rough",
"is-fading-done",
"is-done-message",
);
progressBar.style.width = "0%";
progressText.textContent = "";
ranIndexingCycle = false;
indexingStatus = null;
};
const showActiveIndexingUi = (percentage: number) => {
clearDoneFlashTimer();
progressBarWrapper.classList.remove("is-rough-complete");
progressText.classList.remove("is-rough", "is-fading-done", "is-done-message");
progressBar.style.width = `${Math.max(2, percentage)}%`;
progressBarWrapper.classList.add("is-active");
searchAnchor.classList.add("is-indexing");
searchButton.classList.add("is-indexing");
progressText.textContent = indexingStatus
? `${truncateStatus(indexingStatus)} · ${percentage}%`
: `Indexing ${completedJobs}/${totalJobs} (${percentage}%)`;
progressText.classList.add("is-active");
};
const scheduleCompletionFlash = (rough: boolean) => {
progressBar.style.width = "0%";
progressBarWrapper.classList.remove("is-active");
searchAnchor.classList.remove("is-indexing");
searchButton.classList.remove("is-indexing");
progressText.classList.remove("is-fading-done");
progressText.textContent = rough ? truncateStatus(indexingStatus!, 52) : "Done!";
progressText.classList.toggle("is-rough", rough);
progressBarWrapper.classList.toggle("is-rough-complete", rough);
progressText.classList.add("is-active", "is-done-message");
doneFlashTimer = setTimeout(() => {
doneFlashTimer = null;
progressText.classList.add("is-fading-done");
doneFadeTimer = setTimeout(() => {
doneFadeTimer = null;
resetIdleProgressUi();
}, DONE_FADE_MS);
}, DONE_HOLD_MS);
};
const updateProgressDisplay = () => { const updateProgressDisplay = () => {
const indexingStoppedThisTick = indexingJustStoppedFlag; const indexingStoppedThisTick = indexingJustStoppedFlag;
indexingJustStoppedFlag = false; indexingJustStoppedFlag = false;
@@ -137,9 +189,7 @@ export async function mountSearchBar(
// Stray pulses (missing total, 0 completed, etc.) used to hit the idle // Stray pulses (missing total, 0 completed, etc.) used to hit the idle
// branch and call clearDoneFlashTimer(), killing the Done! hold/fade. // branch and call clearDoneFlashTimer(), killing the Done! hold/fade.
if (doneFlashTimer !== null || doneFadeTimer !== null) { if (doneFlashTimer !== null || doneFadeTimer !== null) {
if (!active) { if (!active) return;
return;
}
clearDoneFlashTimer(); clearDoneFlashTimer();
} }
@@ -150,92 +200,18 @@ export async function mountSearchBar(
(completedJobs >= totalJobs || indexingStoppedThisTick); (completedJobs >= totalJobs || indexingStoppedThisTick);
if (active) { if (active) {
clearDoneFlashTimer(); showActiveIndexingUi(Math.round((completedJobs / totalJobs) * 100));
progressBarWrapper.classList.remove("is-rough-complete");
progressText.classList.remove(
"is-rough",
"is-fading-done",
"is-done-message",
);
const percentage = Math.round((completedJobs / totalJobs) * 100);
progressBar.style.width = `${Math.max(2, percentage)}%`;
progressBarWrapper.classList.add("is-active");
searchAnchor.classList.add("is-indexing");
searchButton.classList.add("is-indexing");
if (indexingStatus) {
progressText.textContent = `${truncateStatus(indexingStatus)} · ${percentage}%`;
} else {
progressText.textContent = `Indexing ${completedJobs}/${totalJobs} (${percentage}%)`;
}
progressText.classList.add("is-active");
return; return;
} }
if (completionEligible) { if (completionEligible) {
// Duplicate end-of-run ticks must not reschedule hold/fade timers if (doneFlashTimer !== null || doneFadeTimer !== null) return;
if (doneFlashTimer !== null || doneFadeTimer !== null) { const rough = indexingStatus != null && statusLooksRough(indexingStatus);
scheduleCompletionFlash(rough);
return; return;
} }
const rough = resetIdleProgressUi();
indexingStatus != null && statusLooksRough(indexingStatus);
progressBar.style.width = "0%";
progressBarWrapper.classList.remove("is-active");
searchAnchor.classList.remove("is-indexing");
searchButton.classList.remove("is-indexing");
progressText.classList.remove("is-fading-done");
progressText.textContent = rough ? truncateStatus(indexingStatus!, 52) : "Done!";
if (rough) {
progressText.classList.add("is-rough");
progressBarWrapper.classList.add("is-rough-complete");
} else {
progressText.classList.remove("is-rough");
progressBarWrapper.classList.remove("is-rough-complete");
}
progressText.classList.add("is-active", "is-done-message");
doneFlashTimer = setTimeout(() => {
doneFlashTimer = null;
progressText.classList.add("is-fading-done");
doneFadeTimer = setTimeout(() => {
doneFadeTimer = null;
ranIndexingCycle = false;
indexingStatus = null;
progressBar.style.width = "0%";
progressBarWrapper.classList.remove("is-active");
progressBarWrapper.classList.remove("is-rough-complete");
searchAnchor.classList.remove("is-indexing");
searchButton.classList.remove("is-indexing");
progressText.classList.remove(
"is-active",
"is-rough",
"is-fading-done",
"is-done-message",
);
progressText.textContent = "";
}, DONE_FADE_MS);
}, DONE_HOLD_MS);
return;
}
clearDoneFlashTimer();
progressBarWrapper.classList.remove("is-active");
progressBarWrapper.classList.remove("is-rough-complete");
searchAnchor.classList.remove("is-indexing");
searchButton.classList.remove("is-indexing");
progressText.classList.remove(
"is-active",
"is-rough",
"is-fading-done",
"is-done-message",
);
progressBar.style.width = "0%";
progressText.textContent = "";
ranIndexingCycle = false;
indexingStatus = null;
}; };
// Listen for indexing progress events // Listen for indexing progress events
+95 -52
View File
@@ -105,6 +105,78 @@ function appendFolderBadgeContent(badge: HTMLElement, folder: Folder): void {
badge.appendChild(document.createTextNode(folder.name)); badge.appendChild(document.createTextNode(folder.name));
} }
const MESSAGE_LIST_ITEM_SELECTOR =
"[class*='MessageList__MessageList___'] ol > li[data-message]";
function getMessageListItems(): NodeListOf<Element> {
return document.querySelectorAll(MESSAGE_LIST_ITEM_SELECTOR);
}
function clearMessageListBadges(
messageItems: NodeListOf<Element>,
restoreSubjectPlain: (subject: Element) => void,
): void {
for (const li of messageItems) {
const subject = li.querySelector("[class*='MessageList__subject___']");
if (
subject &&
(subject.querySelector(".bsplus-msg-badges") ||
subject.querySelector(".bsplus-subject-text"))
) {
restoreSubjectPlain(subject);
} else {
li.querySelector(".bsplus-msg-badges")?.remove();
}
}
}
function getAssignedFolderIds(
msgId: string,
assignments: Record<string, string[]>,
): string[] {
return Object.entries(assignments)
.filter(([, messageIds]) => messageIds.includes(msgId))
.map(([folderId]) => folderId);
}
function ensureMessageBadgeContainer(li: Element): HTMLElement {
const existing = li.querySelector(".bsplus-msg-badges") as HTMLElement | null;
if (existing) return existing;
const badgeContainer = document.createElement("div");
badgeContainer.className = "bsplus-msg-badges";
const subject = li.querySelector("[class*='MessageList__subject___']");
if (subject) {
if (!subject.querySelector(".bsplus-subject-text")) {
const textWrap = document.createElement("span");
textWrap.className = "bsplus-subject-text";
textWrap.textContent = subject.textContent;
subject.textContent = "";
subject.appendChild(textWrap);
}
subject.appendChild(badgeContainer);
} else {
li.appendChild(badgeContainer);
}
return badgeContainer;
}
function createFolderBadge(
folder: Folder,
onFilter: (folderId: string) => void,
): HTMLElement {
const badge = document.createElement("span");
badge.className = "bsplus-msg-badge";
badge.style.background = folder.color;
appendFolderBadgeContent(badge, folder);
badge.title = `Filter by "${folder.name}"`;
badge.addEventListener("click", (e) => {
e.stopPropagation();
onFilter(folder.id);
});
return badge;
}
const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = { const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = {
id: "messageFolders", id: "messageFolders",
name: "Message Folders", name: "Message Folders",
@@ -799,66 +871,37 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
}; };
const applyBadges = () => { const applyBadges = () => {
const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]"); const messageItems = getMessageListItems();
if (!shouldShowBadgesInList()) { if (!shouldShowBadgesInList()) {
for (const li of messageItems) { clearMessageListBadges(messageItems, restoreSubjectPlain);
const subject = li.querySelector("[class*='MessageList__subject___']");
if (subject && (subject.querySelector(".bsplus-msg-badges") || subject.querySelector(".bsplus-subject-text"))) {
restoreSubjectPlain(subject);
} else {
li.querySelector(".bsplus-msg-badges")?.remove();
}
}
return; return;
} }
const folders = getFolders(); const folders = getFolders();
const assignments = getAssignments(); const assignments = getAssignments();
for (const li of messageItems) { const selectFolder = (folderId: string) => {
const msgId = li.getAttribute("data-message"); activeFolderId = folderId;
if (!msgId) continue;
let badgeContainer = li.querySelector(".bsplus-msg-badges") as HTMLElement | null;
const folderIds: string[] = [];
for (const [fId, mIds] of Object.entries(assignments)) {
if (mIds.includes(msgId)) folderIds.push(fId);
}
if (folderIds.length === 0) {
badgeContainer?.remove();
continue;
}
if (!badgeContainer) {
badgeContainer = document.createElement("div");
badgeContainer.className = "bsplus-msg-badges";
const subject = li.querySelector("[class*='MessageList__subject___']");
if (subject) {
if (!subject.querySelector(".bsplus-subject-text")) {
const textWrap = document.createElement("span");
textWrap.className = "bsplus-subject-text";
textWrap.textContent = subject.textContent;
subject.textContent = "";
subject.appendChild(textWrap);
}
subject.appendChild(badgeContainer);
} else {
li.appendChild(badgeContainer);
}
}
badgeContainer.innerHTML = "";
for (const fId of folderIds) {
const folder = folders.find((f) => f.id === fId);
if (!folder) continue;
const badge = document.createElement("span");
badge.className = "bsplus-msg-badge";
badge.style.background = folder.color;
appendFolderBadgeContent(badge, folder);
badge.title = `Filter by "${folder.name}"`;
badge.addEventListener("click", (e) => {
e.stopPropagation();
activeFolderId = folder.id;
applyFolderFilter(); applyFolderFilter();
applyBadges(); applyBadges();
renderSidebarFolders(); renderSidebarFolders();
}); };
badgeContainer.appendChild(badge);
for (const li of messageItems) {
const msgId = li.getAttribute("data-message");
if (!msgId) continue;
const folderIds = getAssignedFolderIds(msgId, assignments);
if (folderIds.length === 0) {
li.querySelector(".bsplus-msg-badges")?.remove();
continue;
}
const badgeContainer = ensureMessageBadgeContainer(li);
badgeContainer.replaceChildren();
for (const folderId of folderIds) {
const folder = folders.find((f) => f.id === folderId);
if (!folder) continue;
badgeContainer.appendChild(createFolderBadge(folder, selectFolder));
} }
} }
}; };
+136 -137
View File
@@ -14,216 +14,215 @@ function escapeHtmlAttr(value: string): string {
.replace(/>/g, "&gt;"); .replace(/>/g, "&gt;");
} }
export function OpenMenuOptions() { function syncDefaultMenuOrder(menu: HTMLElement) {
if (MenuOptionsOpen) return; const childnodes = menu.firstChild!.childNodes;
if (settingsState.defaultmenuorder.length === 0) {
settingsState.defaultmenuorder = Array.from(childnodes).map(
(element) => (element as HTMLElement).dataset.key,
);
return;
}
var container = document.getElementById("container"); if (settingsState.defaultmenuorder.length === childnodes.length) return;
var menu = document.getElementById("menu");
if (settingsState.defaultmenuorder.length == 0) { const nextOrder = [...settingsState.defaultmenuorder];
let childnodes = menu!.firstChild!.childNodes;
let newdefaultmenuorder = [];
for (let i = 0; i < childnodes.length; i++) { for (let i = 0; i < childnodes.length; i++) {
const element = childnodes[i]; const key = (childnodes[i] as HTMLElement).dataset.key;
newdefaultmenuorder.push((element as HTMLElement).dataset.key); if (key && nextOrder.indexOf(key) === -1) nextOrder.push(key);
settingsState.defaultmenuorder = newdefaultmenuorder;
}
}
let childnodes = menu!.firstChild!.childNodes;
if (settingsState.defaultmenuorder.length != childnodes.length) {
for (let i = 0; i < childnodes.length; i++) {
const element = childnodes[i];
if (
settingsState.defaultmenuorder.indexOf(
(element as HTMLElement).dataset.key,
) === -1
) {
let newdefaultmenuorder = settingsState.defaultmenuorder;
newdefaultmenuorder.push((element as HTMLElement).dataset.key);
settingsState.defaultmenuorder = newdefaultmenuorder;
}
} }
settingsState.defaultmenuorder = nextOrder;
} }
MenuOptionsOpen = true; function createMenuEditControls(menu: HTMLElement, container: HTMLElement) {
const cover = document.createElement("div");
var cover = document.createElement("div");
cover.classList.add("notMenuCover"); cover.classList.add("notMenuCover");
menu!.style.zIndex = "20"; menu.style.zIndex = "20";
menu!.style.setProperty("--menuHidden", "flex"); menu.style.setProperty("--menuHidden", "flex");
container!.append(cover); container.append(cover);
var menusettings = document.createElement("div"); const menusettings = document.createElement("div");
menusettings.classList.add("editmenuoption-container"); menusettings.classList.add("editmenuoption-container");
var defaultbutton = document.createElement("div"); const defaultbutton = document.createElement("div");
defaultbutton.classList.add("editmenuoption"); defaultbutton.classList.add("editmenuoption");
defaultbutton.innerText = "Restore Default"; defaultbutton.innerText = "Restore Default";
defaultbutton.id = "restoredefaultoption"; defaultbutton.id = "restoredefaultoption";
var savebutton = document.createElement("div"); const savebutton = document.createElement("div");
savebutton.classList.add("editmenuoption"); savebutton.classList.add("editmenuoption");
savebutton.innerText = "Save"; savebutton.innerText = "Save";
savebutton.id = "savemenuoption"; savebutton.id = "savemenuoption";
menusettings.appendChild(defaultbutton); menusettings.appendChild(defaultbutton);
menusettings.appendChild(savebutton); menusettings.appendChild(savebutton);
menu.appendChild(menusettings);
menu!.appendChild(menusettings); return { cover, menusettings, defaultbutton, savebutton };
}
var ListItems = menu!.firstChild!.childNodes; function prepareMenuItemsForEdit(menu: HTMLElement) {
for (let i = 0; i < ListItems.length; i++) { const listItems = menu.firstChild!.childNodes;
const element1 = ListItems[i]; for (let i = 0; i < listItems.length; i++) {
const element = element1 as HTMLElement; const element = listItems[i] as HTMLElement;
element.classList.add("draggable");
(element as HTMLElement).classList.add("draggable"); if (element.classList.contains("hasChildren")) {
if ((element as HTMLElement).classList.contains("hasChildren")) { element.classList.remove("active");
(element as HTMLElement).classList.remove("active");
(element.firstChild as HTMLElement).classList.remove("noscroll"); (element.firstChild as HTMLElement).classList.remove("noscroll");
} }
const menuKey = escapeHtmlAttr((element as HTMLElement).dataset.key ?? ""); const menuKey = escapeHtmlAttr(element.dataset.key ?? "");
let MenuItemToggle = stringToHTML( const menuItemToggle = stringToHTML(
`<div class="onoffswitch" style="margin: auto 0;"><input class="onoffswitch-checkbox notification menuitem" type="checkbox" id="${menuKey}"><label for="${menuKey}" class="onoffswitch-label"></label>`, `<div class="onoffswitch" style="margin: auto 0;"><input class="onoffswitch-checkbox notification menuitem" type="checkbox" id="${menuKey}"><label for="${menuKey}" class="onoffswitch-label"></label>`,
).firstChild; ).firstChild;
(element as HTMLElement).append(MenuItemToggle!); element.append(menuItemToggle!);
if (!element.dataset.betterseqta) { if (element.dataset.betterseqta) continue;
const a = document.createElement("section");
cloneAttributes(a, element); const section = document.createElement("section");
while (element.firstChild) { cloneAttributes(section, element);
a.appendChild(element.firstChild); while (element.firstChild) section.appendChild(element.firstChild);
} menu.firstChild!.insertBefore(section, element);
menu!.firstChild!.insertBefore(a, element);
element.remove(); element.remove();
} }
return listItems;
} }
if (Object.keys(settingsState.menuitems).length == 0) { function ensureMenuItemSettings(menu: HTMLElement) {
menubuttons = menu!.firstChild!.childNodes; if (Object.keys(settingsState.menuitems).length > 0) return;
let menuItems = {} as any;
for (var i = 0; i < menubuttons.length; i++) { const menuItems: Record<string, { toggle: boolean }> = {};
var id = (menubuttons[i] as HTMLElement).dataset.key; const menuButtons = menu.firstChild!.childNodes;
const element: any = {}; for (let i = 0; i < menuButtons.length; i++) {
element.toggle = true; const id = (menuButtons[i] as HTMLElement).dataset.key;
(menuItems[id as keyof typeof menuItems] as any) = element; if (id) menuItems[id] = { toggle: true };
} }
settingsState.menuitems = menuItems; settingsState.menuitems = menuItems;
} }
var menubuttons: any = document.getElementsByClassName("menuitem"); function applySavedMenuToggleStates() {
const menuItems = settingsState.menuitems as Record<
let menuItems = settingsState.menuitems as any; string,
let buttons = document.getElementsByClassName("menuitem"); { toggle: boolean }
>;
const buttons = document.getElementsByClassName("menuitem");
for (let i = 0; i < buttons.length; i++) { for (let i = 0; i < buttons.length; i++) {
let id = buttons[i].id as string | undefined; const id = buttons[i].id;
if (menuItems[id as keyof typeof menuItems]) { (buttons[i] as HTMLInputElement).checked = id
(buttons[i] as HTMLInputElement).checked = ? (menuItems[id]?.toggle ?? true)
menuItems[id as keyof typeof menuItems].toggle; : true;
} else {
(buttons[i] as HTMLInputElement).checked = true;
} }
} }
let sortable: Sortable | undefined; function createMenuSortable(): Sortable | undefined {
try { try {
var el = document.querySelector("#menu > ul"); const el = document.querySelector("#menu > ul");
if (!el) return undefined;
let sortable: Sortable | undefined;
sortable = Sortable.create(el as HTMLElement, { sortable = Sortable.create(el as HTMLElement, {
draggable: ".draggable", draggable: ".draggable",
dataIdAttr: "data-key", dataIdAttr: "data-key",
animation: 150, animation: 150,
easing: "cubic-bezier(.5,0,.5,1)", easing: "cubic-bezier(.5,0,.5,1)",
onEnd: function () { onEnd: () => saveNewOrder(sortable),
saveNewOrder(sortable);
},
}); });
return sortable;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return undefined;
}
} }
function changeDisplayProperty(element: any) { function changeMenuItemDisplay(element: HTMLInputElement) {
const row = element.parentNode?.parentNode as HTMLElement | null;
if (!row) return;
if (!element.checked) { if (!element.checked) {
element.parentNode.parentNode.style.display = "var(--menuHidden)"; row.style.display = "var(--menuHidden)";
} return;
if (element.checked) {
element.parentNode.parentNode.style.setProperty(
"display",
"flex",
"important",
);
} }
row.style.setProperty("display", "flex", "important");
} }
function StoreMenuSettings() { function storeMenuSettings() {
let menu = document.getElementById("menu"); const menu = document.getElementById("menu");
const menuItems: any = {}; if (!menu) return;
let menubuttons = menu!.firstChild!.childNodes;
const button = document.getElementsByClassName("menuitem");
for (let i = 0; i < menubuttons.length; i++) {
const id = (menubuttons[i] as HTMLElement).dataset.key;
const element: any = {};
element.toggle = (button[i] as HTMLInputElement).checked;
menuItems[id as keyof typeof menuItems] = element; const menuItems: Record<string, { toggle: boolean }> = {};
const menuButtons = menu.firstChild!.childNodes;
const toggles = document.getElementsByClassName("menuitem");
for (let i = 0; i < menuButtons.length; i++) {
const id = (menuButtons[i] as HTMLElement).dataset.key;
if (!id) continue;
menuItems[id] = { toggle: (toggles[i] as HTMLInputElement).checked };
} }
settingsState.menuitems = menuItems; settingsState.menuitems = menuItems;
} }
for (let i = 0; i < menubuttons.length; i++) { function restoreMenuItemsFromEditMode(menu: HTMLElement, listItems: NodeListOf<ChildNode>) {
const element = menubuttons[i]; for (let i = 0; i < listItems.length; i++) {
element.addEventListener("change", () => { const element = listItems[i] as HTMLElement;
element.parentElement.parentElement.getAttribute("data-key");
StoreMenuSettings();
changeDisplayProperty(element);
});
}
function closeAll() {
menusettings?.remove();
cover?.remove();
MenuOptionsOpen = false;
menu!.style.setProperty("--menuHidden", "none");
for (let i = 0; i < ListItems.length; i++) {
const element1 = ListItems[i];
const element = element1 as HTMLElement;
element.classList.remove("draggable"); element.classList.remove("draggable");
element.setAttribute("draggable", "false"); element.setAttribute("draggable", "false");
if (!element.dataset.betterseqta) { if (element.dataset.betterseqta) continue;
const a = document.createElement("li");
cloneAttributes(a, element); const listItem = document.createElement("li");
while (element.firstChild) { cloneAttributes(listItem, element);
a.appendChild(element.firstChild); while (element.firstChild) listItem.appendChild(element.firstChild);
} menu.firstChild!.insertBefore(listItem, element);
menu!.firstChild!.insertBefore(a, element);
element.remove(); element.remove();
} }
menu.querySelectorAll(".onoffswitch").forEach((toggle) => toggle.remove());
} }
let switches = menu!.querySelectorAll(".onoffswitch"); export function OpenMenuOptions() {
for (let i = 0; i < switches.length; i++) { if (MenuOptionsOpen) return;
switches[i].remove();
}
}
cover?.addEventListener("click", closeAll); const container = document.getElementById("container");
savebutton?.addEventListener("click", closeAll); const menu = document.getElementById("menu");
if (!container || !menu) return;
defaultbutton?.addEventListener("click", function () { syncDefaultMenuOrder(menu);
const options = settingsState.defaultmenuorder; MenuOptionsOpen = true;
settingsState.menuorder = options;
ChangeMenuItemPositions(options); const { cover, menusettings, defaultbutton, savebutton } = createMenuEditControls(
menu,
container,
);
const listItems = prepareMenuItemsForEdit(menu);
ensureMenuItemSettings(menu);
applySavedMenuToggleStates();
const menubuttons = document.getElementsByClassName("menuitem");
const sortable = createMenuSortable();
for (let i = 0; i < menubuttons.length; i++) { for (let i = 0; i < menubuttons.length; i++) {
const element = menubuttons[i]; const element = menubuttons[i] as HTMLInputElement;
element.addEventListener("change", () => {
storeMenuSettings();
changeMenuItemDisplay(element);
});
}
const closeAll = () => {
menusettings.remove();
cover.remove();
MenuOptionsOpen = false;
menu.style.setProperty("--menuHidden", "none");
restoreMenuItemsFromEditMode(menu, listItems);
};
cover.addEventListener("click", closeAll);
savebutton.addEventListener("click", closeAll);
defaultbutton.addEventListener("click", () => {
settingsState.menuorder = settingsState.defaultmenuorder;
ChangeMenuItemPositions(settingsState.defaultmenuorder);
for (let i = 0; i < menubuttons.length; i++) {
const element = menubuttons[i] as HTMLInputElement;
element.checked = true; element.checked = true;
element.parentNode.parentNode.style.setProperty( changeMenuItemDisplay(element);
"display",
"flex",
"important",
);
} }
if (sortable) saveNewOrder(sortable); if (sortable) saveNewOrder(sortable);
}); });
+24 -10
View File
@@ -5,28 +5,42 @@ const ALLOWED_HOST_SUFFIXES = [
"github.com", "github.com",
] as const; ] as const;
function isPrivateOrLocalHost(hostname: string): boolean { function normalizeHost(hostname: string): string {
const host = hostname.toLowerCase().replace(/^\[|\]$/g, ""); return hostname.toLowerCase().replace(/^\[|\]$/g, "");
if (host === "localhost" || host.endsWith(".localhost")) return true; }
if (host === "127.0.0.1" || host.startsWith("127.")) return true;
if (host === "::1" || host === "0.0.0.0") return true;
function isNamedLocalHost(host: string): boolean {
return (
host === "localhost" ||
host.endsWith(".localhost") ||
host === "127.0.0.1" ||
host.startsWith("127.") ||
host === "::1" ||
host === "0.0.0.0"
);
}
function isPrivateIpv4(host: string): boolean {
const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
if (ipv4) { if (!ipv4) return false;
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])]; const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
if (a === 10) return true; if (a === 10) return true;
if (a === 172 && b >= 16 && b <= 31) return true; if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true; if (a === 192 && b === 168) return true;
if (a === 169 && b === 254) return true; if (a === 169 && b === 254) return true;
if (a === 127 || a === 0) return true; return a === 127 || a === 0;
} }
if (host.includes(":")) { function isPrivateIpv6(host: string): boolean {
if (!host.includes(":")) return false;
const h = host.split("%")[0]; const h = host.split("%")[0];
if (h.startsWith("fe80") || h.startsWith("fc") || h.startsWith("fd")) return true; return h.startsWith("fe80") || h.startsWith("fc") || h.startsWith("fd");
} }
return false; function isPrivateOrLocalHost(hostname: string): boolean {
const host = normalizeHost(hostname);
return isNamedLocalHost(host) || isPrivateIpv4(host) || isPrivateIpv6(host);
} }
function isAllowedHost(hostname: string): boolean { function isAllowedHost(hostname: string): boolean {