mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-23 13:25:09 +02:00
patches for firefox and other bugs
This commit is contained in:
parent
b84a374099
commit
1091f01f83
@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
.canvasrefined-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;}
|
.canvasrefined-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;}
|
||||||
.canvasrefined-custom-open {max-height: 250px}
|
.canvasrefined-custom-open {max-height: 250px}
|
||||||
#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 420px;}
|
#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 520px;}
|
||||||
.canvasrefined-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important}
|
.canvasrefined-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important}
|
||||||
#better-todo-new-task-date,
|
#better-todo-new-task-date,
|
||||||
#better-todo-new-task-time {
|
#better-todo-new-task-time {
|
||||||
@ -58,10 +58,9 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-inline: 6px;
|
padding-inline: 6px;
|
||||||
}
|
}
|
||||||
#better-todo-new-task-date::-webkit-calendar-picker-indicator {
|
/* The native date/time picker icons are themed in dark mode only via
|
||||||
filter: invert(1) brightness(2);
|
color-scheme (see DARKMODE_CSS in css/darkmodecss.js); light mode keeps the
|
||||||
opacity: .95;
|
default dark glyphs on the light surface. */
|
||||||
}
|
|
||||||
.canvasrefined-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;}
|
.canvasrefined-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;}
|
||||||
/* Better sidebar buttons sit on the --bcsidebar surface and use --bcsidebar-text
|
/* Better sidebar buttons sit on the --bcsidebar surface and use --bcsidebar-text
|
||||||
for labels/icons, so drop the light button chrome and use a subtle hover tint. */
|
for labels/icons, so drop the light button chrome and use a subtle hover tint. */
|
||||||
@ -204,7 +203,7 @@
|
|||||||
.canvasrefined-notes-rendered ul,.canvasrefined-notes-rendered ol {margin:8px 0;padding-left:24px;}
|
.canvasrefined-notes-rendered ul,.canvasrefined-notes-rendered ol {margin:8px 0;padding-left:24px;}
|
||||||
.canvasrefined-notes-rendered li {margin:3px 0;}
|
.canvasrefined-notes-rendered li {margin:3px 0;}
|
||||||
.canvasrefined-notes-rendered li.cr-task {list-style:none;margin-left:-18px;display:flex;align-items:flex-start;gap:6px;}
|
.canvasrefined-notes-rendered li.cr-task {list-style:none;margin-left:-18px;display:flex;align-items:flex-start;gap:6px;}
|
||||||
.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;}
|
.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;cursor:pointer;}
|
||||||
.canvasrefined-notes-rendered blockquote {margin:8px 0;padding:4px 12px;border-left:3px solid var(--bcborders,#c7cdd1);color:var(--bctext-1,#5b6770);background:var(--bcbackground-2,rgba(0,0,0,.03));}
|
.canvasrefined-notes-rendered blockquote {margin:8px 0;padding:4px 12px;border-left:3px solid var(--bcborders,#c7cdd1);color:var(--bctext-1,#5b6770);background:var(--bcbackground-2,rgba(0,0,0,.03));}
|
||||||
.canvasrefined-notes-rendered code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bcbackground-2,#f1f3f5);padding:1px 5px;border-radius:3px;}
|
.canvasrefined-notes-rendered code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bcbackground-2,#f1f3f5);padding:1px 5px;border-radius:3px;}
|
||||||
.canvasrefined-notes-rendered pre {margin:10px 0;padding:10px 12px;background:var(--bcbackground-2,#f1f3f5);border:1px solid var(--bcborders,#e2e6e8);border-radius:4px;overflow:auto;}
|
.canvasrefined-notes-rendered pre {margin:10px 0;padding:10px 12px;background:var(--bcbackground-2,#f1f3f5);border:1px solid var(--bcborders,#e2e6e8);border-radius:4px;overflow:auto;}
|
||||||
|
|||||||
@ -1193,4 +1193,18 @@ hr {
|
|||||||
.event-details strong {
|
.event-details strong {
|
||||||
color: var(--bctext-0) !important;
|
color: var(--bctext-0) !important;
|
||||||
}
|
}
|
||||||
|
/* Theme the native date/time picker icons so they stay visible on the dark
|
||||||
|
sidebar surface. Applied in dark mode only — light mode keeps the default
|
||||||
|
dark glyphs (see css/content.css).
|
||||||
|
color-scheme: dark is the cross-browser way to flip the native picker icon
|
||||||
|
to a light glyph. Firefox 109+ ignores ::-webkit-calendar-picker-indicator
|
||||||
|
(the icon lives in a closed Shadow DOM), so a filter alone does nothing
|
||||||
|
there; color-scheme fixes both Firefox and modern Chrome. Do NOT also apply
|
||||||
|
an invert filter — color-scheme already makes the glyph light, and inverting
|
||||||
|
would flip it back to dark in Chrome. */
|
||||||
|
#better-todo-new-task-date,
|
||||||
|
#better-todo-new-task-time {
|
||||||
|
color-scheme: dark !important;
|
||||||
|
color: var(--bctext-0) !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
@ -500,7 +500,13 @@
|
|||||||
<div id="saved-themes"></div>
|
<div id="saved-themes"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="export-details option-container">
|
<div class="export-details option-container">
|
||||||
<h3 class="header-small">Export Settings</h3>
|
<h3 class="header-small" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;">
|
||||||
|
<span>Export Settings</span>
|
||||||
|
<span style="display:flex;gap:6px;">
|
||||||
|
<button type="button" id="export-check-all" class="customization-button" style="padding:4px 10px;font-size:11px;">Check all</button>
|
||||||
|
<button type="button" id="export-check-none" class="customization-button" style="padding:4px 10px;font-size:11px;">Check none</button>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
<div style="display:flex;gap:30px;">
|
<div style="display:flex;gap:30px;">
|
||||||
<div class="export-inputs">
|
<div class="export-inputs">
|
||||||
<div class="sub-option">
|
<div class="sub-option">
|
||||||
|
|||||||
@ -81,6 +81,8 @@ chrome.runtime.onInstalled.addListener(function () {
|
|||||||
"tab_icons": false,
|
"tab_icons": false,
|
||||||
"todo_hide_feedback": false,
|
"todo_hide_feedback": false,
|
||||||
"todo_full_height": true,
|
"todo_full_height": true,
|
||||||
|
"todo_progress_rings": true,
|
||||||
|
"todo_confetti": true,
|
||||||
"device_dark": false,
|
"device_dark": false,
|
||||||
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
|
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
|
||||||
// "show_updates": false,
|
// "show_updates": false,
|
||||||
|
|||||||
431
js/content.js
431
js/content.js
@ -284,9 +284,7 @@ function createNasaInfoOverlay() {
|
|||||||
const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel");
|
const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel");
|
||||||
|
|
||||||
const populatePanel = async () => {
|
const populatePanel = async () => {
|
||||||
// The NASA worker (background.js getNasaBackground) falls back up to 7 days
|
// Search backward up to 7 days for the APOD actually cached, since today's may not be ready.
|
||||||
// when today's APOD isn't available yet, so search backward for the date
|
|
||||||
// that was actually cached and displayed instead of assuming today.
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const dateStr = date.toISOString().slice(0, 10);
|
const dateStr = date.toISOString().slice(0, 10);
|
||||||
@ -620,8 +618,7 @@ function applyOptionsChanges(changes) {
|
|||||||
loadCardAssignments();
|
loadCardAssignments();
|
||||||
break;
|
break;
|
||||||
case "equal_height_cards":
|
case "equal_height_cards":
|
||||||
// No need to rebuild the assignment rows — just stretch (or reset)
|
// Stretch or reset card heights in place instead of rebuilding rows.
|
||||||
// the card heights in place for a snappy toggle.
|
|
||||||
equalizeCardHeights();
|
equalizeCardHeights();
|
||||||
break;
|
break;
|
||||||
case "custom_cards":
|
case "custom_cards":
|
||||||
@ -833,7 +830,7 @@ async function applyCustomBackground() {
|
|||||||
-webkit-backdrop-filter: blur(8px) saturate(120%) !important;
|
-webkit-backdrop-filter: blur(8px) saturate(120%) !important;
|
||||||
}
|
}
|
||||||
#right-side-wrapper {
|
#right-side-wrapper {
|
||||||
// backdrop-filter: blur(10px) !important;
|
/* backdrop-filter: blur(10px) !important; */
|
||||||
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%);
|
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
@ -1553,6 +1550,7 @@ function buildPlannerNotePayload(form) {
|
|||||||
const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value;
|
const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value;
|
||||||
const dateValue = form.querySelector("#better-todo-new-task-date")?.value;
|
const dateValue = form.querySelector("#better-todo-new-task-date")?.value;
|
||||||
const timeValue = form.querySelector("#better-todo-new-task-time")?.value;
|
const timeValue = form.querySelector("#better-todo-new-task-time")?.value;
|
||||||
|
const link = form.querySelector("#better-todo-new-task-link")?.value?.trim() || "";
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
throw new Error("Task title is required.");
|
throw new Error("Task title is required.");
|
||||||
@ -1570,6 +1568,7 @@ function buildPlannerNotePayload(form) {
|
|||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
details,
|
details,
|
||||||
|
link,
|
||||||
courseId: courseIdRaw ? parseInt(courseIdRaw) : null,
|
courseId: courseIdRaw ? parseInt(courseIdRaw) : null,
|
||||||
// Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes.
|
// Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes.
|
||||||
todoDate: `${dateValue}T${timeValue}:00`,
|
todoDate: `${dateValue}T${timeValue}:00`,
|
||||||
@ -1646,6 +1645,167 @@ async function createCanvasPlannerNote(payload) {
|
|||||||
throw new Error(lastError || "Canvas rejected task creation.");
|
throw new Error(lastError || "Canvas rejected task creation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom task links: Canvas planner notes have no link field, so store a
|
||||||
|
user link in sync storage keyed by the note id. */
|
||||||
|
function getCustomTaskLinks() {
|
||||||
|
return (options && options.custom_task_links) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomTaskLinkId(item) {
|
||||||
|
return item?.plannable_id ?? item?.plannable?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTaskLink(link) {
|
||||||
|
if (!link) return "";
|
||||||
|
link = String(link).trim();
|
||||||
|
if (!link) return "";
|
||||||
|
if (/^https?:\/\//i.test(link)) return link;
|
||||||
|
if (link.startsWith("//")) return "https:" + link;
|
||||||
|
return domain + (link.startsWith("/") ? link : "/" + link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function customTaskHref(item) {
|
||||||
|
const id = getCustomTaskLinkId(item);
|
||||||
|
const links = getCustomTaskLinks();
|
||||||
|
if (id != null && links[String(id)]) {
|
||||||
|
return normalizeTaskLink(links[String(id)]);
|
||||||
|
}
|
||||||
|
const courseId = item?.course_id || item?.plannable?.course_id || item?.context_id;
|
||||||
|
if (courseId) return `${domain}/courses/${courseId}`;
|
||||||
|
return `${domain}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCustomTaskLink(noteId, link) {
|
||||||
|
if (noteId == null) return;
|
||||||
|
const links = { ...getCustomTaskLinks() };
|
||||||
|
const key = String(noteId);
|
||||||
|
if (link && String(link).trim()) {
|
||||||
|
links[key] = String(link).trim();
|
||||||
|
} else {
|
||||||
|
delete links[key];
|
||||||
|
}
|
||||||
|
options = { ...options, custom_task_links: links };
|
||||||
|
chrome.storage.sync.set({ custom_task_links: links });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCustomTaskLink(noteId) {
|
||||||
|
if (noteId == null) return;
|
||||||
|
const links = { ...getCustomTaskLinks() };
|
||||||
|
delete links[String(noteId)];
|
||||||
|
options = { ...options, custom_task_links: links };
|
||||||
|
chrome.storage.sync.set({ custom_task_links: links });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCanvasPlannerNote(id, payload) {
|
||||||
|
if (!id) throw new Error("Missing task id.");
|
||||||
|
const csrfToken = CSRFtoken();
|
||||||
|
const plannerNote = {
|
||||||
|
title: payload.title,
|
||||||
|
todo_date: payload.todoDate,
|
||||||
|
};
|
||||||
|
if (payload.details) plannerNote.details = payload.details;
|
||||||
|
// Sending course_id as empty string disassociates the note from its course.
|
||||||
|
plannerNote.course_id = payload.courseId || "";
|
||||||
|
|
||||||
|
const attempts = [
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"accept": "application/json",
|
||||||
|
"X-CSRF-Token": csrfToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ planner_note: plannerNote }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
|
"accept": "application/json",
|
||||||
|
"X-CSRF-Token": csrfToken,
|
||||||
|
},
|
||||||
|
body: (() => {
|
||||||
|
const formBody = new URLSearchParams();
|
||||||
|
formBody.set("planner_note[title]", plannerNote.title);
|
||||||
|
formBody.set("planner_note[todo_date]", plannerNote.todo_date);
|
||||||
|
if (plannerNote.details) formBody.set("planner_note[details]", plannerNote.details);
|
||||||
|
formBody.set("planner_note[course_id]", plannerNote.course_id);
|
||||||
|
return formBody.toString();
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let lastError = "Canvas rejected task update.";
|
||||||
|
for (const attempt of attempts) {
|
||||||
|
const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: attempt.headers,
|
||||||
|
body: attempt.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200 || response.status === 201) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errData = await response.json();
|
||||||
|
if (errData?.errors?.length) {
|
||||||
|
lastError = errData.errors.join(" ");
|
||||||
|
} else if (errData?.message) {
|
||||||
|
lastError = errData.message;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Keep prior error text when body is not JSON.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(lastError || "Canvas rejected task update.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteCanvasPlannerNote(id) {
|
||||||
|
if (!id) throw new Error("Missing task id.");
|
||||||
|
const csrfToken = CSRFtoken();
|
||||||
|
const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"accept": "application/json",
|
||||||
|
"X-CSRF-Token": csrfToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200 || response.status === 201 || response.status === 204) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError = "Canvas rejected task deletion.";
|
||||||
|
try {
|
||||||
|
const errData = await response.json();
|
||||||
|
if (errData?.errors?.length) lastError = errData.errors.join(" ");
|
||||||
|
else if (errData?.message) lastError = errData.message;
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
throw new Error(lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll a task form field into view, leaving room below for the native date/time picker. */
|
||||||
|
function scrollTodoIntoView(el, smooth = true) {
|
||||||
|
if (!el) return;
|
||||||
|
const sidebar = document.getElementById("right-side-wrapper");
|
||||||
|
const style = sidebar ? getComputedStyle(sidebar) : null;
|
||||||
|
const scrollsSidebar = sidebar &&
|
||||||
|
(style.overflowY === "auto" || style.overflowY === "scroll") &&
|
||||||
|
sidebar.scrollHeight > sidebar.clientHeight;
|
||||||
|
const behavior = smooth ? "smooth" : "auto";
|
||||||
|
if (scrollsSidebar) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const sRect = sidebar.getBoundingClientRect();
|
||||||
|
const elTop = rect.top - sRect.top + sidebar.scrollTop;
|
||||||
|
// Keep the element near the top so the picker below it stays on screen.
|
||||||
|
const target = elTop - (sidebar.clientHeight - rect.height) * 0.3;
|
||||||
|
sidebar.scrollTo({ top: Math.max(0, target), behavior });
|
||||||
|
} else {
|
||||||
|
el.scrollIntoView({ block: "center", behavior });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fillTaskCourseOptions(courseSelect) {
|
function fillTaskCourseOptions(courseSelect) {
|
||||||
const cards = options.custom_cards || {};
|
const cards = options.custom_cards || {};
|
||||||
const courseColors = options.custom_cards_3 || {};
|
const courseColors = options.custom_cards_3 || {};
|
||||||
@ -1705,7 +1865,8 @@ function ensureTodoTaskMenu(location, feedbackElement) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
menu.innerHTML = `
|
menu.innerHTML = `
|
||||||
<div style="display:flex;flex-direction:column;gap:8px;padding:8px;border:1px solid #c7cdd1;border-radius:6px;background:var(--bcbackground-2);">
|
<div style="display:flex;flex-direction:column;gap:8px;padding:8px;border:1px solid #c7cdd1;border-radius:6px;background:var(--bcbackground-2);position:relative;">
|
||||||
|
<button id="better-todo-add-task-close" type="button" class="canvasrefined-custom-btn" title="Close" style="position:absolute;top:4px;right:6px;padding:0 6px;cursor:pointer;line-height:18px;font-size:14px;color:var(--bctext-1);">\u00d7</button>
|
||||||
<input type="text" id="better-todo-new-task-title" class="canvasrefined-custom-input" placeholder="Task title" maxlength="255">
|
<input type="text" id="better-todo-new-task-title" class="canvasrefined-custom-input" placeholder="Task title" maxlength="255">
|
||||||
<textarea id="better-todo-new-task-details" class="canvasrefined-custom-input" placeholder="Details (optional)" style="min-height:70px;resize:vertical;padding-top:6px;padding-bottom:6px;"></textarea>
|
<textarea id="better-todo-new-task-details" class="canvasrefined-custom-input" placeholder="Details (optional)" style="min-height:70px;resize:vertical;padding-top:6px;padding-bottom:6px;"></textarea>
|
||||||
<select id="better-todo-new-task-course" class="canvasrefined-custom-input"></select>
|
<select id="better-todo-new-task-course" class="canvasrefined-custom-input"></select>
|
||||||
@ -1713,11 +1874,15 @@ function ensureTodoTaskMenu(location, feedbackElement) {
|
|||||||
<input type="date" id="better-todo-new-task-date" class="canvasrefined-custom-input">
|
<input type="date" id="better-todo-new-task-date" class="canvasrefined-custom-input">
|
||||||
<input type="time" id="better-todo-new-task-time" class="canvasrefined-custom-input">
|
<input type="time" id="better-todo-new-task-time" class="canvasrefined-custom-input">
|
||||||
</div>
|
</div>
|
||||||
|
<input type="text" id="better-todo-new-task-link" class="canvasrefined-custom-input" placeholder="Link (optional)" maxlength="2048">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
|
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
|
||||||
<span id="better-todo-add-task-status" style="font-size:12px;color:var(--bctext-0);"></span>
|
<span id="better-todo-add-task-status" style="font-size:12px;color:var(--bctext-0);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;">
|
||||||
|
<button id="better-todo-add-task-delete" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;display:none;color:#db3754;" type="button" title="Delete this custom task">Delete</button>
|
||||||
<button id="better-todo-add-task-submit" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;" type="button">Create</button>
|
<button id="better-todo-add-task-submit" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;" type="button">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@ -1729,32 +1894,90 @@ function ensureTodoTaskMenu(location, feedbackElement) {
|
|||||||
courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect));
|
courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect));
|
||||||
|
|
||||||
addTaskButton.addEventListener("click", () => {
|
addTaskButton.addEventListener("click", () => {
|
||||||
|
const willOpen = !menu.classList.contains("canvasrefined-custom-open");
|
||||||
menu.classList.toggle("canvasrefined-custom-open");
|
menu.classList.toggle("canvasrefined-custom-open");
|
||||||
|
if (willOpen) {
|
||||||
|
resetTaskFormToCreate(menu);
|
||||||
|
// Scroll the form up so the picker stays on screen.
|
||||||
|
scrollTodoIntoView(menu, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
menu.querySelector("#better-todo-add-task-submit").addEventListener("click", async () => {
|
const submitTask = async () => {
|
||||||
const status = menu.querySelector("#better-todo-add-task-status");
|
const status = menu.querySelector("#better-todo-add-task-status");
|
||||||
const submitButton = menu.querySelector("#better-todo-add-task-submit");
|
const submitButton = menu.querySelector("#better-todo-add-task-submit");
|
||||||
|
const deleteButton = menu.querySelector("#better-todo-add-task-delete");
|
||||||
status.textContent = "";
|
status.textContent = "";
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
|
if (deleteButton) deleteButton.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = buildPlannerNotePayload(menu);
|
const payload = buildPlannerNotePayload(menu);
|
||||||
await createCanvasPlannerNote(payload);
|
const editingId = menu.dataset.editingId || null;
|
||||||
|
if (editingId) {
|
||||||
|
await updateCanvasPlannerNote(editingId, payload);
|
||||||
|
saveCustomTaskLink(editingId, payload.link);
|
||||||
|
status.textContent = "Task updated.";
|
||||||
|
} else {
|
||||||
|
const created = await createCanvasPlannerNote(payload);
|
||||||
|
const newId = created?.id;
|
||||||
|
if (newId != null) saveCustomTaskLink(newId, payload.link);
|
||||||
status.textContent = "Task created.";
|
status.textContent = "Task created.";
|
||||||
|
}
|
||||||
status.style.color = "#198754";
|
status.style.color = "#198754";
|
||||||
menu.querySelector("#better-todo-new-task-title").value = "";
|
resetTaskFormToCreate(menu);
|
||||||
menu.querySelector("#better-todo-new-task-details").value = "";
|
|
||||||
menu.classList.remove("canvasrefined-custom-open");
|
menu.classList.remove("canvasrefined-custom-open");
|
||||||
|
|
||||||
getAssignments();
|
getAssignments();
|
||||||
clearTodoList();
|
clearTodoList();
|
||||||
createTodoSections(location);
|
createTodoSections(location);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status.textContent = e?.message || "Could not create task.";
|
status.textContent = e?.message || "Could not save task.";
|
||||||
status.style.color = "#db3754";
|
status.style.color = "#db3754";
|
||||||
} finally {
|
} finally {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
|
if (deleteButton) deleteButton.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.querySelector("#better-todo-add-task-submit").addEventListener("click", submitTask);
|
||||||
|
|
||||||
|
// Close (×) button: dismiss the form without creating/editing a task.
|
||||||
|
menu.querySelector("#better-todo-add-task-close")?.addEventListener("click", () => {
|
||||||
|
resetTaskFormToCreate(menu);
|
||||||
|
menu.classList.remove("canvasrefined-custom-open");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reposition the field on focus so the picker opens on screen.
|
||||||
|
["#better-todo-new-task-date", "#better-todo-new-task-time"].forEach((sel) => {
|
||||||
|
const input = menu.querySelector(sel);
|
||||||
|
input?.addEventListener("focus", () => scrollTodoIntoView(input, false));
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelector("#better-todo-add-task-delete").addEventListener("click", async () => {
|
||||||
|
const editingId = menu.dataset.editingId || null;
|
||||||
|
if (!editingId) return;
|
||||||
|
if (!confirm("Delete this custom task? This cannot be undone.")) return;
|
||||||
|
const status = menu.querySelector("#better-todo-add-task-status");
|
||||||
|
const submitButton = menu.querySelector("#better-todo-add-task-submit");
|
||||||
|
const deleteButton = menu.querySelector("#better-todo-add-task-delete");
|
||||||
|
status.textContent = "";
|
||||||
|
submitButton.disabled = true;
|
||||||
|
deleteButton.disabled = true;
|
||||||
|
try {
|
||||||
|
await deleteCanvasPlannerNote(editingId);
|
||||||
|
deleteCustomTaskLink(editingId);
|
||||||
|
resetTaskFormToCreate(menu);
|
||||||
|
menu.classList.remove("canvasrefined-custom-open");
|
||||||
|
getAssignments();
|
||||||
|
clearTodoList();
|
||||||
|
createTodoSections(location);
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = e?.message || "Could not delete task.";
|
||||||
|
status.style.color = "#db3754";
|
||||||
|
} finally {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
deleteButton.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1768,6 +1991,64 @@ function ensureTodoTaskMenu(location, feedbackElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the shared add/edit task form back to a blank "create" state.
|
||||||
|
function resetTaskFormToCreate(menu) {
|
||||||
|
if (!menu) return;
|
||||||
|
menu.querySelector("#better-todo-new-task-title").value = "";
|
||||||
|
menu.querySelector("#better-todo-new-task-details").value = "";
|
||||||
|
menu.querySelector("#better-todo-new-task-link").value = "";
|
||||||
|
const courseSelect = menu.querySelector("#better-todo-new-task-course");
|
||||||
|
if (courseSelect) {
|
||||||
|
fillTaskCourseOptions(courseSelect);
|
||||||
|
updateTaskCourseSelectColor(courseSelect);
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(now);
|
||||||
|
menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(now);
|
||||||
|
const status = menu.querySelector("#better-todo-add-task-status");
|
||||||
|
if (status) { status.textContent = ""; status.style.color = ""; }
|
||||||
|
const del = menu.querySelector("#better-todo-add-task-delete");
|
||||||
|
if (del) del.style.display = "none";
|
||||||
|
const submit = menu.querySelector("#better-todo-add-task-submit");
|
||||||
|
if (submit) submit.textContent = "Create";
|
||||||
|
delete menu.dataset.editingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the shared form pre-filled with a custom task for editing or deletion.
|
||||||
|
function openTaskForEdit(item) {
|
||||||
|
const location = document.getElementById("canvasrefined-todo-list");
|
||||||
|
if (!location) return;
|
||||||
|
const feedbackElement = location.querySelector(".recent_feedback");
|
||||||
|
ensureTodoTaskMenu(location, feedbackElement);
|
||||||
|
const menu = document.getElementById("better-todo-add-task-menu");
|
||||||
|
if (!menu) return;
|
||||||
|
|
||||||
|
const noteId = getCustomTaskLinkId(item);
|
||||||
|
menu.querySelector("#better-todo-new-task-title").value = item?.plannable?.title || "";
|
||||||
|
menu.querySelector("#better-todo-new-task-details").value = item?.plannable?.details || "";
|
||||||
|
const courseSelect = menu.querySelector("#better-todo-new-task-course");
|
||||||
|
const cid = item?.course_id || item?.plannable?.course_id || "";
|
||||||
|
if (courseSelect) courseSelect.value = cid ? String(cid) : "";
|
||||||
|
updateTaskCourseSelectColor(courseSelect);
|
||||||
|
|
||||||
|
const dateObj = new Date(item?.plannable_date || item?.plannable?.todo_date || Date.now());
|
||||||
|
if (!Number.isNaN(dateObj.getTime())) {
|
||||||
|
menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(dateObj);
|
||||||
|
menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(dateObj);
|
||||||
|
}
|
||||||
|
const links = getCustomTaskLinks();
|
||||||
|
menu.querySelector("#better-todo-new-task-link").value = (noteId != null && links[String(noteId)]) || "";
|
||||||
|
|
||||||
|
menu.dataset.editingId = noteId != null ? String(noteId) : "";
|
||||||
|
const del = menu.querySelector("#better-todo-add-task-delete");
|
||||||
|
if (del) del.style.display = "";
|
||||||
|
menu.querySelector("#better-todo-add-task-submit").textContent = "Save";
|
||||||
|
const status = menu.querySelector("#better-todo-add-task-status");
|
||||||
|
if (status) { status.textContent = ""; status.style.color = ""; }
|
||||||
|
menu.classList.add("canvasrefined-custom-open");
|
||||||
|
scrollTodoIntoView(menu, true);
|
||||||
|
}
|
||||||
|
|
||||||
async function createTodoSections(location) {
|
async function createTodoSections(location) {
|
||||||
if (!location.querySelector("#better-todo-header")) {
|
if (!location.querySelector("#better-todo-header")) {
|
||||||
let header = makeElement("div", location, { id: "better-todo-header" });
|
let header = makeElement("div", location, { id: "better-todo-header" });
|
||||||
@ -2071,6 +2352,10 @@ function populateAssignments(iscompleted = false) {
|
|||||||
"#cccccc";
|
"#cccccc";
|
||||||
|
|
||||||
const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true;
|
const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true;
|
||||||
|
const taskHref = isCustomTask ? customTaskHref(item) : (domain + item.html_url);
|
||||||
|
const editButtonSvg = isCustomTask
|
||||||
|
? `<svg class="better-todo-assignment-edit" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:18px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'" title="Edit this custom task"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" stroke="var(--bctext-0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`
|
||||||
|
: "";
|
||||||
const iconSize = isCustomTask ? 26 : 20;
|
const iconSize = isCustomTask ? 26 : 20;
|
||||||
const iconLeftOffset = isCustomTask ? 2 : 5;
|
const iconLeftOffset = isCustomTask ? 2 : 5;
|
||||||
const taskIcon = isCustomTask
|
const taskIcon = isCustomTask
|
||||||
@ -2095,9 +2380,10 @@ function populateAssignments(iscompleted = false) {
|
|||||||
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;position:relative;">
|
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;position:relative;">
|
||||||
<div style="display:flex;flex-direction:column;gap:3px;">
|
<div style="display:flex;flex-direction:column;gap:3px;">
|
||||||
<span style="color:${courseColor};font-size:12px;margin-top:-2px;">${item.context_name}</span>
|
<span style="color:${courseColor};font-size:12px;margin-top:-2px;">${item.context_name}</span>
|
||||||
<a href="${domain + item.html_url}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
|
<a href="${taskHref}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
|
||||||
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
|
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${editButtonSvg}
|
||||||
<svg class="better-todo-assignment-checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:0px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'">
|
<svg class="better-todo-assignment-checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:0px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'">
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||||
@ -2112,6 +2398,14 @@ function populateAssignments(iscompleted = false) {
|
|||||||
console.log("marking ", item.plannable.title, " as complete");
|
console.log("marking ", item.plannable.title, " as complete");
|
||||||
markAs(item, assignment.firstElementChild);
|
markAs(item, assignment.firstElementChild);
|
||||||
});
|
});
|
||||||
|
const editBtn = assignment.querySelector(".better-todo-assignment-edit");
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openTaskForEdit(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document.getElementById("better-todo-see-more")) {
|
if (document.getElementById("better-todo-see-more")) {
|
||||||
@ -3056,11 +3350,7 @@ async function changeColorPreset(colors) {
|
|||||||
Dark mode
|
Dark mode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Light-mode fallback values for the --bc* theme variables. These are always
|
// Light-mode fallbacks for the --bc* variables, always emitted so extension UI renders in light mode; dark mode overrides below.
|
||||||
// emitted (even when dark mode is OFF) so extension UI referencing
|
|
||||||
// var(--bctext-0) / var(--bcborders) / var(--bcbackground-*) still renders
|
|
||||||
// correctly in light mode (e.g. better sidebar/todo icons stay dark instead
|
|
||||||
// of falling back to the SVG initial values). Dark mode overrides these below.
|
|
||||||
const BC_LIGHT_DEFAULTS = {
|
const BC_LIGHT_DEFAULTS = {
|
||||||
"background-0": "#ffffff",
|
"background-0": "#ffffff",
|
||||||
"background-1": "#c7c7c7",
|
"background-1": "#c7c7c7",
|
||||||
@ -3264,12 +3554,8 @@ function createCardAssignment(assignment) {
|
|||||||
|
|
||||||
let cardAssignments;
|
let cardAssignments;
|
||||||
|
|
||||||
/*
|
/* Equal Height Cards: stretch each card's assignment area to match the tallest
|
||||||
Equal Height Cards — when enabled, every dashboard card's assignment area is
|
one, using min-height so cards can still grow. */
|
||||||
stretched to match the tallest one so cards with fewer assignments don't end
|
|
||||||
up shorter than the rest. Uses min-height (never a fixed height) so cards can
|
|
||||||
still grow if their content exceeds the tallest card.
|
|
||||||
*/
|
|
||||||
let equalHeightResizeTimer = null;
|
let equalHeightResizeTimer = null;
|
||||||
|
|
||||||
function equalizeCardHeights() {
|
function equalizeCardHeights() {
|
||||||
@ -3278,8 +3564,7 @@ function equalizeCardHeights() {
|
|||||||
|
|
||||||
const enabled = options.equal_height_cards === true && options.assignments_due === true;
|
const enabled = options.equal_height_cards === true && options.assignments_due === true;
|
||||||
|
|
||||||
// Always clear any previously applied min-height first so we can either
|
// Clear prior min-height so we can measure fresh or fully reset.
|
||||||
// measure natural heights (when enabling) or fully reset (when disabling).
|
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
const area = card.querySelector(".canvasrefined-card-assignment");
|
const area = card.querySelector(".canvasrefined-card-assignment");
|
||||||
if (area) area.style.removeProperty("min-height");
|
if (area) area.style.removeProperty("min-height");
|
||||||
@ -3287,9 +3572,7 @@ function equalizeCardHeights() {
|
|||||||
|
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
|
||||||
// Measure the natural height of each card's assignment area, then stretch
|
// Stretch each assignment area to the tallest one.
|
||||||
// every area to the tallest one. Card headers are uniform, so equalizing
|
|
||||||
// the assignment area makes all cards the same total height.
|
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
const area = card.querySelector(".canvasrefined-card-assignment");
|
const area = card.querySelector(".canvasrefined-card-assignment");
|
||||||
@ -3781,13 +4064,7 @@ function delayDashboardNotesStorage(text) {
|
|||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Fallback Markdown renderer used only if js/markdown.js failed to load. */
|
||||||
Built-in fallback Markdown renderer. Used only if js/markdown.js failed to load
|
|
||||||
(window.renderMarkdown missing) so the notes still render formatted output instead
|
|
||||||
of showing raw markdown text. Covers the common subset: headings, bold/italic/strike,
|
|
||||||
inline + fenced code, links, images, lists, task lists, blockquotes, hr, paragraphs.
|
|
||||||
All user text is HTML-escaped before formatting.
|
|
||||||
*/
|
|
||||||
function crRenderMarkdownFallback(src) {
|
function crRenderMarkdownFallback(src) {
|
||||||
if (src == null) return "";
|
if (src == null) return "";
|
||||||
const escapeHtml = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
const escapeHtml = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||||
@ -3804,6 +4081,7 @@ function crRenderMarkdownFallback(src) {
|
|||||||
const out = [];
|
const out = [];
|
||||||
const lines = text.split("\n");
|
const lines = text.split("\n");
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing
|
||||||
const inline = (t) => {
|
const inline = (t) => {
|
||||||
let h = escapeHtml(t);
|
let h = escapeHtml(t);
|
||||||
h = h.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, url, title) =>
|
h = h.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, url, title) =>
|
||||||
@ -3827,7 +4105,7 @@ function crRenderMarkdownFallback(src) {
|
|||||||
out.push(`<blockquote>${q.join("<br>")}</blockquote>`); continue;
|
out.push(`<blockquote>${q.join("<br>")}</blockquote>`); continue;
|
||||||
}
|
}
|
||||||
if (/^\s*[-*+]\s+/.test(line)) {
|
if (/^\s*[-*+]\s+/.test(line)) {
|
||||||
const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*[-*+]\s+(.*)$/); if (!m) break; const tk = m[1].match(/^\[([ xX])\]\s+(.*)$/); if (tk) { items.push(`<li class="cr-task"><input type="checkbox" disabled${/x/i.test(tk[1]) ? " checked" : ""}> ${inline(tk[2])}</li>`); } else { items.push(`<li>${inline(m[1])}</li>`); } i++; } out.push(`<ul>${items.join("")}</ul>`); continue;
|
const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*[-*+]\s+(.*)$/); if (!m) break; const tk = m[1].match(/^\[([ xX])\]\s+(.*)$/); if (tk) { items.push(`<li class="cr-task" data-cr-task="${taskSeq++}"><input type="checkbox"${/x/i.test(tk[1]) ? " checked" : ""}> ${inline(tk[2])}</li>`); } else { items.push(`<li>${inline(m[1])}</li>`); } i++; } out.push(`<ul>${items.join("")}</ul>`); continue;
|
||||||
}
|
}
|
||||||
if (/^\s*\d+\.\s+/.test(line)) {
|
if (/^\s*\d+\.\s+/.test(line)) {
|
||||||
const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*\d+\.\s+(.*)$/); if (!m) break; items.push(`<li>${inline(m[1])}</li>`); i++; } out.push(`<ol>${items.join("")}</ol>`); continue;
|
const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*\d+\.\s+(.*)$/); if (!m) break; items.push(`<li>${inline(m[1])}</li>`); i++; } out.push(`<ol>${items.join("")}</ol>`); continue;
|
||||||
@ -3840,20 +4118,14 @@ function crRenderMarkdownFallback(src) {
|
|||||||
|
|
||||||
function renderDashboardNotesPreview(preview, text) {
|
function renderDashboardNotesPreview(preview, text) {
|
||||||
if (!preview) return;
|
if (!preview) return;
|
||||||
// Skip identical re-renders: writing innerHTML is a childList mutation that the
|
// Skip identical re-renders to avoid a self-sustaining observer loop.
|
||||||
// dashboard MutationObserver picks up, which re-calls loadDashboardNotes, which
|
|
||||||
// re-renders... Without this guard the notes box drives a tight self-sustaining
|
|
||||||
// loop (setTimeout 0) that starves the main thread so the render never paints.
|
|
||||||
if (preview._crLastText === text) return;
|
if (preview._crLastText === text) return;
|
||||||
preview._crLastText = text;
|
preview._crLastText = text;
|
||||||
const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback;
|
const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback;
|
||||||
preview.innerHTML = renderer(text);
|
preview.innerHTML = renderer(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Insert/wrap Markdown formatting at the selection and fire an input event. */
|
||||||
Insert/wrap Markdown formatting in the notes editor at the current selection.
|
|
||||||
Dispatches a synthetic `input` event so the live preview + storage handler runs.
|
|
||||||
*/
|
|
||||||
function notesApplyFormat(editor, action) {
|
function notesApplyFormat(editor, action) {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
const start = editor.selectionStart;
|
const start = editor.selectionStart;
|
||||||
@ -3947,6 +4219,34 @@ function notesApplyFormat(editor, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toggle the Nth rendered Markdown task checkbox and re-render, using a task
|
||||||
|
ordinal that skips fenced code blocks. */
|
||||||
|
function toggleDashboardNoteTask(editor, taskIndex) {
|
||||||
|
if (!editor) return;
|
||||||
|
const value = editor.value || "";
|
||||||
|
const lines = value.split("\n");
|
||||||
|
let inFence = false;
|
||||||
|
let count = 0;
|
||||||
|
for (let idx = 0; idx < lines.length; idx++) {
|
||||||
|
const line = lines[idx];
|
||||||
|
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
||||||
|
if (inFence) continue;
|
||||||
|
const m = line.match(/^(\s*[-*+]\s+)\[([ xX])\](\s+)/);
|
||||||
|
if (!m) continue;
|
||||||
|
if (count === taskIndex) {
|
||||||
|
const newMark = m[2] === " " ? "x" : " ";
|
||||||
|
lines[idx] = m[1] + "[" + newMark + "]" + m[3] + line.slice(m[0].length);
|
||||||
|
editor.value = lines.join("\n");
|
||||||
|
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
const notes = editor.closest(".canvasrefined-dashboard-notes");
|
||||||
|
const rendered = notes ? notes.querySelector(".canvasrefined-notes-rendered") : null;
|
||||||
|
if (rendered) renderDashboardNotesPreview(rendered, editor.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DASHBOARD_NOTES_HTML = `
|
const DASHBOARD_NOTES_HTML = `
|
||||||
<div class="canvasrefined-notes-toolbar" role="toolbar" aria-label="Format notes">
|
<div class="canvasrefined-notes-toolbar" role="toolbar" aria-label="Format notes">
|
||||||
<button type="button" class="cr-fmt" data-action="bold" title="Bold (Ctrl/Cmd+B)"><strong>B</strong></button>
|
<button type="button" class="cr-fmt" data-action="bold" title="Bold (Ctrl/Cmd+B)"><strong>B</strong></button>
|
||||||
@ -3990,7 +4290,18 @@ function wireDashboardNotes(notes) {
|
|||||||
renderDashboardNotesPreview(rendered, editor.value);
|
renderDashboardNotesPreview(rendered, editor.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
rendered.addEventListener("click", enterEdit);
|
rendered.addEventListener("click", (e) => {
|
||||||
|
// Clicking a checkbox toggles the task; clicking the text edits.
|
||||||
|
const t = e.target;
|
||||||
|
if (t && t.nodeType === 1 && t.tagName === "INPUT" && t.type === "checkbox" && t.closest && t.closest("li.cr-task")) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const li = t.closest("li.cr-task");
|
||||||
|
const taskIndex = parseInt(li.dataset.crTask, 10);
|
||||||
|
if (!Number.isNaN(taskIndex)) toggleDashboardNoteTask(editor, taskIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enterEdit();
|
||||||
|
});
|
||||||
rendered.addEventListener("focus", enterEdit);
|
rendered.addEventListener("focus", enterEdit);
|
||||||
editor.addEventListener("blur", exitEdit);
|
editor.addEventListener("blur", exitEdit);
|
||||||
editor.addEventListener("input", function () {
|
editor.addEventListener("input", function () {
|
||||||
@ -3998,11 +4309,10 @@ function wireDashboardNotes(notes) {
|
|||||||
delayDashboardNotesStorage(this.value);
|
delayDashboardNotesStorage(this.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toolbar buttons: keep focus in the editor (mousedown preventDefault stops the
|
// preventDefault on the toolbar keeps focus in the editor across misclicks.
|
||||||
// button from stealing focus and collapsing back to the rendered view), then apply
|
const toolbar = notes.querySelector(".canvasrefined-notes-toolbar");
|
||||||
// the formatting. Enters edit mode first if the user formats from the rendered view.
|
if (toolbar) toolbar.addEventListener("mousedown", e => e.preventDefault());
|
||||||
notes.querySelectorAll(".cr-fmt").forEach(btn => {
|
notes.querySelectorAll(".cr-fmt").forEach(btn => {
|
||||||
btn.addEventListener("mousedown", e => e.preventDefault());
|
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
if (!notes.classList.contains("is-editing")) enterEdit();
|
if (!notes.classList.contains("is-editing")) enterEdit();
|
||||||
notesApplyFormat(editor, btn.dataset.action);
|
notesApplyFormat(editor, btn.dataset.action);
|
||||||
@ -4044,9 +4354,7 @@ function loadDashboardNotes() {
|
|||||||
notes.style.display = "";
|
notes.style.display = "";
|
||||||
const editor = notes.querySelector(".canvasrefined-notes-editor");
|
const editor = notes.querySelector(".canvasrefined-notes-editor");
|
||||||
const rendered = notes.querySelector(".canvasrefined-notes-rendered");
|
const rendered = notes.querySelector(".canvasrefined-notes-rendered");
|
||||||
// While editing, the textarea is the source of truth: don't clobber it from
|
// Only sync and render in view mode; the textarea is the source of truth while editing.
|
||||||
// storage and don't waste a render on the hidden rendered view (which would
|
|
||||||
// also feed the observer loop). Only sync + render in view mode.
|
|
||||||
if (!notes.classList.contains("is-editing")) {
|
if (!notes.classList.contains("is-editing")) {
|
||||||
if (editor && editor.value !== (options.dashboard_notes_text || "")) {
|
if (editor && editor.value !== (options.dashboard_notes_text || "")) {
|
||||||
editor.value = options.dashboard_notes_text || "";
|
editor.value = options.dashboard_notes_text || "";
|
||||||
@ -4134,9 +4442,7 @@ function changeGradientCards() {
|
|||||||
if (options.gradient_cards === true) {
|
if (options.gradient_cards === true) {
|
||||||
let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero');
|
let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero');
|
||||||
|
|
||||||
// Only create + append the style once; never re-append an already-
|
// Create the style once; re-appending triggers the MutationObserver and re-runs this function.
|
||||||
// attached element, since appending to <html> triggers the
|
|
||||||
// MutationObserver in checkDashboardReady() and re-runs this function.
|
|
||||||
let cardcss = document.querySelector("#gradientcss");
|
let cardcss = document.querySelector("#gradientcss");
|
||||||
if (!cardcss) {
|
if (!cardcss) {
|
||||||
cardcss = document.createElement('style');
|
cardcss = document.createElement('style');
|
||||||
@ -4144,9 +4450,7 @@ function changeGradientCards() {
|
|||||||
document.documentElement.appendChild(cardcss);
|
document.documentElement.appendChild(cardcss);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the full CSS into a string first, then only touch the DOM
|
// Build CSS into a string and only touch the DOM if it changed.
|
||||||
// if the content actually changed. This keeps #gradientcss from being
|
|
||||||
// cleared/rewritten on every observer tick.
|
|
||||||
let css = "";
|
let css = "";
|
||||||
for (let i = 0; i < cardheads.length; i++) {
|
for (let i = 0; i < cardheads.length; i++) {
|
||||||
let colorone = cardheads[i].style.backgroundColor.split(',');
|
let colorone = cardheads[i].style.backgroundColor.split(',');
|
||||||
@ -4323,7 +4627,12 @@ async function getData(url) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let data = await response.json();
|
let data = await response.json();
|
||||||
return data
|
// Deep-clone via JSON to unwrap Firefox Xray objects so nested props are mutable.
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(data));
|
||||||
|
} catch (_) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -94,6 +94,7 @@ via innerHTML. Exposes window.renderMarkdown(text) -> html.
|
|||||||
const out = [];
|
const out = [];
|
||||||
const n = lines.length;
|
const n = lines.length;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing
|
||||||
const isToken = (s) => /^\u0000\d+\u0000$/.test(s);
|
const isToken = (s) => /^\u0000\d+\u0000$/.test(s);
|
||||||
|
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
@ -140,7 +141,7 @@ via innerHTML. Exposes window.renderMarkdown(text) -> html.
|
|||||||
if (task) {
|
if (task) {
|
||||||
const checked = /x/i.test(task[1]);
|
const checked = /x/i.test(task[1]);
|
||||||
items.push(
|
items.push(
|
||||||
'<li class="cr-task"><input type="checkbox" disabled' +
|
'<li class="cr-task" data-cr-task="' + taskSeq++ + '"><input type="checkbox"' +
|
||||||
(checked ? " checked" : "") + "> " + inline(task[2])
|
(checked ? " checked" : "") + "> " + inline(task[2])
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
42
js/popup.js
42
js/popup.js
@ -38,10 +38,7 @@ const syncedSubOptions = [
|
|||||||
];
|
];
|
||||||
const localSwitches = [];
|
const localSwitches = [];
|
||||||
|
|
||||||
// Theme export categories. Only visual/appearance settings belong in a
|
// Theme export only carries visual settings, never personal productivity data.
|
||||||
// shared theme; personal productivity features (reminders, dashboard notes,
|
|
||||||
// the card-assignments list and its display prefs, card grades, item counts)
|
|
||||||
// are intentionally excluded so exported themes never carry personal stuff.
|
|
||||||
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
|
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
|
||||||
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
|
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
|
||||||
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight"];
|
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight"];
|
||||||
@ -433,10 +430,7 @@ function toggleBetterSidebarSubOptions(betterSidebarOn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the Better Todo List is on, its own "Hide Recent Feedback" sub-option
|
// Hide the standalone feedback toggle when Better Todo's own sub-option replaces it.
|
||||||
// (todo_hide_feedback) replaces the standalone "Hide recent feedback" toggle,
|
|
||||||
// so hide the big #hide_feedback toggle to avoid showing two controls for the
|
|
||||||
// same thing. Mirrors toggleBetterSidebarSubOptions.
|
|
||||||
function toggleBetterTodoSubOptions(betterTodoOn) {
|
function toggleBetterTodoSubOptions(betterTodoOn) {
|
||||||
const hideFeedbackEl = document.getElementById("hide_feedback");
|
const hideFeedbackEl = document.getElementById("hide_feedback");
|
||||||
if (hideFeedbackEl) {
|
if (hideFeedbackEl) {
|
||||||
@ -444,10 +438,7 @@ function toggleBetterTodoSubOptions(betterTodoOn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the sub-options of a big toggle when it is turned off.
|
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
|
||||||
// For gpa_calc / assignments_due / better_todo the whole .sub-options block is
|
|
||||||
// hidden. For auto_dark ("schedule dark mode") only the start/end time clocks
|
|
||||||
// (.timesets) are hidden, leaving the device-dark checkbox visible.
|
|
||||||
function toggleSubOptionsVisibility(option, isOn) {
|
function toggleSubOptionsVisibility(option, isOn) {
|
||||||
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark"];
|
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark"];
|
||||||
if (!togglesWithSubOptions.includes(option)) return;
|
if (!togglesWithSubOptions.includes(option)) return;
|
||||||
@ -480,9 +471,7 @@ function setupFeatureSearch(menu) {
|
|||||||
function shouldSkip(el) {
|
function shouldSkip(el) {
|
||||||
// Skip overlays / scrapped containers that aren't real features
|
// Skip overlays / scrapped containers that aren't real features
|
||||||
if (el.closest('#submit-popup, #browser-settings-popup, #opt-in, #card-edit-menu')) return true;
|
if (el.closest('#submit-popup, #browser-settings-popup, #opt-in, #card-edit-menu')) return true;
|
||||||
// Skip anything inside a statically-hidden option-container
|
// Skip statically-hidden containers but keep dynamically-hidden sub-options.
|
||||||
// (e.g. the scrapped "remind" block, "Submit your theme").
|
|
||||||
// Dynamically-hidden sub-options (whose parent toggle is off) are kept.
|
|
||||||
let node = el;
|
let node = el;
|
||||||
while (node && node !== document.body) {
|
while (node && node !== document.body) {
|
||||||
if (node.classList && node.classList.contains('option-container') && node.style.display === 'none') return true;
|
if (node.classList && node.classList.contains('option-container') && node.style.display === 'none') return true;
|
||||||
@ -604,9 +593,7 @@ function setupFeatureSearch(menu) {
|
|||||||
// Home: sub-options / timesets / labelled rows (e.g. "Use dd/mm", "Start time", max-items)
|
// Home: sub-options / timesets / labelled rows (e.g. "Use dd/mm", "Start time", max-items)
|
||||||
document.querySelectorAll(".options .sub-option, .options .timeset, .options .sub-options > div").forEach(sub => {
|
document.querySelectorAll(".options .sub-option, .options .timeset, .options .sub-options > div").forEach(sub => {
|
||||||
if (shouldSkip(sub)) return;
|
if (shouldSkip(sub)) return;
|
||||||
// Skip statically-hidden sub-options (e.g. the "hover preview" TODO).
|
// Skip statically-hidden sub-options; dynamically-hidden ones stay indexed.
|
||||||
// Dynamically-hidden sub-options have display:none on their parent
|
|
||||||
// .sub-options, not on themselves, so they stay indexed.
|
|
||||||
if (sub.classList.contains("sub-option") && sub.style.display === "none") return;
|
if (sub.classList.contains("sub-option") && sub.style.display === "none") return;
|
||||||
const text = labelOf(sub);
|
const text = labelOf(sub);
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
@ -1123,6 +1110,21 @@ function setup() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toggle all export checkboxes and dispatch one change event to refresh output.
|
||||||
|
const exportCheckboxes = () => document.querySelectorAll(".export-details input");
|
||||||
|
const refreshExport = () => {
|
||||||
|
const first = exportCheckboxes()[0];
|
||||||
|
if (first) first.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
};
|
||||||
|
document.querySelector("#export-check-all")?.addEventListener("click", () => {
|
||||||
|
exportCheckboxes().forEach(c => { c.checked = true; });
|
||||||
|
refreshExport();
|
||||||
|
});
|
||||||
|
document.querySelector("#export-check-none")?.addEventListener("click", () => {
|
||||||
|
exportCheckboxes().forEach(c => { c.checked = false; });
|
||||||
|
refreshExport();
|
||||||
|
});
|
||||||
|
|
||||||
// activate revert to original button
|
// activate revert to original button
|
||||||
document.querySelector("#theme-revert").addEventListener("click", () => {
|
document.querySelector("#theme-revert").addEventListener("click", () => {
|
||||||
chrome.storage.local.get("previous_theme", local => {
|
chrome.storage.local.get("previous_theme", local => {
|
||||||
@ -1575,9 +1577,7 @@ function saveCurrentTheme() {
|
|||||||
|
|
||||||
|
|
||||||
function displayThemeList(direction = 0) {
|
function displayThemeList(direction = 0) {
|
||||||
// render the full sorted theme list (filtered by any active search).
|
// Render the sorted theme list, filtered by any active search.
|
||||||
// Previously this was an empty stub, so the browser showed nothing
|
|
||||||
// until the search box fired displayThemeSearchList directly.
|
|
||||||
let themesToShow = allThemes;
|
let themesToShow = allThemes;
|
||||||
if (searchFor) {
|
if (searchFor) {
|
||||||
themesToShow = allThemes.filter(theme => theme.title.toLowerCase().includes(searchFor.toLowerCase()));
|
themesToShow = allThemes.filter(theme => theme.title.toLowerCase().includes(searchFor.toLowerCase()));
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Canvas Refined",
|
"name": "Canvas Refined",
|
||||||
"description": "Even More Feature packed extension for Canvas.",
|
"description": "Even More Feature packed extension for Canvas.",
|
||||||
"version": "6.3.0",
|
"version": "6.3.1",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icon/icon-16.png",
|
"16": "icon/icon-16.png",
|
||||||
"32": "icon/icon-32.png",
|
"32": "icon/icon-32.png",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user