lesson more edit

This commit is contained in:
Cai
2025-09-18 11:47:24 +00:00
parent 15963190cb
commit 6575ee80bc
7 changed files with 638 additions and 83 deletions

View File

@@ -0,0 +1,115 @@
// ======= 工具:创建编辑按钮(含铅笔 SVG =======
function createEditButton(onClick) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'edit-btn';
btn.setAttribute('aria-label', '编辑标题');
btn.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<i class="fas fa-pencil-alt"></i>
</svg>
`;
btn.addEventListener('click', (e) => {
e.stopPropagation();
onClick && onClick();
});
return btn;
}
// ======= 模态框:打开标题编辑器 =======
function openTitleEditor({ title = '修改标题', initialValue = '', onSubmit }) {
const backdrop = document.createElement('div');
backdrop.className = 'title_change_modal-backdrop';
const title_change_modal = document.createElement('div');
title_change_modal.className = 'title_change_modal';
title_change_modal.setAttribute('role', 'dialog');
title_change_modal.setAttribute('aria-title_change_modal', 'true');
title_change_modal.innerHTML = `
<h3 id="dialog-title">${title}</h3>
<div class="field">
<label for="title-input" class="sr-only">标题</label>
<input id="title-input" type="text" placeholder="请输入标题" value="${escapeHtmlAttr(initialValue)}" />
<div class="error" id="title-error"></div>
</div>
<div class="actions">
<button type="button" class="btn" id="cancel-btn">取消</button>
<button type="button" class="btn primary" id="ok-btn">确定</button>
</div>
`;
backdrop.appendChild(title_change_modal);
document.body.appendChild(backdrop);
const input = title_change_modal.querySelector('#title-input');
const okBtn = title_change_modal.querySelector('#ok-btn');
const cancelBtn = title_change_modal.querySelector('#cancel-btn');
const errBox = title_change_modal.querySelector('#title-error');
const prevFocus = document.activeElement;
const close = () => {
document.body.removeChild(backdrop);
if (prevFocus && prevFocus.focus) prevFocus.focus();
};
const submit = () => {
const raw = input.value;
const result = onSubmit ? onSubmit(raw) : { ok: true };
if (result && result.ok) {
close();
} else if (result && result.message) {
errBox.textContent = result.message;
input.setAttribute('aria-invalid', 'true');
input.focus();
}
};
okBtn.addEventListener('click', submit);
cancelBtn.addEventListener('click', close);
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) close(); // 点击遮罩关闭
});
title_change_modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') close();
if (e.key === 'Enter') submit();
});
// 初始聚焦
setTimeout(() => input.focus(), 0);
}
// ======= 验证与规范化 =======
function validateTitle(raw) {
if (typeof raw !== 'string') return { ok: false, message: '标题格式不正确' };
// 1) 去掉 Markdown 井号前缀(若用户手动输入了 ###
let t = raw.replace(/^#+\s*/, '');
// 2) 替换换行为单空格(标题单行)
t = t.replace(/\r?\n+/g, ' ');
// 3) 去首尾空白 & 折叠多空格
t = t.trim().replace(/\s{2,}/g, ' ');
// 4) 基础规则
if (t.length === 0) return { ok: false, message: '标题不能为空' };
if (t.length > 80) return { ok: false, message: '标题不能超过 80 个字符' };
// 5) 可选:禁止只包含标点
if (!/[0-9A-Za-z\u4e00-\u9fa5]/.test(t)) {
return { ok: false, message: '标题需包含字母/数字/中文' };
}
return { ok: true, value: t };
}
// 显示时的规范化(比如初始渲染)
function normalizeTitle(s) {
if (!s) return '';
return s.replace(/^#+\s*/, '').replace(/\r?\n+/g, ' ').trim().replace(/\s{2,}/g, ' ');
}
// 简单转义,避免把初始值插进 input 时破坏属性
function escapeHtmlAttr(s) {
return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}