mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-20 20:24:52 +02:00
md in notes
This commit is contained in:
parent
2ac702887f
commit
37dc65f488
@ -56,11 +56,18 @@ Canvas Refined adds more with more to come!
|
||||
- Searching themes (the original didn't actually impliment that)
|
||||
- made the dark mode into a css file instead of a reallllllly long string
|
||||
- Card Styles (image size, card roundness, card spacing, width, height, theme compatible)
|
||||
- Custom Background (by URL, theme compatible)
|
||||
- Custom Background
|
||||
- Use presets or add your own by URL
|
||||
- Theme compatible
|
||||
- Daily backgrounds
|
||||
- Popup UI revamp
|
||||
- NEW Better todo list
|
||||
- Better sidebar
|
||||
- Simplified UI
|
||||
- Some options won't show up when not needed
|
||||
- Markdown in dashboard notes
|
||||
- Smaller additions:
|
||||
- Equal height cards for card assignments
|
||||
|
||||
## Planned Features (by priority)
|
||||
- auto rotate theme + theme history + fix theme submissions
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
"Canvas_Refined": {
|
||||
"message": "Canvas Refined"
|
||||
},
|
||||
"search_features": {
|
||||
"message": "Search features..."
|
||||
},
|
||||
"updates": {
|
||||
"message": "Updates"
|
||||
},
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
.canvasrefined-export-output {position: fixed;height:100vh;width:100vw;top:0;left:0;z-index:10000;background:#000000c7;display:flex;align-items:center;justify-content:center}
|
||||
.canvasrefined-export-copy { font-size:12px;white-space: pre-wrap;max-height:80vh;overflow:auto;padding:8px}
|
||||
.canvasrefined-export-output-inner {width: 50%;background:#000;color:#fff;}
|
||||
.canvasrefined-dashboard-notes {width: 100%; box-sizing: border-box; overflow: hidden; resize: none;}
|
||||
.canvasrefined-dashboard-notes {width:100%;box-sizing:border-box;margin:18px 0 14px;border:1px solid var(--bcborders,#c7cdd1);border-radius:4px;overflow:hidden;background:var(--bcbackground-1,#fff);}
|
||||
.canvasrefined-card-container {padding-bottom: 4px;}
|
||||
.canvasrefined-assignment-container {margin: 0;padding:0;color: var(--ic-brand-font-color-dark-lightened-30);display: flex;justify-content:space-between;font-size:14px;align-items: center;transition:.2s all;}
|
||||
.canvasrefined-card-header-container {align-items: center;display: flex;justify-content: space-between;padding: 0 18px;color:var(--ic-brand-font-color-dark-lightened-30);font-size:16px;font-weight:700;margin: 0;}
|
||||
@ -97,7 +97,7 @@
|
||||
.canvasrefined-gpa-courses {min-width:600px;width:max-content;}
|
||||
.canvasrefined-gpa-course-top, .canvasrefined-gpa-course-bottom {display: flex;}
|
||||
.canvasrefined-gpa-header {font-weight:700;font-size:16px;margin-top:0;margin-bottom:6px;padding-bottom:6px;}
|
||||
.canvasrefined-dashboard-notes {width: 100%; box-sizing: border-box; margin-top: 18px;border: 1px solid #c7cdd1;border-radius:3px;padding:8px 14px;resize:vertical}
|
||||
.canvasrefined-notes-editor {box-sizing:border-box;width:100%;min-height:140px;border:none;background:transparent;resize:vertical;outline:none;font-family:inherit;font-size:14px;line-height:1.5;padding:12px 16px;color:var(--bctext-0,var(--ic-brand-font-color-dark-lightened-30,#2d3b45));}
|
||||
.canvasrefined-todosidebar, #canvasrefined-todo-list, #canvasrefined-announcement-list {margin: 0}
|
||||
.canvasrefined-todo-container {display: flex; margin-top: 14px;position: relative;transition: .2s all;}
|
||||
.canvasrefined-removing {max-height: 0;margin-top: 0}
|
||||
@ -176,4 +176,42 @@
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Dashboard notes (Markdown) ===== */
|
||||
.canvasrefined-notes-toolbar {display:flex;flex-wrap:wrap;gap:2px;padding:6px 8px;align-items:center;background:var(--bcbackground-2,#f5f7f8);border-bottom:1px solid var(--bcborders,#c7cdd1);}
|
||||
.cr-fmt {border:1px solid transparent;background:transparent;color:var(--bctext-1,#5b6770);font:inherit;font-size:13px;line-height:1;padding:5px 7px;border-radius:4px;cursor:pointer;}
|
||||
.cr-fmt:hover {background:var(--bcbackground-1,rgba(0,0,0,.06));color:var(--bctext-0,#2d3b45);}
|
||||
.cr-fmt:active {background:var(--bcbackground-1,rgba(0,0,0,.12));}
|
||||
.cr-fmt code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;}
|
||||
.cr-fmt-sep {width:1px;height:18px;align-self:center;background:var(--bcborders,#c7cdd1);margin:0 3px;}
|
||||
.canvasrefined-notes-surface {position:relative;background:var(--bcbackground-1,#fff);}
|
||||
.canvasrefined-notes-rendered {box-sizing:border-box;width:100%;min-height:140px;padding:12px 16px;font-size:14px;line-height:1.5;color:var(--bctext-0,var(--ic-brand-font-color-dark-lightened-30,#2d3b45));overflow-wrap:break-word;cursor:text;outline:none;border-radius:0 0 4px 4px;}
|
||||
.canvasrefined-notes-rendered:hover {background:var(--bcbackground-2,rgba(0,0,0,.02));}
|
||||
.canvasrefined-notes-rendered:empty::before {content:"Click to add a note…";color:var(--bctext-2,#9aa3ab);font-style:italic;}
|
||||
.canvasrefined-notes-rendered > :first-child {margin-top:0;}
|
||||
.canvasrefined-notes-rendered > :last-child {margin-bottom:0;}
|
||||
.canvasrefined-notes-rendered h1,.canvasrefined-notes-rendered h2,.canvasrefined-notes-rendered h3,.canvasrefined-notes-rendered h4,.canvasrefined-notes-rendered h5,.canvasrefined-notes-rendered h6 {margin:14px 0 6px;font-weight:600;line-height:1.3;color:var(--bctext-0,#1f2d35);}
|
||||
.canvasrefined-notes-rendered h1 {font-size:1.5em;}
|
||||
.canvasrefined-notes-rendered h2 {font-size:1.3em;}
|
||||
.canvasrefined-notes-rendered h3 {font-size:1.15em;}
|
||||
.canvasrefined-notes-rendered h4 {font-size:1em;}
|
||||
.canvasrefined-notes-rendered p {margin:8px 0;}
|
||||
.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.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 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 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 code {background:transparent;padding:0;}
|
||||
.canvasrefined-notes-rendered a {color:var(--bclinks,var(--ic-link-color,#0072b5));text-decoration:underline;}
|
||||
.canvasrefined-notes-rendered a:hover {text-decoration:none;}
|
||||
.canvasrefined-notes-rendered img {max-width:100%;height:auto;border-radius:3px;}
|
||||
.canvasrefined-notes-rendered hr {border:none;border-top:1px solid var(--bcborders,#c7cdd1);margin:14px 0;}
|
||||
.canvasrefined-notes-rendered strong {font-weight:600;}
|
||||
.canvasrefined-notes-rendered del {color:var(--bctext-2,#9aa3ab);}
|
||||
.canvasrefined-notes-editor::placeholder {color:var(--bctext-2,#9aa3ab);}
|
||||
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-toolbar {display:none;}
|
||||
.canvasrefined-dashboard-notes.is-editing .canvasrefined-notes-rendered {display:none;}
|
||||
.canvasrefined-dashboard-notes:not(.is-editing) .canvasrefined-notes-editor {display:none;}
|
||||
@ -252,4 +252,21 @@ input[type="checkbox"]:checked {background: #8dd28d;}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Header feature search ===== */
|
||||
.header-search { position: relative; margin-left: auto; display: flex; align-items: center; gap: 8px; width: 170px; max-width: 42%; background: var(--inputbg); border-radius: 8px; padding: 7px 10px; box-sizing: border-box; }
|
||||
.header-search-icon { color: #9a9a9a; flex: none; }
|
||||
.header-search:focus-within { box-shadow: 0 0 0 1.5px #56Caf0; }
|
||||
#feature-search { flex: 1; min-width: 0; background: none; border: none; outline: none; color: #e2e2e2; font-family: inherit; font-size: 13px; font-weight: 600; }
|
||||
#feature-search::placeholder { color: #8d8d8d; font-weight: 500; }
|
||||
.search-results { position: absolute; top: calc(100% + 6px); right: 0; min-width: 240px; max-width: 320px; max-height: 320px; overflow-y: auto; background: #0e0e0ee0; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); border-radius: 8px; padding: 5px; z-index: 1000; display: none; box-shadow: 0 8px 24px rgba(0,0,0,.45); }
|
||||
.search-results.open { display: block; }
|
||||
.search-result { display: flex; flex-direction: column; gap: 1px; padding: 8px 10px; border-radius: 6px; cursor: pointer; }
|
||||
.search-result.active, .search-result:hover { background: #ffffff14; }
|
||||
.search-result-title { font-size: 13px; font-weight: 600; color: #f0f0f0; }
|
||||
.search-result-category { font-size: 11px; color: #9a9a9a; }
|
||||
.search-result.empty { text-align: center; color: #8d8d8d; font-size: 12px; cursor: default; padding: 12px; }
|
||||
.search-result.empty:hover { background: none; }
|
||||
.search-highlight { outline: 2px solid #56Caf0; outline-offset: 2px; border-radius: 8px; animation: search-pulse 1.8s ease-out; }
|
||||
@keyframes search-pulse { 0% { box-shadow: 0 0 0 0 #56Caf099; } 40% { box-shadow: 0 0 0 6px #56Caf000; } 100% { box-shadow: 0 0 0 0 #56Caf000; } }
|
||||
@ -24,6 +24,11 @@
|
||||
<div class="header">
|
||||
<img id="bclogo" src="../icon/icon-128.png">
|
||||
<h1 data-i18n="Canvas_Refined">Canvas Refined</h1>
|
||||
<div class="header-search" id="header-search">
|
||||
<svg class="header-search-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" /><path d="M21 21l-6 -6" /></svg>
|
||||
<input type="text" id="feature-search" data-i18n-placeholder="search_features" placeholder="Search features..." autocomplete="off" spellcheck="false">
|
||||
<div class="search-results" id="feature-search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="more-options-container">
|
||||
<button id="customize-dark-btn" class="big-button tab-btn">
|
||||
|
||||
@ -37,6 +37,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
"assignment_date_format": false,
|
||||
"dashboard_notes": false,
|
||||
"dashboard_notes_text": "",
|
||||
"dashboard_notes_mode": "edit",
|
||||
"better_todo": false,
|
||||
"todo_hr24": false,
|
||||
"todo_separate_scrollbar": false,
|
||||
|
||||
286
js/content.js
286
js/content.js
@ -657,6 +657,8 @@ function applyOptionsChanges(changes) {
|
||||
changeGradientCards();
|
||||
break;
|
||||
case "dashboard_notes":
|
||||
case "dashboard_notes_text":
|
||||
case "dashboard_notes_mode":
|
||||
loadDashboardNotes();
|
||||
break;
|
||||
case "dashboard_grades":
|
||||
@ -4133,7 +4135,6 @@ Dashboard notes
|
||||
*/
|
||||
|
||||
let dashboardNotesTimer;
|
||||
let dashboardNotesResizeFrame;
|
||||
function delayDashboardNotesStorage(text) {
|
||||
clearTimeout(dashboardNotesTimer);
|
||||
dashboardNotesTimer = setTimeout(() => {
|
||||
@ -4141,39 +4142,278 @@ function delayDashboardNotesStorage(text) {
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function resizeDashboardNotes(notes) {
|
||||
if (!notes) return;
|
||||
notes.style.height = "auto";
|
||||
notes.style.height = `${notes.scrollHeight + 5}px`;
|
||||
/*
|
||||
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) {
|
||||
if (src == null) return "";
|
||||
const escapeHtml = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
const sanitizeUrl = (u) => {
|
||||
const v = String(u == null ? "" : u).trim();
|
||||
if (!v) return "";
|
||||
if (/^(https?:|mailto:|ftp:|tel:)/i.test(v)) return v;
|
||||
if (/^(javascript:|vbscript:|file:|data:)/i.test(v)) return "#";
|
||||
if (/^[#/?]/.test(v)) return v;
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return "#";
|
||||
return v;
|
||||
};
|
||||
let text = String(src).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
const out = [];
|
||||
const lines = text.split("\n");
|
||||
let i = 0;
|
||||
const inline = (t) => {
|
||||
let h = escapeHtml(t);
|
||||
h = h.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, url, title) =>
|
||||
`<img src="${sanitizeUrl(url)}" alt="${alt}"${title ? ` title="${title}"` : ""}>`);
|
||||
h = h.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, txt, url, title) =>
|
||||
`<a href="${sanitizeUrl(url)}" target="_blank" rel="noopener noreferrer"${title ? ` title="${title}"` : ""}>${txt}</a>`);
|
||||
h = h.replace(/`([^`\n]+)`/g, (m, c) => `<code>${c}</code>`);
|
||||
h = h.replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>");
|
||||
h = h.replace(/~~([^~]+?)~~/g, "<del>$1</del>");
|
||||
h = h.replace(/(^|[^*])\*([^*]+?)\*(?!\*)/g, "$1<em>$2</em>");
|
||||
return h;
|
||||
};
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (/^\s*$/.test(line)) { i++; continue; }
|
||||
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
||||
if (h) { const l = h[1].length; out.push(`<h${l}>${inline(h[2])}</h${l}>`); i++; continue; }
|
||||
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { out.push("<hr>"); i++; continue; }
|
||||
if (/^>\s?/.test(line)) {
|
||||
const q = []; while (i < lines.length && /^>\s?/.test(lines[i])) { q.push(inline(lines[i].replace(/^>\s?/, ""))); i++; }
|
||||
out.push(`<blockquote>${q.join("<br>")}</blockquote>`); continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 para = [line]; i++; while (i < lines.length && !/^\s*$/.test(lines[i]) && !/^(#{1,6}\s|\s*[-*+]\s|\s*\d+\.\s|>)/.test(lines[i])) { para.push(lines[i]); i++; }
|
||||
out.push(`<p>${para.map(inline).join("<br>")}</p>`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function scheduleDashboardNotesResize(notes) {
|
||||
if (dashboardNotesResizeFrame) cancelAnimationFrame(dashboardNotesResizeFrame);
|
||||
dashboardNotesResizeFrame = requestAnimationFrame(() => {
|
||||
dashboardNotesResizeFrame = null;
|
||||
resizeDashboardNotes(notes);
|
||||
function renderDashboardNotesPreview(preview, text) {
|
||||
if (!preview) return;
|
||||
// Skip identical re-renders: writing innerHTML is a childList mutation that the
|
||||
// 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;
|
||||
preview._crLastText = text;
|
||||
const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback;
|
||||
preview.innerHTML = renderer(text);
|
||||
}
|
||||
|
||||
/*
|
||||
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) {
|
||||
if (!editor) return;
|
||||
const start = editor.selectionStart;
|
||||
const end = editor.selectionEnd;
|
||||
const value = editor.value;
|
||||
const fire = () => {
|
||||
editor.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
editor.focus();
|
||||
};
|
||||
|
||||
const wrap = (before, after, placeholder) => {
|
||||
const had = end > start;
|
||||
const sel = had ? value.slice(start, end) : (placeholder || "");
|
||||
editor.setRangeText(before + sel + after, start, end, "end");
|
||||
editor.selectionStart = start + before.length;
|
||||
editor.selectionEnd = start + before.length + sel.length;
|
||||
fire();
|
||||
};
|
||||
|
||||
// Range covering every line touched by the selection.
|
||||
const lineBlock = () => {
|
||||
const ls = start === 0 ? 0 : value.lastIndexOf("\n", start - 1) + 1;
|
||||
let le = value.indexOf("\n", end);
|
||||
if (le === -1) le = value.length;
|
||||
return { ls, le, block: value.slice(ls, le) };
|
||||
};
|
||||
|
||||
const togglePrefix = (prefix) => {
|
||||
const { ls, le, block } = lineBlock();
|
||||
const lines = block.split("\n");
|
||||
const allHave = lines.every((l) => l.startsWith(prefix));
|
||||
const newBlock = lines.map((l) => allHave ? l.slice(prefix.length) : prefix + l).join("\n");
|
||||
editor.setRangeText(newBlock, ls, le, "end");
|
||||
editor.selectionStart = ls;
|
||||
editor.selectionEnd = ls + newBlock.length;
|
||||
fire();
|
||||
};
|
||||
|
||||
switch (action) {
|
||||
case "bold": wrap("**", "**", "bold"); break;
|
||||
case "italic": wrap("*", "*", "italic"); break;
|
||||
case "strike": wrap("~~", "~~", "strikethrough"); break;
|
||||
case "code": wrap("`", "`", "code"); break;
|
||||
case "h1": togglePrefix("# "); break;
|
||||
case "h2": togglePrefix("## "); break;
|
||||
case "list": togglePrefix("- "); break;
|
||||
case "numbered": togglePrefix("1. "); break;
|
||||
case "quote": togglePrefix("> "); break;
|
||||
case "task": {
|
||||
const { ls, le, block } = lineBlock();
|
||||
const marker = block.match(/^-\s*\[([ xX])\]\s+/);
|
||||
const bullet = block.match(/^[-*+]\s+/);
|
||||
let newBlock;
|
||||
if (marker) {
|
||||
newBlock = block.slice(marker[0].length);
|
||||
} else if (bullet) {
|
||||
newBlock = "- [ ] " + block.slice(bullet[0].length);
|
||||
} else {
|
||||
newBlock = "- [ ] " + block;
|
||||
}
|
||||
editor.setRangeText(newBlock, ls, le, "end");
|
||||
editor.selectionStart = ls;
|
||||
editor.selectionEnd = ls + newBlock.length;
|
||||
fire();
|
||||
break;
|
||||
}
|
||||
case "link": {
|
||||
const had = end > start;
|
||||
const sel = had ? value.slice(start, end) : "text";
|
||||
editor.setRangeText("[" + sel + "](url)", start, end, "end");
|
||||
const urlStart = start + 1 + sel.length + 2; // after "]("
|
||||
editor.selectionStart = urlStart;
|
||||
editor.selectionEnd = urlStart + 3; // select "url"
|
||||
fire();
|
||||
break;
|
||||
}
|
||||
case "hr": {
|
||||
const lead = start > 0 && value[start - 1] !== "\n" ? "\n" : "";
|
||||
editor.setRangeText(lead + "---\n", start, start, "end");
|
||||
fire();
|
||||
break;
|
||||
}
|
||||
case "codeblock": {
|
||||
const had = end > start;
|
||||
const sel = had ? value.slice(start, end) : "code";
|
||||
const lead = start > 0 && value[start - 1] !== "\n" ? "\n" : "";
|
||||
editor.setRangeText(lead + "```\n" + sel + "\n```", start, end, "end");
|
||||
fire();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DASHBOARD_NOTES_HTML = `
|
||||
<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="italic" title="Italic (Ctrl/Cmd+I)"><em>I</em></button>
|
||||
<button type="button" class="cr-fmt" data-action="strike" title="Strikethrough"><s>S</s></button>
|
||||
<button type="button" class="cr-fmt" data-action="code" title="Inline code"><code></></code></button>
|
||||
<span class="cr-fmt-sep"></span>
|
||||
<button type="button" class="cr-fmt" data-action="h1" title="Heading 1">H1</button>
|
||||
<button type="button" class="cr-fmt" data-action="h2" title="Heading 2">H2</button>
|
||||
<span class="cr-fmt-sep"></span>
|
||||
<button type="button" class="cr-fmt" data-action="list" title="Bullet list">•</button>
|
||||
<button type="button" class="cr-fmt" data-action="numbered" title="Numbered list">1.</button>
|
||||
<button type="button" class="cr-fmt" data-action="task" title="Task list">☐</button>
|
||||
<button type="button" class="cr-fmt" data-action="quote" title="Quote">“</button>
|
||||
<span class="cr-fmt-sep"></span>
|
||||
<button type="button" class="cr-fmt" data-action="link" title="Insert link">Link</button>
|
||||
<button type="button" class="cr-fmt" data-action="hr" title="Horizontal rule">—</button>
|
||||
<button type="button" class="cr-fmt" data-action="codeblock" title="Code block">```</button>
|
||||
</div>
|
||||
<div class="canvasrefined-notes-surface">
|
||||
<div class="canvasrefined-notes-rendered" tabindex="0" aria-label="Dashboard notes — click to edit" title="Click to edit"></div>
|
||||
<textarea class="canvasrefined-notes-editor" placeholder="Type Markdown — click away to render" spellcheck="false"></textarea>
|
||||
</div>
|
||||
`;
|
||||
|
||||
function wireDashboardNotes(notes) {
|
||||
const editor = notes.querySelector(".canvasrefined-notes-editor");
|
||||
const rendered = notes.querySelector(".canvasrefined-notes-rendered");
|
||||
editor.value = options.dashboard_notes_text || "";
|
||||
renderDashboardNotesPreview(rendered, editor.value);
|
||||
|
||||
const enterEdit = () => {
|
||||
if (notes.classList.contains("is-editing")) return;
|
||||
notes.classList.add("is-editing");
|
||||
editor.focus();
|
||||
const len = editor.value.length;
|
||||
editor.setSelectionRange(len, len);
|
||||
};
|
||||
const exitEdit = () => {
|
||||
notes.classList.remove("is-editing");
|
||||
renderDashboardNotesPreview(rendered, editor.value);
|
||||
};
|
||||
|
||||
rendered.addEventListener("click", enterEdit);
|
||||
rendered.addEventListener("focus", enterEdit);
|
||||
editor.addEventListener("blur", exitEdit);
|
||||
editor.addEventListener("input", function () {
|
||||
options.dashboard_notes_text = this.value;
|
||||
delayDashboardNotesStorage(this.value);
|
||||
});
|
||||
|
||||
// Toolbar buttons: keep focus in the editor (mousedown preventDefault stops the
|
||||
// button from stealing focus and collapsing back to the rendered view), then apply
|
||||
// the formatting. Enters edit mode first if the user formats from the rendered view.
|
||||
notes.querySelectorAll(".cr-fmt").forEach(btn => {
|
||||
btn.addEventListener("mousedown", e => e.preventDefault());
|
||||
btn.addEventListener("click", () => {
|
||||
if (!notes.classList.contains("is-editing")) enterEdit();
|
||||
notesApplyFormat(editor, btn.dataset.action);
|
||||
});
|
||||
});
|
||||
|
||||
editor.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") { e.preventDefault(); editor.blur(); return; } // Esc: render
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
if (!mod) return;
|
||||
const k = e.key.toLowerCase();
|
||||
if (k === "b") { e.preventDefault(); notesApplyFormat(editor, "bold"); }
|
||||
else if (k === "i") { e.preventDefault(); notesApplyFormat(editor, "italic"); }
|
||||
else if (k === "enter") { e.preventDefault(); editor.blur(); } // Ctrl/Cmd+Enter: render
|
||||
});
|
||||
}
|
||||
|
||||
function loadDashboardNotes() {
|
||||
const container = document.querySelector("#DashboardCard_Container");
|
||||
if (options.dashboard_notes === true) {
|
||||
if (!container) return;
|
||||
let notes = document.querySelector('.canvasrefined-dashboard-notes');
|
||||
// Rebuild older (split edit/preview) markup into the new single-surface layout.
|
||||
if (notes && !notes.querySelector(".canvasrefined-notes-surface")) {
|
||||
notes.remove();
|
||||
notes = null;
|
||||
}
|
||||
if (!notes) {
|
||||
notes = document.createElement("textarea");
|
||||
notes = document.createElement("div");
|
||||
notes.classList.add("canvasrefined-dashboard-notes");
|
||||
notes.placeholder = "Enter notes here";
|
||||
document.querySelector("#DashboardCard_Container").prepend(notes);
|
||||
notes.value = options.dashboard_notes_text;
|
||||
notes.style.display = "block";
|
||||
resizeDashboardNotes(notes);
|
||||
notes.addEventListener('input', function () {
|
||||
options.dashboard_notes_text = this.value;
|
||||
delayDashboardNotesStorage(this.value);
|
||||
scheduleDashboardNotesResize(this);
|
||||
});
|
||||
notes.innerHTML = DASHBOARD_NOTES_HTML;
|
||||
// Mount as a full-width sibling above the card grid. Prepending inside the
|
||||
// DashboardCard_Container makes the notes a masonry/grid cell (narrow & broken).
|
||||
const parent = container.parentNode;
|
||||
if (parent) parent.insertBefore(notes, container);
|
||||
else container.prepend(notes);
|
||||
wireDashboardNotes(notes);
|
||||
} else {
|
||||
notes.style.display = "block";
|
||||
resizeDashboardNotes(notes);
|
||||
notes.style.display = "";
|
||||
const editor = notes.querySelector(".canvasrefined-notes-editor");
|
||||
const rendered = notes.querySelector(".canvasrefined-notes-rendered");
|
||||
// While editing, the textarea is the source of truth: don't clobber it from
|
||||
// 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 (editor && editor.value !== (options.dashboard_notes_text || "")) {
|
||||
editor.value = options.dashboard_notes_text || "";
|
||||
}
|
||||
renderDashboardNotesPreview(rendered, editor ? editor.value : "");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let notes = document.querySelector('.canvasrefined-dashboard-notes');
|
||||
|
||||
203
js/markdown.js
Normal file
203
js/markdown.js
Normal file
@ -0,0 +1,203 @@
|
||||
/*
|
||||
CanvasRefined - lightweight Markdown renderer for dashboard notes.
|
||||
|
||||
Renders a small, notes-friendly subset of Markdown to HTML:
|
||||
headings, bold, italic, strikethrough, inline code, fenced code blocks,
|
||||
links, images, autolinks, blockquotes, ordered/unordered lists, task
|
||||
lists, horizontal rules, and paragraphs.
|
||||
|
||||
All user-supplied text is HTML-escaped before any formatting is applied,
|
||||
and links/images are URL-sanitized, so the returned HTML is safe to assign
|
||||
via innerHTML. Exposes window.renderMarkdown(text) -> html.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function sanitizeUrl(url) {
|
||||
const u = String(url == null ? "" : url).trim();
|
||||
if (u === "") return "";
|
||||
if (/^(https?:|mailto:|ftp:|tel:)/i.test(u)) return u;
|
||||
if (/^(javascript:|vbscript:|file:|data:)/i.test(u)) return "#";
|
||||
if (/^[#/?]/.test(u)) return u;
|
||||
// Block any other explicit protocol we did not whitelist.
|
||||
if (/^[a-z][a-z0-9+.\-]*:/i.test(u)) return "#";
|
||||
return u;
|
||||
}
|
||||
|
||||
function renderMarkdown(src) {
|
||||
if (src == null) return "";
|
||||
src = String(src).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||
|
||||
// Stash protected HTML (code blocks/spans, autolinks) behind NUL-delimited
|
||||
// tokens so they survive escaping and inline formatting untouched.
|
||||
const stash = [];
|
||||
const keep = (html) => {
|
||||
stash.push(html);
|
||||
return "\u0000" + (stash.length - 1) + "\u0000";
|
||||
};
|
||||
|
||||
// 1. Fenced code blocks: ```lang\n code ```
|
||||
src = src.replace(/```([\w-]*)\n?([\s\S]*?)```/g, (m, lang, code) => {
|
||||
code = code.replace(/^\n/, "").replace(/\n$/, "");
|
||||
const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : "";
|
||||
return keep(`<pre><code${langAttr}>${escapeHtml(code)}</code></pre>`);
|
||||
});
|
||||
|
||||
// 2. Inline code: `code`
|
||||
src = src.replace(/`([^`\n]+)`/g, (m, code) => keep(`<code>${escapeHtml(code)}</code>`));
|
||||
|
||||
// 3. Autolinks: <https://example.com>
|
||||
src = src.replace(/<(https?:\/\/[^\s<>]+)>/g, (m, url) => {
|
||||
const safe = escapeHtml(sanitizeUrl(url));
|
||||
return keep(`<a href="${safe}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a>`);
|
||||
});
|
||||
|
||||
// 4. Inline formatting, applied to escaped text so nothing the user
|
||||
// typed can become live HTML. The URL capture allows one level of
|
||||
// balanced parentheses, e.g. https://en.wikipedia.org/wiki/Foo_(bar).
|
||||
const inline = (text) => {
|
||||
let t = escapeHtml(text);
|
||||
// images:  or 
|
||||
t = t.replace(/!\[([^\]]*)\]\(((?:[^()\s]|\([^)\s]*\))*)(?:\s+"([^"]*)")?\)/g,
|
||||
(m, alt, url, title) => {
|
||||
const tAttr = title ? ` title="${title}"` : "";
|
||||
return `<img src="${sanitizeUrl(url)}" alt="${alt}"${tAttr}>`;
|
||||
});
|
||||
// links: [text](url) or [text](url "title")
|
||||
t = t.replace(/\[([^\]]+)\]\(((?:[^()\s]|\([^)\s]*\))*)(?:\s+"([^"]*)")?\)/g,
|
||||
(m, txt, url, title) => {
|
||||
const tAttr = title ? ` title="${title}"` : "";
|
||||
return `<a href="${sanitizeUrl(url)}" target="_blank" rel="noopener noreferrer"${tAttr}>${txt}</a>`;
|
||||
});
|
||||
// bold: **text** or __text__
|
||||
t = t.replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>");
|
||||
t = t.replace(/__([^_]+?)__/g, "<strong>$1</strong>");
|
||||
// strikethrough: ~~text~~
|
||||
t = t.replace(/~~([^~]+?)~~/g, "<del>$1</del>");
|
||||
// italic: *text* (avoid *** by requiring non-star borders)
|
||||
t = t.replace(/(^|[^*])\*([^*]+?)\*(?!\*)/g, "$1<em>$2</em>");
|
||||
// italic: _text_ (skip word-internal underscores like file_name)
|
||||
t = t.replace(/(^|[^_\w])_([^_]+?)_(?!\w)/g, "$1<em>$2</em>");
|
||||
return t;
|
||||
};
|
||||
|
||||
const lines = src.split("\n");
|
||||
const out = [];
|
||||
const n = lines.length;
|
||||
let i = 0;
|
||||
const isToken = (s) => /^\u0000\d+\u0000$/.test(s);
|
||||
|
||||
while (i < n) {
|
||||
const line = lines[i];
|
||||
|
||||
// Blank line separates blocks.
|
||||
if (/^\s*$/.test(line)) { i++; continue; }
|
||||
|
||||
// A stashed block (e.g. a fenced code block) sitting on its own line.
|
||||
if (isToken(line)) { out.push(line); i++; continue; }
|
||||
|
||||
// ATX heading: # Title (optional closing hashes)
|
||||
const h = line.match(/^(#{1,6})\s+(.*?)(?:\s+#{1,6})?$/);
|
||||
if (h) {
|
||||
const lvl = h[1].length;
|
||||
out.push("<h" + lvl + ">" + inline(h[2]) + "</h" + lvl + ">");
|
||||
i++; continue;
|
||||
}
|
||||
|
||||
// Horizontal rule: --- / *** / ___
|
||||
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
||||
out.push("<hr>");
|
||||
i++; continue;
|
||||
}
|
||||
|
||||
// Blockquote: > ...
|
||||
if (/^>{1}\s?/.test(line)) {
|
||||
const quote = [];
|
||||
while (i < n && /^>{1}\s?/.test(lines[i])) {
|
||||
quote.push(inline(lines[i].replace(/^>{1}\s?/, "")));
|
||||
i++;
|
||||
}
|
||||
out.push("<blockquote>" + quote.join("<br>") + "</blockquote>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered list: - / * / + (supports task lists: - [ ] / - [x])
|
||||
if (/^\s*[-*+]\s+/.test(line)) {
|
||||
const items = [];
|
||||
while (i < n) {
|
||||
const m = lines[i].match(/^\s*[-*+]\s+(.*)$/);
|
||||
if (m) {
|
||||
const task = m[1].match(/^\[([ xX])\]\s+(.*)$/);
|
||||
if (task) {
|
||||
const checked = /x/i.test(task[1]);
|
||||
items.push(
|
||||
'<li class="cr-task"><input type="checkbox" disabled' +
|
||||
(checked ? " checked" : "") + "> " + inline(task[2])
|
||||
);
|
||||
} else {
|
||||
items.push("<li>" + inline(m[1]));
|
||||
}
|
||||
i++; continue;
|
||||
}
|
||||
// Lazy continuation (indented) of the previous item.
|
||||
if (/^\s+\S/.test(lines[i]) && items.length) {
|
||||
items[items.length - 1] += "<br>" + inline(lines[i].trim());
|
||||
i++; continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
out.push("<ul>" + items.map((li) => li + "</li>").join("") + "</ul>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered list: 1.
|
||||
if (/^\s*\d+\.\s+/.test(line)) {
|
||||
const items = [];
|
||||
while (i < n) {
|
||||
const m = lines[i].match(/^\s*\d+\.\s+(.*)$/);
|
||||
if (m) { items.push("<li>" + inline(m[1])); i++; continue; }
|
||||
if (/^\s+\S/.test(lines[i]) && items.length) {
|
||||
items[items.length - 1] += "<br>" + inline(lines[i].trim());
|
||||
i++; continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
out.push("<ol>" + items.map((li) => li + "</li>").join("") + "</ol>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Paragraph: gather consecutive lines until a block boundary.
|
||||
const para = [line];
|
||||
i++;
|
||||
while (i < n) {
|
||||
const l = lines[i];
|
||||
if (/^\s*$/.test(l)) break;
|
||||
if (isToken(l)) break;
|
||||
if (/^#{1,6}\s+/.test(l)) break;
|
||||
if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(l)) break;
|
||||
if (/^>{1}\s?/.test(l)) break;
|
||||
if (/^\s*[-*+]\s+/.test(l)) break;
|
||||
if (/^\s*\d+\.\s+/.test(l)) break;
|
||||
para.push(l);
|
||||
i++;
|
||||
}
|
||||
out.push("<p>" + para.map(inline).join("<br>") + "</p>");
|
||||
}
|
||||
|
||||
let html = out.join("\n");
|
||||
// Restore stashed HTML.
|
||||
html = html.replace(/\u0000(\d+)\u0000/g, (m, idx) => stash[+idx] || "");
|
||||
return html;
|
||||
}
|
||||
|
||||
window.renderMarkdown = renderMarkdown;
|
||||
})();
|
||||
301
js/popup.js
301
js/popup.js
@ -77,6 +77,7 @@ const defaultOptions = {
|
||||
"assignment_date_format": false,
|
||||
"dashboard_notes": false,
|
||||
"dashboard_notes_text": "",
|
||||
"dashboard_notes_mode": "edit",
|
||||
"better_todo": false,
|
||||
"better_sidebar": false,
|
||||
"sidebar_scale": 100,
|
||||
@ -433,6 +434,299 @@ function toggleSubOptionsVisibility(option, isOn) {
|
||||
}
|
||||
}
|
||||
|
||||
function setupFeatureSearch(menu) {
|
||||
const searchInput = document.querySelector("#feature-search");
|
||||
const resultsEl = document.querySelector("#feature-search-results");
|
||||
const headerSearch = document.querySelector("#header-search");
|
||||
if (!searchInput || !resultsEl || !headerSearch) return;
|
||||
|
||||
// Map (reference-keyed) each .tab element -> the button id that opens it.
|
||||
// A plain object won't work: DOM elements stringify to the same key.
|
||||
const tabElToBtnId = new Map();
|
||||
Object.entries(menu.tabs).forEach(([btnId, info]) => {
|
||||
const tabEl = document.querySelector(info.tab);
|
||||
if (tabEl) tabElToBtnId.set(tabEl, btnId);
|
||||
});
|
||||
|
||||
function shouldSkip(el) {
|
||||
// Skip overlays / scrapped containers that aren't real features
|
||||
if (el.closest('#submit-popup, #browser-settings-popup, #opt-in, #card-edit-menu')) return true;
|
||||
// Skip anything inside a statically-hidden option-container
|
||||
// (e.g. the scrapped "remind" block, "Submit your theme").
|
||||
// Dynamically-hidden sub-options (whose parent toggle is off) are kept.
|
||||
let node = el;
|
||||
while (node && node !== document.body) {
|
||||
if (node.classList && node.classList.contains('option-container') && node.style.display === 'none') return true;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function labelOf(sub) {
|
||||
const label = sub.querySelector("label");
|
||||
if (label) return label.textContent.trim();
|
||||
const st = sub.querySelector(".sub-text");
|
||||
if (st) return st.textContent.trim();
|
||||
return "";
|
||||
}
|
||||
|
||||
function keyIdOf(sub) {
|
||||
const label = sub.querySelector("label");
|
||||
if (label && label.getAttribute("for")) return label.getAttribute("for");
|
||||
const input = sub.querySelector("input");
|
||||
if (input && input.id) return input.id;
|
||||
return labelOf(sub);
|
||||
}
|
||||
|
||||
function categoryFor(el) {
|
||||
const tabEl = el.closest(".tab");
|
||||
if (tabEl) {
|
||||
const btnId = tabElToBtnId.get(tabEl);
|
||||
if (btnId) {
|
||||
const btn = document.getElementById(btnId);
|
||||
const span = btn && btn.querySelector("span");
|
||||
if (span) return span.textContent.trim();
|
||||
}
|
||||
return "Section";
|
||||
}
|
||||
if (el.classList && el.classList.contains("tab-btn")) return "Section";
|
||||
if (el.classList && el.classList.contains("option")) return "Settings";
|
||||
const owner = el.closest(".option");
|
||||
if (owner) {
|
||||
const name = owner.querySelector(".option-name");
|
||||
if (name) return name.textContent.trim();
|
||||
}
|
||||
return "Settings";
|
||||
}
|
||||
|
||||
function openTab(btnId) {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (btn) btn.click();
|
||||
}
|
||||
|
||||
function showMain() {
|
||||
const back = document.querySelector(".back-btn");
|
||||
if (back) back.click();
|
||||
}
|
||||
|
||||
// Reveal any hidden sub-option blocks between el and .main so the target
|
||||
// is visible (purely visual; doesn't change stored settings).
|
||||
function revealAncestors(el) {
|
||||
const main = document.querySelector(".main");
|
||||
let node = el;
|
||||
while (node && node !== main && node !== document.body) {
|
||||
if (node.style && node.style.display === "none") node.style.display = "";
|
||||
node = node.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
function highlight(el) {
|
||||
el.classList.add("search-highlight");
|
||||
setTimeout(() => el.classList.remove("search-highlight"), 2000);
|
||||
}
|
||||
|
||||
function goToMainElement(el) {
|
||||
showMain();
|
||||
revealAncestors(el);
|
||||
requestAnimationFrame(() => {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
highlight(el);
|
||||
});
|
||||
}
|
||||
|
||||
function goToTabElement(el) {
|
||||
const tabEl = el.closest(".tab");
|
||||
const btnId = tabElToBtnId.get(tabEl);
|
||||
if (btnId) openTab(btnId);
|
||||
requestAnimationFrame(() => {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
highlight(el);
|
||||
});
|
||||
}
|
||||
|
||||
let featureIndex = null;
|
||||
let currentMatches = [];
|
||||
let activeIndex = -1;
|
||||
|
||||
function buildIndex() {
|
||||
const index = [];
|
||||
const seen = new Set();
|
||||
function add(entry) {
|
||||
if (!entry.text || seen.has(entry.key)) return;
|
||||
seen.add(entry.key);
|
||||
index.push(entry);
|
||||
}
|
||||
|
||||
// Big section buttons (Edit Dark Mode, Cards, Themes, Styles, GPA Settings, Report issue)
|
||||
document.querySelectorAll(".more-options-container .tab-btn").forEach(btn => {
|
||||
const span = btn.querySelector("span");
|
||||
add({ key: "tab:" + btn.id, text: span ? span.textContent.trim() : "", el: btn, action: () => openTab(btn.id) });
|
||||
});
|
||||
|
||||
// Home: option toggles
|
||||
document.querySelectorAll(".options .option").forEach(opt => {
|
||||
if (!opt.id) return;
|
||||
const name = opt.querySelector(".option-name");
|
||||
if (!name) return;
|
||||
if (shouldSkip(opt)) return;
|
||||
add({ key: "option:" + opt.id, text: name.textContent.trim(), el: opt, action: () => goToMainElement(opt) });
|
||||
});
|
||||
|
||||
// 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 => {
|
||||
if (shouldSkip(sub)) return;
|
||||
// Skip statically-hidden sub-options (e.g. the "hover preview" TODO).
|
||||
// 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;
|
||||
const text = labelOf(sub);
|
||||
if (!text) return;
|
||||
add({ key: "sub:" + keyIdOf(sub), text, el: sub, action: () => goToMainElement(sub) });
|
||||
});
|
||||
|
||||
// Home: custom Canvas URL
|
||||
const customDomain = document.querySelector("#customDomain");
|
||||
if (customDomain && !shouldSkip(customDomain)) {
|
||||
const wrap = customDomain.closest(".customDomain");
|
||||
const label = wrap ? wrap.querySelector("[data-i18n='enter_url']") : null;
|
||||
add({ key: "custom:customDomain", text: label ? label.textContent.trim() : "Canvas URL", el: wrap || customDomain, action: () => goToMainElement(wrap || customDomain) });
|
||||
}
|
||||
|
||||
// Tabs: section headings (e.g. "Presets", "Custom styles", "Popular Palettes", "Custom Background")
|
||||
document.querySelectorAll(".tab .header-small").forEach(h => {
|
||||
if (shouldSkip(h)) return;
|
||||
const text = h.textContent.trim();
|
||||
if (!text) return;
|
||||
const btnId = tabElToBtnId.get(h.closest(".tab"));
|
||||
add({ key: "heading:" + (btnId || "?") + ":" + text, text, el: h, action: () => goToTabElement(h) });
|
||||
});
|
||||
|
||||
// Tabs: checkbox sub-options (e.g. "Daily Random Image", "Custom Card Styles")
|
||||
document.querySelectorAll(".tab .sub-option").forEach(sub => {
|
||||
if (shouldSkip(sub)) return;
|
||||
const text = labelOf(sub);
|
||||
if (!text) return;
|
||||
const btnId = tabElToBtnId.get(sub.closest(".tab"));
|
||||
add({ key: "tabsub:" + (btnId || "?") + ":" + keyIdOf(sub), text, el: sub, action: () => goToTabElement(sub) });
|
||||
});
|
||||
|
||||
// Dark mode color fields (e.g. "Sidebar Text", "Background Main")
|
||||
document.querySelectorAll(".tab .color-type-header").forEach(h => {
|
||||
if (shouldSkip(h)) return;
|
||||
const text = h.textContent.trim();
|
||||
if (!text) return;
|
||||
const btnId = tabElToBtnId.get(h.closest(".tab"));
|
||||
add({ key: "color:" + (btnId || "?") + ":" + text, text, el: h, action: () => goToTabElement(h) });
|
||||
});
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function filterFeatures(query) {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
if (!featureIndex) featureIndex = buildIndex();
|
||||
const scored = [];
|
||||
for (const entry of featureIndex) {
|
||||
const t = entry.text.toLowerCase();
|
||||
let score = -1;
|
||||
if (t === q) score = 1000;
|
||||
else if (t.startsWith(q)) score = 500 - t.length;
|
||||
else {
|
||||
const idx = t.indexOf(q);
|
||||
if (idx !== -1) score = 200 - idx;
|
||||
else if (t.split(/\s+/).some(w => w.toLowerCase().startsWith(q))) score = 50;
|
||||
}
|
||||
if (score >= 0) scored.push({ entry, score });
|
||||
}
|
||||
scored.sort((a, b) => b.score - a.score || a.entry.text.length - b.entry.text.length);
|
||||
return scored.slice(0, 12).map(s => s.entry);
|
||||
}
|
||||
|
||||
function renderResults(matches) {
|
||||
currentMatches = matches;
|
||||
activeIndex = matches.length ? 0 : -1;
|
||||
resultsEl.innerHTML = "";
|
||||
if (!matches.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "search-result empty";
|
||||
empty.textContent = "No features found";
|
||||
resultsEl.appendChild(empty);
|
||||
resultsEl.classList.add("open");
|
||||
return;
|
||||
}
|
||||
matches.forEach((entry, i) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "search-result" + (i === activeIndex ? " active" : "");
|
||||
item.setAttribute("role", "option");
|
||||
const title = document.createElement("span");
|
||||
title.className = "search-result-title";
|
||||
title.textContent = entry.text;
|
||||
const cat = document.createElement("span");
|
||||
cat.className = "search-result-category";
|
||||
cat.textContent = categoryFor(entry.el);
|
||||
item.appendChild(title);
|
||||
item.appendChild(cat);
|
||||
item.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
selectResult(i);
|
||||
});
|
||||
resultsEl.appendChild(item);
|
||||
});
|
||||
resultsEl.classList.add("open");
|
||||
}
|
||||
|
||||
function setActive(i) {
|
||||
activeIndex = i;
|
||||
const items = resultsEl.querySelectorAll(".search-result");
|
||||
items.forEach((el, idx) => el.classList.toggle("active", idx === i));
|
||||
const active = resultsEl.querySelector(".search-result.active");
|
||||
if (active) active.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
function closeResults() {
|
||||
resultsEl.classList.remove("open");
|
||||
resultsEl.innerHTML = "";
|
||||
currentMatches = [];
|
||||
activeIndex = -1;
|
||||
}
|
||||
|
||||
function selectResult(i) {
|
||||
const entry = currentMatches[i];
|
||||
if (!entry) return;
|
||||
closeResults();
|
||||
searchInput.value = "";
|
||||
searchInput.blur();
|
||||
entry.action();
|
||||
}
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
if (!searchInput.value.trim()) { closeResults(); return; }
|
||||
renderResults(filterFeatures(searchInput.value));
|
||||
});
|
||||
searchInput.addEventListener("focus", () => {
|
||||
if (searchInput.value.trim()) renderResults(filterFeatures(searchInput.value));
|
||||
});
|
||||
searchInput.addEventListener("keydown", (e) => {
|
||||
if (!currentMatches.length) {
|
||||
if (e.key === "Escape") searchInput.blur();
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); setActive((activeIndex + 1) % currentMatches.length); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); setActive((activeIndex - 1 + currentMatches.length) % currentMatches.length); }
|
||||
else if (e.key === "Enter") { e.preventDefault(); if (activeIndex >= 0) selectResult(activeIndex); }
|
||||
else if (e.key === "Escape") { e.preventDefault(); closeResults(); searchInput.blur(); }
|
||||
});
|
||||
searchInput.addEventListener("blur", () => setTimeout(closeResults, 150));
|
||||
|
||||
const icon = headerSearch.querySelector(".header-search-icon");
|
||||
if (icon) icon.addEventListener("click", () => searchInput.focus());
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest("#header-search")) closeResults();
|
||||
});
|
||||
}
|
||||
|
||||
function setup() {
|
||||
|
||||
const menu = {
|
||||
@ -671,6 +965,13 @@ function setup() {
|
||||
document.querySelectorAll('[data-i18n]').forEach(text => {
|
||||
text.innerText = chrome.i18n.getMessage(text.dataset.i18n);
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
const msg = chrome.i18n.getMessage(el.dataset.i18nPlaceholder);
|
||||
if (msg) el.placeholder = msg;
|
||||
});
|
||||
|
||||
// header feature search (search bar that jumps to features)
|
||||
setupFeatureSearch(menu);
|
||||
|
||||
// activate dark mode inspector button
|
||||
document.querySelector("#inspector-btn").addEventListener("click", async function () {
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://*/*"],
|
||||
"js": ["css/darkmodecss.js", "js/backgrounds.js", "js/content.js"],
|
||||
"js": ["css/darkmodecss.js", "js/backgrounds.js", "js/markdown.js", "js/content.js"],
|
||||
"css": ["css/content.css"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user