fix(submit): repair placeholder, code block, hidden block and settings toggles

- placeholder: hide the body ::before hint once the editor body has focus, so
  clicking an empty document no longer leaves the hint sitting under the caret
- code block: edit as a bare <pre> and convert to pre > code on save. TinyMCE
  treats pre > code as an inline boundary, so typing there leaked into the
  following paragraph; Enlighter also splits lines on newlines, so the <br>
  the editor produced rendered the whole block as one overflowing line
- code block: align the iframe rendering with Enlighter (Menlo 13px/1.4,
  #f8f9fa, 8px radius, 12px 17px padding, nowrap) in both content CSS files
- code block: move the caret into the block after insertion
- hidden block: hang the block-delete overlay on documentElement instead of
  body. data-mce-bogus only skips serialization, so the extra body child made
  mceInsertContent wipe the document and drop the first hidden block
- settings: give the toggle a real 44x24 hit area, align it to the top of the
  row, add aria-labels and show the disabled state
- styles: drop the duplicated @use/@import of publish-drawer and block-tools
- media insert/embed now serialize through serializeBlocks so code blocks are
  never stored in editor form
This commit is contained in:
kate522 committed 2026-11-10 16:04:49 +08:00
1 parent 4f9347b6aa
commit ec582ac9f9
11 files changed
+248 -56

No files matched your search

@@ -29,13 +29,15 @@ export function dividerMarkup(): string {
}
/**
* 代码块:与前台文章页一致,只落 pre > code。
* 代码块。
*
* 前台由 EnlighterJS 按「全局功能 → 代码高亮」统一高亮,因此正文里不再写
* 语言 / 主题属性,也不需要编辑器内的语言角标。
* 编辑器里放一个可直接输入的裸 `<pre>`:TinyMCE 会把 `pre > code` 当成行内边界,
* 光标落不进去、输入会把内容拆到段落里。落库前由 `serializeBlocks()` 换成前台
* Enlighter 认得的 `pre > code`(换行写成真正的换行符),打开已有内容时
* `hydrateBlocks()` 再还原成可编辑的 `<pre>`。
*/
export function codeBlockMarkup(): string {
return '<pre><code></code></pre><p></p>';
return '<pre><br></pre><p></p>';
}
export type TableOptions = {
+102 -5
View File
@@ -2,9 +2,14 @@
* 编辑器内容 ↔ 保存格式的转换。
*
* 编辑器里把公式、注释渲染成可读的样子(方便写作时看到最终效果),
* 但落库必须回到短代码,服务端才能稳定渲染。隐藏内容与代码块本身就是
* 但落库必须回到短代码,服务端才能稳定渲染。隐藏内容本身就是
* 短代码 / 前台结构,不需要转换。
*
* 代码块例外:前台由 EnlighterJS 按「全局功能 → 代码高亮」统一高亮,认的是
* `pre > code`,并把源码里的换行当作分行;而 TinyMCE 把 `pre > code` 当成
* 行内边界,光标落不进去、输入会被拆成段落。因此编辑器里用裸 `<pre>` 编辑,
* 保存前套回 `pre > code` 并把 `<br>` 还原成换行符。
*
* @package Jio
*/
@@ -17,17 +22,104 @@ const decodeEntities = (value: string): string => {
return area.value;
};
/**
* 读出代码块的源码行。
*
* 两种形态都要认:编辑器里的裸 `<pre>`(换行是 `<br>`),以及前台落库的
* `pre > code`(换行是文本里的 `\n`)。`<code>` 只是容器,直接走进去读,
* 不重复计数。
*/
const readCodeLines = (pre: Element): string[] => {
const lines: string[] = [];
let current = '';
const flush = (): void => {
lines.push(current);
current = '';
};
const walk = (node: Node): void => {
node.childNodes.forEach((child) => {
if (child.nodeType === 3) {
String(child.nodeValue || '')
.replace(/ /g, ' ')
.split('\n')
.forEach((part, index) => {
if (index > 0) {
flush();
}
current += part;
});
return;
}
if (child.nodeType !== 1) {
return;
}
if ((child as Element).nodeName === 'BR') {
flush();
return;
}
walk(child);
});
};
walk(pre);
flush();
// 末尾的空行只是排版残留(裸 pre 收尾的 <br>、落库时补的换行符)。
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
lines.pop();
}
return lines;
};
/**
* 编辑器里的裸 `<pre>` → 前台的 `pre > code`。
*
* `<br>` 与文本里的换行都换成分行符:Enlighter 直接按文本切行,留着 `<br>`
* 会把整块压成一行、长行溢出卡片。
*/
const serializeCodeBlock = (pre: Element): void => {
const lines = readCodeLines(pre);
const code = document.createElement('code');
code.textContent = lines.length === 0 ? '' : `${lines.join('\n')}\n`;
pre.replaceChildren(code);
};
/**
* 前台 `pre > code` → 编辑器里可直接输入的裸 `<pre>`。
*/
const hydrateCodeBlock = (pre: Element): void => {
const lines = readCodeLines(pre);
pre.replaceChildren();
if (lines.length === 0) {
pre.appendChild(document.createElement('br'));
return;
}
lines.forEach((line, index) => {
if (index > 0) {
pre.appendChild(document.createElement('br'));
}
if (line !== '') {
pre.appendChild(document.createTextNode(line));
}
});
};
/**
* 保存前:把编辑器里的可视化块换回短代码 / 纯标记。
*/
export function serializeBlocks(html: string): string {
if (!html || html.indexOf('data-jio-') === -1) {
if (!html) {
return html;
}
const container = document.createElement('div');
container.innerHTML = html;
// 代码块:裸 pre → 前台的 pre > code(换行写成换行符)。
container.querySelectorAll('pre').forEach((pre) => serializeCodeBlock(pre));
// 公式
container.querySelectorAll<HTMLElement>('[data-jio-math-source]').forEach((node) => {
const formula = node.getAttribute('data-jio-math-source') || '';
@@ -51,14 +143,19 @@ export function serializeBlocks(html: string): string {
}
/**
* 打开已有内容时:把短代码还原成编辑器里的可视化块。
* 打开已有内容时:把短代码 / 前台的代码块结构还原成编辑器里的可视化块。
*/
export function hydrateBlocks(html: string): string {
if (!html || html.indexOf('[jio_') === -1) {
if (!html) {
return html;
}
return html
const container = document.createElement('div');
container.innerHTML = html;
container.querySelectorAll('pre').forEach((pre) => hydrateCodeBlock(pre));
return container.innerHTML
.replace(
/\[jio_math display="([01])"\]([\s\S]*?)\[\/jio_math\]/g,
(_match, display: string, formula: string) =>