mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-25 06:15:09 +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)
|
- Searching themes (the original didn't actually impliment that)
|
||||||
- made the dark mode into a css file instead of a reallllllly long string
|
- 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)
|
- 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
|
- Popup UI revamp
|
||||||
- NEW Better todo list
|
- NEW Better todo list
|
||||||
- Better sidebar
|
- Better sidebar
|
||||||
- Simplified UI
|
- 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)
|
## Planned Features (by priority)
|
||||||
- auto rotate theme + theme history + fix theme submissions
|
- auto rotate theme + theme history + fix theme submissions
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
"Canvas_Refined": {
|
"Canvas_Refined": {
|
||||||
"message": "Canvas Refined"
|
"message": "Canvas Refined"
|
||||||
},
|
},
|
||||||
|
"search_features": {
|
||||||
|
"message": "Search features..."
|
||||||
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"message": "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-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-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-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-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-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;}
|
.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-courses {min-width:600px;width:max-content;}
|
||||||
.canvasrefined-gpa-course-top, .canvasrefined-gpa-course-bottom {display: flex;}
|
.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-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-todosidebar, #canvasrefined-todo-list, #canvasrefined-announcement-list {margin: 0}
|
||||||
.canvasrefined-todo-container {display: flex; margin-top: 14px;position: relative;transition: .2s all;}
|
.canvasrefined-todo-container {display: flex; margin-top: 14px;position: relative;transition: .2s all;}
|
||||||
.canvasrefined-removing {max-height: 0;margin-top: 0}
|
.canvasrefined-removing {max-height: 0;margin-top: 0}
|
||||||
@ -177,3 +177,41 @@
|
|||||||
opacity: .5;
|
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;}
|
||||||
@ -253,3 +253,20 @@ input[type="checkbox"]:checked {background: #8dd28d;}
|
|||||||
opacity: 1;
|
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">
|
<div class="header">
|
||||||
<img id="bclogo" src="../icon/icon-128.png">
|
<img id="bclogo" src="../icon/icon-128.png">
|
||||||
<h1 data-i18n="Canvas_Refined">Canvas Refined</h1>
|
<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>
|
||||||
<div class="more-options-container">
|
<div class="more-options-container">
|
||||||
<button id="customize-dark-btn" class="big-button tab-btn">
|
<button id="customize-dark-btn" class="big-button tab-btn">
|
||||||
|
|||||||
@ -37,6 +37,7 @@ chrome.runtime.onInstalled.addListener(function () {
|
|||||||
"assignment_date_format": false,
|
"assignment_date_format": false,
|
||||||
"dashboard_notes": false,
|
"dashboard_notes": false,
|
||||||
"dashboard_notes_text": "",
|
"dashboard_notes_text": "",
|
||||||
|
"dashboard_notes_mode": "edit",
|
||||||
"better_todo": false,
|
"better_todo": false,
|
||||||
"todo_hr24": false,
|
"todo_hr24": false,
|
||||||
"todo_separate_scrollbar": false,
|
"todo_separate_scrollbar": false,
|
||||||
|
|||||||
286
js/content.js
286
js/content.js
@ -657,6 +657,8 @@ function applyOptionsChanges(changes) {
|
|||||||
changeGradientCards();
|
changeGradientCards();
|
||||||
break;
|
break;
|
||||||
case "dashboard_notes":
|
case "dashboard_notes":
|
||||||
|
case "dashboard_notes_text":
|
||||||
|
case "dashboard_notes_mode":
|
||||||
loadDashboardNotes();
|
loadDashboardNotes();
|
||||||
break;
|
break;
|
||||||
case "dashboard_grades":
|
case "dashboard_grades":
|
||||||
@ -4133,7 +4135,6 @@ Dashboard notes
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
let dashboardNotesTimer;
|
let dashboardNotesTimer;
|
||||||
let dashboardNotesResizeFrame;
|
|
||||||
function delayDashboardNotesStorage(text) {
|
function delayDashboardNotesStorage(text) {
|
||||||
clearTimeout(dashboardNotesTimer);
|
clearTimeout(dashboardNotesTimer);
|
||||||
dashboardNotesTimer = setTimeout(() => {
|
dashboardNotesTimer = setTimeout(() => {
|
||||||
@ -4141,39 +4142,278 @@ function delayDashboardNotesStorage(text) {
|
|||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeDashboardNotes(notes) {
|
/*
|
||||||
if (!notes) return;
|
Built-in fallback Markdown renderer. Used only if js/markdown.js failed to load
|
||||||
notes.style.height = "auto";
|
(window.renderMarkdown missing) so the notes still render formatted output instead
|
||||||
notes.style.height = `${notes.scrollHeight + 5}px`;
|
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) {
|
function renderDashboardNotesPreview(preview, text) {
|
||||||
if (dashboardNotesResizeFrame) cancelAnimationFrame(dashboardNotesResizeFrame);
|
if (!preview) return;
|
||||||
dashboardNotesResizeFrame = requestAnimationFrame(() => {
|
// Skip identical re-renders: writing innerHTML is a childList mutation that the
|
||||||
dashboardNotesResizeFrame = null;
|
// dashboard MutationObserver picks up, which re-calls loadDashboardNotes, which
|
||||||
resizeDashboardNotes(notes);
|
// 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() {
|
function loadDashboardNotes() {
|
||||||
|
const container = document.querySelector("#DashboardCard_Container");
|
||||||
if (options.dashboard_notes === true) {
|
if (options.dashboard_notes === true) {
|
||||||
|
if (!container) return;
|
||||||
let notes = document.querySelector('.canvasrefined-dashboard-notes');
|
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) {
|
if (!notes) {
|
||||||
notes = document.createElement("textarea");
|
notes = document.createElement("div");
|
||||||
notes.classList.add("canvasrefined-dashboard-notes");
|
notes.classList.add("canvasrefined-dashboard-notes");
|
||||||
notes.placeholder = "Enter notes here";
|
notes.innerHTML = DASHBOARD_NOTES_HTML;
|
||||||
document.querySelector("#DashboardCard_Container").prepend(notes);
|
// Mount as a full-width sibling above the card grid. Prepending inside the
|
||||||
notes.value = options.dashboard_notes_text;
|
// DashboardCard_Container makes the notes a masonry/grid cell (narrow & broken).
|
||||||
notes.style.display = "block";
|
const parent = container.parentNode;
|
||||||
resizeDashboardNotes(notes);
|
if (parent) parent.insertBefore(notes, container);
|
||||||
notes.addEventListener('input', function () {
|
else container.prepend(notes);
|
||||||
options.dashboard_notes_text = this.value;
|
wireDashboardNotes(notes);
|
||||||
delayDashboardNotesStorage(this.value);
|
|
||||||
scheduleDashboardNotesResize(this);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
notes.style.display = "block";
|
notes.style.display = "";
|
||||||
resizeDashboardNotes(notes);
|
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 {
|
} else {
|
||||||
let notes = document.querySelector('.canvasrefined-dashboard-notes');
|
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,
|
"assignment_date_format": false,
|
||||||
"dashboard_notes": false,
|
"dashboard_notes": false,
|
||||||
"dashboard_notes_text": "",
|
"dashboard_notes_text": "",
|
||||||
|
"dashboard_notes_mode": "edit",
|
||||||
"better_todo": false,
|
"better_todo": false,
|
||||||
"better_sidebar": false,
|
"better_sidebar": false,
|
||||||
"sidebar_scale": 100,
|
"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() {
|
function setup() {
|
||||||
|
|
||||||
const menu = {
|
const menu = {
|
||||||
@ -671,6 +965,13 @@ function setup() {
|
|||||||
document.querySelectorAll('[data-i18n]').forEach(text => {
|
document.querySelectorAll('[data-i18n]').forEach(text => {
|
||||||
text.innerText = chrome.i18n.getMessage(text.dataset.i18n);
|
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
|
// activate dark mode inspector button
|
||||||
document.querySelector("#inspector-btn").addEventListener("click", async function () {
|
document.querySelector("#inspector-btn").addEventListener("click", async function () {
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://*/*"],
|
"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"],
|
"css": ["css/content.css"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user