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

@@ -207,14 +207,16 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
// 表格 / 公式 / 注释 / 代码都走参数弹窗,不再是固定内容。
assert.match(appSource, /<BlockDialog/);
assert.match(appSource, /setBlockDialog\(kind as BlockDialogKind\)/);
// 代码块直接插入,不弹窗、不选语言。
assert.match(appSource, /insertHtml\(codeBlockMarkup\(\)\)/);
// 代码块直接插入,不弹窗、不选语言;插入后光标留在代码块里。
assert.match(appSource, /insertCodeBlock\(\)/);
assert.match(appSource, /editor\.selection\.setCursorLocation\(pre, 0\)/);
assert.doesNotMatch(appSource, /setCodeDialog/);
assert.doesNotMatch(read('assets/creator-studio/src/components/block-dialog.tsx'), /kind === 'code'/);
assert.match(read('assets/creator-studio/src/components/block-dialog.tsx'), /onSubmit\(\{ kind, rows: clamp\(rows, 1, 20\), cols: clamp\(cols, 1, 10\), header, striped \}\)/);
assert.match(appSource, /onInsertBlock=\{insertBlock\}/);
assert.match(appSource, /onInsertQuote=\{insertQuote\}/);
const markup = read('assets/creator-studio/src/utils/block-markup.ts');
const serialize = read('assets/creator-studio/src/utils/serialize.ts');
assert.match(markup, /jio-math/);
// 引用沿用主题的 quote_q 结构与配色类。
assert.match(markup, /quote_q/);
@@ -225,16 +227,23 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
assert.match(markup, /data-jio-math-source/);
assert.match(read('assets/creator-studio/src/utils/serialize.ts'), /\[jio_math display=/);
assert.match(read('assets/creator-studio/src/utils/serialize.ts'), /\[jio_footnote\]/);
// 代码块与前台文章页一致:pre > code,不写语言 / 主题属性。
assert.match(markup, /<pre><code><\/code><\/pre>/);
// 代码块:编辑器里是裸 pre(TinyMCE 把 pre > code 当行内边界,光标落不进去),
// 保存时才由 serializeBlocks 套回前台的 pre > code。不写语言 / 主题属性。
assert.match(markup, /<pre><br><\/pre><p><\/p>/);
assert.doesNotMatch(markup, /<pre><code><\/code><\/pre>/);
assert.match(serialize, /querySelectorAll\('pre'\)[\s\S]*?serializeCodeBlock/);
assert.match(serialize, /createElement\('code'\)/);
assert.match(serialize, /hydrateCodeBlock/);
assert.doesNotMatch(markup, /enlighter-pre/);
assert.doesNotMatch(markup, /data-enlighter-language/);
assert.doesNotMatch(read('assets/creator-studio/src/editor-runtime.ts'), /syncCodeLabels/);
assert.doesNotMatch(read('assets/creator-studio/src/components/block-dialog.tsx'), /CODE_LANGUAGES/);
// 外观:编辑器 iframe 与前台投稿编辑器保持同一套代码块样式。
// 外观:编辑器 iframe 与前台 Enlighter 渲染值一致(不折行、浅底、8px 圆角)。
const editorCss = read('assets/css/frontend/pages/submit-editor.css');
assert.doesNotMatch(editorCss, /\.enlighter-pre/);
assert.match(editorCss, /pre code \{/);
assert.match(editorCss, /pre \{[^}]*white-space: pre;/);
assert.match(editorCss, /pre \{[^}]*background: #f8f9fa/);
// 隐藏内容沿用 .tinymce-hide 结构,短代码作为文本落库。
assert.match(markup, /tinymce-hide/);
assert.match(markup, /\[jio_hide mode="login"\]/);
@@ -301,13 +310,16 @@ test('top bar is masthead + centered toolbar, with no brand text in the toolbar
assert.doesNotMatch(rest, /'home'\s*=>\s*esc_url_raw\(\$home\)/);
});
test('block delete overlay lives inside the editor iframe and stays hidden', () => {
test('block delete overlay lives outside the editable body and stays hidden', () => {
const runtime = read('assets/creator-studio/src/editor-runtime.ts');
const contentStyle = read('assets/creator-studio/src/editor-content-style.ts');
const pageStyles = read('assets/creator-studio/src/styles/_block-tools.scss');
// 浮层插在 TinyMCE iframe 的 body 里。
assert.match(runtime, /doc\.body\.appendChild\(bar\)/);
// 浮层挂在 iframe 的 documentElement 上,不能进 body:data-mce-bogus 只跳过
// 序列化,body 里多一个非常规子节点会让 mceInsertContent 把整篇正文清空
// (表现为「第一次点隐藏块没有插进正文」)。
assert.match(runtime, /doc\.documentElement\.appendChild\(bar\)/);
assert.doesNotMatch(runtime, /doc\.body\.appendChild\(bar\)/);
assert.match(runtime, /bar\.id = 'jio-block-actions'/);
// iframe 是独立文档:页面级样式表管不到它,隐藏规则必须写在 content_style。
assert.match(contentStyle, /#jio-block-actions\s*\{[^}]*position:\s*absolute/);