/** * 笔记详情页的**分段编辑**(v52 用户 2026-10-15 第 4 条) * ============================================================ * 用户原话:「笔记这个功能模块,在详情页面,应该具备分段编辑功能,就是说,我在一个笔记页面, * 可以对其中一个段落单独编辑,其他的内容不影响,而不用跳转到编辑页面,因为当页面中的 * 分块很多的时候,跳转到编辑页面难以定位到想要编辑的那一个内容块,在鼠标划过内容块的时候, * 这个内容块右侧出现一个编辑按钮,点击后当前的内容块变成编辑框,编辑完成后, * 点击保存,页面动态更新。」 * * 做法: * · 模板给每一块包 `.note-read-item[data-block][data-kind][data-raw]`; * · 鼠标划过 → CSS 让右上角的「编辑」出现; * · 点编辑 → 这一块**就地**换成 textarea(段落)或 换图提示(图片), * 旁边的「保存 / 取消」是就地长出来的; * · 保存 → POST `notes/block`(**按块主键改一行**)→ 拿回 {html} → 只替换这一块的渲染。 * * ⚠️ 为什么必须"按块主键改一行"而不是复用整条保存: * 整条保存是"整组替换"(先删光再插),会**换掉每个块的 id** —— * 前端手里那个 `data-block` 当场失效;更糟的是两个人同时改不同段落会互相覆盖。 * 按主键改就没有这些问题(用户要的正是"其他内容不影响")。 * * ⚠️ 与 `note_editor.js` 的分工:那个是**编辑页**的分块编辑器(整条保存), * 这个只在**阅读态**跑。两者共用的是"渲染出的样子"(都由服务端 blockHtml 出), * 但**不共用**任何绑定逻辑 —— 阅读页根本不加载 note_editor.js(见 layout.php 的按需加载)。 */ (function () { 'use strict'; var body = document.querySelector('.note-read-body[data-note-id]'); if (!body) { return; } var noteId = parseInt(body.getAttribute('data-note-id') || '0', 10) || 0; if (!noteId) { return; } var csrf = (function () { var i = document.querySelector('input[name="_csrf"]'); return i ? i.value : ''; })(); /** 当前正在编辑的那一块(同时只允许一块进编辑态 —— 多了用户自己也乱) */ var editing = null; function esc(s) { return String(s == null ? '' : s); } /* ---------------- 进入编辑态 ---------------- */ function startEdit(item) { if (editing === item) { return; } if (editing) { cancelEdit(editing); } var kind = item.getAttribute('data-kind') || 'text'; var raw = esc(item.getAttribute('data-raw')); var blockId = item.getAttribute('data-block'); // 把原内容收起来,编辑完取消时要能还原 item.setAttribute('data-saved-html', item.innerHTML); var box = document.createElement('div'); box.className = 'note-item-editor'; if (kind === 'image') { /* 图片块:分段编辑里**不做**"换图"(那是重传附件的事,编辑器里已有完整流程)。 这里明确告诉用户去哪儿换,别给一个点了没反应的框。 */ box.innerHTML = '

图片块请用右上角的「编辑」进整页编辑器替换。

' + '
' + ' ' + '
'; } else { var ta = document.createElement('textarea'); ta.className = 'input note-item-ta'; ta.rows = Math.max(3, Math.min(20, raw.split('\n').length + 1)); ta.value = raw; box.appendChild(ta); var ops = document.createElement('div'); ops.className = 'note-item-ops'; ops.innerHTML = '' + '' + ''; box.appendChild(ops); } item.innerHTML = ''; item.appendChild(box); item.classList.add('is-editing'); editing = item; var ta2 = box.querySelector('textarea'); if (ta2) { ta2.focus(); // 光标放到末尾(用户多半是想接着改,而不是从头重写) try { ta2.setSelectionRange(ta2.value.length, ta2.value.length); } catch (e) {} } } /* ---------------- 取消:还原成进来之前的样子 ---------------- */ function cancelEdit(item) { var html = item.getAttribute('data-saved-html'); if (html != null) { item.innerHTML = html; item.removeAttribute('data-saved-html'); } item.classList.remove('is-editing'); if (editing === item) { editing = null; } } /* ---------------- 保存:只提交这一块 ---------------- */ function saveEdit(item) { var ta = item.querySelector('textarea'); var tip = item.querySelector('.note-item-tip'); if (!ta) { return; } var blockId = item.getAttribute('data-block'); var kind = item.getAttribute('data-kind') || 'text'; var val = ta.value; // 原样没动 → 直接退出编辑态(省一次请求,也不让"保存"按钮假忙一场) if (val === esc(item.getAttribute('data-raw'))) { cancelEdit(item); return; } var fd = new FormData(); fd.append('_csrf', csrf); fd.append('note_id', String(noteId)); fd.append('block_id', String(blockId)); fd.append('kind', kind); fd.append('content', val); if (tip) { tip.textContent = '保存中…'; } var btn = item.querySelector('[data-item-save]'); if (btn) { btn.disabled = true; } fetch('index.php?r=notes/block', { method: 'POST', body: fd, credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then(function (r) { return r.json().catch(function () { return null; }); }) .then(function (d) { if (!d || !d.ok || !d.block) { if (tip) { tip.textContent = (d && d.msg) ? d.msg : '没保存上,再试一次。'; } if (btn) { btn.disabled = false; } return; } /* 成功:把这一块换成服务端渲染好的内容(**含那块「编辑」按钮** —— 保存后要能接着再改,别让它消失)。 ⚠️ 不能直接写 d.block.html 了事:图片/段落的**外壳**(.note-read-item / .note-read-block)也要一起还原,否则 `data-raw` 还是旧值、 再点编辑会看到旧内容。 */ var rawNew = esc(d.block.content); item.setAttribute('data-raw', rawNew); item.removeAttribute('data-saved-html'); item.classList.remove('is-editing'); var inner = ''; if (d.block.kind === 'image') { // 图片块的 html 恒为空(见 Note::blockHtml),这里保留原来的
inner = '

(图片块)

'; } else { inner = '
' + esc(d.block.html) + '
'; } item.innerHTML = inner + editButtonHtml(blockId); if (editing === item) { editing = null; } }) .catch(function () { if (tip) { tip.textContent = '网络出错,没保存上。'; } if (btn) { btn.disabled = false; } }); } function editButtonHtml(blockId) { return ''; } /* ---------------- 事件委托 ---------------- */ body.addEventListener('click', function (e) { var t = e.target; if (!t || !t.closest) { return; } var editBtn = t.closest('[data-item-edit]'); if (editBtn) { var item = editBtn.closest('.note-read-item'); if (item) { startEdit(item); } return; } if (t.closest('[data-item-save]')) { var it2 = t.closest('.note-read-item'); if (it2) { saveEdit(it2); } return; } if (t.closest('[data-item-cancel]')) { var it3 = t.closest('.note-read-item'); if (it3) { cancelEdit(it3); } return; } /* 点在这一块**外面** → 收掉编辑态(点了别处通常就是"算了")。 点在自己那一块里不动作,否则写一半点到正文就没了。 */ if (editing && !t.closest('.note-read-item')) { cancelEdit(editing); } }); // Esc = 取消(与站内其它浮层同一手感) document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && editing) { cancelEdit(editing); } }); /* Ctrl/Cmd + Enter = 保存(写长段落时手不用离开键盘) */ body.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { var it = e.target && e.target.closest ? e.target.closest('.note-read-item') : null; if (it) { e.preventDefault(); saveEdit(it); } } }); })();