mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-25 14:25:10 +02:00
finished cool feature 2 and it's very cool
This commit is contained in:
parent
713f871024
commit
ca072f236d
618
js/content.js
618
js/content.js
@ -6763,18 +6763,6 @@ function setGradeAnalyticsFitY(fit) {
|
|||||||
chrome.storage.local.set({ [GA_FIT_Y_KEY]: fit });
|
chrome.storage.local.set({ [GA_FIT_Y_KEY]: fit });
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Imagine-If mode" toggle state, remembered across page loads.
|
|
||||||
const GA_IMAGINE_KEY = "grade_analytics_imagine_if";
|
|
||||||
|
|
||||||
async function getGradeAnalyticsImagineIf() {
|
|
||||||
const result = await chrome.storage.local.get(GA_IMAGINE_KEY);
|
|
||||||
return result[GA_IMAGINE_KEY] ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setGradeAnalyticsImagineIf(on) {
|
|
||||||
chrome.storage.local.set({ [GA_IMAGINE_KEY]: on });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final-grade calculator settings, stored per course so each course's final
|
// Final-grade calculator settings, stored per course so each course's final
|
||||||
// weight and goal survive reloads: { weight, target, show }. The needed
|
// weight and goal survive reloads: { weight, target, show }. The needed
|
||||||
// score itself is never stored — it's always recomputed against the live
|
// score itself is never stored — it's always recomputed against the live
|
||||||
@ -6803,7 +6791,10 @@ function saveGaCalcSettings() {
|
|||||||
let gaObserver = null;
|
let gaObserver = null;
|
||||||
let gaOpen = false; // panel open on this page view
|
let gaOpen = false; // panel open on this page view
|
||||||
let gaFitY = false; // scale the line chart Y axis to fit the data
|
let gaFitY = false; // scale the line chart Y axis to fit the data
|
||||||
let gaImagineIf = false; // "Imagine-If mode" enabled on this page view
|
let gaImagineIf = false; // "Imagine-If mode" enabled on this page view (never persisted — always off on load)
|
||||||
|
let gaScenario = null; // imagine-if working copy of groups + assignments
|
||||||
|
let gaIfCounter = 0; // unique ids for user-added groups/assignments
|
||||||
|
let gaOriginalFinalHtml = null; // Total row's original grade span innerHTML, for restore
|
||||||
let gaTab = "overview"; // active panel tab: "overview" | "calc" | "heatmap"
|
let gaTab = "overview"; // active panel tab: "overview" | "calc" | "heatmap"
|
||||||
let gaCalc = null; // final-grade calculator settings for this course
|
let gaCalc = null; // final-grade calculator settings for this course
|
||||||
let gaCourseId = null; // course whose data is cached
|
let gaCourseId = null; // course whose data is cached
|
||||||
@ -6864,11 +6855,10 @@ function watchGradeAnalytics() {
|
|||||||
scheduleGradeAnalyticsSync();
|
scheduleGradeAnalyticsSync();
|
||||||
// Restore the open/closed state and Y-axis preference the user last
|
// Restore the open/closed state and Y-axis preference the user last
|
||||||
// chose, then inject the panel below the Print Grades header.
|
// chose, then inject the panel below the Print Grades header.
|
||||||
Promise.all([getGradeAnalyticsOpenState(), getGradeAnalyticsFitY(), getGaCalcSettings(courseId), getGradeAnalyticsImagineIf()]).then(([open, fit, calc, imagine]) => {
|
Promise.all([getGradeAnalyticsOpenState(), getGradeAnalyticsFitY(), getGaCalcSettings(courseId)]).then(([open, fit, calc]) => {
|
||||||
gaOpen = open;
|
gaOpen = open;
|
||||||
gaFitY = fit;
|
gaFitY = fit;
|
||||||
gaCalc = calc;
|
gaCalc = calc;
|
||||||
gaImagineIf = imagine;
|
|
||||||
const panel = ensureGradeAnalyticsPanel();
|
const panel = ensureGradeAnalyticsPanel();
|
||||||
if (panel) applyGaCalcState(panel);
|
if (panel) applyGaCalcState(panel);
|
||||||
if (gaOpen && gaData) renderGradeAnalytics();
|
if (gaOpen && gaData) renderGradeAnalytics();
|
||||||
@ -6889,6 +6879,13 @@ function syncGradeAnalyticsUI() {
|
|||||||
if (!gradeAnalyticsActive()) return;
|
if (!gradeAnalyticsActive()) return;
|
||||||
const panel = ensureGradeAnalyticsPanel();
|
const panel = ensureGradeAnalyticsPanel();
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
|
// Imagine-If: Canvas re-renders can wipe the overwritten Total block or
|
||||||
|
// swap in a new grades table (grading-period switch) — reapply, and
|
||||||
|
// rebuild the scenario when the table's rows changed.
|
||||||
|
if (gaImagineIf) {
|
||||||
|
gaRenderImagineIf();
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
}
|
||||||
// Self-heal: the one-shot render after data loads can be a no-op when the
|
// Self-heal: the one-shot render after data loads can be a no-op when the
|
||||||
// panel was created before Canvas finished laying out the page (zero-size
|
// panel was created before Canvas finished laying out the page (zero-size
|
||||||
// canvases) or while the body was still hidden. gaSetupCanvas leaves the
|
// canvases) or while the body was still hidden. gaSetupCanvas leaves the
|
||||||
@ -6905,6 +6902,10 @@ function syncGradeAnalyticsUI() {
|
|||||||
|
|
||||||
function removeGradeAnalyticsPanel() {
|
function removeGradeAnalyticsPanel() {
|
||||||
gaOpen = false;
|
gaOpen = false;
|
||||||
|
// Never leave a hypothetical Total or inline editors behind when the
|
||||||
|
// panel goes away.
|
||||||
|
gaClearImagineUI();
|
||||||
|
gaRestoreImagineTotal();
|
||||||
document.getElementById("canvasrefined-grade-analytics")?.remove();
|
document.getElementById("canvasrefined-grade-analytics")?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6922,8 +6923,8 @@ function applyGradeAnalyticsOpenState(panel) {
|
|||||||
btn.setAttribute("aria-expanded", String(gaOpen));
|
btn.setAttribute("aria-expanded", String(gaOpen));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Syncs the "Imagine-If mode" button's DOM to the in-memory (storage-backed)
|
// Syncs the "Imagine-If mode" button's DOM to the in-memory state.
|
||||||
// state. Called at panel creation and whenever an already-attached panel is
|
// Called at panel creation and whenever an already-attached panel is
|
||||||
// reused, mirroring applyGradeAnalyticsOpenState.
|
// reused, mirroring applyGradeAnalyticsOpenState.
|
||||||
function applyGradeAnalyticsImagineState(panel) {
|
function applyGradeAnalyticsImagineState(panel) {
|
||||||
const btn = panel.querySelector("#canvasrefined-ga-imagine");
|
const btn = panel.querySelector("#canvasrefined-ga-imagine");
|
||||||
@ -7020,7 +7021,7 @@ function ensureGradeAnalyticsPanel() {
|
|||||||
<span style="width:11px;height:11px;border-radius:3px;display:inline-block;background:${GA_ZONE_COLORS[15]};"></span>
|
<span style="width:11px;height:11px;border-radius:3px;display:inline-block;background:${GA_ZONE_COLORS[15]};"></span>
|
||||||
<span style="width:11px;height:11px;border-radius:3px;display:inline-block;background:${GA_ZONE_COLORS[19]};"></span>
|
<span style="width:11px;height:11px;border-radius:3px;display:inline-block;background:${GA_ZONE_COLORS[19]};"></span>
|
||||||
<span>100%</span>
|
<span>100%</span>
|
||||||
<span style="margin-left:12px;display:inline-flex;align-items:center;gap:4px;"><span style="width:12px;height:12px;border-radius:3px;display:inline-block;background:color-mix(in srgb, var(--bctext-1) 20%, transparent);"></span>no graded work due</span>
|
<span style="margin-left:12px;display:inline-flex;align-items:center;gap:4px;"><span style="width:12px;height:12px;border-radius:3px;display:inline-block;background:color-mix(in srgb, var(--bctext-1) 20%, transparent);"></span>No grade/assignment</span>
|
||||||
</div>
|
</div>
|
||||||
<style>#canvasrefined-grade-analytics .canvasrefined-ga-hcell:hover{outline:1px solid var(--bctext-0);outline-offset:1px;}</style>
|
<style>#canvasrefined-grade-analytics .canvasrefined-ga-hcell:hover{outline:1px solid var(--bctext-0);outline-offset:1px;}</style>
|
||||||
<div id="canvasrefined-ga-heatmap-tip" style="position:absolute;display:none;pointer-events:none;z-index:100;background:var(--bcbackground-1);color:var(--bctext-0);border:1px solid var(--bcborders);border-radius:6px;padding:6px 10px;font-size:12px;white-space:nowrap;box-shadow:0 4px 14px rgba(0,0,0,0.25);"></div>
|
<div id="canvasrefined-ga-heatmap-tip" style="position:absolute;display:none;pointer-events:none;z-index:100;background:var(--bcbackground-1);color:var(--bctext-0);border:1px solid var(--bcborders);border-radius:6px;padding:6px 10px;font-size:12px;white-space:nowrap;box-shadow:0 4px 14px rgba(0,0,0,0.25);"></div>
|
||||||
@ -7038,13 +7039,13 @@ function ensureGradeAnalyticsPanel() {
|
|||||||
if (gaOpen && gaData) renderGradeAnalytics();
|
if (gaOpen && gaData) renderGradeAnalytics();
|
||||||
});
|
});
|
||||||
// "Imagine-If mode" button on the right side of the panel header. The
|
// "Imagine-If mode" button on the right side of the panel header. The
|
||||||
// state is remembered across pages via chrome.storage; the active style
|
// toggle is per-page-view only (always off on load); the active style
|
||||||
// highlights the button while the mode is on.
|
// highlights the button while the mode is on.
|
||||||
imagineBtn.addEventListener("click", () => {
|
imagineBtn.addEventListener("click", () => {
|
||||||
gaImagineIf = !gaImagineIf;
|
gaImagineIf = !gaImagineIf;
|
||||||
setGradeAnalyticsImagineIf(gaImagineIf);
|
|
||||||
applyGradeAnalyticsImagineState(panel);
|
applyGradeAnalyticsImagineState(panel);
|
||||||
if (gaImagineIf && gaOpen && gaData) renderGradeAnalytics();
|
if (gaImagineIf) gaEnterImagineIf();
|
||||||
|
else gaExitImagineIf();
|
||||||
});
|
});
|
||||||
// "Fit Y axis" scales the line chart's Y axis to the data instead of a
|
// "Fit Y axis" scales the line chart's Y axis to the data instead of a
|
||||||
// fixed 0-100; the choice is remembered across pages via chrome.storage.
|
// fixed 0-100; the choice is remembered across pages via chrome.storage.
|
||||||
@ -7899,6 +7900,581 @@ window.addEventListener("resize", () => {
|
|||||||
if (panel && gaOpen && gaData) renderGradeAnalytics();
|
if (panel && gaOpen && gaData) renderGradeAnalytics();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Imagine-If mode ---------------------------------------------------------
|
||||||
|
//
|
||||||
|
// A hypothetical-grade sandbox rendered directly on the grades table (never
|
||||||
|
// inside the Grade Analytics panel): each assignment row gets inline
|
||||||
|
// score / points-possible inputs plus a group selector and remove button,
|
||||||
|
// each group-total row gets a weight input and remove button, the "+ Add
|
||||||
|
// assignment" button sits at the top of the table (above the first
|
||||||
|
// assignment), and the "+ Add group" button sits above the Total row. The
|
||||||
|
// hypothetical total is computed here — Canvas's weighting algorithm
|
||||||
|
// (groups with counted work contribute pct × weight, scaled up to 100% when
|
||||||
|
// the used weights total less than 100; point-based totals when no group has
|
||||||
|
// weight) — and previewed by overwriting the table's Total row (the
|
||||||
|
// tr.final_grade percentage span) with an obvious Imagine-If badge. Nothing is ever sent to Canvas; the page is
|
||||||
|
// restored to the pixel the moment the mode is turned off.
|
||||||
|
|
||||||
|
// Standard grading scheme for the hypothetical letter grade (a course's
|
||||||
|
// actual scheme isn't exposed on this page). 80.5% → B− matches Canvas's
|
||||||
|
// default cutoffs.
|
||||||
|
const GA_LETTER_SCALE = [
|
||||||
|
["A", 93], ["A−", 90], ["B+", 87], ["B", 83], ["B−", 80],
|
||||||
|
["C+", 77], ["C", 73], ["C−", 70], ["D+", 67], ["D", 63], ["D−", 60],
|
||||||
|
];
|
||||||
|
|
||||||
|
function gaLetterFor(pct) {
|
||||||
|
if (pct == null || !isFinite(pct)) return null;
|
||||||
|
for (const [letter, min] of GA_LETTER_SCALE) if (pct >= min) return letter;
|
||||||
|
return "F";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The table's Total row (tr.final_grade) — the recalculated grade
|
||||||
|
// overwrites its percentage span while Imagine-If mode is on.
|
||||||
|
function gaFindTotalGradeSpan() {
|
||||||
|
const row = document.querySelector("#grades_summary tr.final_grade");
|
||||||
|
return row ? row.querySelector("td.assignment_score .tooltip .grade") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaFindTotalTitleCell() {
|
||||||
|
const row = document.querySelector("#grades_summary tr.final_grade");
|
||||||
|
return row ? row.querySelector("th.title") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature of the page's grades table (row ids). When it changes (grading
|
||||||
|
// period switch, SPA navigation) the scenario is rebuilt from the new table.
|
||||||
|
function gaImagineTableSig() {
|
||||||
|
const table = document.querySelector("#grades_summary");
|
||||||
|
return table ? [...table.querySelectorAll("tr.student_assignment")].map(tr => tr.id).join(",") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot of the page's groups and assignments the scenario starts from.
|
||||||
|
// Scores come from the hidden "original_score" spans (immune to Canvas's own
|
||||||
|
// What-If edits); ungraded rows keep score: null so they only count once the
|
||||||
|
// user types a score for them.
|
||||||
|
function gaBuildImagineScenario() {
|
||||||
|
const table = document.querySelector("#grades_summary");
|
||||||
|
if (!table) return null;
|
||||||
|
const groups = [];
|
||||||
|
for (const tr of table.querySelectorAll("tr.group_total")) {
|
||||||
|
const gid = (tr.querySelector(".assignment_group_id")?.textContent || "").trim();
|
||||||
|
if (!gid) continue;
|
||||||
|
const name = (tr.querySelector("th.title")?.textContent || "").trim() || ("Group " + gid);
|
||||||
|
const w = parseFloat((tr.querySelector(".group_weight")?.textContent || "").trim());
|
||||||
|
groups.push({ gid, name, weight: isFinite(w) ? w : 0 });
|
||||||
|
}
|
||||||
|
const assignments = [];
|
||||||
|
for (const tr of table.querySelectorAll("tr.student_assignment")) {
|
||||||
|
if (tr.classList.contains("group_total") || tr.classList.contains("final_grade")) continue;
|
||||||
|
const a = gaParseAssignmentRow(tr);
|
||||||
|
assignments.push({
|
||||||
|
key: tr.id || ("row-" + assignments.length),
|
||||||
|
title: (a.title || "Assignment").trim() || "Assignment",
|
||||||
|
score: a.score,
|
||||||
|
points: a.points,
|
||||||
|
gid: (a.gid || "").trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { groups, assignments };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hypothetical total for the scenario, computed from scratch (never read
|
||||||
|
// from the page's Total). Only assignments with a score, a positive
|
||||||
|
// denominator, and a group that still exists count. Weighted when any
|
||||||
|
// group has weight (Canvas's GradeCalculator: sum pct × weight over groups
|
||||||
|
// with counted work, scale up to 100% when the used weights total < 100,
|
||||||
|
// use raw when ≥ 100), otherwise points earned / points possible.
|
||||||
|
function gaComputeImagineTotal() {
|
||||||
|
if (!gaScenario) return null;
|
||||||
|
const groups = new Map();
|
||||||
|
let totalWeightAll = 0;
|
||||||
|
for (const g of gaScenario.groups) {
|
||||||
|
if (g.deleted) continue;
|
||||||
|
const w = isFinite(g.weight) ? g.weight : 0;
|
||||||
|
groups.set(String(g.gid), w);
|
||||||
|
totalWeightAll += w;
|
||||||
|
}
|
||||||
|
// With every group removed the total falls back to plain points, so all
|
||||||
|
// assignments count regardless of their (dangling) group id.
|
||||||
|
const noGroups = groups.size === 0;
|
||||||
|
const stats = new Map(); // gid -> {score, pts}
|
||||||
|
for (const a of gaScenario.assignments) {
|
||||||
|
if (a.deleted || a.score == null || !(a.points > 0)) continue;
|
||||||
|
const gid = String(a.gid ?? "");
|
||||||
|
if (!noGroups && !groups.has(gid)) continue; // group deleted / unassigned
|
||||||
|
const s = stats.get(gid) || { score: 0, pts: 0 };
|
||||||
|
s.score += a.score;
|
||||||
|
s.pts += a.points;
|
||||||
|
stats.set(gid, s);
|
||||||
|
}
|
||||||
|
if (!stats.size) return null;
|
||||||
|
if (totalWeightAll > 0) {
|
||||||
|
let weighted = 0, used = 0;
|
||||||
|
for (const [gid, s] of stats) {
|
||||||
|
const w = groups.get(gid) || 0;
|
||||||
|
weighted += (s.score / s.pts) * w;
|
||||||
|
used += w;
|
||||||
|
}
|
||||||
|
if (used > 0) return used < 100 ? (weighted / used) * 100 : weighted;
|
||||||
|
// Only zero-weighted groups have counted work — fall back to points.
|
||||||
|
}
|
||||||
|
let score = 0, pts = 0;
|
||||||
|
for (const s of stats.values()) { score += s.score; pts += s.pts; }
|
||||||
|
return pts > 0 ? (score / pts) * 100 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total-row content for the hypothetical grade — clearly not the real
|
||||||
|
// grade: a purple Imagine-If badge and the hypothetical total (with letter)
|
||||||
|
// replace the percentage span, and an italic note under the "Total" label
|
||||||
|
// restates the real grade.
|
||||||
|
function gaImagineTotalHtml(pct) {
|
||||||
|
const letter = gaLetterFor(pct);
|
||||||
|
const pctText = pct == null || !isFinite(pct) ? "—" : pct.toFixed(1) + "%";
|
||||||
|
// The wrapper is an inline-flex row with align-items:center so the
|
||||||
|
// badge pill sits vertically centered with the grade text. No
|
||||||
|
// flex-wrap: the narrow score cell would stack the items instead.
|
||||||
|
return `<span style="display:inline-flex;align-items:center;gap:6px;">`
|
||||||
|
+ `<span style="background:#7c3aed;color:#fff;font-size:10px;font-weight:700;letter-spacing:0.5px;padding:1px 6px;border-radius:999px;text-transform:uppercase;white-space:nowrap;">Imagine-If</span>`
|
||||||
|
+ `<span style="font-weight:700;color:#7c3aed;">${pctText}</span>`
|
||||||
|
+ (letter ? `<span style="color:#7c3aed;">(${letter})</span>` : "")
|
||||||
|
+ `</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Overwrites (or restores) the table's Total row. Writes are guarded so the
|
||||||
|
// GA DOM observer never loops: the last written HTML is cached on the
|
||||||
|
// element and identical writes are skipped.
|
||||||
|
function gaApplyImagineTotal() {
|
||||||
|
if (!gaImagineIf) { gaRestoreImagineTotal(); return; }
|
||||||
|
gaIfUpdateGroupPcts();
|
||||||
|
const span = gaFindTotalGradeSpan();
|
||||||
|
if (!span) return;
|
||||||
|
const html = gaImagineTotalHtml(gaComputeImagineTotal());
|
||||||
|
if (!(span.dataset.gaImagine && span._gaImagineHtml === html)) {
|
||||||
|
if (!span.dataset.gaImagine) gaOriginalFinalHtml = span.innerHTML;
|
||||||
|
span.dataset.gaImagine = "1";
|
||||||
|
span._gaImagineHtml = html;
|
||||||
|
span.innerHTML = html;
|
||||||
|
}
|
||||||
|
// Italic note under the "Total" label restating the real grade.
|
||||||
|
const th = gaFindTotalTitleCell();
|
||||||
|
if (th) {
|
||||||
|
let note = th.querySelector(".canvasrefined-ga-if-note");
|
||||||
|
if (!note) {
|
||||||
|
note = document.createElement("div");
|
||||||
|
note.className = "canvasrefined-ga-if-note";
|
||||||
|
note.style.cssText = "font-size:11px;font-style:italic;color:var(--bctext-1);margin-top:2px;";
|
||||||
|
th.appendChild(note);
|
||||||
|
}
|
||||||
|
if (note.dataset.gaIfHtml !== "Imagine-If scenario; not your actual grade.") {
|
||||||
|
note.dataset.gaIfHtml = "Imagine-If scenario; not your actual grade.";
|
||||||
|
note.textContent = "Imagine-If scenario; not your actual grade.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaRestoreImagineTotal() {
|
||||||
|
const span = gaFindTotalGradeSpan();
|
||||||
|
if (span && span.dataset.gaImagine) {
|
||||||
|
delete span.dataset.gaImagine;
|
||||||
|
delete span._gaImagineHtml;
|
||||||
|
if (gaOriginalFinalHtml != null) span.innerHTML = gaOriginalFinalHtml;
|
||||||
|
}
|
||||||
|
document.querySelectorAll("#grades_summary tr.final_grade .canvasrefined-ga-if-note").forEach(n => n.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact inline styles for controls injected into the grades table.
|
||||||
|
// Canvas's table CSS gives selects/inputs an 11px bottom margin and a tall
|
||||||
|
// native select box, which made our rows taller than the page's own —
|
||||||
|
// margin:0 and a fixed select height keep the rows even.
|
||||||
|
const GA_IF_TD_INPUT = "box-sizing:border-box;padding:3px 6px;border-radius:5px;border:1px solid var(--bcborders);background:var(--bcbackground-1);color:var(--bctext-0);font-size:12px;margin:0;";
|
||||||
|
const GA_IF_TD_NUM = GA_IF_TD_INPUT + "width:58px;";
|
||||||
|
const GA_IF_TD_SEL = GA_IF_TD_INPUT + "max-width:170px;height:26px;padding:2px 4px;";
|
||||||
|
const GA_IF_TD_BTN = GA_IF_TD_INPUT + "cursor:pointer;white-space:nowrap;";
|
||||||
|
|
||||||
|
// <select> of the scenario's live groups for one assignment row. A group
|
||||||
|
// that was removed leaves its assignments on "— not counted —" until the
|
||||||
|
// user reassigns them.
|
||||||
|
function gaIfSelectHtml(gid) {
|
||||||
|
const groups = gaScenario ? gaScenario.groups.filter(g => !g.deleted) : [];
|
||||||
|
return `<select data-ga-if-groupsel title="Imagine-If assignment group" style="${GA_IF_TD_SEL}">`
|
||||||
|
+ `<option value="">— not counted —</option>`
|
||||||
|
+ groups.map(g => `<option value="${gaEscHtml(g.gid)}"${String(gid) === String(g.gid) ? " selected" : ""}>${gaEscHtml(g.name)}</option>`).join("")
|
||||||
|
+ `</select>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-renders every group <select>'s options (e.g. after a group was added or
|
||||||
|
// removed), preserving the current selection when it still exists. Skips the
|
||||||
|
// select the user is interacting with.
|
||||||
|
function gaIfRefreshSelects(table) {
|
||||||
|
for (const sel of table.querySelectorAll("select[data-ga-if-groupsel]")) {
|
||||||
|
if (document.activeElement === sel) continue;
|
||||||
|
const cur = sel.value;
|
||||||
|
const groups = gaScenario ? gaScenario.groups.filter(g => !g.deleted) : [];
|
||||||
|
sel.innerHTML = `<option value="">— not counted —</option>`
|
||||||
|
+ groups.map(g => `<option value="${gaEscHtml(g.gid)}"${String(cur) === String(g.gid) ? " selected" : ""}>${gaEscHtml(g.name)}</option>`).join("");
|
||||||
|
sel.value = groups.some(g => String(g.gid) === cur) ? cur : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon-only remove button (✕) for a row's last cell.
|
||||||
|
function gaIfDelButton(attr, title) {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.setAttribute(attr, "");
|
||||||
|
btn.title = title;
|
||||||
|
btn.textContent = "✕";
|
||||||
|
btn.style.cssText = GA_IF_TD_BTN + "padding:3px 8px;";
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Injects the scenario controls into one assignment row: score (numerator)
|
||||||
|
// and points possible (denominator) inputs replace the read-only "5 / 5"
|
||||||
|
// display in the score cell, a group <select> replaces the group-name
|
||||||
|
// context line under the title, and an icon-only remove button goes in the
|
||||||
|
// row's last cell.
|
||||||
|
function gaIfBuildAssignmentControls(tr, a) {
|
||||||
|
if (tr.querySelector(".canvasrefined-ga-if-edit")) return;
|
||||||
|
const scoreTd = tr.querySelector("td.assignment_score");
|
||||||
|
if (!scoreTd) return;
|
||||||
|
const tooltip = scoreTd.querySelector("span.tooltip");
|
||||||
|
if (tooltip) { tooltip.style.display = "none"; tooltip.dataset.gaIfHidden = "1"; }
|
||||||
|
const edit = document.createElement("span");
|
||||||
|
edit.className = "canvasrefined-ga-if-edit";
|
||||||
|
edit.style.cssText = "display:inline-flex;align-items:center;gap:4px;";
|
||||||
|
edit.innerHTML = `<input data-ga-if-score type="number" step="any" placeholder="—" value="${a.score == null ? "" : a.score}" title="Imagine-If score (numerator); blank = not counted" style="${GA_IF_TD_NUM}">`
|
||||||
|
+ ` <span style="color:var(--bctext-1);">/</span> `
|
||||||
|
+ `<input data-ga-if-pts type="number" step="any" min="0" placeholder="—" value="${a.points == null ? "" : a.points}" title="Imagine-If points possible (denominator)" style="${GA_IF_TD_NUM}">`;
|
||||||
|
(scoreTd.querySelector(".score_holder") || scoreTd).appendChild(edit);
|
||||||
|
const th = tr.querySelector("th.title");
|
||||||
|
const ctx = th?.querySelector("div.context");
|
||||||
|
if (ctx) { ctx.style.display = "none"; ctx.dataset.gaIfHidden = "1"; }
|
||||||
|
const ctl = document.createElement("div");
|
||||||
|
ctl.className = "canvasrefined-ga-if-ctl";
|
||||||
|
ctl.style.cssText = "display:flex;align-items:center;gap:6px;margin-top:4px;flex-wrap:wrap;";
|
||||||
|
ctl.innerHTML = gaIfSelectHtml(a.gid);
|
||||||
|
(th || tr).appendChild(ctl);
|
||||||
|
// Icon-only remove button in the row's last cell.
|
||||||
|
const lastTd = tr.cells[tr.cells.length - 1];
|
||||||
|
if (lastTd && !lastTd.querySelector("[data-ga-if-adel]")) {
|
||||||
|
lastTd.appendChild(gaIfDelButton("data-ga-if-adel", "Remove this assignment from the scenario"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Injects the scenario controls into one group-total row: a weight input in
|
||||||
|
// the score cell (next to the group percentage, which shows the hypothetical
|
||||||
|
// value — see gaIfUpdateGroupPcts) and an icon-only remove button in the
|
||||||
|
// row's last cell.
|
||||||
|
function gaIfBuildGroupControls(tr, g) {
|
||||||
|
if (tr.querySelector(".canvasrefined-ga-if-edit")) return;
|
||||||
|
const scoreTd = tr.querySelector("td.assignment_score");
|
||||||
|
if (!scoreTd) return;
|
||||||
|
const edit = document.createElement("span");
|
||||||
|
edit.className = "canvasrefined-ga-if-edit";
|
||||||
|
edit.style.cssText = "display:inline-flex;align-items:center;gap:4px;margin-left:8px;";
|
||||||
|
edit.innerHTML = `<input data-ga-if-gweight type="number" min="0" max="100" step="0.1" value="${g.weight}" title="Imagine-If group weight (% of grade)" style="${GA_IF_TD_NUM}">`
|
||||||
|
+ ` <span style="color:var(--bctext-1);">%</span>`;
|
||||||
|
(scoreTd.querySelector(".score_holder") || scoreTd).appendChild(edit);
|
||||||
|
// Icon-only remove button in the row's last cell.
|
||||||
|
const lastTd = tr.cells[tr.cells.length - 1];
|
||||||
|
if (lastTd && !lastTd.querySelector("[data-ga-if-gdel]")) {
|
||||||
|
lastTd.appendChild(gaIfDelButton("data-ga-if-gdel", "Remove this group from the scenario (its assignments stop counting until reassigned)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A brand-new assignment row (user-added): same 8-cell shape as the page's
|
||||||
|
// own rows, with an editable title. Custom class only — never
|
||||||
|
// "student_assignment", so the table signature and the page parsers ignore
|
||||||
|
// our rows.
|
||||||
|
function gaIfMakeAssignmentRow(a) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "canvasrefined-ga-if-newrow";
|
||||||
|
tr.innerHTML = `
|
||||||
|
<th class="title" scope="row"><input data-ga-if-title type="text" value="${gaEscHtml(a.title)}" title="Assignment name" style="${GA_IF_TD_INPUT}width:100%;max-width:260px;"></th>
|
||||||
|
<td class="due"></td><td class="submitted"></td><td class="status"></td>
|
||||||
|
<td class="assignment_score"><div class="score_holder" style="position:relative;"></div></td>
|
||||||
|
<td class="asset_processors_cell"></td><td class="details"></td><td></td>`;
|
||||||
|
// Row separators matching the page's own rows. Two things make this
|
||||||
|
// subtle: the dark-mode CSS paints every .ic-Table td border dark with
|
||||||
|
// !important (which beats a normal inline style), and in the
|
||||||
|
// collapsed-borders model CELL borders beat ROW borders — so the
|
||||||
|
// separator must be set on the cells, with !important (an inline
|
||||||
|
// !important beats the stylesheet's), to win the edge conflicts and
|
||||||
|
// paint the white line the custom background shows on real rows.
|
||||||
|
for (const cell of tr.cells) {
|
||||||
|
cell.style.setProperty("border-top", "1px solid var(--bctext-1,#e2e2e2)", "important");
|
||||||
|
cell.style.setProperty("border-bottom", "1px solid var(--bctext-1,#e2e2e2)", "important");
|
||||||
|
}
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A brand-new group-total row (user-added), with an editable name.
|
||||||
|
function gaIfMakeGroupRow(g) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "canvasrefined-ga-if-newrow";
|
||||||
|
tr.innerHTML = `
|
||||||
|
<th class="title" scope="row"><input data-ga-if-gname type="text" value="${gaEscHtml(g.name)}" title="Group name" style="${GA_IF_TD_INPUT}width:100%;max-width:260px;"></th>
|
||||||
|
<td class="due"></td><td class="submitted"></td><td class="status"></td>
|
||||||
|
<td class="assignment_score"><div class="score_holder" style="position:relative;"><span class="tooltip"><span class="grade">—</span></span></div></td>
|
||||||
|
<td class="asset_processors_cell"></td><td class="details"></td><td></td>`;
|
||||||
|
// Same cell-level separators as gaIfMakeAssignmentRow.
|
||||||
|
for (const cell of tr.cells) {
|
||||||
|
cell.style.setProperty("border-top", "1px solid var(--bctext-1,#e2e2e2)", "important");
|
||||||
|
cell.style.setProperty("border-bottom", "1px solid var(--bctext-1,#e2e2e2)", "important");
|
||||||
|
}
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New assignments go right below the table header, above the first real
|
||||||
|
// assignment (stacking in add order).
|
||||||
|
function gaIfInsertAssignmentRow(table, tr) {
|
||||||
|
const lastNew = [...table.querySelectorAll("tr.canvasrefined-ga-if-newrow[data-ga-if-key]")].pop();
|
||||||
|
if (lastNew) { lastNew.after(tr); return; }
|
||||||
|
const firstReal = [...table.querySelectorAll("tr.student_assignment")]
|
||||||
|
.find(r => !r.classList.contains("group_total") && !r.classList.contains("final_grade"));
|
||||||
|
if (firstReal) { firstReal.before(tr); return; }
|
||||||
|
(table.querySelector("tr.group_total") || table.querySelector("tr.final_grade") || table.lastElementChild).before(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaIfInsertGroupRow(table, tr) {
|
||||||
|
const lastNew = [...table.querySelectorAll("tr.canvasrefined-ga-if-newrow[data-ga-if-gid]")].pop();
|
||||||
|
const lastReal = [...table.querySelectorAll("tr.group_total")].pop();
|
||||||
|
(lastNew || lastReal || table.querySelector("tr.final_grade") || table.lastElementChild).after(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "+ Add assignment" button sits on its own row at the top of the
|
||||||
|
// table — right below the header, above the first (or first user-added)
|
||||||
|
// assignment — so new assignments are created right where they appear.
|
||||||
|
function gaIfEnsureAddAssignmentRow(table) {
|
||||||
|
if (table.querySelector("#canvasrefined-ga-if-add-asg")) return;
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "canvasrefined-ga-if-addrow";
|
||||||
|
tr.innerHTML = `<td colspan="8" style="border:none!important;padding:6px 8px;">`
|
||||||
|
+ `<button type="button" id="canvasrefined-ga-if-add-asg" style="${GA_IF_TD_BTN}padding:4px 10px;">+ Add assignment</button>`
|
||||||
|
+ `</td>`;
|
||||||
|
const firstNew = table.querySelector("tr.canvasrefined-ga-if-newrow[data-ga-if-key]");
|
||||||
|
const firstReal = [...table.querySelectorAll("tr.student_assignment")]
|
||||||
|
.find(r => !r.classList.contains("group_total") && !r.classList.contains("final_grade"));
|
||||||
|
const anchor = firstNew || firstReal;
|
||||||
|
if (anchor) anchor.before(tr);
|
||||||
|
else (table.querySelector("tr.group_total") || table.querySelector("tr.final_grade") || table.lastElementChild).before(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "+ Add group" button sits on its own row right above the table's
|
||||||
|
// Total row, where the group totals live.
|
||||||
|
function gaIfEnsureAddGroupRow(table) {
|
||||||
|
if (table.querySelector("#canvasrefined-ga-if-add-group")) return;
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = "canvasrefined-ga-if-addrow";
|
||||||
|
tr.innerHTML = `<td colspan="8" style="border:none!important;padding:10px 8px;">`
|
||||||
|
+ `<button type="button" id="canvasrefined-ga-if-add-group" style="${GA_IF_TD_BTN}padding:6px 12px;">+ Add group</button>`
|
||||||
|
+ `</td>`;
|
||||||
|
(table.querySelector("tr.final_grade") || table.lastElementChild).before(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrites each group row's percentage to the hypothetical value for the
|
||||||
|
// current scenario (original text is stashed for restore). A dash means the
|
||||||
|
// group has no counted work.
|
||||||
|
function gaIfUpdateGroupPcts() {
|
||||||
|
const table = document.querySelector("#grades_summary");
|
||||||
|
if (!table || !gaScenario) return;
|
||||||
|
const stats = new Map();
|
||||||
|
for (const a of gaScenario.assignments) {
|
||||||
|
if (a.deleted || a.score == null || !(a.points > 0)) continue;
|
||||||
|
if (!gaScenario.groups.some(g => !g.deleted && String(g.gid) === String(a.gid))) continue;
|
||||||
|
const s = stats.get(String(a.gid)) || { score: 0, pts: 0 };
|
||||||
|
s.score += a.score;
|
||||||
|
s.pts += a.points;
|
||||||
|
stats.set(String(a.gid), s);
|
||||||
|
}
|
||||||
|
for (const tr of table.querySelectorAll("tr[data-ga-if-gid]")) {
|
||||||
|
const gradeEl = tr.querySelector("td.assignment_score .tooltip .grade");
|
||||||
|
if (!gradeEl) continue;
|
||||||
|
if (gradeEl.dataset.gaIfOrig == null) gradeEl.dataset.gaIfOrig = gradeEl.textContent;
|
||||||
|
const s = stats.get(String(tr.dataset.gaIfGid));
|
||||||
|
const txt = s && s.pts > 0 ? ((s.score / s.pts) * 100).toFixed(2).replace(/\.?0+$/, "") + "%" : "—";
|
||||||
|
if (gradeEl.textContent !== txt) gradeEl.textContent = txt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes every trace of the inline UI: injected controls and rows, hidden
|
||||||
|
// originals, hidden deleted rows, and the rewritten group percentages.
|
||||||
|
function gaClearImagineUI(table) {
|
||||||
|
table = table || document.querySelector("#grades_summary");
|
||||||
|
if (!table) return;
|
||||||
|
delete table.dataset.gaIfUi;
|
||||||
|
delete table.dataset.gaIfBuilt;
|
||||||
|
table.querySelectorAll(".canvasrefined-ga-if-edit, .canvasrefined-ga-if-ctl, tr.canvasrefined-ga-if-newrow, tr.canvasrefined-ga-if-addrow").forEach(el => el.remove());
|
||||||
|
table.querySelectorAll("[data-ga-if-hidden]").forEach(el => { el.style.display = ""; delete el.dataset.gaIfHidden; });
|
||||||
|
table.querySelectorAll(".grade[data-ga-if-orig]").forEach(el => { el.textContent = el.dataset.gaIfOrig; delete el.dataset.gaIfOrig; });
|
||||||
|
table.querySelectorAll("tr[data-ga-if-deleted]").forEach(tr => { tr.style.display = ""; delete tr.dataset.gaIfDeleted; });
|
||||||
|
table.querySelectorAll("tr[data-ga-if-key], tr[data-ga-if-gid]").forEach(tr => { delete tr.dataset.gaIfKey; delete tr.dataset.gaIfGid; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// One delegated listener set on the grades table handles every scenario
|
||||||
|
// edit, so rows can be injected/rebuilt freely without detaching handlers.
|
||||||
|
function gaIfBindTable(table) {
|
||||||
|
if (table.dataset.gaIfBound) return;
|
||||||
|
table.dataset.gaIfBound = "1";
|
||||||
|
table.addEventListener("input", e => {
|
||||||
|
if (!gaScenario || !gaImagineIf) return;
|
||||||
|
const t = e.target;
|
||||||
|
const tr = t.closest("tr");
|
||||||
|
if (!tr) return;
|
||||||
|
if (t.matches("[data-ga-if-score],[data-ga-if-pts],[data-ga-if-title]")) {
|
||||||
|
const a = gaScenario.assignments.find(x => String(x.key) === String(tr.dataset.gaIfKey));
|
||||||
|
if (!a) return;
|
||||||
|
if (t.matches("[data-ga-if-title]")) a.title = t.value;
|
||||||
|
else if (t.matches("[data-ga-if-score]")) { const v = parseFloat(t.value); a.score = t.value.trim() !== "" && isFinite(v) ? v : null; }
|
||||||
|
else { const v = parseFloat(t.value); a.points = t.value.trim() !== "" && isFinite(v) ? v : null; }
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
} else if (t.matches("[data-ga-if-gname],[data-ga-if-gweight]")) {
|
||||||
|
const g = gaScenario.groups.find(x => String(x.gid) === String(tr.dataset.gaIfGid));
|
||||||
|
if (!g) return;
|
||||||
|
if (t.matches("[data-ga-if-gname]")) g.name = t.value;
|
||||||
|
else { const w = parseFloat(t.value); g.weight = isFinite(w) ? w : 0; }
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
table.addEventListener("change", e => {
|
||||||
|
if (!gaScenario || !gaImagineIf) return;
|
||||||
|
const t = e.target;
|
||||||
|
if (!t.matches("select[data-ga-if-groupsel]")) return;
|
||||||
|
const tr = t.closest("tr");
|
||||||
|
const a = gaScenario.assignments.find(x => String(x.key) === String(tr?.dataset.gaIfKey));
|
||||||
|
if (a) { a.gid = t.value; gaApplyImagineTotal(); }
|
||||||
|
});
|
||||||
|
table.addEventListener("click", e => {
|
||||||
|
if (!gaScenario || !gaImagineIf) return;
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (!btn) return;
|
||||||
|
const table = btn.closest("table") || document.querySelector("#grades_summary");
|
||||||
|
if (btn.matches("[data-ga-if-adel]")) {
|
||||||
|
const tr = btn.closest("tr");
|
||||||
|
const a = gaScenario.assignments.find(x => String(x.key) === String(tr.dataset.gaIfKey));
|
||||||
|
if (!a) return;
|
||||||
|
a.deleted = true;
|
||||||
|
if (a._new) tr.remove();
|
||||||
|
else { tr.style.display = "none"; tr.dataset.gaIfDeleted = "1"; }
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
} else if (btn.matches("[data-ga-if-gdel]")) {
|
||||||
|
const tr = btn.closest("tr");
|
||||||
|
const g = gaScenario.groups.find(x => String(x.gid) === String(tr.dataset.gaIfGid));
|
||||||
|
if (!g) return;
|
||||||
|
g.deleted = true;
|
||||||
|
if (g._new) tr.remove();
|
||||||
|
else { tr.style.display = "none"; tr.dataset.gaIfDeleted = "1"; }
|
||||||
|
gaIfRefreshSelects(table);
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
} else if (btn.id === "canvasrefined-ga-if-add-asg") {
|
||||||
|
const groups = gaScenario.groups.filter(g => !g.deleted);
|
||||||
|
const a = { key: "new-asg-" + (++gaIfCounter), title: "New assignment", score: null, points: 100, gid: groups[0] ? String(groups[0].gid) : "", _new: true };
|
||||||
|
gaScenario.assignments.push(a);
|
||||||
|
const tr = gaIfMakeAssignmentRow(a);
|
||||||
|
gaIfInsertAssignmentRow(table, tr);
|
||||||
|
tr.dataset.gaIfKey = a.key;
|
||||||
|
gaIfBuildAssignmentControls(tr, a);
|
||||||
|
tr.scrollIntoView({ block: "nearest" });
|
||||||
|
tr.querySelector("[data-ga-if-title]")?.focus();
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
} else if (btn.id === "canvasrefined-ga-if-add-group") {
|
||||||
|
const g = { gid: "new-group-" + (++gaIfCounter), name: "New group", weight: 0, _new: true };
|
||||||
|
gaScenario.groups.push(g);
|
||||||
|
const tr = gaIfMakeGroupRow(g);
|
||||||
|
gaIfInsertGroupRow(table, tr);
|
||||||
|
tr.dataset.gaIfGid = g.gid;
|
||||||
|
gaIfBuildGroupControls(tr, g);
|
||||||
|
gaIfRefreshSelects(table);
|
||||||
|
tr.scrollIntoView({ block: "nearest" });
|
||||||
|
tr.querySelector("[data-ga-if-gname]")?.focus();
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds the inline UI on the grades table itself. The scenario is rebuilt
|
||||||
|
// from the page when the table's rows change (grading-period switch, SPA
|
||||||
|
// navigation); otherwise the already-built UI is left alone so typing is
|
||||||
|
// never clobbered. If Canvas wiped our controls (row re-render), the built
|
||||||
|
// count no longer matches and the UI is rebuilt from the scenario.
|
||||||
|
function gaRenderImagineIf(force) {
|
||||||
|
if (!gaImagineIf) return;
|
||||||
|
const table = document.querySelector("#grades_summary");
|
||||||
|
if (!table) return;
|
||||||
|
const sig = gaImagineTableSig();
|
||||||
|
if (!gaScenario || gaScenario.sig !== sig) {
|
||||||
|
const sc = gaBuildImagineScenario();
|
||||||
|
if (!sc) return; // table hasn't rendered yet; the DOM observer retries
|
||||||
|
gaScenario = { sig, groups: sc.groups, assignments: sc.assignments };
|
||||||
|
force = true;
|
||||||
|
}
|
||||||
|
if (!force
|
||||||
|
&& table.dataset.gaIfUi === sig
|
||||||
|
&& Number(table.dataset.gaIfBuilt || 0) === table.querySelectorAll("tr[data-ga-if-key]").length) return;
|
||||||
|
gaClearImagineUI(table);
|
||||||
|
table.dataset.gaIfUi = sig;
|
||||||
|
gaIfBindTable(table);
|
||||||
|
|
||||||
|
const rowById = new Map();
|
||||||
|
for (const tr of table.querySelectorAll("tr.student_assignment")) {
|
||||||
|
if (tr.classList.contains("group_total") || tr.classList.contains("final_grade")) continue;
|
||||||
|
if (tr.id) rowById.set(tr.id, tr);
|
||||||
|
}
|
||||||
|
const groupRowByGid = new Map();
|
||||||
|
for (const tr of table.querySelectorAll("tr.group_total")) {
|
||||||
|
const gid = (tr.querySelector(".assignment_group_id")?.textContent || "").trim();
|
||||||
|
if (gid) groupRowByGid.set(gid, tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let built = 0;
|
||||||
|
for (const a of gaScenario.assignments) {
|
||||||
|
let tr = a._new ? table.querySelector(`tr[data-ga-if-key="${CSS.escape(a.key)}"]`) : rowById.get(a.key);
|
||||||
|
if (a.deleted) {
|
||||||
|
if (tr) { tr.style.display = "none"; tr.dataset.gaIfDeleted = "1"; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!tr) {
|
||||||
|
if (!a._new) continue; // the page row vanished — nothing to edit
|
||||||
|
tr = gaIfMakeAssignmentRow(a);
|
||||||
|
gaIfInsertAssignmentRow(table, tr);
|
||||||
|
}
|
||||||
|
tr.dataset.gaIfKey = a.key;
|
||||||
|
gaIfBuildAssignmentControls(tr, a);
|
||||||
|
built++;
|
||||||
|
}
|
||||||
|
for (const g of gaScenario.groups) {
|
||||||
|
let tr = g._new ? table.querySelector(`tr[data-ga-if-gid="${CSS.escape(g.gid)}"]`) : groupRowByGid.get(g.gid);
|
||||||
|
if (g.deleted) {
|
||||||
|
if (tr) { tr.style.display = "none"; tr.dataset.gaIfDeleted = "1"; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!tr) {
|
||||||
|
if (!g._new) continue;
|
||||||
|
tr = gaIfMakeGroupRow(g);
|
||||||
|
gaIfInsertGroupRow(table, tr);
|
||||||
|
}
|
||||||
|
tr.dataset.gaIfGid = g.gid;
|
||||||
|
gaIfBuildGroupControls(tr, g);
|
||||||
|
}
|
||||||
|
gaIfEnsureAddAssignmentRow(table);
|
||||||
|
gaIfEnsureAddGroupRow(table);
|
||||||
|
table.dataset.gaIfBuilt = String(built);
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enters/leaves the mode: builds/removes the inline table UI and
|
||||||
|
// overwrites/restores the sidebar Total block.
|
||||||
|
function gaEnterImagineIf() {
|
||||||
|
gaRenderImagineIf();
|
||||||
|
gaApplyImagineTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaExitImagineIf() {
|
||||||
|
gaClearImagineUI();
|
||||||
|
gaRestoreImagineTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function getApiData() {
|
function getApiData() {
|
||||||
if (current_page === "/" || current_page === "" || options.better_todo || options.better_sidebar) {
|
if (current_page === "/" || current_page === "" || options.better_todo || options.better_sidebar) {
|
||||||
getAssignments();
|
getAssignments();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user