todolist preview

This commit is contained in:
Guy Sandler 2026-08-23 21:45:29 -07:00
parent f26cac0e9c
commit 765ecbf2fa
15 changed files with 147 additions and 23 deletions

View File

@ -54,7 +54,7 @@
"message": "Max. Anzahl Elemente: "
},
"hover_preview": {
"message": "Bewegen Sie den Mauszeiger, um Details in der Vorschau anzuzeigen"
"message": "Vorschau beim Überfahren"
},
"hide_completed": {
"message": "Abgeschlossene ausblenden"

View File

@ -84,7 +84,7 @@
"message": "Max Items: "
},
"hover_preview": {
"message": "Hover To Preview Details"
"message": "Previews on hover"
},
"hide_completed": {
"message": "Hide Completed"

View File

@ -54,7 +54,7 @@
"message": "Artículos máximos: "
},
"hover_preview": {
"message": "Pasa el cursor para obtener una vista previa de los detalles"
"message": "Vista previa al pasar el cursor"
},
"hide_completed": {
"message": "Ocultar completado"

View File

@ -54,7 +54,7 @@
"message": "Nombre maximal d'articles : "
},
"hover_preview": {
"message": "Survolez pour prévisualiser les détails"
"message": "Aperçu au survol"
},
"hide_completed": {
"message": "Masquer terminé"

View File

@ -54,7 +54,7 @@
"message": "Numero massimo di articoli: "
},
"hover_preview": {
"message": "Passa il mouse per visualizzare l'anteprima dei dettagli"
"message": "Anteprima al passaggio del mouse"
},
"hide_completed": {
"message": "Nascondi Completato"

View File

@ -54,7 +54,7 @@
"message": "最大アイテム数: "
},
"hover_preview": {
"message": "詳細をプレビューするにはマウスを移動します"
"message": "ホバーでプレビュー"
},
"hide_completed": {
"message": "完了を非表示"

View File

@ -54,7 +54,7 @@
"message": "Itens máximos: "
},
"hover_preview": {
"message": "Passe o mouse para visualizar os detalhes"
"message": "Pré-visualização ao passar o cursor"
},
"hide_completed": {
"message": "Ocultar Concluído"

View File

@ -54,7 +54,7 @@
"message": "Максимальное количество предметов: "
},
"hover_preview": {
"message": "Наведите курсор, чтобы просмотреть детали"
"message": "Предпросмотр при наведении"
},
"hide_completed": {
"message": "Скрыть Завершено"

View File

@ -54,7 +54,7 @@
"message": "Max artiklar: "
},
"hover_preview": {
"message": "Håll muspekaren för att förhandsgranska detaljer"
"message": "Förhandsgranskning vid hover"
},
"hide_completed": {
"message": "Göm slutförd"

View File

@ -54,7 +54,7 @@
"message": "最大物品数: "
},
"hover_preview": {
"message": "将鼠标悬停预览详细信息"
"message": "悬停预览"
},
"hide_completed": {
"message": "隐藏已完成"

View File

@ -54,7 +54,7 @@
"message": "最大物品数: "
},
"hover_preview": {
"message": "将鼠标悬停以预览详细信息"
"message": "懸停預覽"
},
"hide_completed": {
"message": "隐藏已完成"

View File

@ -125,10 +125,10 @@
#canvasrefined-announcement-list {margin-bottom:1.0625rem;}
.canvasrefined-todo-item-loader {margin-top: 18px;}
.canvasrefined-todo-item-loader .canvasrefined-skeleton-text {height:12px;margin-top: 8px;margin-bottom:0;margin-left:25px}
.canvasrefined-hover-preview {position: absolute;top: -6px;left: -400px;display: none; background: #fff; color: #000; max-height: 300%; width: 350px; padding:12px;border-radius:5px; overflow: hidden;box-shadow: 0 2px 5px rgba(0,0,0,.3)}
.canvasrefined-preview-text {text-overflow: ellipsis;}
.canvasrefined-preview-title {font-weight:bold;font-size:14px;}
.canvasrefined-hover-preview::after {content:'';position:absolute;width:100%;height: 18px;bottom:0;left:0;background:linear-gradient(0deg, #fff 50%, transparent)}
.canvasrefined-hover-preview {position: absolute;top: -6px;left: -400px;display: none; background: var(--bcbackground-0, #fff); color: var(--bctext-0, #000); max-height: 300%; width: 350px; padding:12px;border-radius:5px; overflow: hidden;box-shadow: 0 2px 5px rgba(0,0,0,.3)}
.canvasrefined-preview-text {text-overflow: ellipsis;color: var(--bctext-0, #000);}
.canvasrefined-preview-title {font-weight:bold;font-size:14px;color: var(--bctext-0, #000);}
.canvasrefined-hover-preview::after {content:'';position:absolute;width:100%;height: 18px;bottom:0;left:0;background:linear-gradient(0deg, var(--bcbackground-0, #fff) 50%, transparent)}
.canvasrefined-link-image {height: 21px; width: 21px;padding: 0px}
.canvasrefined-link-image:hover {filter: brightness(66%)}
.canvasrefined-gpa-header {font-size:16px;font-weight: bold;padding: 0;margin: 0;}

View File

@ -215,11 +215,6 @@
<span class="option-name" data-i18n="better_todo">Better Todo List</span>
</div>
<div class="sub-options">
<div class="sub-option" style="display:none;">
<input type="checkbox" id="hover_preview" name="hover_preview">
<label for="hover_preview" class="sub-text" data-i18n="hover_preview">Hover to preview
details</label> <!-- TODO: Later -->
</div>
<div class="sub-option">
<input type="checkbox" id="todo_separate_scrollbar" name="todo_separate_scrollbar">
<label for="todo_separate_scrollbar" class="sub-text" data-i18n="todo_separate_scrollbar">Separate Scrollbar</label>
@ -232,6 +227,10 @@
<input type="checkbox" id="todo_full_height" name="todo_full_height">
<label for="todo_full_height" class="sub-text" data-i18n="todo_full_height">Force Full Height</label> <!-- TO REMOVE and replace with full height -->
</div>
<div class="sub-option">
<input type="checkbox" id="hover_preview" name="hover_preview">
<label for="hover_preview" class="sub-text" data-i18n="hover_preview">Previews on hover</label>
</div>
<div class="sub-option">
<input type="checkbox" id="todo_hide_feedback" name="todo_hide_feedback">
<label for="todo_hide_feedback" class="sub-text" data-i18n="todo_hide_feedback">Hide Recent Feedback</label>

View File

@ -2746,6 +2746,128 @@ function applyTodoAlternateColors() {
todoAltStyleEl.textContent = `:root{--cr-todo-icon:${color};}`;
}
// --- Hover preview (Better Todo List: "Previews on hover") ---
// The todo list is rendered by createTodoSections -> populateAssignments /
// populateAnnouncements (the old loadBetterTodo renderer is no longer called).
// A single shared, body-level tooltip is reused across items so it is never
// clipped by the sidebar's scroll/overflow containers. It reuses the
// .canvasrefined-hover-preview class so existing light/dark styling applies.
let todoPreviewDelay = null;
let todoPreviewToken = 0;
const todoPreviewCache = new Map();
function stripHtmlPreview(html) {
if (!html) return "";
return String(html).replace(/<\/?[^>]+(>|$)/g, " ").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
}
function getTodoPreviewEl() {
let el = document.getElementById("canvasrefined-todo-preview");
if (el) return el;
el = document.createElement("div");
el.id = "canvasrefined-todo-preview";
el.className = "canvasrefined-hover-preview";
el.innerHTML = '<p class="canvasrefined-preview-title"></p><p class="canvasrefined-preview-text"></p>';
el.style.position = "fixed";
el.style.zIndex = "100001";
el.style.width = "300px";
el.style.maxWidth = "90vw";
el.style.maxHeight = "260px";
document.body.append(el);
return el;
}
function positionTodoPreview(el, anchor) {
const r = anchor.getBoundingClientRect();
const gap = 10;
const pw = el.offsetWidth || 300;
const ph = el.offsetHeight || 160;
let left = r.left - pw - gap;
let top = r.top;
if (left < gap) {
left = r.right + gap;
if (left + pw > window.innerWidth - gap) left = Math.max(gap, window.innerWidth - pw - gap);
}
if (top + ph > window.innerHeight - gap) top = Math.max(gap, window.innerHeight - ph - gap);
el.style.left = left + "px";
el.style.top = top + "px";
}
async function getTodoPreviewText(item) {
const type = item.plannable_type;
const id = item.plannable_id;
const key = type + ":" + id;
if (todoPreviewCache.has(key)) return todoPreviewCache.get(key);
// Custom task (planner note): the description is already on the planner
// item as item.plannable.details, so no API call is needed.
if (type === "planner_note" || (item.planner_override && item.planner_override.custom === true)) {
const raw = item.plannable && item.plannable.details ? item.plannable.details : "";
const text = stripHtmlPreview(raw) || "No details given";
todoPreviewCache.set(key, text);
return text;
}
let url = null;
let field = "description";
if (type === "assignment") {
url = `${domain}/api/v1/courses/${item.course_id}/assignments/${id}`;
} else if (type === "quiz") {
url = `${domain}/api/v1/courses/${item.course_id}/quizzes/${id}`;
} else if (type === "discussion_topic" || type === "announcement") {
url = `${domain}/api/v1/courses/${item.course_id}/discussion_topics/${id}`;
field = "message";
}
if (!url) {
const text = "No preview available";
todoPreviewCache.set(key, text);
return text;
}
try {
const data = await getData(url);
const raw = data && data[field] ? data[field] : "";
const text = stripHtmlPreview(raw) || "No details given";
todoPreviewCache.set(key, text);
return text;
} catch (e) {
return "Couldn't load preview";
}
}
function hideTodoPreview() {
todoPreviewToken++; // cancel any pending async text update
const el = document.getElementById("canvasrefined-todo-preview");
if (el) el.style.display = "none";
}
async function showTodoPreview(anchor, item) {
const token = ++todoPreviewToken;
const el = getTodoPreviewEl();
const title = el.querySelector(".canvasrefined-preview-title");
const text = el.querySelector(".canvasrefined-preview-text");
title.textContent = item.plannable && item.plannable.title ? item.plannable.title : "";
text.textContent = "Loading…";
el.style.display = "block";
positionTodoPreview(el, anchor);
const content = await getTodoPreviewText(item);
if (token !== todoPreviewToken) return; // a newer hover (or hide) superseded this one
if (el.style.display !== "block") return; // user already moved away
text.textContent = content;
positionTodoPreview(el, anchor); // reposition now that the height is known
}
function attachTodoHoverPreview(anchor, item) {
if (options.hover_preview !== true) return;
anchor.addEventListener("mouseenter", () => {
clearTimeout(todoPreviewDelay);
todoPreviewDelay = setTimeout(() => {
if (anchor.matches(":hover")) showTodoPreview(anchor, item);
}, 250);
});
anchor.addEventListener("mouseleave", () => {
clearTimeout(todoPreviewDelay);
hideTodoPreview();
});
}
function populateAssignments(iscompleted = false) {
const today = new Date();
today.setHours(0,0,0,0);
@ -2870,6 +2992,7 @@ function populateAssignments(iscompleted = false) {
openTaskForEdit(item);
});
}
attachTodoHoverPreview(assignment, item);
});
if (document.getElementById("better-todo-see-more")) {
@ -2949,6 +3072,7 @@ function populateAnnouncements() {
</div>
</div>
`;
attachTodoHoverPreview(announcement, item);
});
}
@ -3634,9 +3758,10 @@ async function loadBetterTodo() {
delay = setTimeout(async () => {
if (listItem.classList.contains("canvasrefined-todo-hover")) {
previewTitle.textContent = item.plannable.title;
// custom assignment
// custom assignment (planner note): preview its description/details
if (customItem) {
previewText.textContent = "Custom assignment";
const details = item.plannable && item.plannable.details ? item.plannable.details : "";
previewText.textContent = details === "" ? "No details given" : details.replace(/<\/?[^>]+(>|$)/g, " ");
} else {
console.log(item);
let found = false;

View File

@ -49,7 +49,7 @@ const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight"];
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_feedback", "tab_icons"];
const exportSidebar = ["better_sidebar", "sidebar_scale"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_full_height", "todo_confetti", "todo_progress_rings", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "hover_preview"];
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted"];
const exportBackground = ["customBackgroundLink", "customBackgroundScale", "customBackgroundDaily", "customBackgroundNasaDaily", "fitImageToScreen", "bg_opacity", "sidebar_opacity", "bg_blur", "sidebar_blur"];
// Master "On/off toggles" = every visual toggle (no GPA, no dark-mode schedule,