use markdown to edit
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user