feat(creator-studio): portal dropdowns, hidden content, two-step block delete
Toolbar - New ToolMenu renders menus through a portal on body, so they are no longer clipped by the horizontally scrolling toolbar and no longer fall back to a native <select>. Triggers reuse .jio-creator__tool-btn geometry, fixing the heading button's mismatched height/size/position. - Heading / list / align / quote / hidden-content are all dropdowns. - Lists: unordered + ordered via InsertOrderedList/InsertUnorderedList. The TOC button is removed (it also used the wrong icon). - Adds left / center / right alignment. - Adds hidden content: comment / login / VIP / paid, wired to the existing [reply], [jio_hide mode="login"], [vip] and [payshow] shortcodes. Editor content - Quote blocks now match the live article: full .quote_q structure, four colour variables, and an SVG-mask quote mark that does not depend on the Font Awesome font being present inside the iframe. - Block math is centred on a transparent background instead of a full-width grey bar. - Code blocks are plain <pre><code>, identical to the article page; language and theme selection is gone and EnlighterJS handles highlighting globally. The classic submit editor's code modal follows the same shape. - Hidden content reuses the .tinymce-hide structure and shows a readable label while keeping the shortcode text in the DOM for saving. Block deletion - Two-step delete, WPS style: the first click only selects and highlights the block, the second click removes it. Clicking elsewhere or Esc cancels. Contracts updated and extended; 884 widget-system tests pass.
This commit is contained in:
1 parent
3e48fdf61e
commit
c5bdbc0457
16 files changed
+1090
-545
No files matched your search
@@ -162,8 +162,8 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
|
||||
assert.match(appSource, /className="jio-creator__bar"/);
|
||||
assert.match(appSource, /undoManager\.undo\(\)/);
|
||||
assert.match(appSource, /undoManager\.redo\(\)/);
|
||||
assert.match(appSource, /editor\.formatter\.toggle\(value \|\| 'h2'\)/);
|
||||
assert.match(editorToolbar, /value=\{formatValue === 'blockquote' \? 'p' : formatValue\}/);
|
||||
assert.match(appSource, /execCommand\('FormatBlock', false, block\)/);
|
||||
assert.match(appSource, /JustifyCenter|JustifyRight|JustifyLeft/);
|
||||
assert.match(editorToolbar, /canUndo/);
|
||||
assert.match(editorToolbar, /canRedo/);
|
||||
assert.match(editorToolbar, /onCommand\('Bold'\)/);
|
||||
@@ -171,18 +171,39 @@ test('fixed top toolbar has TinyMCE formatting, media, and extension actions', (
|
||||
assert.match(editorToolbar, /onOpenMedia\('image'\)/);
|
||||
assert.match(editorToolbar, /onOpenMedia\('video'\)/);
|
||||
assert.match(editorToolbar, /onClick=\{onOpenLink\}/);
|
||||
// 引用不再是标题下拉的一部分,而是独立按钮(带样式菜单)。
|
||||
// 引用不再是标题下拉的一部分,而是独立菜单(四种配色)。
|
||||
assert.match(editorToolbar, /onInsertQuote/);
|
||||
assert.doesNotMatch(editorToolbar, /<option value="blockquote"/);
|
||||
assert.doesNotMatch(editorToolbar, /onCommand\('format', 'blockquote'\)/);
|
||||
for (const kind of ['list', 'toc', 'divider', 'code', 'footnote', 'math', 'table']) {
|
||||
// 标题级别 / 列表 / 对齐 / 引用 / 隐藏内容都是下拉,不再是原生 select。
|
||||
assert.doesNotMatch(editorToolbar, /jio-creator__format-select/);
|
||||
assert.doesNotMatch(editorToolbar, /<option value=/);
|
||||
assert.match(editorToolbar, /<ToolMenu/);
|
||||
for (const kind of ['divider', 'code', 'footnote', 'math', 'table']) {
|
||||
assert.ok(editorToolbar.includes(`onInsertBlock('${kind}')`), `missing block button: ${kind}`);
|
||||
}
|
||||
// 列表改为下拉:有序 / 无序两种,走 TinyMCE 的列表命令。
|
||||
assert.ok(editorToolbar.includes("key: 'ul'"), 'missing unordered list option');
|
||||
assert.ok(editorToolbar.includes("key: 'ol'"), 'missing ordered list option');
|
||||
assert.match(appSource, /InsertOrderedList|InsertUnorderedList/);
|
||||
// 目录按钮已删除,工具栏不再输出目录入口。
|
||||
assert.ok(!editorToolbar.includes("onInsertBlock('toc')"), 'TOC button must be removed');
|
||||
assert.ok(!editorToolbar.includes('tocMarkup'), 'tocMarkup must not be used');
|
||||
// 三个对齐按钮:居左 / 居中 / 居右。
|
||||
for (const align of ['left', 'center', 'right']) {
|
||||
assert.ok(editorToolbar.includes(`key: '${align}'`), `missing align option: ${align}`);
|
||||
}
|
||||
// 隐藏内容四档可见性。
|
||||
for (const kind of ['reply', 'login', 'vip', 'payshow']) {
|
||||
assert.ok(editorToolbar.includes(`key: '${kind}'`), `missing hidden-content kind: ${kind}`);
|
||||
}
|
||||
assert.match(editorToolbar, /onInsertHidden/);
|
||||
assert.match(appSource, /wrapSelection\(\(inner\) => hiddenContentMarkup\(kind, inner\)\)/);
|
||||
// 表格 / 公式 / 注释 / 代码都走参数弹窗,不再是固定内容。
|
||||
assert.match(appSource, /<BlockDialog/);
|
||||
assert.match(appSource, /setBlockDialog\(kind as BlockDialogKind\)/);
|
||||
// 代码块直接插入,不弹窗。
|
||||
assert.match(appSource, /insertHtml\(codeBlockMarkup\('generic'\)\)/);
|
||||
// 代码块直接插入,不弹窗、不选语言。
|
||||
assert.match(appSource, /insertHtml\(codeBlockMarkup\(\)\)/);
|
||||
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 \}\)/);
|
||||
@@ -199,10 +220,20 @@ 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\]/);
|
||||
assert.match(markup, /\[jio_toc\]/);
|
||||
// 代码块角标不再输出固定「高亮代码」文案。
|
||||
assert.doesNotMatch(read('assets/css/frontend/pages/submit-editor.css'), /高亮代码/);
|
||||
assert.doesNotMatch(read('assets/css/tinymce.css'), /content:\s*'高亮代码'/);
|
||||
// 代码块与前台文章页一致:pre > code,不写语言 / 主题属性。
|
||||
assert.match(markup, /<pre><code><\/code><\/pre>/);
|
||||
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 与前台投稿编辑器保持同一套代码块样式。
|
||||
const editorCss = read('assets/css/frontend/pages/submit-editor.css');
|
||||
assert.doesNotMatch(editorCss, /\.enlighter-pre/);
|
||||
assert.match(editorCss, /pre code \{/);
|
||||
// 隐藏内容沿用 .tinymce-hide 结构,短代码作为文本落库。
|
||||
assert.match(markup, /tinymce-hide/);
|
||||
assert.match(markup, /\[jio_hide mode="login"\]/);
|
||||
assert.match(markup, /\[payshow\]/);
|
||||
|
||||
const blocks = read('src/Content/ContentBlocks.php');
|
||||
const shortcodes = read('src/Core/Shortcodes.php');
|
||||
@@ -282,6 +313,63 @@ test('block delete overlay lives inside the editor iframe and stays hidden', ()
|
||||
assert.doesNotMatch(pageStyles, /\.jio-creator__block-actions\s*\{/);
|
||||
// data-mce-bogus 让 TinyMCE 序列化、字数和选区都跳过这个浮层。
|
||||
assert.match(runtime, /setAttribute\('data-mce-bogus', 'all'\)/);
|
||||
|
||||
// 两步删除:第一次点击只选中,第二次才真删;不再有关闭 / 叉号语义。
|
||||
const firstClick = runtime.indexOf('if (selected !== block) {');
|
||||
const secondClick = runtime.indexOf('// 第二次点击:真正删除。');
|
||||
assert.ok(firstClick !== -1 && secondClick !== -1, 'two-step delete branches both exist');
|
||||
assert.ok(firstClick < secondClick, 'first click only selects');
|
||||
assert.ok(
|
||||
runtime.indexOf('block.classList.add(SELECTED_CLASS)', firstClick) < secondClick,
|
||||
'selection class is applied on the first click'
|
||||
);
|
||||
assert.match(contentStyle, /\.jio-creator__block-selected\s*\{/);
|
||||
assert.match(runtime, /再次点击删除此块/);
|
||||
});
|
||||
|
||||
// 工具栏下拉:菜单必须 portal 到 body,否则会被横向滚动的工具条裁剪。
|
||||
test('toolbar dropdowns render through a portal with shared trigger geometry', () => {
|
||||
const menu = read('assets/creator-studio/src/components/tool-menu.tsx');
|
||||
const styles = read('assets/creator-studio/src/styles/studio.scss');
|
||||
const toolbar = read('assets/creator-studio/src/components/editor-toolbar.tsx');
|
||||
|
||||
assert.match(menu, /createPortal/);
|
||||
assert.match(menu, /document\.body/);
|
||||
// 定位写成内联 style,只保留 left / top / width。
|
||||
assert.match(menu, /style=\{\{\s*left:/);
|
||||
assert.match(styles, /\.jio-creator__menu\s*\{[^}]*position:\s*fixed/);
|
||||
assert.match(styles, /\.jio-creator__menu\s*\{[^}]*box-shadow/);
|
||||
// 下拉触发器复用 .jio-creator__tool-btn,高度 / 字号 / 定位与其它按钮一致。
|
||||
assert.doesNotMatch(styles, /\.jio-creator__format-field\s*\{/);
|
||||
assert.doesNotMatch(styles, /\.jio-creator__format-select\s*\{/);
|
||||
assert.doesNotMatch(toolbar, /jio-creator__quote-menu/);
|
||||
});
|
||||
|
||||
// 引用块 / 隐藏内容 / 公式的编辑器内表现。
|
||||
test('quote, hidden content, and block math render like the live article', () => {
|
||||
const contentStyle = read('assets/creator-studio/src/editor-content-style.ts');
|
||||
const markup = read('assets/creator-studio/src/utils/block-markup.ts');
|
||||
const css = read('assets/css/frontend/pages/submit-editor.css');
|
||||
|
||||
// 引用:四种配色都有独立变量,引号标记不再依赖 FA 字体。
|
||||
for (const cls of ['qe_wzk_c-red', 'qe_wzk_lan', 'qe_wzk_lv']) {
|
||||
assert.match(contentStyle, new RegExp(`\\.quote_q\\.${cls}`));
|
||||
assert.match(css, new RegExp(`\\.${cls}\\s*\\{`));
|
||||
}
|
||||
assert.match(contentStyle, /\.quote_q \.quote-mce::after|\.quote_q\.quote-mce::after/);
|
||||
assert.match(contentStyle, /-webkit-mask/);
|
||||
// 引用保持整块可识别,内部仍可直接编辑。
|
||||
assert.match(markup, /class="\$\{classes\.join\(' '\)\}" contenteditable="false"><div contenteditable="true">/);
|
||||
|
||||
// 隐藏内容:短代码文本被 CSS 隐藏,只显示可读标签。
|
||||
assert.match(contentStyle, /\.tinymce-hide > \.hide-before::before/);
|
||||
assert.match(contentStyle, /content: attr\(data-jio-hide-label\)/);
|
||||
assert.match(css, /content: attr\(data-jio-hide-label\)/);
|
||||
|
||||
// 公式独占一行时居中,不再铺满一条灰底横条。
|
||||
assert.match(contentStyle, /\.jio-math--block\s*\{[^}]*text-align:\s*center/);
|
||||
assert.match(contentStyle, /\.jio-math--block\s*\{[^}]*background:\s*transparent/);
|
||||
assert.doesNotMatch(contentStyle, /\.jio-math--block\s*\{[^}]*background:\s*#/);
|
||||
});
|
||||
|
||||
test('creator studio build artifacts and contracts exist', () => {
|
||||
|
||||
@@ -224,16 +224,19 @@ test('front submit exposes the complete reference media and formatting toolset',
|
||||
assert.match(editor, /new-dplayer post-dplayer dplayer/);
|
||||
assert.match(editor, /file-download-box/);
|
||||
assert.match(editor, /wp-block-embed is-type-video/);
|
||||
assert.match(editor, /data-enlighter-language/);
|
||||
assert.match(editor, /data-enlighter-theme/);
|
||||
assert.match(editor, /enlighter-pre/);
|
||||
// 代码块与前台文章页一致(pre > code),语言由全局代码高亮设置决定,编辑器不再选语言 / 主题。
|
||||
assert.match(editor, /'<pre><code>' \+ encoded \+ '<\/code><\/pre>'/);
|
||||
assert.doesNotMatch(editor, /data-enlighter-language/);
|
||||
assert.doesNotMatch(editor, /data-enlighter-theme/);
|
||||
assert.doesNotMatch(editor, /enlighter-pre/);
|
||||
assert.match(editor, /qe_wzk_c-red/);
|
||||
assert.match(editor, /qe_wzk_lan/);
|
||||
assert.match(editor, /qe_wzk_lv/);
|
||||
assert.match(editorCss, /\.quote_q/);
|
||||
assert.match(editorCss, /\.quote_q \[contenteditable="true"\][\s\S]*?box-shadow:\s*none/);
|
||||
assert.match(editorCss, /\.file-download-box/);
|
||||
assert.match(editorCss, /\.enlighter-pre/);
|
||||
assert.match(editorCss, /pre code \{/);
|
||||
assert.doesNotMatch(editorCss, /\.enlighter-pre/);
|
||||
assert.doesNotMatch(editor, /shop|forum|bbs_/i);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user