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:
1 parent
4f9347b6aa
commit
ec582ac9f9
11 files changed
+248
-56
No files matched your search
@@ -241,6 +241,7 @@ export function PublishSettings({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pay.enabled}
|
||||
aria-label={__('开启付费内容', 'jio')}
|
||||
onChange={(event) => {
|
||||
const enabled = event.target.checked;
|
||||
onPayEnabledChange(enabled);
|
||||
@@ -269,6 +270,7 @@ export function PublishSettings({
|
||||
type="checkbox"
|
||||
checked={scheduleEnabled}
|
||||
disabled={!canSchedule}
|
||||
aria-label={__('开启定时发布', 'jio')}
|
||||
onChange={(event) => onScheduleChange(event.target.checked, scheduleAt || defaultScheduleTime())}
|
||||
/>
|
||||
<span aria-hidden="true" />
|
||||
|
||||
@@ -34,6 +34,9 @@ p { margin: 0 0 1.1em; }
|
||||
* (front-submit-editor-basics.js)在正文为空时给 body 打 data-jio-empty,
|
||||
* 这里用 ::before 渲染提示。不插 DOM 节点,避免被 setContent 清掉、
|
||||
* 也避免占住首行挡住点击。
|
||||
*
|
||||
* 点击正文(body 获得焦点)后就不再显示:提示文字压在光标位置上,作者会以为
|
||||
* 点击没生效。焦点离开且正文仍为空时再出现。
|
||||
*/
|
||||
body[data-jio-empty="1"]::before {
|
||||
position: absolute;
|
||||
@@ -44,6 +47,7 @@ body[data-jio-empty="1"]::before {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
body[data-jio-empty="1"]:focus::before { display: none; }
|
||||
|
||||
h1, h2, h3, h4, h5, h6 { margin: 1.5em 0 0.65em; line-height: 1.4; }
|
||||
img, iframe { max-width: 100%; }
|
||||
@@ -110,21 +114,27 @@ img { height: auto; }
|
||||
}
|
||||
|
||||
/*
|
||||
* 代码块:与前台文章页保持一致,只用 pre > code。
|
||||
* 前台由 EnlighterJS 高亮,编辑器里给出同一份底色与等宽排版。
|
||||
* 代码块:与前台文章页一致。
|
||||
*
|
||||
* 前台由 EnlighterJS 把 pre > code 重绘成 .enlighter-default:Menlo 13px/
|
||||
* 18.2px、#f8f9fa 底色、8px 圆角、上下 20px 间距、行内 12px 内边距,超长行
|
||||
* 横向滚动而不换行。编辑器里只有裸 pre(保存前才套回 code),所以这里直接
|
||||
* 复刻同一组数值;换行行为必须保持 nowrap,否则编辑器里看到的是折行、
|
||||
* 发布后却是横向滚动。
|
||||
*/
|
||||
pre {
|
||||
margin: 0 0 1.1em;
|
||||
padding: 14px 18px;
|
||||
margin: 0 0 20px;
|
||||
padding: 12px 17px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f2f4f5;
|
||||
color: #2b2b2b;
|
||||
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||
background: #f8f9fa;
|
||||
color: #555;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.4;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
}
|
||||
pre code {
|
||||
border: 0;
|
||||
|
||||
@@ -125,11 +125,14 @@ const SELECTED_CLASS = 'jio-creator__block-selected';
|
||||
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
|
||||
*
|
||||
* 与 WPS 一致的两步删除:第一次点击只是选中该块并高亮,第二次点击才真正删除。
|
||||
* 选中态下按钮改为醒目的红色「再次点击删除」,且鼠标移开不会消失,避免误删。
|
||||
* 选中态下按钮改为醒目的「再次点击删除」,且鼠标移开不会消失,避免误删。
|
||||
*
|
||||
* 浮层直接插在 iframe 的 body 里,因此用 data-mce-bogus 让 TinyMCE 在
|
||||
* 序列化、字数和选区计算时完全忽略它;它的样式写在 editor-content-style.ts
|
||||
* (父页面样式表管不到 iframe 内部)。
|
||||
* 浮层挂在 iframe 的 `documentElement` 而不是 `body` 里:`data-mce-bogus` 只让
|
||||
* TinyMCE 跳过序列化,选区计算仍会把它算进 body 的子节点;body 里多一个非常规
|
||||
* 子节点时,插入块(例如「隐藏」)会在 mceInsertContent 里把整篇正文清空。挂在
|
||||
* html 上既不影响正文,也能用同一个坐标系定位。
|
||||
*
|
||||
* 它的样式写在 editor-content-style.ts(父页面样式表管不到 iframe 内部)。
|
||||
*/
|
||||
export function installBlockActions(editor: any): void {
|
||||
const doc = editor.getDoc();
|
||||
@@ -146,7 +149,7 @@ export function installBlockActions(editor: any): void {
|
||||
'<span class="jio-creator__block-action-icon" aria-hidden="true">✕</span>' +
|
||||
'<span class="jio-creator__block-action-text"></span>' +
|
||||
'</button>';
|
||||
doc.body.appendChild(bar);
|
||||
doc.documentElement.appendChild(bar);
|
||||
|
||||
const button: HTMLButtonElement | null = bar.querySelector('[data-jio-block-delete]');
|
||||
const text: HTMLElement | null = bar.querySelector('.jio-creator__block-action-text');
|
||||
|
||||
@@ -540,6 +540,31 @@ function Studio(): JSX.Element {
|
||||
syncEditorHeight(editor);
|
||||
}, [focusEditor]);
|
||||
|
||||
/**
|
||||
* 代码块:插入后把光标直接放进代码块,作者可以立刻开始敲代码。
|
||||
*
|
||||
* `insertContent` 结束后光标落在块后的空段落里,这对外层结构没问题,但
|
||||
* 对代码块不合适 —— 作者会以为插进来的块只是占位。
|
||||
*/
|
||||
const insertCodeBlock = useCallback(() => {
|
||||
const editor = focusEditor();
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
editor.undoManager.transact(() => {
|
||||
editor.insertContent(codeBlockMarkup());
|
||||
});
|
||||
const pre = editor.selection.getNode()?.closest?.('pre')
|
||||
|| Array.from(editor.getBody().querySelectorAll('pre')).pop();
|
||||
if (pre) {
|
||||
editor.selection.setCursorLocation(pre, 0);
|
||||
}
|
||||
editor.nodeChanged();
|
||||
updateContent(serializeBlocks(editor.getContent()));
|
||||
updateHistoryAvailability(editor);
|
||||
syncEditorHeight(editor);
|
||||
}, [focusEditor]);
|
||||
|
||||
// 直接插入、无需参数的小块;需要参数的块打开通用弹窗。
|
||||
const insertBlock = useCallback((kind: string) => {
|
||||
rememberSelection();
|
||||
@@ -548,7 +573,7 @@ function Studio(): JSX.Element {
|
||||
return;
|
||||
}
|
||||
if (kind === 'code') {
|
||||
insertHtml(codeBlockMarkup());
|
||||
insertCodeBlock();
|
||||
return;
|
||||
}
|
||||
if (kind === 'ul' || kind === 'ol') {
|
||||
@@ -565,7 +590,7 @@ function Studio(): JSX.Element {
|
||||
if (kind === 'divider') {
|
||||
insertHtml(dividerMarkup());
|
||||
}
|
||||
}, [focusEditor, insertHtml, rememberSelection, updateBlockValue]);
|
||||
}, [focusEditor, insertCodeBlock, insertHtml, rememberSelection, updateBlockValue]);
|
||||
|
||||
/**
|
||||
* 把当前选区(或光标所在的顶层段落)替换成一个整块。
|
||||
@@ -682,7 +707,7 @@ function Studio(): JSX.Element {
|
||||
if (markup) {
|
||||
target.insertContent(markup);
|
||||
target.nodeChanged();
|
||||
updateContent(target.getContent());
|
||||
updateContent(serializeBlocks(target.getContent()));
|
||||
updateHistoryAvailability(target);
|
||||
}
|
||||
},
|
||||
@@ -691,7 +716,7 @@ function Studio(): JSX.Element {
|
||||
if (target) {
|
||||
target.insertContent(`${html}<p></p>`);
|
||||
target.nodeChanged();
|
||||
updateContent(target.getContent());
|
||||
updateContent(serializeBlocks(target.getContent()));
|
||||
updateHistoryAvailability(target);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -135,6 +135,7 @@ export function PayPanel({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pay.enabled}
|
||||
aria-label={__('开启付费内容', 'jio')}
|
||||
onChange={(event) => patch({ enabled: event.target.checked })}
|
||||
/>
|
||||
<span aria-hidden="true" />
|
||||
|
||||
@@ -201,32 +201,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 开关:44×24 的胶囊 + 20px 圆钮。
|
||||
*
|
||||
* 触发器是包住 input 的 label,尺寸与胶囊完全一致(行高会被 label 撑出
|
||||
* 3px 的空白,导致它在行内看起来偏下)。整块可点:点击胶囊任意位置都能
|
||||
* 切换,而不是只有那个 1px 的隐藏 input。
|
||||
*/
|
||||
.jio-creator-toggle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 42px;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: #d6dfda;
|
||||
transition: background 0.18s ease;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 2px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(24, 39, 34, 0.18);
|
||||
content: "";
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
@@ -237,13 +256,22 @@
|
||||
}
|
||||
|
||||
input:checked + span::after {
|
||||
transform: translateX(18px);
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
input:focus-visible + span {
|
||||
outline: 2px solid $accent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
input:disabled + span {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* 不可用时整块也不能点,避免看起来可切换。 */
|
||||
&:has(input:disabled) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.jio-creator-pay {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
@use './tokens' as *;
|
||||
@use './publish-drawer' as *;
|
||||
@use './block-tools';
|
||||
|
||||
|
||||
/*
|
||||
@@ -636,9 +637,15 @@ body.jio-page-submit.has-static-nav {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/*
|
||||
* 设置行:说明在左、控件在右。
|
||||
*
|
||||
* 控件顶对齐而不是垂直居中:说明文字折成两行时,居中的开关会悬在行中间,
|
||||
* 与标题对不上,看起来像是错位。
|
||||
*/
|
||||
.jio-creator__setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 16px 0;
|
||||
@@ -808,9 +815,6 @@ body.jio-page-submit.has-static-nav {
|
||||
}
|
||||
}
|
||||
|
||||
@import './_publish-drawer';
|
||||
@import './_block-tools';
|
||||
|
||||
/* ---------- 手机 / 平板 ---------- */
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -115,19 +115,22 @@
|
||||
--quote-color: #23790e;
|
||||
}
|
||||
|
||||
/* 代码块:与前台文章页一致,只用 pre > code,由 EnlighterJS 在发布页高亮。 */
|
||||
/* 代码块:与前台文章页一致(EnlighterJS 的 .enlighter-default 外观)。
|
||||
* 编辑器里是裸 pre,保存前才套回 pre > code,因此换行行为必须与前台一致:
|
||||
* 超长行横向滚动,不折行。 */
|
||||
pre {
|
||||
margin: 0 0 1.1em;
|
||||
padding: 14px 18px;
|
||||
margin: 0 0 20px;
|
||||
padding: 12px 17px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f2f4f5;
|
||||
color: #2b2b2b;
|
||||
font-family: Consolas, Monaco, "Courier New", monospace;
|
||||
background: #f8f9fa;
|
||||
color: #555;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.4;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
pre code {
|
||||
@@ -212,7 +215,8 @@ pre code {
|
||||
* 刻意不插 DOM 节点:插入的段落会被 setContent 清掉(首次加载不显示),
|
||||
* 也会占住首行、挡住点击空白处落光标。
|
||||
*
|
||||
* 绝对定位覆盖在空段落上,与原生 placeholder 的观感一致。
|
||||
* 绝对定位覆盖在空段落上,与原生 placeholder 的观感一致;点击正文(body
|
||||
* 获得焦点)后立即隐藏,否则提示会压在光标上,让人以为点击没生效。
|
||||
*/
|
||||
body[data-jio-empty="1"]::before {
|
||||
position: absolute;
|
||||
@@ -224,6 +228,10 @@ body[data-jio-empty="1"]::before {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body[data-jio-empty="1"]:focus::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* body 撑满编辑区:内容很短时点击正文下方的空白区域也应落光标,
|
||||
* 否则只有文字所在的那一行能点。
|
||||
|
||||
@@ -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/);
|
||||
|
||||
Reference in new issue
Block a user