use markdown to edit

This commit is contained in:
Cai
2025-11-03 16:14:57 +08:00
parent ce88ae645e
commit 33844786a1
9 changed files with 1126 additions and 53 deletions

View File

@@ -801,10 +801,11 @@ function ValidTextareaNoBlankHead(textarea) {
// 更新 textarea 的值(如果有变化)
textarea.value = lines.join("\n");
}
const editors = document.querySelectorAll("textarea");
const editorsDoc = document.querySelectorAll("textarea");
const PREFIX = "### ";
// 记录每个 textarea 最新的“标题内容”(不含前缀)
const lastTitleMap = new Map();
// 防止循环触发
let isProgrammaticUpdate = false;
function getFirstLine(text) {
@@ -823,7 +824,7 @@ const lastTitleMap = new Map();
return { line: normalized, title };
}
// 初始化 lastTitleMap把当前值规范化一次
editors.forEach(ed => {
editorsDoc.forEach(ed => {
const rawFirst = getFirstLine(ed.value || "");
const { line, title } = normalizeFirstLine(rawFirst);
if (line !== rawFirst) {
@@ -837,7 +838,7 @@ const lastTitleMap = new Map();
function syncOthers(sourceEditor, newTitle) {
isProgrammaticUpdate = true;
try {
editors.forEach(ed => {
editorsDoc.forEach(ed => {
if (ed === sourceEditor) return;
const first = getFirstLine(ed.value || "");
const { line } = normalizeFirstLine(first);
@@ -851,56 +852,204 @@ const lastTitleMap = new Map();
isProgrammaticUpdate = false;
}
}
editors.forEach(editor => {
editor.addEventListener("input", (event) => {
const textarea = event.target
const value = event.target.value;
const type = event.target.dataset.type;
// 在这里做提示或校验
console.log(`内容已更改: ${event.target.id} => ${value}`);
// ==== 前置:你的三编辑器实例与 id 映射(按你实际为准)====
const ID_LESSON = 'editor-lesson';
const ID_PROMPT = 'editor-prompt';
const ID_SCORE = 'editor-score';
ValidTextareaNoBlankHead(event.target)
// editors: { [textareaId]: EasyMDEInstance }
const editorIds = [ID_LESSON, ID_PROMPT, ID_SCORE];
const rawFirst = getFirstLine(value);
// 全局防抖/重入保护:避免程序化更新再触发同步
// // 记录上一次各编辑器的标题文本(不含 "### " 前缀)
// const lastTitleMap = new Map();
// —— 工具:从 EasyMDE 拿值 / 设值(会同步 textarea因为 forceSync:true——
const getMD = (id) => editors[id].value();
const setMD = (id, text) => editors[id].value(text);
// —— 工具:仅替换“首行”为 normalizedLine尽量不破坏光标与撤销栈 ——
// line 是完整首行文本(含 "### "
function setFirstLineInEditor(ed, normalizedLine) {
const cm = ed.codemirror;
cm.operation(() => {
const doc = cm.getDoc();
const firstLine = doc.getLine(0) ?? "";
if (firstLine === normalizedLine) return;
// 记住用户光标
const cursors = doc.listSelections();
// 用 replaceRange 仅替换首行
doc.replaceRange(
normalizedLine,
{ line: 0, ch: 0 },
{ line: 0, ch: firstLine.length }
);
// 尽力恢复光标(如果原先在首行,偏移可能要校正)
doc.setSelections(cursors);
});
}
// —— 工具:从 Markdown 文本取首行(兼容你现有函数)
function getFirstLineFromText(text) {
const idx = text.indexOf('\n');
return idx === -1 ? text : text.slice(0, idx);
}
// —— 工具:把 normalizedLine 写回文本首行(备用:整段 setValue——
function setFirstLineInText(text, normalizedLine) {
const idx = text.indexOf('\n');
return idx === -1 ? normalizedLine : (normalizedLine + text.slice(idx));
}
// —— 同步另外两个编辑器的首行(仅标题行),避免全文覆盖 ——
function syncOthersTitle(fromId, titleText /*不含前缀*/, normalizedLine /*含### */) {
editorIds.forEach((id) => {
if (id === fromId) return;
const ed = editors[id];
if (!ed) return;
const currentFirst = ed.codemirror.getDoc().getLine(0) ?? "";
if (currentFirst !== normalizedLine) {
setFirstLineInEditor(ed, normalizedLine);
lastTitleMap.set(ed, titleText);
}
});
}
// —— 更新 tab 按钮上的 “*” 修改状态(保持你原逻辑)——
function refreshTabDirtyBadges() {
let btn = document.getElementById("tab-btn-lesson");
if (CURRENT.active.lesson !== getMD(ID_LESSON)) {
btn.innerText = AddModifiedStat(btn.innerText);
} else {
btn.innerText = ClearModifiedStat(btn.innerText);
}
btn = document.getElementById("tab-btn-prompt");
if (CURRENT.active.prompt !== getMD(ID_PROMPT)) {
btn.innerText = AddModifiedStat(btn.innerText);
} else {
btn.innerText = ClearModifiedStat(btn.innerText);
}
btn = document.getElementById("tab-btn-score");
if (CURRENT.active.score !== getMD(ID_SCORE)) {
btn.innerText = AddModifiedStat(btn.innerText);
} else {
btn.innerText = ClearModifiedStat(btn.innerText);
}
}
// —— 单个编辑器的 change 处理器工厂 ——
function bindEditorChangeHandler(textareaId) {
const ed = editors[textareaId];
const cm = ed.codemirror;
// 初始化 lastTitleMap
lastTitleMap.set(ed, "");
cm.on('change', () => {
if (isProgrammaticUpdate) return;
const value = ed.value();
const rawFirst = getFirstLineFromText(value);
// 你自己的校验(如果要继续保留)
// ValidTextareaNoBlankHead(ed.element.nextSibling /* textarea DOM? */);
// 归一化首行,要求 "### " 前缀
const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
// 若首行不规范,程序化改回去(仅替换首行)
if (normalizedLine !== rawFirst) {
isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
isProgrammaticUpdate = true;
try {
textarea.value = setFirstLine(value, normalizedLine);
setFirstLineInEditor(ed, normalizedLine);
} finally {
isProgrammaticUpdate = false;
}
}
const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
// 标题变化则同步到其他两个编辑器(只同步首行)
const lastTitle = lastTitleMap.get(ed) ?? "";
if (currentTitle !== lastTitle) {
lastTitleMap.set(textarea, currentTitle);// 更新自身记录
syncOthers(textarea, currentTitle);
lastTitleMap.set(ed, currentTitle);
isProgrammaticUpdate = true;
try {
syncOthersTitle(textareaId, currentTitle, normalizedLine);
} finally {
isProgrammaticUpdate = false;
}
}
let btn = document.getElementById("tab-btn-lesson");
if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
btn.innerText = AddModifiedStat(btn.innerText)
}else{
btn.innerText = ClearModifiedStat(btn.innerText)
}
btn = document.getElementById("tab-btn-prompt");
if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
btn.innerText = AddModifiedStat(btn.innerText)
}else{
btn.innerText = ClearModifiedStat(btn.innerText)
}
btn = document.getElementById("tab-btn-score");
if (CURRENT.active.score != document.getElementById("editor-score").value){
btn.innerText = AddModifiedStat(btn.innerText)
}else{
btn.innerText = ClearModifiedStat(btn.innerText)
// 刷新三个 Tab 的“修改中 * ”标识
refreshTabDirtyBadges();
// 如果你还有右侧预览:
if (typeof renderToPreview === 'function' && textareaId === activeId) {
renderToPreview(ed.value());
}
});
});
}
// 绑定三个编辑器
editorIds.forEach(bindEditorChangeHandler);
// editorsDoc.forEach(editor => {
// editor.addEventListener("input", (event) => {
// const textarea = event.target
// const value = event.target.value;
// const type = event.target.dataset.type;
// // 在这里做提示或校验
// console.log(`内容已更改: ${event.target.id} => ${value}`);
// ValidTextareaNoBlankHead(event.target)
// const rawFirst = getFirstLine(value);
// const { line: normalizedLine, title: currentTitle } = normalizeFirstLine(rawFirst);
// if (normalizedLine !== rawFirst) {
// isProgrammaticUpdate = true;// 把首行改回以 '### ' 开头
// try {
// textarea.value = setFirstLine(value, normalizedLine);
// } finally {
// isProgrammaticUpdate = false;
// }
// }
// const lastTitle = lastTitleMap.get(textarea) ?? "";// —— 2) 如标题内容有变化,则同步到其他两个 —— //
// if (currentTitle !== lastTitle) {
// lastTitleMap.set(textarea, currentTitle);// 更新自身记录
// syncOthers(textarea, currentTitle);
// }
// let btn = document.getElementById("tab-btn-lesson");
// if (CURRENT.active.lesson != document.getElementById("editor-lesson").value){
// btn.innerText = AddModifiedStat(btn.innerText)
// }else{
// btn.innerText = ClearModifiedStat(btn.innerText)
// }
// btn = document.getElementById("tab-btn-prompt");
// if (CURRENT.active.prompt != document.getElementById("editor-prompt").value){
// btn.innerText = AddModifiedStat(btn.innerText)
// }else{
// btn.innerText = ClearModifiedStat(btn.innerText)
// }
// btn = document.getElementById("tab-btn-score");
// if (CURRENT.active.score != document.getElementById("editor-score").value){
// btn.innerText = AddModifiedStat(btn.innerText)
// }else{
// btn.innerText = ClearModifiedStat(btn.innerText)
// }
// });
// });
// 把“某个步骤”对应的三个片段载入到三个编辑器
// 把“某个步骤”对应的三个片段载入到三个编辑器
function loadStepToEditors(phaseText, stepText) {
// 三份文档里都取对应 phase/step 的片段(不存在则回退)
const L = CURRENT.parsed.lesson, P = CURRENT.parsed.prompt, S = CURRENT.parsed.score;
@@ -918,9 +1067,9 @@ function loadStepToEditors(phaseText, stepText) {
const pText = sliceSection(subP, stepText, 3) || promptPhase;
const sText = sliceSection(subS, stepText, 3) || scorePhase;
document.getElementById('editor-lesson').value = lText;
document.getElementById('editor-prompt').value = pText;
document.getElementById('editor-score').value = sText;
editors['editor-lesson'].value(lText);
editors['editor-prompt'].value(pText);
editors['editor-score'].value(sText);
CURRENT.active={ //保存打开时的三个文本
phase:phaseText,
@@ -938,16 +1087,32 @@ function loadStepToEditors(phaseText, stepText) {
}
// Tab 切换
document.addEventListener('click', (e)=>{
if (e.target.classList.contains('tab-btn')) {
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
e.target.classList.add('active');
const id = e.target.dataset.target;
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
});
document.addEventListener('click', (e) => {
if (e.target.classList.contains('tab-btn')) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
const id = e.target.dataset.target;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById(id).classList.add('active');
// 广播 tab 变更(告诉别人当前激活的 tab id / 以及要用哪个 editorId
const editorIdMap = {
'tab-lesson' : 'editor-lesson',
'tab-prompt' : 'editor-prompt',
'tab-score' : 'editor-score',
};
const activeBtnId = e.target.id; // 如 'tab-btn-lesson'
const editorId =
activeBtnId === 'tab-btn-lesson' ? 'editor-lesson' :
activeBtnId === 'tab-btn-prompt' ? 'editor-prompt' :
'editor-score';
document.dispatchEvent(new CustomEvent('tab:change', {
detail: { tabId: id, editorId }
}));
}
});
// 保存全部:将三个编辑器中的文本保存回相应的 markdown 文件
document.getElementById('save-all-btn')?.addEventListener('click', async () => {