diff --git a/src/pageState.js b/src/pageState.js index 0bc3eba9..491d2179 100644 --- a/src/pageState.js +++ b/src/pageState.js @@ -513,100 +513,104 @@ function isTrustedMessage(event) { return event.source === window && event.origin === window.location.origin; } +function executeReactFiberAction(fiberInstance, action, payload) { + switch (action) { + case "getState": + return fiberInstance.getState(payload.key); + case "setState": + if ( + payload.updateObject && + typeof payload.updateObject === "object" && + !Array.isArray(payload.updateObject) + ) { + fiberInstance.setState(payload.updateObject); + } else { + console.warn("[pageState] setState rejected: only plain objects are allowed"); + } + return {}; + case "getProp": + return fiberInstance.getProp(payload.propName); + case "setProp": + fiberInstance.setProp(payload.propName, payload.value); + return {}; + case "forceUpdate": + fiberInstance.forceUpdate(); + return {}; + default: + console.warn(`[pageState] Unknown action: ${action}`); + return null; + } +} + +function sanitizeResponseForPostMessage(response) { + if (response !== null && typeof response === "object") { + response = makeSerializable(response); + } + + try { + deepFunctionCheck(response); + } catch (functionError) { + console.warn("[pageState] Function detected in response, cleaning:", functionError.message); + return `[Cleaned Response - Function found at: ${functionError.message}]`; + } + + try { + if (typeof structuredClone === "function") { + structuredClone(response); + } else { + JSON.parse(JSON.stringify(response)); + } + } catch (cloneError) { + console.warn("[pageState] Response not cloneable, fallback:", 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( + { + type: "reactFiberResponse", + response, + messageId, + }, + window.location.origin, + ); +} + +function handleTriggerKeyboardEvent(data) { + const { key, code, altKey, ctrlKey, metaKey, shiftKey, keyCode } = data; + const keyboardEvent = new KeyboardEvent("keydown", { + key, + code, + keyCode: keyCode || 0, + which: keyCode || 0, + altKey: altKey || false, + ctrlKey: ctrlKey || false, + metaKey: metaKey || false, + shiftKey: shiftKey || false, + bubbles: true, + cancelable: true, + }); + document.dispatchEvent(keyboardEvent); +} + window.addEventListener("message", (event) => { if (!isTrustedMessage(event)) return; if (event.data.type === "reactFiberRequest") { - const { selector, action, payload, debug, messageId } = event.data; - const fiberInstance = ReactFiber.find(selector, { - debug, - }); + handleReactFiberRequest(event.data); + return; + } - let response; - switch (action) { - case "getState": - response = fiberInstance.getState(payload.key); - break; - case "setState": - if ( - payload.updateObject && - typeof payload.updateObject === "object" && - !Array.isArray(payload.updateObject) - ) { - fiberInstance.setState(payload.updateObject); - } else { - console.warn("[pageState] setState rejected: only plain objects are allowed"); - } - response = {}; - break; - - case "getProp": - response = fiberInstance.getProp(payload.propName); - break; - case "setProp": - fiberInstance.setProp(payload.propName, payload.value); - response = {}; - break; - case "forceUpdate": - fiberInstance.forceUpdate(); - response = {}; - break; - default: - console.warn(`[pageState] Unknown action: ${action}`); - response = null; - } - - if (response !== null && typeof response === "object") { - response = makeSerializable(response); - } - - // Final safety check before postMessage - try { - deepFunctionCheck(response); - } catch (functionError) { - console.warn("[pageState] Function detected in response, cleaning:", functionError.message); - response = `[Cleaned Response - Function found at: ${functionError.message}]`; - } - - // Additional structured clone test - try { - // Test if the object can be cloned (same algorithm as postMessage) - if (typeof structuredClone === 'function') { - structuredClone(response); - } else { - // Fallback for older browsers - try JSON round-trip - JSON.parse(JSON.stringify(response)); - } - } catch (cloneError) { - console.warn("[pageState] Response not cloneable, fallback:", cloneError.message); - response = `[Uncloneable Response: ${cloneError.message}]`; - } - - window.postMessage( - { - type: "reactFiberResponse", - response, - messageId, - }, - 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', { - key, - code, - keyCode: keyCode || 0, - which: keyCode || 0, - altKey: altKey || false, - ctrlKey: ctrlKey || false, - metaKey: metaKey || false, - shiftKey: shiftKey || false, - bubbles: true, - cancelable: true - }); - - document.dispatchEvent(keyboardEvent); + if (event.data.type === "triggerKeyboardEvent") { + handleTriggerKeyboardEvent(event.data); } }); diff --git a/src/plugins/built-in/assessmentsAverage/utils.ts b/src/plugins/built-in/assessmentsAverage/utils.ts index 216a4584..e17c35b3 100644 --- a/src/plugins/built-in/assessmentsAverage/utils.ts +++ b/src/plugins/built-in/assessmentsAverage/utils.ts @@ -1040,7 +1040,15 @@ function resolveTabSetClasses(): Record { 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 { const selectedItem = document.querySelector( "[class*='AssessmentItem__AssessmentItem___'][class*='selected___']", ) as HTMLElement | null; @@ -1056,14 +1064,11 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) { ? (api.storage.weightings?.[assessmentID] as WeightingEntry | undefined) : undefined; const rawWeight = entry?.weight; - const weightingUnavailable = rawWeight === "N/A"; - const autoWeight = rawWeight && rawWeight !== "processing" && rawWeight !== "N/A" ? rawWeight : undefined; - const override = assessmentID ? api.storage.weightingOverrides?.[assessmentID] : undefined; @@ -1076,6 +1081,22 @@ async function buildWeightingsTabContent(api: any, sheet: HTMLElement) { ? "No weighting was found in the marksheet. Set one manually." : "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 = `