mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 13:57:17 +01:00
widget system done!
This commit is contained in:
parent
6268150cd6
commit
1ee82a68b0
@ -131,11 +131,14 @@
|
||||
"widget_position": {
|
||||
"message": "Position"
|
||||
},
|
||||
"widget_order_top": {
|
||||
"message": "Top container order"
|
||||
"widget_order_title": {
|
||||
"message": "Widget order"
|
||||
},
|
||||
"widget_order_bottom": {
|
||||
"message": "Bottom container order"
|
||||
"widget_short_notes": {
|
||||
"message": "Notes"
|
||||
},
|
||||
"widget_short_gpa": {
|
||||
"message": "GPA"
|
||||
},
|
||||
"grade_settings": {
|
||||
"message": "Grade Settings"
|
||||
|
||||
@ -448,18 +448,20 @@
|
||||
container-style card and the expanded calculator keeps full width. */
|
||||
.canvasrefined-gpa-card.container-mode {width:100%;box-sizing:border-box;margin:0 0 4px;display:block;}
|
||||
.canvasrefined-gpa.container-mode {margin:0 0 18px;}
|
||||
/* Dashboard notes placed inside the card grid (before/after cards): shaped
|
||||
like a course card (262px wide, rounded, Canvas card shadow) and kept at a
|
||||
similar height so the grid row stays balanced. */
|
||||
/* Dashboard notes placed inside the card grid (before/after cards): styled
|
||||
exactly like the GPA card / Canvas course cards — 262px wide, rounded,
|
||||
card shadow, and the same 36px gutters so it aligns in the grid. */
|
||||
.canvasrefined-dashboard-notes.in-cards {
|
||||
width: 262px;
|
||||
min-height: 260px;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
margin: 36px 0 0 36px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,.3);
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
vertical-align: top;
|
||||
}
|
||||
.canvasrefined-dashboard-notes.in-cards .canvasrefined-notes-surface {flex: 1; display: flex; flex-direction: column;}
|
||||
.canvasrefined-dashboard-notes.in-cards .canvasrefined-notes-editor,
|
||||
|
||||
@ -381,11 +381,27 @@ input[type="checkbox"]:checked {background: #8dd28d;}
|
||||
|
||||
/* Widget container-order strips (Widgets tab): boxes sized by each widget's
|
||||
% of the shared container, separated by draggable dividers. */
|
||||
.widget-order-strip {display:flex;align-items:stretch;width:100%;height:64px;margin-top:8px;}
|
||||
.widget-order-strip {display:flex;align-items:stretch;width:100%;height:44px;margin-top:6px;}
|
||||
.wos-item {border:1px solid #ffffff2e;background:var(--inputbg);border-radius:7px;display:flex;align-items:center;padding:0 10px;font-size:12px;color:#e2e2e2;min-width:0;overflow:hidden;white-space:nowrap;cursor:grab;user-select:none;}
|
||||
.wos-item.dragging {opacity:.45;}
|
||||
.wos-divider {flex:0 0 6px;width:6px;align-self:stretch;margin:0 5px;border-radius:3px;background:#8a8a8a;cursor:col-resize;}
|
||||
.wos-divider:hover {background:#e2e2e2;}
|
||||
/* Widget order box (Widgets tab): one shared "Widget order" rectangle with
|
||||
a section per area (Top / Before cards / After cards / Bottom); each
|
||||
section shows only when 2+ enabled widgets share that area. */
|
||||
.widget-order-section + .widget-order-section {margin-top: 16px;}
|
||||
.widget-order-label {font-size: 12px;color: #c7c7c7;}
|
||||
.widget-order-strip {margin-top: 6px;}
|
||||
/* Before/after strips: sequence only, so boxes share the width evenly with
|
||||
a gap between them instead of draggable dividers. */
|
||||
.widget-order-strip[data-area="cards_start"],
|
||||
.widget-order-strip[data-area="cards_end"] {column-gap: 8px;}
|
||||
.widget-order-strip[data-area="cards_start"] .wos-item,
|
||||
.widget-order-strip[data-area="cards_end"] .wos-item {flex: 1 1 0;}
|
||||
/* Widget sections (Widgets tab): two side-by-side columns; add future
|
||||
widgets as one more .option-container inside .widgets-grid. */
|
||||
.widgets-grid {display:grid;grid-template-columns:1fr 1fr;gap:8px;align-items:stretch;margin-top:8px;}
|
||||
.widgets-grid .option-container {margin-top:0;box-sizing:border-box;}
|
||||
/* Widget position dropdowns (Widgets tab) — mirror #todo_timeframe exactly:
|
||||
same font, padding, radius, colors and full-width stacked layout. */
|
||||
.widget-pos {font-size:12px;background:var(--inputbg);color:#e2e2e2;border:none;border-radius:7px;padding:6px 8px;font-family:inherit;width:100%;box-sizing:border-box;margin-top:2px;margin-bottom:6px;color-scheme:dark;}
|
||||
|
||||
114
html/popup.html
114
html/popup.html
@ -1113,66 +1113,78 @@
|
||||
<h2 style="margin-top: 0" data-i18n="widgets">Widgets</h2>
|
||||
<button class="big-button back-btn" data-i18n="back">Back</button>
|
||||
</div>
|
||||
<!-- Widget ordering: shown only when 2+ widgets share the top/bottom container -->
|
||||
<!-- Widget ordering: one shared box titled "Widget order"; each area
|
||||
section shows only when 2+ enabled widgets share that area -->
|
||||
<div class="option-container" id="widget-order-container" style="display:none">
|
||||
<div id="widget-order-top-wrap" style="display:none">
|
||||
<h3 class="header-small" data-i18n="widget_order_top">Top container order</h3>
|
||||
<h3 class="header-small" data-i18n="widget_order_title">Widget order</h3>
|
||||
<div class="widget-order-section" id="widget-order-top-wrap" style="display:none">
|
||||
<div class="widget-order-label" data-i18n="pos_top">Top</div>
|
||||
<div class="widget-order-strip" id="widget-order-top"></div>
|
||||
</div>
|
||||
<div id="widget-order-below-wrap" style="display:none">
|
||||
<h3 class="header-small" data-i18n="widget_order_bottom">Bottom container order</h3>
|
||||
<div class="widget-order-section" id="widget-order-cards_start-wrap" style="display:none">
|
||||
<div class="widget-order-label" data-i18n="pos_cards_start">Before cards</div>
|
||||
<div class="widget-order-strip" id="widget-order-cards_start"></div>
|
||||
</div>
|
||||
<div class="widget-order-section" id="widget-order-cards_end-wrap" style="display:none">
|
||||
<div class="widget-order-label" data-i18n="pos_cards_end">After cards</div>
|
||||
<div class="widget-order-strip" id="widget-order-cards_end"></div>
|
||||
</div>
|
||||
<div class="widget-order-section" id="widget-order-below-wrap" style="display:none">
|
||||
<div class="widget-order-label" data-i18n="pos_below">Bottom</div>
|
||||
<div class="widget-order-strip" id="widget-order-below"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Widgets: add one option-container per widget here -->
|
||||
<div class="option-container">
|
||||
<div class="option" id="dashboard_notes">
|
||||
<input type="radio" id="off" name="dashboard_notes">
|
||||
<input type="radio" id="on" name="dashboard_notes">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div><span class="option-name" data-i18n="dashboard_notes">Dashboard Notes</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="dashboard_notes_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="dashboard_notes_position" class="widget-pos">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
<!-- Widgets: two-column grid; add one option-container per widget here -->
|
||||
<div class="widgets-grid">
|
||||
<div class="option-container">
|
||||
<div class="option" id="dashboard_notes">
|
||||
<input type="radio" id="off" name="dashboard_notes">
|
||||
<input type="radio" id="on" name="dashboard_notes">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div><span class="option-name" data-i18n="dashboard_notes">Dashboard Notes</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="dashboard_notes_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="dashboard_notes_position" class="widget-pos">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="gpa_calc">
|
||||
<input type="radio" id="off" name="gpa_calc">
|
||||
<input type="radio" id="on" name="gpa_calc">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
<div class="option-container">
|
||||
<div class="option" id="gpa_calc">
|
||||
<input type="radio" id="off" name="gpa_calc">
|
||||
<input type="radio" id="on" name="gpa_calc">
|
||||
<div class="slider">
|
||||
<div class="sliderknob"></div>
|
||||
<div class="sliderbg"></div>
|
||||
</div>
|
||||
<span class="option-name" data-i18n="gpa_calculator">GPA calculator</span>
|
||||
</div>
|
||||
<span class="option-name" data-i18n="gpa_calculator">GPA calculator</span>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="gpa_calc_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="gpa_calc_position" class="widget-pos">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_cumulative" name="gpa_calc_cumulative">
|
||||
<label for="gpa_calc_cumulative" class="sub-text" data-i18n="gpa_calc_cumulative">Show cumulative GPA</label>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_weighted" name="gpa_calc_weighted">
|
||||
<label for="gpa_calc_weighted" class="sub-text" data-i18n="gpa_calc_weighted">Show weighted GPA</label>
|
||||
<div class="sub-options">
|
||||
<div class="sub-option" style="flex-direction:column;align-items:stretch;gap:4px;">
|
||||
<label for="gpa_calc_position" class="sub-text" data-i18n="widget_position">Position</label>
|
||||
<select id="gpa_calc_position" class="widget-pos">
|
||||
<option value="top" data-i18n="pos_top">Top</option>
|
||||
<option value="cards_start" data-i18n="pos_cards_start">Before cards</option>
|
||||
<option value="cards_end" data-i18n="pos_cards_end">After cards</option>
|
||||
<option value="below" data-i18n="pos_below">Bottom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_cumulative" name="gpa_calc_cumulative">
|
||||
<label for="gpa_calc_cumulative" class="sub-text" data-i18n="gpa_calc_cumulative">Show cumulative GPA</label>
|
||||
</div>
|
||||
<div class="sub-option" style="margin-top:5px">
|
||||
<input type="checkbox" id="gpa_calc_weighted" name="gpa_calc_weighted">
|
||||
<label for="gpa_calc_weighted" class="sub-text" data-i18n="gpa_calc_weighted">Show weighted GPA</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
70
js/popup.js
70
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user