-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/content.js b/js/content.js
index 1eaf893..55e735a 100644
--- a/js/content.js
+++ b/js/content.js
@@ -1031,6 +1031,7 @@ function applyOptionsChanges(changes) {
const row = document.querySelector(".canvasrefined-widget-row[data-area='" + area + "']");
if (row) applyWidgetRowLayout(row, area);
});
+ applyCardWidgetOrder(document.querySelector(".ic-DashboardCard__box__container"));
// A widget moved between areas (or joined/left a shared row) needs
// the OTHER widgets to re-place too: each widget only repositions
// itself on its own position key, so without this the widget left
@@ -7825,11 +7826,13 @@ function placeGPAElements(dashboardContainer, container, container2) {
dashboardContainer.insertBefore(container, dashboardContainer.firstChild);
dashboardContainer.insertBefore(container2, container.nextSibling);
}
+ applyCardWidgetOrder(dashboardContainer);
} else if (pos === "cards_end") {
if (dashboardContainer.lastElementChild !== container || container2.nextElementSibling !== container) {
dashboardContainer.appendChild(container2);
dashboardContainer.appendChild(container);
}
+ applyCardWidgetOrder(dashboardContainer);
} else if (pos === "top") {
if (container2.parentNode !== parent || container2.nextElementSibling !== container || container.nextElementSibling !== dashboardContainer) {
if (parent) {
@@ -7868,6 +7871,72 @@ function containerWidgetOrder(area) {
}).filter(e => e && (e.id === "dashboard_notes" || e.id === "gpa_calc"));
}
+// Card-grid order ("Before cards"/"After cards" positions): saved order of
+// the card-shaped widgets living inside the card grid, used to decide which
+// one comes first when several share the same anchor.
+function cardWidgetOrder() {
+ const saved = options.widget_order && options.widget_order.cards;
+ if (!Array.isArray(saved)) return [];
+ return saved.map(e => {
+ if (typeof e === "string") return e;
+ if (e && typeof e === "object" && e.id) return e.id;
+ return null;
+ }).filter(id => id === "dashboard_notes" || id === "gpa_calc");
+}
+
+// Normalize the relative order of card-shaped widgets inside the card grid.
+// Each widget keeps its own anchor (before vs. after the course cards);
+// widgets sharing an anchor are sequenced by the saved card order. Only
+// re-inserts when the DOM order is actually wrong, so idle passes don't ping
+// the dashboard mutation observer into another pass.
+function applyCardWidgetOrder(grid) {
+ if (!grid) return;
+ const saved = cardWidgetOrder();
+ const units = [];
+ const notes = grid.querySelector(":scope > .canvasrefined-dashboard-notes");
+ if (notes && notes.classList.contains("in-cards")) {
+ units.push({
+ id: "dashboard_notes",
+ anchor: options.dashboard_notes_position === "cards_start" ? "start" : "end",
+ els: [notes]
+ });
+ }
+ const gpaCard = grid.querySelector(":scope > .canvasrefined-gpa-card");
+ if (gpaCard && !gpaCard.classList.contains("container-mode")) {
+ const pos = gpaCalcPosition();
+ const anchor = pos === "cards_start" ? "start" : "end";
+ // The GPA renders as two adjacent direct children (hidden calculator
+ // + visible card). Keep the pair in its natural placement order so
+ // the check below doesn't fight the placement code.
+ const calc = grid.querySelector(":scope > .canvasrefined-gpa");
+ const els = calc ? (anchor === "start" ? [calc, gpaCard] : [gpaCard, calc]) : [gpaCard];
+ units.push({ id: "gpa_calc", anchor, els });
+ }
+ if (units.length < 2) return;
+ const rank = id => {
+ const i = saved.indexOf(id);
+ return i === -1 ? saved.length : i;
+ };
+ ["start", "end"].forEach(anchor => {
+ const group = units.filter(u => u.anchor === anchor)
+ .sort((a, b) => rank(a.id) - rank(b.id));
+ if (group.length < 2) return;
+ const desired = group.flatMap(u => u.els);
+ const positions = desired.map(el => [...grid.children].indexOf(el));
+ const sorted = [...positions].sort((a, b) => a - b);
+ const inOrder = desired.every((_, i) =>
+ anchor === "start" ? positions[i] === sorted[i] : positions[i] === sorted[sorted.length - 1 - i]);
+ if (inOrder) return;
+ if (anchor === "start") {
+ // Insert in reverse so the first widget of the group ends up
+ // leftmost, right before the course cards.
+ for (let i = desired.length - 1; i >= 0; i--) grid.insertBefore(desired[i], grid.firstChild);
+ } else {
+ desired.forEach(el => grid.appendChild(el));
+ }
+ });
+}
+
// Both widgets must share one row: normalize the anchor to the outer
// DashboardCard_Container. The GPA's dashboardContainer can be the inner
// .ic-DashboardCard__box__container, whose parent is INSIDE the card
@@ -8493,24 +8562,41 @@ function wireDashboardNotes(notes) {
// Mount the notes at the position chosen in the Widgets tab:
// top — full-width container above the card grid
-// cards_start / cards_end — card-shaped cell inside the grid, before/after cards
+// cards_start / cards_end — card-shaped cell inside the card grid (same
+// grid the GPA card uses), before/after cards
// below — full-width container below the grid, with some margin
function placeDashboardNotes(notes, container) {
const pos = options.dashboard_notes_position || "top";
- if (notes.dataset.crPos === pos) return;
- notes.classList.toggle("in-cards", pos === "cards_start" || pos === "cards_end");
- const parent = container.parentNode;
if (pos === "top" || pos === "below") {
+ notes.classList.remove("in-cards");
+ const slot = notes.closest && notes.closest(".canvasrefined-widget-slot");
+ const row = slot && slot.parentNode;
+ // Already in the right shared row? Leave it alone.
+ if (notes.dataset.crPos === pos && row && row.dataset.area === pos && slot.dataset.widget === "dashboard_notes") return;
// Full-width container above/below the cards; shares the row with any
// other container-mode widget, widths from the Widgets tab.
placeWidgetInRow("dashboard_notes", pos, [notes], container);
} else {
+ notes.classList.add("in-cards");
+ // Same container the GPA card uses for before/after-cards placement,
+ // resolved here (not at call sites) so every entry path lands in the
+ // real card grid.
+ const grid = document.querySelector(".ic-DashboardCard__box__container") || container;
+ if (notes.dataset.crPos === pos && notes.parentNode === grid) return;
releaseWidgetFromRow("dashboard_notes", [notes], container);
if (pos === "cards_start") {
- container.prepend(notes);
+ grid.prepend(notes);
} else {
- container.appendChild(notes);
+ grid.appendChild(notes);
}
+ applyCardWidgetOrder(grid);
+ // The card grid is Canvas's jQuery sortable list; refresh it so our
+ // injected cell is recognized when cards are drag-reordered.
+ try {
+ if (grid !== container && window.jQuery && window.jQuery.fn && window.jQuery.fn.sortable) {
+ window.jQuery(grid).sortable('refresh');
+ }
+ } catch (e) {}
}
notes.dataset.crPos = pos;
}
diff --git a/js/popup.js b/js/popup.js
index 8bcce2c..6b5b76c 100644
--- a/js/popup.js
+++ b/js/popup.js
@@ -1005,8 +1005,8 @@ function toggleAlternateColorsVisibility(darkModeOn) {
// Widgets tab: position dropdowns for widgets that support placement.
const WIDGET_LIST = [
- { id: "dashboard_notes", nameKey: "dashboard_notes" },
- { id: "gpa_calc", nameKey: "gpa_calculator" },
+ { id: "dashboard_notes", nameKey: "dashboard_notes", shortKey: "widget_short_notes" },
+ { id: "gpa_calc", nameKey: "gpa_calculator", shortKey: "widget_short_gpa" },
];
function widgetPosFromSync(id, sync) {
@@ -1037,37 +1037,50 @@ function parseWidgetOrderEntries(raw) {
function widgetDisplayName(id) {
const info = WIDGET_LIST.find(w => w.id === id);
- return (info && chrome.i18n.getMessage(info.nameKey)) || id;
+ return (info && chrome.i18n.getMessage(info.shortKey || info.nameKey)) || id;
}
+// Live-refresh hook for the widget-order UI, set by setupWidgetOrder and
+// called directly after the popup itself changes a widget's position or
+// enabled state — so the order box updates instantly without waiting for
+// the storage.onChanged round-trip.
+let refreshWidgetOrder = null;
+
function setupWidgetOrder() {
const wrap = document.getElementById("widget-order-container");
if (!wrap) return;
const KEYS = ["dashboard_notes", "gpa_calc", "dashboard_notes_position", "gpa_calc_position", "gpa_calc_prepend", "widget_order"];
+ const AREAS = ["top", "cards_start", "cards_end", "below"];
+
const render = (sync) => {
- const areas = { top: [], below: [] };
+ const areas = { top: [], cards_start: [], cards_end: [], below: [] };
WIDGET_LIST.forEach(w => {
if (sync[w.id] !== true) return;
const pos = widgetPosFromSync(w.id, sync);
- if (pos === "top" || pos === "below") areas[pos].push(w.id);
+ if (areas[pos]) areas[pos].push(w.id);
});
let any = false;
- ["top", "below"].forEach(area => {
+ AREAS.forEach(area => {
const strip = document.getElementById("widget-order-" + area);
const wrapEl = document.getElementById("widget-order-" + area + "-wrap");
if (!strip || !wrapEl) return;
strip.dataset.area = area;
const ids = areas[area];
+ // A section only makes sense once 2+ widgets share the same
+ // position — with a single widget there is nothing to order.
if (ids.length < 2) {
wrapEl.style.display = "none";
return;
}
any = true;
wrapEl.style.display = "";
- // Saved entries first (order + widths), then any newcomers.
- const entries = parseWidgetOrderEntries((sync.widget_order || {})[area]).filter(e => ids.includes(e.id));
+ // Saved entries first (order + widths), then any newcomers. The
+ // before/after groups share the single "cards" saved order; each
+ // strip only shows the widgets actually in its group.
+ const saved = (sync.widget_order || {})[area.startsWith("cards") ? "cards" : area];
+ const entries = parseWidgetOrderEntries(saved).filter(e => ids.includes(e.id));
ids.forEach(id => { if (!entries.some(e => e.id === id)) entries.push({ id, size: null }); });
renderWidgetOrderStrip(strip, entries);
});
@@ -1075,7 +1088,7 @@ function setupWidgetOrder() {
};
// Reordering: drag a box horizontally past a neighbor.
- ["top", "below"].forEach(area => {
+ AREAS.forEach(area => {
const strip = document.getElementById("widget-order-" + area);
if (!strip) return;
strip.addEventListener("dragover", e => {
@@ -1094,6 +1107,7 @@ function setupWidgetOrder() {
});
const refresh = () => chrome.storage.sync.get(KEYS, render);
+ refreshWidgetOrder = refresh;
refresh();
chrome.storage.onChanged.addListener((changes, area) => {
@@ -1104,8 +1118,10 @@ function setupWidgetOrder() {
function renderWidgetOrderStrip(strip, entries) {
strip.textContent = "";
+ // Before/after strips are sequence only — no width resizing, no dividers.
+ const resizable = !strip.dataset.area.startsWith("cards");
entries.forEach((entry, i) => {
- if (i > 0) {
+ if (resizable && i > 0) {
const divider = document.createElement("div");
divider.className = "wos-divider";
wireWidgetOrderDivider(strip, divider);
@@ -1114,8 +1130,10 @@ function renderWidgetOrderStrip(strip, entries) {
const item = document.createElement("div");
item.className = "wos-item";
item.dataset.id = entry.id;
- item.dataset.size = entry.size == null ? (100 / entries.length) : entry.size;
- item.style.flexBasis = item.dataset.size + "%";
+ if (resizable) {
+ item.dataset.size = entry.size == null ? (100 / entries.length) : entry.size;
+ item.style.flexBasis = item.dataset.size + "%";
+ }
item.draggable = true;
item.textContent = widgetDisplayName(entry.id);
item.addEventListener("dragstart", e => {
@@ -1177,15 +1195,26 @@ function wireWidgetOrderDivider(strip, divider) {
function saveWidgetStrip(area, strip) {
const items = [...strip.querySelectorAll(".wos-item")];
if (items.length < 2) return;
- let sizes = items.map(el => parseFloat(el.dataset.size) || 100 / items.length);
- const total = sizes.reduce((a, b) => a + b, 0) || 100;
- sizes = sizes.map(v => v / total * 100);
- const rounded = sizes.map(v => Math.round(v));
- rounded[rounded.length - 1] += 100 - rounded.reduce((a, b) => a + b, 0);
- const order = items.map((el, i) => ({ id: el.dataset.id, size: rounded[i] }));
+ const order = items.map(el => ({ id: el.dataset.id }));
+ // Before/after strips both save into the shared "cards" order, merged
+ // with the entries of the other group so nothing is lost.
+ const storageArea = area.startsWith("cards") ? "cards" : area;
chrome.storage.sync.get(["widget_order"], sync => {
const wo = Object.assign({}, sync.widget_order || {});
- wo[area] = order;
+ if (storageArea === "cards") {
+ const ids = new Set(order.map(e => e.id));
+ const other = (sync.widget_order && Array.isArray(sync.widget_order.cards) ? sync.widget_order.cards : [])
+ .filter(e => e && !ids.has(e.id));
+ wo.cards = other.concat(order);
+ } else {
+ // Container rows still carry each widget's % width.
+ let sizes = items.map(el => parseFloat(el.dataset.size) || 100 / items.length);
+ const total = sizes.reduce((a, b) => a + b, 0) || 100;
+ sizes = sizes.map(v => v / total * 100);
+ const rounded = sizes.map(v => Math.round(v));
+ rounded[rounded.length - 1] += 100 - rounded.reduce((a, b) => a + b, 0);
+ wo[storageArea] = items.map((el, i) => ({ id: el.dataset.id, size: rounded[i] }));
+ }
chrome.storage.sync.set({ widget_order: wo });
});
}
@@ -1859,6 +1888,9 @@ function setup() {
chrome.storage.sync.set({ "hide_course_images": false });
}
toggleSubOptionsVisibility(option, status);
+ if (option === "dashboard_notes" || option === "gpa_calc") {
+ if (refreshWidgetOrder) refreshWidgetOrder();
+ }
});
});
toggleBetterSidebarSubOptions(sync["better_sidebar"] === true);