refactor(submit): drop the hover block-delete overlay
Hovering a quote, hidden block, code block or table showed a ✕ button in the block's top-right corner. It read as a stray close button and is not wanted. Removed - installBlockActions() and the two-step select/delete flow it drove - the block-actions markup, positioning, selected-state CSS in the iframe content style - BLOCK_SELECTOR, which only the overlay used Deleting blocks still works: removeAdjacentBlock() keeps Backspace / Delete next to a structural block (table, pre, hr, quote, hidden block) working, and that is covered by both code and tests.
This commit is contained in:
1 parent
49f1212e33
commit
a061ec86ce
6 files changed
+25
-288
No files matched your search
@@ -2,8 +2,7 @@
|
|||||||
* TinyMCE iframe 内的正文样式。
|
* TinyMCE iframe 内的正文样式。
|
||||||
*
|
*
|
||||||
* 编辑器正文跑在 iframe 里,页面级 SCSS 影响不到它,所以这里显式给出:
|
* 编辑器正文跑在 iframe 里,页面级 SCSS 影响不到它,所以这里显式给出:
|
||||||
* 正文排版、主题引用 .quote_q、隐藏内容 .tinymce-hide、表格、公式、
|
* 正文排版、主题引用 .quote_q、隐藏内容 .tinymce-hide、表格、公式。
|
||||||
* 块级删除控件与块选中态。
|
|
||||||
*
|
*
|
||||||
* @package Jio
|
* @package Jio
|
||||||
*/
|
*/
|
||||||
@@ -53,66 +52,6 @@ h1, h2, h3, h4, h5, h6 { margin: 1.5em 0 0.65em; line-height: 1.4; }
|
|||||||
img, iframe { max-width: 100%; }
|
img, iframe { max-width: 100%; }
|
||||||
img { height: auto; }
|
img { height: auto; }
|
||||||
|
|
||||||
/*
|
|
||||||
* 块级删除控件。它由 editor-runtime 插进 iframe 的 body,样式必须写在这里:
|
|
||||||
* 父页面样式表管不到 iframe 内部,缺这条规则时按钮会退化成正文顶部的常显小方块。
|
|
||||||
* 正文是流式布局且 iframe 高度随内容增长,所以按整篇正文定位,而不是相对某个块。
|
|
||||||
*/
|
|
||||||
#jio-block-actions {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 5;
|
|
||||||
top: 6px;
|
|
||||||
left: 6px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
#jio-block-actions.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
#jio-block-actions .jio-creator__block-action {
|
|
||||||
display: inline-flex;
|
|
||||||
height: 26px;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 0 9px;
|
|
||||||
border: 1px solid #e3e7e5;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fff;
|
|
||||||
color: #7d8683;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 2px 8px rgba(24, 39, 34, 0.08);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#jio-block-actions .jio-creator__block-action-icon {
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
#jio-block-actions .jio-creator__block-action:hover {
|
|
||||||
border-color: #cdd6d2;
|
|
||||||
color: #4f5957;
|
|
||||||
}
|
|
||||||
#jio-block-actions.is-selected .jio-creator__block-action {
|
|
||||||
border-color: #e0a99b;
|
|
||||||
background: #fdf2ee;
|
|
||||||
color: #b4573f;
|
|
||||||
}
|
|
||||||
#jio-block-actions.is-selected .jio-creator__block-action:hover {
|
|
||||||
border-color: #cf8574;
|
|
||||||
background: #fbe7e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 第一次点击选中的块:虚线高亮,明确「还没删」。 */
|
|
||||||
.jio-creator__block-selected {
|
|
||||||
outline: 2px dashed rgba(180, 87, 63, 0.55);
|
|
||||||
outline-offset: 3px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 代码块:与前台文章页一致。
|
* 代码块:与前台文章页一致。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* TinyMCE 编辑器增强:块级两步删除、自适应高度。
|
* TinyMCE 编辑器增强:块级删除键、自适应高度。
|
||||||
*
|
*
|
||||||
* 这些行为都作用在编辑器 iframe 内部,与 React 状态无关,因此独立成模块。
|
* 这些行为都作用在编辑器 iframe 内部,与 React 状态无关,因此独立成模块。
|
||||||
*
|
*
|
||||||
* @package Jio
|
* @package Jio
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 可整体删除的块级元素选择器。 */
|
|
||||||
const BLOCK_SELECTOR = 'table,pre,hr,.quote_q,.tinymce-hide,p';
|
|
||||||
|
|
||||||
/** 结构块(不能从内部被拆开的块)。 */
|
/** 结构块(不能从内部被拆开的块)。 */
|
||||||
const STRUCTURAL_SELECTOR = 'table,pre,hr,.quote_q,.tinymce-hide,.jio-math--block';
|
const STRUCTURAL_SELECTOR = 'table,pre,hr,.quote_q,.tinymce-hide,.jio-math--block';
|
||||||
|
|
||||||
@@ -123,7 +120,7 @@ export function syncEditorHeight(editor: any): void {
|
|||||||
* 变高。这里在 iframe 内监听资源加载与尺寸变化,加载完成后重新同步一次,
|
* 变高。这里在 iframe 内监听资源加载与尺寸变化,加载完成后重新同步一次,
|
||||||
* 否则图片下方的段落会被 iframe 的固定高度裁掉。
|
* 否则图片下方的段落会被 iframe 的固定高度裁掉。
|
||||||
*
|
*
|
||||||
* 只在 body 上挂一次(installBlockActions 同一条 init 路径),避免重复监听。
|
* 只在 body 上挂一次(与高度同步共用同一条 init 路径),避免重复监听。
|
||||||
*/
|
*/
|
||||||
export function installHeightWatchers(editor: any): void {
|
export function installHeightWatchers(editor: any): void {
|
||||||
const doc = editor?.getDoc?.();
|
const doc = editor?.getDoc?.();
|
||||||
@@ -150,186 +147,3 @@ export function installHeightWatchers(editor: any): void {
|
|||||||
new (window as any).ResizeObserver(() => syncEditorHeight(editor)).observe(doc.body);
|
new (window as any).ResizeObserver(() => syncEditorHeight(editor)).observe(doc.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 块选中状态写在块自身的 class 上,跨 iframe 重排也能保留。 */
|
|
||||||
const SELECTED_CLASS = 'jio-creator__block-selected';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 给表格 / 代码块 / 引用等块渲染一个浮层删除按钮。
|
|
||||||
*
|
|
||||||
* 与 WPS 一致的两步删除:第一次点击只是选中该块并高亮,第二次点击才真正删除。
|
|
||||||
* 选中态下按钮改为醒目的「再次点击删除」,且鼠标移开不会消失,避免误删。
|
|
||||||
*
|
|
||||||
* 浮层挂在 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();
|
|
||||||
if (!doc || doc.getElementById('jio-block-actions')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bar = doc.createElement('div');
|
|
||||||
bar.id = 'jio-block-actions';
|
|
||||||
bar.className = 'jio-creator__block-actions';
|
|
||||||
// "all" 让 TinyMCE 连同子节点一起跳过。
|
|
||||||
bar.setAttribute('data-mce-bogus', 'all');
|
|
||||||
bar.innerHTML =
|
|
||||||
'<button type="button" class="jio-creator__block-action" data-jio-block-delete>' +
|
|
||||||
'<span class="jio-creator__block-action-icon" aria-hidden="true">✕</span>' +
|
|
||||||
'<span class="jio-creator__block-action-text"></span>' +
|
|
||||||
'</button>';
|
|
||||||
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');
|
|
||||||
let hovered: HTMLElement | null = null;
|
|
||||||
let selected: HTMLElement | null = null;
|
|
||||||
|
|
||||||
/** 底部状态栏以外的提示文字。 */
|
|
||||||
const labelFor = (state: 'idle' | 'selected'): string =>
|
|
||||||
state === 'selected' ? '再次点击删除此块' : '选中此块';
|
|
||||||
|
|
||||||
const paint = (state: 'idle' | 'selected'): void => {
|
|
||||||
bar.classList.toggle('is-selected', state === 'selected');
|
|
||||||
if (text) {
|
|
||||||
text.textContent = labelFor(state);
|
|
||||||
}
|
|
||||||
const label = labelFor(state);
|
|
||||||
button?.setAttribute('title', label);
|
|
||||||
button?.setAttribute('aria-label', label);
|
|
||||||
};
|
|
||||||
|
|
||||||
const place = (block: HTMLElement): void => {
|
|
||||||
const rect = block.getBoundingClientRect();
|
|
||||||
// 右上角贴住块,但不越出正文左边界(窄块时按钮会溢出)。
|
|
||||||
const left = Math.max(rect.left + 8, rect.right - 96);
|
|
||||||
bar.style.top = `${rect.top + doc.documentElement.scrollTop + 6}px`;
|
|
||||||
bar.style.left = `${left + doc.documentElement.scrollLeft}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const show = (block: HTMLElement | null): void => {
|
|
||||||
if (!block || block === doc.body) {
|
|
||||||
bar.classList.remove('is-visible');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 空段落没有删除价值,交给退格键处理。
|
|
||||||
if (block.tagName === 'P' && (block.textContent || '').trim() === '' && !block.querySelector('img,iframe,table,pre')) {
|
|
||||||
bar.classList.remove('is-visible');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hovered = block;
|
|
||||||
place(block);
|
|
||||||
bar.classList.add('is-visible');
|
|
||||||
};
|
|
||||||
|
|
||||||
const deselect = (): void => {
|
|
||||||
if (selected) {
|
|
||||||
selected.classList.remove(SELECTED_CLASS);
|
|
||||||
}
|
|
||||||
selected = null;
|
|
||||||
paint('idle');
|
|
||||||
if (hovered) {
|
|
||||||
show(hovered);
|
|
||||||
} else {
|
|
||||||
bar.classList.remove('is-visible');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
doc.addEventListener('mouseover', (event: MouseEvent) => {
|
|
||||||
const block = (event.target as HTMLElement)?.closest?.(BLOCK_SELECTOR) as HTMLElement | null;
|
|
||||||
if (!block || block === doc.body) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 选中块时保持浮层贴在选中块上,不受悬停目标影响。
|
|
||||||
if (selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
show(block);
|
|
||||||
});
|
|
||||||
|
|
||||||
doc.addEventListener('mouseleave', () => {
|
|
||||||
hovered = null;
|
|
||||||
if (selected) {
|
|
||||||
place(selected);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bar.classList.remove('is-visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
bar.addEventListener('mousedown', (event: MouseEvent) => event.preventDefault());
|
|
||||||
const onClick = (event: MouseEvent): void => {
|
|
||||||
const target = (event.target as HTMLElement)?.closest('[data-jio-block-delete]');
|
|
||||||
const block = selected || hovered;
|
|
||||||
if (!target || !block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (selected !== block) {
|
|
||||||
// 第一次点击:只选中,不删除。
|
|
||||||
if (selected) {
|
|
||||||
selected.classList.remove(SELECTED_CLASS);
|
|
||||||
}
|
|
||||||
selected = block;
|
|
||||||
block.classList.add(SELECTED_CLASS);
|
|
||||||
paint('selected');
|
|
||||||
place(block);
|
|
||||||
bar.classList.add('is-visible');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第二次点击:真正删除。
|
|
||||||
const victim = block;
|
|
||||||
const parent = victim.parentElement;
|
|
||||||
victim.classList.remove(SELECTED_CLASS);
|
|
||||||
selected = null;
|
|
||||||
hovered = null;
|
|
||||||
bar.classList.remove('is-visible', 'is-selected');
|
|
||||||
paint('idle');
|
|
||||||
editor.undoManager.transact(() => {
|
|
||||||
editor.dom.remove(victim);
|
|
||||||
});
|
|
||||||
if (editor.getBody().children.length === 0) {
|
|
||||||
editor.setContent('<p></p>');
|
|
||||||
}
|
|
||||||
if (parent && parent.children.length === 0) {
|
|
||||||
editor.setContent('<p></p>');
|
|
||||||
}
|
|
||||||
const first = editor.getBody().firstChild;
|
|
||||||
if (first) {
|
|
||||||
editor.selection.setCursorLocation(first, 0);
|
|
||||||
}
|
|
||||||
editor.nodeChanged();
|
|
||||||
editor.undoManager.add();
|
|
||||||
syncEditorHeight(editor);
|
|
||||||
};
|
|
||||||
bar.addEventListener('click', onClick);
|
|
||||||
|
|
||||||
// 点击正文其它位置或按 Esc 取消选中态。
|
|
||||||
doc.addEventListener('mousedown', (event: MouseEvent) => {
|
|
||||||
if (bar.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selected) {
|
|
||||||
deselect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
doc.addEventListener('keydown', (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape' && selected) {
|
|
||||||
deselect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 内容被撤销 / 重排后,被选中的块可能已不在文档里。
|
|
||||||
editor.on('Undo Redo SetContent', () => {
|
|
||||||
if (selected && !doc.body.contains(selected)) {
|
|
||||||
selected = null;
|
|
||||||
paint('idle');
|
|
||||||
bar.classList.remove('is-visible', 'is-selected');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
|
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
|
||||||
import { type Draft, type Limits, initialDraft } from './draft';
|
import { type Draft, type Limits, initialDraft } from './draft';
|
||||||
import {
|
import {
|
||||||
installBlockActions,
|
|
||||||
installHeightWatchers,
|
installHeightWatchers,
|
||||||
removeAdjacentBlock,
|
removeAdjacentBlock,
|
||||||
syncEditorHeight,
|
syncEditorHeight,
|
||||||
@@ -445,7 +444,6 @@ function Studio(): JSX.Element {
|
|||||||
editor.setContent(hydrateBlocks(editor.getContent()));
|
editor.setContent(hydrateBlocks(editor.getContent()));
|
||||||
}
|
}
|
||||||
syncEditorHeight(editor);
|
syncEditorHeight(editor);
|
||||||
installBlockActions(editor);
|
|
||||||
installHeightWatchers(editor);
|
installHeightWatchers(editor);
|
||||||
updateHistoryAvailability(editor);
|
updateHistoryAvailability(editor);
|
||||||
updateFormatValue(editor);
|
updateFormatValue(editor);
|
||||||
|
|||||||
@@ -48,9 +48,8 @@
|
|||||||
/**
|
/**
|
||||||
* 正文自身的副本:剔除 TinyMCE 的辅助节点。
|
* 正文自身的副本:剔除 TinyMCE 的辅助节点。
|
||||||
*
|
*
|
||||||
* 编辑器会把块级删除浮层(#jio-block-actions)之类带文字的工具节点插进
|
* 编辑器会把带文字的工具节点插进 body,它们都带 data-mce-bogus。直接读
|
||||||
* body,它们都带 data-mce-bogus。直接读 body.textContent 会把「选中此块」
|
* body.textContent 会把工具文案当成正文,导致空正文永远判成非空、占位符不显示。
|
||||||
* 当成正文,导致空正文永远判成非空、占位符不显示。
|
|
||||||
*/
|
*/
|
||||||
function contentClone(body) {
|
function contentClone(body) {
|
||||||
var clone = body.cloneNode(true);
|
var clone = body.cloneNode(true);
|
||||||
|
|||||||
@@ -312,38 +312,25 @@ test('top bar is masthead + centered toolbar, with no brand text in the toolbar
|
|||||||
assert.doesNotMatch(rest, /'home'\s*=>\s*esc_url_raw\(\$home\)/);
|
assert.doesNotMatch(rest, /'home'\s*=>\s*esc_url_raw\(\$home\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('block delete overlay lives outside the editable body and stays hidden', () => {
|
// 块右上角的悬停删除浮层已移除:删除统一走键盘(退格 / Delete)与工具栏。
|
||||||
|
// 浮层留在这里会被误当成「不需要的关闭按钮」,也曾在 body 里破坏 mceInsertContent。
|
||||||
|
test('hover block-delete overlay is gone', () => {
|
||||||
const runtime = read('assets/creator-studio/src/editor-runtime.ts');
|
const runtime = read('assets/creator-studio/src/editor-runtime.ts');
|
||||||
const contentStyle = read('assets/creator-studio/src/editor-content-style.ts');
|
const contentStyle = read('assets/creator-studio/src/editor-content-style.ts');
|
||||||
const pageStyles = read('assets/creator-studio/src/styles/_block-tools.scss');
|
const app = read('assets/creator-studio/src/index.tsx');
|
||||||
|
|
||||||
// 浮层挂在 iframe 的 documentElement 上,不能进 body:data-mce-bogus 只跳过
|
assert.doesNotMatch(runtime, /jio-block-actions/);
|
||||||
// 序列化,body 里多一个非常规子节点会让 mceInsertContent 把整篇正文清空
|
assert.doesNotMatch(runtime, /jio-creator__block-actions/);
|
||||||
// (表现为「第一次点隐藏块没有插进正文」)。
|
assert.doesNotMatch(runtime, /installBlockActions/);
|
||||||
assert.match(runtime, /doc\.documentElement\.appendChild\(bar\)/);
|
assert.doesNotMatch(runtime, /BLOCK_SELECTOR/);
|
||||||
assert.doesNotMatch(runtime, /doc\.body\.appendChild\(bar\)/);
|
assert.doesNotMatch(contentStyle, /jio-block-actions/);
|
||||||
assert.match(runtime, /bar\.id = 'jio-block-actions'/);
|
assert.doesNotMatch(contentStyle, /jio-creator__block-selected/);
|
||||||
// iframe 是独立文档:页面级样式表管不到它,隐藏规则必须写在 content_style。
|
assert.doesNotMatch(app, /installBlockActions/);
|
||||||
assert.match(contentStyle, /#jio-block-actions\s*\{[^}]*position:\s*absolute/);
|
|
||||||
assert.match(contentStyle, /#jio-block-actions\s*\{[^}]*opacity:\s*0/);
|
|
||||||
assert.match(contentStyle, /#jio-block-actions\.is-visible\s*\{[^}]*opacity:\s*1/);
|
|
||||||
assert.match(contentStyle, /body\s*\{[^}]*position:\s*relative/);
|
|
||||||
// 页面级 SCSS 不再重复定义 iframe 内部的选择器(否则只是死样式)。
|
|
||||||
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) {');
|
assert.match(runtime, /export function removeAdjacentBlock/);
|
||||||
const secondClick = runtime.indexOf('// 第二次点击:真正删除。');
|
assert.match(runtime, /STRUCTURAL_SELECTOR/);
|
||||||
assert.ok(firstClick !== -1 && secondClick !== -1, 'two-step delete branches both exist');
|
assert.match(app, /removeAdjacentBlock\(editor, event\)/);
|
||||||
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,否则会被横向滚动的工具条裁剪。
|
// 工具栏下拉:菜单必须 portal 到 body,否则会被横向滚动的工具条裁剪。
|
||||||
|
|||||||
@@ -140,13 +140,13 @@ test('placeholder never becomes a DOM node in the editable body', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('editor chrome injected into the body does not count as content', () => {
|
test('editor chrome injected into the body does not count as content', () => {
|
||||||
// 真实页面上 editor-runtime 会把块删除浮层插进 body,它带 data-mce-bogus
|
// TinyMCE 会往 body 里插带 data-mce-bogus 的工具节点,其中可能含文字。
|
||||||
// 但含文字(「✕ 选中此块」)。若按 body.textContent 判断,空正文永远非空,
|
// 若按 body.textContent 判断,空正文永远非空,占位符就永远不显示 ——
|
||||||
// 占位符就永远不显示 —— 这正是「首次加载不显示」的真实原因。
|
// 这正是「首次加载不显示」的真实原因。
|
||||||
const { doc, editor } = createEditor(
|
const { doc, editor } = createEditor(
|
||||||
'<p><br data-mce-bogus="1"></p>' +
|
'<p><br data-mce-bogus="1"></p>' +
|
||||||
'<div id="jio-block-actions" data-mce-bogus="all">' +
|
'<div data-mce-bogus="all">' +
|
||||||
'<button type="button"><span>✕</span><span>选中此块</span></button></div>',
|
'<button type="button"><span>✕</span><span>工具节点</span></button></div>',
|
||||||
{ placeholder: '请输入正文' }
|
{ placeholder: '请输入正文' }
|
||||||
);
|
);
|
||||||
const plugin = loadPlugin()[PLUGIN];
|
const plugin = loadPlugin()[PLUGIN];
|
||||||
|
|||||||
Reference in new issue
Block a user