135 lines
4.7 KiB
JavaScript
135 lines
4.7 KiB
JavaScript
// ======= 工具:创建编辑按钮(含铅笔 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 setLoading = (on) => {
|
||
okBtn.disabled = on;
|
||
cancelBtn.disabled = on;
|
||
okBtn.textContent = on ? '保存中…' : '确定';
|
||
};
|
||
const close = () => {
|
||
document.body.removeChild(backdrop);
|
||
if (prevFocus && prevFocus.focus) prevFocus.focus();
|
||
};
|
||
|
||
const submit = () => {
|
||
errBox.textContent = '';
|
||
setLoading(true);
|
||
|
||
const maybePromise = onSubmit ? onSubmit(input.value) : { ok: true };
|
||
|
||
Promise
|
||
.resolve(maybePromise)
|
||
.then(result => {
|
||
if (result && result.ok) {
|
||
close();
|
||
} else {
|
||
// 显示错误但不关闭
|
||
const msg = result && result.message ? result.message : '提交失败';
|
||
errBox.textContent = msg;
|
||
input.setAttribute('aria-invalid', 'true');
|
||
input.focus();
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error('Submit error:', err);
|
||
errBox.textContent = '提交异常,请稍后再试';
|
||
input.setAttribute('aria-invalid', 'true');
|
||
input.focus();
|
||
})
|
||
.finally(() => setLoading(false));
|
||
};
|
||
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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|