- 视频可拖动大小
- 表格可移除编辑区域
This commit is contained in:
yanmao 2021-12-16 00:30:58 +08:00
parent c46845d865
commit f84824aba8
58 changed files with 1503 additions and 449 deletions

View File

@ -72,7 +72,7 @@
display: flex;
> span {
position: relative;
margin-left: 40px;
margin-left: 24px;
display: inline-block;
color: @c-text;
height: @s-nav-height;
@ -113,7 +113,7 @@
}
+ *:not(a) {
margin-left: 40px;
margin-left: 24px;
}
// second nav

2
.gitignore vendored
View File

@ -23,3 +23,5 @@
# log
*.log
.vscode
**/ffmpeg

View File

@ -31,47 +31,7 @@ new Engine(...,{
})
```
`defaultData`: Default drop-down query list display data
`onSearch`: the method to query, or configure the action, choose one of the two
`onSelect`: Call back after selecting an item in the list, here you can return a custom value combined with key and name to form a new value and store it in cardValue. And it will return together after executing the getList command
`onClick`: Triggered when clicking on the "mention"
`onMouseEnter`: Triggered when the mouse moves over the "mention"
`onRender`: custom rendering list
`onRenderItem`: custom rendering list item
`onLoading`: custom rendering loading status
`onEmpty`: custom render empty state
`action`: query address, always use `GET` request, parameter `keyword`
`data`: When querying, these data will be sent to the server at the same time
```ts
//List data displayed by default
defaultData?: Array<{ key: string, name: string, avatar?: string}>
//Method for query, or configure action, choose one of the two
onSearch?:(keyword: string) => Promise<Array<{ key: string, name: string, avatar?: string}>>
//Call back after selecting an item in the list, here you can return a custom value combined with key and name to form a new value and store it in cardValue. And it will return together after executing the getList command
onSelect?: (data: {[key:string]: string}) => void | {[key: string]: string}
//Click event on "mention"
onClick?:(data: {[key:string]: string}) => void
// Triggered when the mouse moves over the "mention"
onMouseEnter?:(node: NodeInterface, data: {[key:string]: string}) => void
//Customize the rendering list, bindItem can bind the required properties and events for the list item
onRender?: (data: MentionItem, root: NodeInterface, bindItem: (node: NodeInterface, data: {[key:string]: string}) => NodeInterface) => Promise<string | NodeInterface | void>;
//Custom rendering list items
onRenderItem?: (item: MentionItem, root: NodeInterface) => string | NodeInterface | void
// Customize the rendering loading status
onLoading?: (root: NodeInterface) => string | NodeInterface | void
// Custom render empty state
onEmpty?: (root: NodeInterface) => string | NodeInterface | void
/**
* look for the address
*/
@ -124,3 +84,136 @@ Get all mentions in the document
//Return Array<{ key: string, name: string}>
engine.command.executeMethod('mention', 'getList');
```
## Plug-in events
`mention:default`: default drop-down query list to display data
```ts
this.engine.on('mention:default', () => {
return [];
});
```
`mention:search`: Method of query, or configure action, choose one of the two
```ts
this.engine.on('mention:search', (keyword) => {
return new Promise((resolve) => {
query({ keyword })
.then((result) => {
resolve(result);
})
.catch(() => resolve([]));
});
});
```
`mention:select`: Call back after selecting an item in the list, here you can return a custom value combined with key and name to form a new value and store it in cardValue. And will return together after executing the getList command
```ts
this.engine.on('mention:select', (data) => {
data['test'] = 'test';
return data;
});
```
`mention:item-click`: triggered when clicking on "mention"
```ts
this.engine.on(
'mention:item-click',
(root: NodeInterface, { key, name }: { key: string; name: string }) => {
console.log('mention click:', key, '-', name);
},
);
```
`mention:enter`: Triggered when the mouse moves over the "mention"
```ts
this.engine.on(
'mention:enter',
(layout: NodeInterface, { name }: { key: string; name: string }) => {
ReactDOM.render(
<div style={{ padding: 5 }}>
<p>This is name: {name}</p>
<p>Configure the mention:enter event of the mention plugin</p>
<p>Use ReactDOM.render to customize rendering here</p>
<p>Use ReactDOM.render to customize rendering here</p>
</div>,
layout.get<HTMLElement>()!,
);
},
);
```
`mention:render`: custom rendering list
```ts
this.engine.on(
'mention:render',
(
root: NodeInterface,
data: Array<MentionItem>,
bindItem: (
node: NodeInterface,
data: { [key: string]: string },
) => NodeInterface,
) => {
return new Promise<void>((resolve) => {
const renderCallback = (items: { [key: string]: Element }) => {
// Traverse the DOM node of each item
Object.keys(items).forEach((key) => {
const element = items[key];
const item = data.find((d) => d.key === key);
if (!item) return;
// Bind the attributes and events of each list item to meet the functional needs of the up, down, left, and right selection in the editor
bindItem($(element), item);
});
resolve();
};
ReactDOM.render(
<MentionList data={data} callback={renderCallback} />,
root.get<HTMLElement>()!,
);
});
},
);
```
`mention:render-item`: custom rendering list item
```ts
this.engine.on('mention:render-item', (data, root) => {
const item = $(`<div>${data}</div>`);
root.append(item);
return item;
});
```
`mention:loading`: custom rendering loading status
```ts
this.engine.on('mention:loading', (data, root) => {
root.html(`<div>${data}</div>`);
// or
ReactDOM.render(
<div className="data-mention-loading">Loading...</div>,
root.get<HTMLElement>()!,
);
});
```
`mention:empty`: custom render empty state
```ts
this.engine.on('mention:empty', (root) => {
root.html('<div>No data found</div>');
// or
ReactDOM.render(
<div className="data-mention-empty">Empty</div>,
root.get<HTMLElement>()!,
);
});
```

View File

@ -31,49 +31,9 @@ new Engine(...,{
})
```
`defaultData`: 默认下拉查询列表展示数据
`onSearch`: 查询时的方法,或者配置 action二选其一
`onSelect`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
`onClick`: 在“提及”上单击时触发
`onMouseEnter`: 鼠标移入“提及”上时触发
`onRender`: 自定义渲染列表
`onRenderItem`: 自定义渲染列表项
`onLoading`: 自定渲染加载状态
`onEmpty`: 自定渲染空状态
`action`: 查询地址,始终使用 `GET` 请求,参数 `keyword`
`data`: 查询时同时将这些数据一起传到到服务端
```ts
//默认展示的列表数据
defaultData?: Array<{ key: string, name: string, avatar?: string}>
//查询时的方法,或者配置 action二选其一
onSearch?:(keyword: string) => Promise<Array<{ key: string, name: string, avatar?: string}>>
//选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
onSelect?: (data: {[key:string]: string}) => void | {[key: string]: string}
//在“提及”上单击事件
onClick?:(data: {[key:string]: string}) => void
//鼠标移入“提及”上时触发
onMouseEnter?:(node: NodeInterface, data: {[key:string]: string}) => void
//自定义渲染列表bindItem 可以为列表项绑定需要的属性和事件
onRender?: (data: MentionItem, root: NodeInterface, bindItem: (node: NodeInterface, data: {[key:string]: string}) => NodeInterface) => Promise<string | NodeInterface | void>;
//自定义渲染列表项
onRenderItem?: (item: MentionItem, root: NodeInterface) => string | NodeInterface | void
// 自定渲染加载状态
onLoading?: (root: NodeInterface) => string | NodeInterface | void
// 自定渲染空状态
onEmpty?: (root: NodeInterface) => string | NodeInterface | void
/**
* 查询地址
* 查询地址,或者监听 mention:search 事件执行查询
*/
action?: string;
/**
@ -124,3 +84,136 @@ parse?: (
//返回 Array<{ key: string, name: string}>
engine.command.executeMethod('mention', 'getList');
```
## 插件事件
`mention:default`: 默认下拉查询列表展示数据
```ts
this.engine.on('mention:default', () => {
return [];
});
```
`mention:search`: 查询时的方法,或者配置 action二选其一
```ts
this.engine.on('mention:search', (keyword) => {
return new Promise((resolve) => {
query({ keyword })
.then((result) => {
resolve(result);
})
.catch(() => resolve([]));
});
});
```
`mention:select`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
```ts
this.engine.on('mention:select', (data) => {
data['test'] = 'test';
return data;
});
```
`mention:item-click`: 在“提及”上单击时触发
```ts
this.engine.on(
'mention:item-click',
(root: NodeInterface, { key, name }: { key: string; name: string }) => {
console.log('mention click:', key, '-', name);
},
);
```
`mention:enter`: 鼠标移入“提及”上时触发
```ts
this.engine.on(
'mention:enter',
(layout: NodeInterface, { name }: { key: string; name: string }) => {
ReactDOM.render(
<div style={{ padding: 5 }}>
<p>This is name: {name}</p>
<p>配置 mention 插件的 mention:enter 事件</p>
<p>此处使用 ReactDOM.render 自定义渲染</p>
<p>Use ReactDOM.render to customize rendering here</p>
</div>,
layout.get<HTMLElement>()!,
);
},
);
```
`mention:render`: 自定义渲染列表
```ts
this.engine.on(
'mention:render',
(
root: NodeInterface,
data: Array<MentionItem>,
bindItem: (
node: NodeInterface,
data: { [key: string]: string },
) => NodeInterface,
) => {
return new Promise<void>((resolve) => {
const renderCallback = (items: { [key: string]: Element }) => {
// 遍历每个项的DOM节点
Object.keys(items).forEach((key) => {
const element = items[key];
const item = data.find((d) => d.key === key);
if (!item) return;
// 绑定每个列表项所属的属性、事件,以满足编辑器中上下左右选择的功能需要
bindItem($(element), item);
});
resolve();
};
ReactDOM.render(
<MentionList data={data} callback={renderCallback} />,
root.get<HTMLElement>()!,
);
});
},
);
```
`mention:render-item`: 自定义渲染列表项
```ts
this.engine.on('mention:render-item', (data, root) => {
const item = $(`<div>${data}</div>`);
root.append(item);
return item;
});
```
`mention:loading`: 自定渲染加载状态
```ts
this.engine.on('mention:loading', (data, root) => {
root.html(`<div>${data}</div>`);
// or
ReactDOM.render(
<div className="data-mention-loading">Loading...</div>,
root.get<HTMLElement>()!,
);
});
```
`mention:empty`: 自定渲染空状态
```ts
this.engine.on('mention:empty', (root) => {
root.html('<div>没有查询到数据</div>');
// or
ReactDOM.render(
<div className="data-mention-empty">Empty</div>,
root.get<HTMLElement>()!,
);
});
```

View File

@ -41,6 +41,17 @@ new Engine(...,{
})
```
### Overflow display
```ts
overflow?: {
// Relative to the maximum displayable width on the left side of the editor
maxLeftWidth?: () => number;
// Relative to the maximum displayable width on the right side of the editor
maxRightWidth?: () => number;
};
```
## Command
```ts

View File

@ -41,6 +41,17 @@ new Engine(...,{
})
```
### 溢出展示
```ts
overflow?: {
// 相对编辑器左侧最大能展示的宽度
maxLeftWidth?: () => number;
// 相对于编辑器右侧最大能展示的宽度
maxRightWidth?: () => number;
};
```
## 命令
```ts

View File

@ -40,6 +40,14 @@ new Engine(...,{
})
```
### Whether to display the video title
Default Display
```ts
showTitle?: boolean
```
### File Upload
`action`: upload address, always use `POST` request

View File

@ -40,6 +40,14 @@ new Engine(...,{
})
```
### 是否显示视频标题
默认显示
```ts
showTitle?: boolean
```
### 文件上传
`action`: 上传地址,始终使用 `POST` 请求

View File

@ -44,9 +44,9 @@
.doc-comment-layer {
position: absolute;
top: 20px;
width: calc(50% - 454px);
left: calc(50% + 428px);
padding-left: 24px;
padding-left: 16px;
min-width: 260px;
right: 16px;
}
.doc-comment-title {

View File

@ -3,6 +3,7 @@ import {
CardEntry,
PluginOptions,
NodeInterface,
$,
} from '@aomao/engine';
//引入插件 begin
import Redo from '@aomao/plugin-redo';
@ -43,7 +44,6 @@ import LineHeight from '@aomao/plugin-line-height';
import Mention, { MentionComponent } from '@aomao/plugin-mention';
import Embed, { EmbedComponent } from '@aomao/plugin-embed';
import Test, { TestComponent } from './plugins/test';
//import Mind, { MindComponent } from '@aomao/plugin-mind';
import {
ToolbarPlugin,
ToolbarComponent,
@ -98,7 +98,6 @@ export const plugins: Array<PluginEntry> = [
Mention,
Embed,
Test,
//Mind
];
export const cards: Array<CardEntry> = [
@ -115,10 +114,35 @@ export const cards: Array<CardEntry> = [
MentionComponent,
TestComponent,
EmbedComponent,
//MindComponent
];
export const pluginConfig: { [key: string]: PluginOptions } = {
[Table.pluginName]: {
overflow: {
maxLeftWidth: () => {
// 编辑区域位置
const rect = $('.editor-content')
.get<HTMLElement>()
?.getBoundingClientRect();
const editorLeft = rect?.left || 0;
// 减去大纲的宽度
const width = editorLeft - $('.data-toc-wrapper').width();
// 留 16px 的间隔
return width <= 0 ? 100 : width - 16;
},
maxRightWidth: () => {
// 编辑区域位置
const rect = $('.editor-content')
.get<HTMLElement>()
?.getBoundingClientRect();
const editorRigth = (rect?.right || 0) - (rect?.width || 0);
// 减去评论区域的宽度
const width = editorRigth - $('.doc-comment-layer').width();
// 留 16px 的间隔
return width <= 0 ? 100 : width - 16;
},
},
},
[MarkRange.pluginName]: {
//标记类型集合
keys: ['comment'],
@ -153,7 +177,7 @@ export const pluginConfig: { [key: string]: PluginOptions } = {
},
[Video.pluginName]: {
onBeforeRender: (status: string, url: string) => {
return url + `?token=12323`;
return url;
},
},
[Math.pluginName]: {

View File

@ -90,8 +90,6 @@
}
.editor-container {
background: #fafafa;
background-color: #fafafa;
padding: 24px 0 64px;
height: calc(100vh - 138px);
width: 100%;
@ -111,7 +109,6 @@
width: 812px;
margin: 0 auto;
background: #fff;
border: 1px solid #f0f0f0;
min-height: 800px;
@media @mobile {
width: auto;
@ -121,7 +118,7 @@
}
.editor-content .am-engine {
padding: 40px 60px 60px;
padding: 40px 0 60px;
@media @mobile {
padding: 18px 0 0 0;

View File

@ -1,9 +1,8 @@
.data-toc-wrapper {
position: absolute;
top: 20px;
width: calc(50% - 454px);
right: calc(50% + 428px);
padding-right: 24px;
min-width: 210px;
padding: 0 16px;
}
.data-toc-title {

View File

@ -1,9 +0,0 @@
.doc-editor-mode {
font-size: 12px;
background: #ffffff;
padding: 0;
z-index: 9999;
position: fixed;
left: 10px;
top: 68px;
}

View File

@ -0,0 +1,38 @@
.doc-editor-mode {
font-size: 12px;
background: #ffffff;
padding: 0;
z-index: 9999;
position: fixed;
left: 10px;
top: 68px;
}
body {
.am-engine h1,
.am-engine h2,
.am-engine h3,
.am-engine h4,
.am-engine h5,
.am-engine h6 {
margin-top: 1em;
margin-bottom: 8px;
}
.am-engine > [data-id],
.am-engine > div[data-card-key] {
margin: 8px 0;
}
.am-engine table,
.am-engine tbody,
.am-engine tr,
.am-engine td {
margin: 0;
}
.am-engine > ul > li {
margin-top: 4px;
margin-bottom: 4px;
}
}

View File

@ -10,7 +10,7 @@ import Space from 'antd/es/space';
import Button from 'antd/es/button';
import 'antd/es/space/style';
import 'antd/es/button/style';
import './editor.css';
import './editor.less';
const localMember =
typeof localStorage === 'undefined' ? null : localStorage.getItem('member');
@ -72,7 +72,7 @@ export default () => {
return (
<Context.Provider value={{ lang }}>
<Space className="doc-editor-mode">
{/* <Space className="doc-editor-mode">
<Button
size="small"
disabled={readonly}
@ -89,7 +89,7 @@ export default () => {
>
{lang === 'zh-CN' ? '编辑模式' : 'Edit mode'}
</Button>
</Space>
</Space> */}
<Editor
lang={lang}
placeholder="这里是编辑区域哦~"

View File

@ -52,7 +52,7 @@ function startServer() {
if (action === 'ready') {
client.add(ws, data.doc_id, {
id: getId(data.doc_id, uid),
name: `Guest-${uid}`,
name: `G-${uid}`,
});
}
} catch (error) {}

View File

@ -322,6 +322,7 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface {
const className = 'card-selected-other';
if (selected) this.root.addClass(className);
else this.root.removeClass(className);
return center;
}
onActivate(activated: boolean) {
if (!this.resize) return;
@ -335,7 +336,7 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface {
rgb: string;
},
): NodeInterface | void {
this.onSelectByOther(activated, value);
return this.onSelectByOther(activated, value);
}
onChange?(trigger: 'remote' | 'local', node: NodeInterface): void;
destroy() {

View File

@ -14,6 +14,7 @@ import { DATA_ELEMENT, TRIGGER_CARD_ID, UI } from '../../constants';
import { $ } from '../../node';
import { isEngine, isMobile } from '../../utils';
import Position from '../../position';
import placements from '../../position/placements';
import './index.css';
export const isCardToolbarItemOptions = (
@ -30,6 +31,7 @@ class CardToolbar implements CardToolbarInterface {
private position: Position;
#hideTimeout: NodeJS.Timeout | null = null;
#showTimeout: NodeJS.Timeout | null = null;
#defaultAlign: keyof typeof placements = 'topLeft';
constructor(editor: EditorInterface, card: CardInterface) {
this.editor = editor;
@ -41,6 +43,10 @@ class CardToolbar implements CardToolbarInterface {
}
}
setDefaultAlign(align: keyof typeof placements) {
this.#defaultAlign = align;
}
clearHide = () => {
if (this.#hideTimeout) clearTimeout(this.#hideTimeout);
this.#hideTimeout = null;
@ -287,14 +293,14 @@ class CardToolbar implements CardToolbarInterface {
(this.card.constructor as CardEntry).cardName,
);
if (this.toolbar) this.toolbar.show();
let prevAlign = 'topLeft';
let prevAlign = this.#defaultAlign;
setTimeout(() => {
this.position.bind(
container,
this.card.isMaximize
? this.card.getCenter().first()!
: this.card.root,
'topLeft',
this.#defaultAlign,
this.offset,
(rect) => {
if (
@ -311,7 +317,7 @@ class CardToolbar implements CardToolbarInterface {
this.position.update(false);
} else if (
this.offset &&
rect.align === 'topLeft' &&
rect.align === this.#defaultAlign &&
rect.align !== prevAlign
) {
this.position.setOffset(this.offset);

View File

@ -441,6 +441,7 @@ class ChangeModel implements ChangeInterface {
let node: NodeInterface | null = $(childNodes[0]);
let prev: NodeInterface | null = null;
const appendNodes = [];
let startRangeNodeParent = startRange.node.parent();
while (node && node.length > 0) {
nodeApi.removeSide(node);
const next: NodeInterface | null = node.next();
@ -458,6 +459,22 @@ class ChangeModel implements ChangeInterface {
if (!next) {
range.select(node, true).collapse(false);
}
// 被删除了重新设置开始节点位置
if (
startRange &&
(!startRangeNodeParent || startRangeNodeParent.length === 0)
) {
const children = node.children();
startRangeNodeParent = node.parent();
startRange = {
node: node,
offset:
children.length === 1 &&
children[0].nodeName === 'BR'
? 0
: range.startOffset,
};
}
node = next;
}
if (mergeNode[0]) {

View File

@ -490,11 +490,11 @@ class NativeEvent {
return;
if (files.length === 0) {
change.cacheRangeBeforeCommand();
this.paste(source);
setTimeout(() => {
// 如果 text 和 html 都有,就解析 text
pasteMarkdown(source, text || '');
}, 200);
this.paste(source);
}
});

View File

@ -27,6 +27,7 @@ import Request, {
import Scrollbar from './scrollbar';
import Position from './position';
import { $, getHashId, uuid } from './node';
import Resizer from './resizer';
export * from './types';
export * from './utils';
@ -69,4 +70,5 @@ export {
isRangeInterface,
isRange,
isSelection,
Resizer,
};

View File

@ -105,12 +105,6 @@ class RangeColoring implements RangeColoringInterface {
child = $(
`<div class="${USER_BACKGROUND_CLASS}" ${DATA_UUID}="${uuid}" ${DATA_COLOR}="${color}" />`,
);
child.css({
position: 'absolute',
top: 0,
left: 0,
'pointer-events': 'none',
});
this.root.append(child);
targetCanvas = new TinyCanvas({
container: child.get<HTMLElement>()!,
@ -118,6 +112,12 @@ class RangeColoring implements RangeColoringInterface {
child[0]['__canvas'] = targetCanvas;
}
child.css({
position: 'absolute',
top: 0,
left: 0,
'pointer-events': 'none',
});
child[0]['__range'] = range.cloneRange();
const parentWidth = this.root.width();
const parentHeight = this.root.height();
@ -140,6 +140,14 @@ class RangeColoring implements RangeColoringInterface {
if (!!result) {
if (Array.isArray(result)) subRanges = result;
else {
if (result.x < 0) {
targetCanvas.resize(
parentWidth - result.x,
parentHeight,
);
child.css('left', `${result.x}px`);
result.x = 0;
}
targetCanvas.clearRect(result);
targetCanvas.drawRect({ ...result.toJSON(), ...fill });
return [range];

View File

@ -3,10 +3,10 @@ import { EditorInterface } from '../types/engine';
import { PluginOptions, PluginInterface } from '../types/plugin';
abstract class PluginEntry<T extends PluginOptions = {}>
implements PluginInterface
implements PluginInterface<T>
{
protected readonly editor: EditorInterface;
protected options: T;
options: T;
constructor(editor: EditorInterface, options: PluginOptions) {
this.editor = editor;
this.options = (options || {}) as T;

View File

@ -14,7 +14,7 @@ class Position {
#root?: NodeInterface;
#onUpdate?: (rect: any) => void;
#updateTimeout?: NodeJS.Timeout;
#observer?: MutationObserver;
#observer?: ResizeObserver;
constructor(editor: EditorInterface) {
this.#editor = editor;
@ -43,7 +43,7 @@ class Position {
this.#editor.scrollNode?.on('scroll', this.updateListener);
}
let size = { width: target.width(), height: target.height() };
this.#observer = new MutationObserver(() => {
this.#observer = new ResizeObserver(() => {
const width = target.width();
const height = target.height();
@ -54,13 +54,7 @@ class Position {
};
this.updateListener();
});
this.#observer.observe(target.get<HTMLElement>()!, {
attributes: true,
attributeFilter: ['style'],
attributeOldValue: true,
childList: true,
subtree: true,
});
this.#observer.observe(target.get<HTMLElement>()!);
this.update();
}

View File

@ -1,4 +1,4 @@
.data-image-resizer {
.data-resizer {
position: absolute;
width: 100%;
height: 100%;
@ -7,37 +7,10 @@
bottom: 0px;
right: 0;
z-index: 1;
outline: 2px solid #1890FF;
max-width: initial !important;
}
.data-image-resizer-holder {
position: absolute;
width: 12px;
height: 12px;
border: 2px solid #fff;
background: #1890FF;
display: inline-block;
}
.data-image-resizer-holder-right-top {
top: -6px;
right: -6px;
cursor: nesw-resize;
}
.data-image-resizer-holder-right-bottom {
bottom: -6px;
right: -6px;
cursor: nwse-resize;
}
.data-image-resizer-holder-left-bottom {
bottom: -6px;
left: -6px;
cursor: nesw-resize;
}
.data-image-resizer-holder-left-top {
left: -6px;
top: -6px;
cursor: nwse-resize;
}
.data-image-resizer-bg {
.data-resizer img {
position: absolute;
top: 0;
left: 0;
@ -46,13 +19,39 @@
cursor: pointer;
width: 100%;
height: 100%;
opacity: 0;
}
.data-image-resizer-bg-active {
opacity: 0.3;
}
.data-resizer-holder {
position: absolute;
width: 14px;
height: 14px;
border: 2px solid #fff;
border-radius: 50%;
background: #1890FF;
display: inline-block;
}
.data-resizer-holder-right-top {
top: -6px;
right: -6px;
cursor: nesw-resize;
}
.data-resizer-holder-right-bottom {
bottom: -6px;
right: -6px;
cursor: nwse-resize;
}
.data-resizer-holder-left-bottom {
bottom: -6px;
left: -6px;
cursor: nesw-resize;
}
.data-resizer-holder-left-top {
left: -6px;
top: -6px;
cursor: nwse-resize;
}
.data-image-resizer-number {
.data-resizer-number {
position: absolute;
display: inline-block;
line-height: 24px;
@ -68,31 +67,31 @@
transform: scale(0.8);
}
.data-image-resizer-number-right-top {
.data-resizer-number-right-top {
top: 0px;
right: -6px;
transform: translateX(100%) scale(0.8);
}
.data-image-resizer-number-right-bottom {
.data-resizer-number-right-bottom {
right: -6px;
bottom: 0px;
transform: translateX(100%) scale(0.8);
}
.data-image-resizer-number-left-bottom {
.data-resizer-number-left-bottom {
left: -6px;
bottom: 0px;
transform: translateX(-100%) scale(0.8);
}
.data-image-resizer-number-left-top {
.data-resizer-number-left-top {
left: -6px;
top: 0px;
transform: translateX(-100%) scale(0.8);
}
.data-image-resizer-number-active {
.data-resizer-number-active {
opacity: 1;
visibility: visible;
}

View File

@ -1,38 +1,22 @@
import { $, NodeInterface, EventListener, isMobile } from '@aomao/engine';
import type { NodeInterface, EventListener } from '../types';
import type {
ResizerInterface,
ResizerOptions,
Point,
ResizerPosition,
Size,
} from '../types';
import { $ } from '../node';
import { isMobile } from '../utils';
import './index.css';
export type Options = {
src: string;
width: number;
height: number;
maxWidth: number;
rate: number;
onChange?: (size: Size) => void;
};
export type Position =
| 'right-top'
| 'left-top'
| 'right-bottom'
| 'left-bottom';
export type Point = {
x: number;
y: number;
};
export type Size = {
width: number;
height: number;
};
class Resizer {
private options: Options;
class Resizer implements ResizerInterface {
private options: ResizerOptions;
private root: NodeInterface;
private image: NodeInterface;
private image?: NodeInterface;
private resizerNumber: NodeInterface;
private point: Point = { x: 0, y: 0 };
private position?: Position;
private position?: ResizerPosition;
private size: Size;
maxWidth: number;
/**
@ -40,11 +24,12 @@ class Resizer {
*/
private resizing: boolean = false;
constructor(options: Options) {
constructor(options: ResizerOptions) {
this.options = options;
this.root = $(this.renderTemplate(options.src));
this.image = this.root.find('img');
this.resizerNumber = this.root.find('.data-image-resizer-number');
this.root = $(this.renderTemplate(options.imgUrl));
if (options.imgUrl) this.image = this.root.find('img');
this.image?.hide();
this.resizerNumber = this.root.find('.data-resizer-number');
const { width, height } = this.options;
this.size = {
width,
@ -53,19 +38,19 @@ class Resizer {
this.maxWidth = this.options.maxWidth;
}
renderTemplate(src: string) {
renderTemplate(imgUrl?: string) {
return `
<div class="data-image-resizer">
<img class="data-image-resizer-bg data-image-resizer-bg-active" src="${src}" />
<div class="data-image-resizer-holder data-image-resizer-holder-right-top"></div>
<div class="data-image-resizer-holder data-image-resizer-holder-right-bottom"></div>
<div class="data-image-resizer-holder data-image-resizer-holder-left-bottom"></div>
<div class="data-image-resizer-holder data-image-resizer-holder-left-top"></div>
<span class="data-image-resizer-number"></span>
<div class="data-resizer">
${imgUrl ? `<img src="${imgUrl}">` : ''}
<div class="data-resizer-holder data-resizer-holder-right-top"></div>
<div class="data-resizer-holder data-resizer-holder-right-bottom"></div>
<div class="data-resizer-holder data-resizer-holder-left-bottom"></div>
<div class="data-resizer-holder data-resizer-holder-left-top"></div>
<span class="data-resizer-number"></span>
</div>`;
}
onMouseDown(event: MouseEvent | TouchEvent, position: Position) {
onMouseDown(event: MouseEvent | TouchEvent, position: ResizerPosition) {
if (this.resizing) return;
event.preventDefault();
event.stopPropagation();
@ -97,11 +82,10 @@ class Resizer {
};
this.position = position;
this.resizing = true;
this.resizerNumber.addClass(
`data-image-resizer-number-${this.position}`,
);
this.resizerNumber.addClass('data-image-resizer-number-active');
this.image.show();
this.root.addClass('data-resizing');
this.resizerNumber.addClass(`data-resizer-number-${this.position}`);
this.resizerNumber.addClass('data-resizer-number-active');
this.image?.show();
document.addEventListener(
isMobile ? 'touchmove' : 'mousemove',
this.onMouseMove,
@ -140,13 +124,11 @@ class Resizer {
width: clientWidth,
height: clientHeight,
};
this.resizerNumber.removeClass(
`data-image-resizer-number-${this.position}`,
);
this.resizerNumber.removeClass('data-image-resizer-number-active');
this.resizerNumber.removeClass(`data-resizer-number-${this.position}`);
this.resizerNumber.removeClass('data-resizer-number-active');
this.position = undefined;
this.resizing = false;
this.root.removeClass('data-resizing');
document.removeEventListener(
isMobile ? 'touchmove' : 'mousemove',
this.onMouseMove,
@ -157,7 +139,7 @@ class Resizer {
);
const { onChange } = this.options;
if (onChange) onChange(this.size);
this.image.hide();
this.image?.hide();
};
updateSize(width: number, height: number) {
@ -166,6 +148,10 @@ class Resizer {
} else {
width = this.size.width + width;
}
this.setSize(width, height);
}
setSize(width: number, height: number) {
if (width < 24) {
width = 24;
}
@ -181,10 +167,6 @@ class Resizer {
}
width = Math.round(width);
height = Math.round(height);
this.setSize(width, height);
}
setSize(width: number, height: number) {
this.root.css({
width: width + 'px',
height: height + 'px',
@ -193,37 +175,33 @@ class Resizer {
}
on(eventType: string, listener: EventListener) {
this.image.on(eventType, listener);
this.image?.on(eventType, listener);
}
off(eventType: string, listener: EventListener) {
this.image.off(eventType, listener);
this.image?.off(eventType, listener);
}
render() {
const { width, height } = this.options;
this.root.css({
width: `${width}px`,
height: `${height}px`,
});
this.setSize(width, height);
this.root
.find('.data-image-resizer-holder-right-top')
.find('.data-resizer-holder-right-top')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'right-top');
});
this.root
.find('.data-image-resizer-holder-right-bottom')
.find('.data-resizer-holder-right-bottom')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'right-bottom');
});
this.root
.find('.data-image-resizer-holder-left-bottom')
.find('.data-resizer-holder-left-bottom')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'left-bottom');
});
this.root
.find('.data-image-resizer-holder-left-top')
.find('.data-resizer-holder-left-top')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'left-top');
});

View File

@ -40,7 +40,6 @@
background: #888;
}
.data-scrollable .data-scrollbar.data-scrollbar-x {
width: 100%;
height: 8px;
bottom: 0px;
}

View File

@ -1,4 +1,5 @@
import { EventEmitter2 } from 'eventemitter2';
import domAlign from 'dom-align';
import { DATA_ELEMENT, UI } from '../constants';
import { NodeInterface } from '../types';
import { $ } from '../node';
@ -11,6 +12,12 @@ export type ScrollbarDragging = {
position: number;
};
export type ScrollbarCustomeOptions = {
onScrollX?: (x: number) => number;
getOffsetWidth?: (width: number) => number;
getScrollLeft?: (left: number) => number;
};
class Scrollbar extends EventEmitter2 {
private container: NodeInterface;
private x: boolean;
@ -36,6 +43,7 @@ class Scrollbar extends EventEmitter2 {
#content?: NodeInterface;
shadowTimer?: NodeJS.Timeout;
#enableScroll: boolean = true;
#scroll?: ScrollbarCustomeOptions;
/**
* @param {nativeNode} container
* @param {boolean} x
@ -47,12 +55,14 @@ class Scrollbar extends EventEmitter2 {
x: boolean = true,
y: boolean = false,
shadow: boolean = true,
scroll?: ScrollbarCustomeOptions,
) {
super();
this.container = isNode(container) ? $(container) : container;
this.x = x;
this.y = y;
this.shadow = shadow;
this.#scroll = scroll;
this.init();
}
@ -72,7 +82,7 @@ class Scrollbar extends EventEmitter2 {
const children = this.container.children();
let hasScrollbar = false;
children.each((child) => {
if ($(child).hasClass('data-scrollbar')) {
if (!hasScrollbar && $(child).hasClass('data-scrollbar')) {
hasScrollbar = true;
}
});
@ -110,12 +120,14 @@ class Scrollbar extends EventEmitter2 {
}
}
refresh() {
refresh = () => {
const element = this.container.get<HTMLElement>();
if (element) {
const { offsetWidth, offsetHeight, scrollLeft, scrollTop } =
element;
const offsetWidth = this.#scroll?.getOffsetWidth
? this.#scroll.getOffsetWidth(element.offsetWidth)
: element.offsetWidth;
const { offsetHeight, scrollTop } = element;
const contentElement = this.#content?.get<HTMLElement>();
const sPLeft = removeUnit(this.container.css('padding-left'));
const sPRight = removeUnit(this.container.css('padding-right'));
@ -170,14 +182,30 @@ class Scrollbar extends EventEmitter2 {
if (
this.x &&
contentElement &&
element.scrollWidth - sPLeft - sPRight !==
element.scrollWidth - sPLeft - sPRight >
contentElement.offsetWidth
) {
element.scrollLeft -=
let left =
element.scrollWidth -
sPLeft -
sPRight -
contentElement.offsetWidth;
if (this.#scroll) {
const { onScrollX, getScrollLeft } = this.#scroll;
left = getScrollLeft
? getScrollLeft(-0) + element.scrollLeft - left
: element.scrollLeft - left;
if (onScrollX) {
const result = onScrollX(left);
if (result > 0) element.scrollLeft = result;
else element.scrollLeft = 0;
}
this.scroll({ left });
} else {
element.scrollLeft -= left;
}
return;
}
// 实际内容高度小于容器滚动高度(有内容删除了)
@ -194,10 +222,13 @@ class Scrollbar extends EventEmitter2 {
contentElement.offsetHeight;
return;
}
this.reRenderX(scrollLeft);
const left = this.#scroll?.getScrollLeft
? this.#scroll.getScrollLeft(element.scrollLeft)
: element.scrollLeft;
this.reRenderX(left);
this.reRenderY(scrollTop);
}
}
};
/**
* 使
@ -212,13 +243,26 @@ class Scrollbar extends EventEmitter2 {
this.#enableScroll = false;
}
scroll = (event: Event) => {
const { target } = event;
if (!target) return;
scroll = (event: Event | { top?: number; left?: number }) => {
let top = 0;
let left = 0;
if (!this.#scroll && event instanceof Event) {
const { scrollTop, scrollLeft } = event.target as HTMLElement;
top = scrollTop;
left = scrollLeft;
} else if (!(event instanceof Event)) {
if (event.top === undefined) {
event.top = this.container.get<HTMLElement>()?.scrollTop || 0;
}
if (event.left === undefined) {
event.left = this.container.get<HTMLElement>()?.scrollLeft || 0;
}
top = event.top;
left = event.left;
} else return;
const { scrollTop, scrollLeft } = target as HTMLElement;
this.reRenderX(scrollLeft);
this.reRenderY(scrollTop);
this.reRenderX(left);
this.reRenderY(top);
};
wheelXScroll = (event: any) => {
@ -227,12 +271,29 @@ class Scrollbar extends EventEmitter2 {
const dir = wheelValue > 0 ? 'up' : 'down';
const containerElement = this.container.get<HTMLElement>();
if (!containerElement) return;
let left = containerElement.scrollLeft + (dir === 'up' ? -20 : 20);
const containerWidth = this.#scroll?.getOffsetWidth
? this.#scroll.getOffsetWidth(containerElement.offsetWidth)
: containerElement.offsetWidth;
const step = Math.max(containerWidth / 8, 20);
let left =
(this.#scroll?.getScrollLeft
? this.#scroll.getScrollLeft(containerElement.scrollLeft)
: containerElement.scrollLeft) + (dir === 'up' ? -step : step);
left =
dir === 'up'
? Math.max(0, left)
: Math.min(left, this.sWidth - this.oWidth);
if (this.#scroll) {
const { onScrollX } = this.#scroll;
if (onScrollX) {
const result = onScrollX(left);
if (result > 0) containerElement.scrollLeft = result;
else containerElement.scrollLeft = 0;
}
this.scroll({ left });
} else {
containerElement.scrollLeft = left;
}
};
wheelYScroll = (event: any) => {
@ -241,7 +302,9 @@ class Scrollbar extends EventEmitter2 {
const dir = wheelValue > 0 ? 'up' : 'down';
const containerElement = this.container.get<HTMLElement>();
if (!containerElement) return;
let top = containerElement.scrollTop + (dir === 'up' ? -20 : 20);
const containerHeight = containerElement.offsetHeight;
const step = Math.max(containerHeight / 8, 20);
let top = containerElement.scrollTop + (dir === 'up' ? -step : step);
top =
dir === 'up'
? Math.max(0, top)
@ -325,6 +388,7 @@ class Scrollbar extends EventEmitter2 {
childList: true,
subtree: true,
});
window.addEventListener('resize', this.refresh);
// 绑定滚动条事件
this.bindXScrollEvent();
this.bindYScrollEvent();
@ -361,8 +425,19 @@ class Scrollbar extends EventEmitter2 {
this.slideX?.css('left', left + 'px');
let min = left / (this.oWidth - this.xWidth);
min = Math.min(1, min);
this.container.get<HTMLElement>()!.scrollLeft =
(this.sWidth - this.oWidth) * min;
const containerElement = this.container.get<HTMLElement>()!;
const x = (this.sWidth - this.oWidth) * min;
if (this.#scroll) {
const { onScrollX } = this.#scroll;
if (onScrollX) {
const result = onScrollX(x);
if (result > 0) containerElement.scrollLeft = result;
else containerElement.scrollLeft = 0;
}
this.scroll({ left: x });
} else {
containerElement.scrollLeft = x;
}
}
};
@ -463,7 +538,13 @@ class Scrollbar extends EventEmitter2 {
reRenderShadow = (width: number) => {
if (this.shadow) {
this.shadowLeft?.css('left', width + 'px');
const element = this.container.get<HTMLElement>();
if (element) {
this.shadowLeft?.css(
'left',
(this.#scroll ? element.scrollLeft : width) + 'px',
);
}
this.shadowRight?.css('left', width + this.oWidth - 4 + 'px');
}
};
@ -476,7 +557,12 @@ class Scrollbar extends EventEmitter2 {
min = Math.min(1, min);
this.slideX?.css('left', (this.oWidth - this.xWidth) * min + 'px');
this.reRenderShadow(left);
this.emit('change');
if (left === removeUnit(this.scrollBarX?.css('left') || '0'))
return;
this.emit('change', {
x: left,
y: removeUnit(this.scrollBarY?.css('top') || '0'),
});
}
};
@ -487,7 +573,11 @@ class Scrollbar extends EventEmitter2 {
let min = value <= 0 ? 0 : top / value;
min = Math.min(1, min);
this.slideY?.css('top', (this.oHeight - this.yHeight) * min + 'px');
this.emit('change');
if (top === removeUnit(this.scrollBarX?.css('top') || '0')) return;
this.emit('change', {
x: removeUnit(this.scrollBarX?.css('left') || '0'),
y: top,
});
}
};
@ -526,6 +616,7 @@ class Scrollbar extends EventEmitter2 {
this.shadowRight?.remove();
}
this.#observer?.disconnect();
window.removeEventListener('resize', this.refresh);
}
}

View File

@ -1,18 +1,9 @@
import { DATA_ELEMENT } from '../../constants/root';
import { NodeInterface } from '../../types/node';
import { Placement } from '../../types/position';
import { $ } from '../../node';
import './index.css';
type Placement =
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight'
| 'left'
| 'right';
const template = (options: { placement: Placement }) => {
return `
<div ${DATA_ELEMENT}="tooltip" class="data-tooltip data-tooltip-placement-${options.placement} data-tooltip-hidden" style="transform-origin: 50% 45px 0px;">

View File

@ -8,6 +8,7 @@ import {
ToolbarItemOptions,
} from './toolbar';
import { CardActiveTrigger, CardType } from '../card/enum';
import { Placement } from './position';
export type CardOptions = {
editor: EditorInterface;
@ -53,6 +54,7 @@ export interface CardToolbarInterface {
* @param offset [tx,ty,bx,by]
*/
setOffset(offset: Array<number>): void;
setDefaultAlign(align: Placement): void;
/**
*
*/

View File

@ -21,3 +21,5 @@ export * from './block';
export * from './request';
export * from './tiny-canvas';
export * from './parser';
export * from './resizer';
export * from './position';

View File

@ -22,8 +22,12 @@ export interface PluginEntry {
readonly pluginName: string;
}
export interface PluginInterface {
export interface PluginInterface<T extends PluginOptions = {}> {
readonly kind: string;
/**
*
**/
options: T;
/**
* readonly
*/

View File

@ -0,0 +1,13 @@
export type Placement =
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight'
| 'left'
| 'leftTop'
| 'leftBottom'
| 'right'
| 'rightTop'
| 'rightBottom';

View File

@ -0,0 +1,33 @@
export interface ResizerInterface {
on(eventType: string, listener: EventListener): void;
off(eventType: string, listener: EventListener): void;
setSize(width: number, height: number): void;
maxWidth: number;
render(): void;
destroy(): void;
}
export type ResizerOptions = {
imgUrl?: string;
width: number;
height: number;
maxWidth: number;
rate: number;
onChange?: (size: Size) => void;
};
export type ResizerPosition =
| 'right-top'
| 'left-top'
| 'right-bottom'
| 'left-bottom';
export type Point = {
x: number;
y: number;
};
export type Size = {
width: number;
height: number;
};

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import classnames from 'classnames-es-ts';
import { EngineInterface } from '@aomao/engine';
import { EngineInterface, Placement } from '@aomao/engine';
import Popover from 'antd/es/popover';
import 'antd/es/popover/style';
@ -17,19 +17,7 @@ export type CollapseItemProps = {
disabled?: boolean;
onDisabled?: () => boolean;
className?: string;
placement?:
| 'right'
| 'top'
| 'left'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom';
placement?: Placement;
onClick?: (event: React.MouseEvent, name: string) => void | boolean;
onMouseDown?: (event: React.MouseEvent) => void;
};

View File

@ -1,17 +1,16 @@
import { PswpInterface } from '@/types';
import type { PswpInterface } from '@/types';
import type { EditorInterface, NodeInterface } from '@aomao/engine';
import {
$,
EditorInterface,
isEngine,
escape,
NodeInterface,
sanitizeUrl,
Tooltip,
isMobile,
Resizer,
CardType,
} from '@aomao/engine';
import Pswp from '../pswp';
import Resizer from '../resizer';
import './index.css';
export type Status = 'uploading' | 'done' | 'error';
@ -295,12 +294,12 @@ class Image {
this.meta.css({
'background-color': '',
width: '',
height: '',
//height: '',
});
this.image.css({
width: '',
height: '',
//height: '',
});
const img = this.image.get<HTMLImageElement>();
@ -334,7 +333,7 @@ class Image {
}
this.image.css('width', `${width}px`);
this.image.css('height', `${height}px`);
//this.image.css('height', `${height}px`);
}
changeSize(width: number, height: number) {
@ -359,7 +358,7 @@ class Image {
this.size.height = height;
this.image.css({
width: `${width}px`,
height: `${height}px`,
//height: `${height}px`,
});
const { onChange } = this.options;
@ -450,7 +449,7 @@ class Image {
if (isMobile || !isEngine(this.editor) || this.editor.readonly) return;
// 拖动调整图片大小
const resizer = new Resizer({
src: this.getSrc(),
imgUrl: this.getSrc(),
width: clientWidth,
height: clientHeight,
rate: this.rate,
@ -542,7 +541,7 @@ class Image {
if (this.src) {
this.image.css({
width: width + 'px',
height: height + 'px',
//height: height + 'px',
});
const { onChange } = this.options;
if (width > 0 && height > 0) {

View File

@ -228,6 +228,23 @@ class ImageComponent extends Card<ImageValue> {
else this.image?.blur();
}
onSelectByOther(
selected: boolean,
value?: {
color: string;
rgb: string;
},
): NodeInterface | void {
this.image?.root?.css(
'outline',
selected ? '2px solid ' + value!.color : '',
);
const className = 'card-selected-other';
if (selected) this.root.addClass(className);
else this.root.removeClass(className);
return this.image?.root;
}
render(loadingBg?: string): string | void | NodeInterface {
const value = this.getValue();
if (!value) return;
@ -253,20 +270,6 @@ class ImageComponent extends Card<ImageValue> {
},
onChange: (size) => {
if (size) this.setSize(size);
if (this.type === CardType.BLOCK && this.image) {
const maxWidth = this.image.getMaxWidth();
const offset = (maxWidth - this.image.root.width()) / 2;
if (value.status === 'done') {
this.toolbarModel?.setOffset([
-offset - 12,
0,
-offset - 12,
0,
]);
}
if (this.activated)
this.toolbarModel?.showCardToolbar();
}
},
onError: () => {
this.isLocalError = true;
@ -290,12 +293,8 @@ class ImageComponent extends Card<ImageValue> {
}
didRender() {
if (
this.type === CardType.INLINE &&
this.getValue()?.status === 'done'
) {
this.toolbarModel?.setOffset([-12, 0, -12, 0]);
}
super.didRender();
this.toolbarModel?.setDefaultAlign('top');
}
}

View File

@ -20,7 +20,7 @@
}
.pswp .data-pswp-tool-bar .btn {
color: #D9D9D9;
color: #f8f9fa;
display: inline-block;
width: 32px;
height: 32px;

View File

@ -42,6 +42,7 @@ class Pswp extends EventEmitter2 implements PswpInterface {
hideAnimationDuration: 0,
closeOnVerticalDrag: isMobile,
tapToClose: true,
bgOpacity: 0.8,
barsSize: {
top: 44,
bottom: 80,

View File

@ -31,49 +31,9 @@ new Engine(...,{
})
```
`defaultData`: 默认下拉查询列表展示数据
`onSearch`: 查询时的方法,或者配置 action二选其一
`onSelect`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
`onClick`: 在“提及”上单击时触发
`onMouseEnter`: 鼠标移入“提及”上时触发
`onRender`: 自定义渲染列表
`onRenderItem`: 自定义渲染列表项
`onLoading`: 自定渲染加载状态
`onEmpty`: 自定渲染空状态
`action`: 查询地址,始终使用 `GET` 请求,参数 `keyword`
`data`: 查询时同时将这些数据一起传到到服务端
```ts
//默认展示的列表数据
defaultData?: Array<{ key: string, name: string, avatar?: string}>
//查询时的方法,或者配置 action二选其一
onSearch?:(keyword: string) => Promise<Array<{ key: string, name: string, avatar?: string}>>
//选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
onSelect?: (data: {[key:string]: string}) => void | {[key: string]: string}
//在“提及”上单击事件
onClick?:(data: {[key:string]: string}) => void
//鼠标移入“提及”上时触发
onMouseEnter?:(node: NodeInterface, data: {[key:string]: string}) => void
//自定义渲染列表bindItem 可以为列表项绑定需要的属性和事件
onRender?: (data: MentionItem, root: NodeInterface, bindItem: (node: NodeInterface, data: {[key:string]: string}) => NodeInterface) => Promise<string | NodeInterface | void>;
//自定义渲染列表项
onRenderItem?: (item: MentionItem, root: NodeInterface) => string | NodeInterface | void
// 自定渲染加载状态
onLoading?: (root: NodeInterface) => string | NodeInterface | void
// 自定渲染空状态
onEmpty?: (root: NodeInterface) => string | NodeInterface | void
/**
* 查询地址
* 查询地址,或者监听 mention:search 事件执行查询
*/
action?: string;
/**
@ -124,3 +84,136 @@ parse?: (
//返回 Array<{ key: string, name: string}>
engine.command.executeMethod('mention', 'getList');
```
## 插件事件
`mention:default`: 默认下拉查询列表展示数据
```ts
this.engine.on('mention:default', () => {
return [];
});
```
`mention:search`: 查询时的方法,或者配置 action二选其一
```ts
this.engine.on('mention:search', (keyword) => {
return new Promise((resolve) => {
query({ keyword })
.then((result) => {
resolve(result);
})
.catch(() => resolve([]));
});
});
```
`mention:select`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
```ts
this.engine.on('mention:select', (data) => {
data['test'] = 'test';
return data;
});
```
`mention:item-click`: 在“提及”上单击时触发
```ts
this.engine.on(
'mention:item-click',
(root: NodeInterface, { key, name }: { key: string; name: string }) => {
console.log('mention click:', key, '-', name);
},
);
```
`mention:enter`: 鼠标移入“提及”上时触发
```ts
this.engine.on(
'mention:enter',
(layout: NodeInterface, { name }: { key: string; name: string }) => {
ReactDOM.render(
<div style={{ padding: 5 }}>
<p>This is name: {name}</p>
<p>配置 mention 插件的 mention:enter 事件</p>
<p>此处使用 ReactDOM.render 自定义渲染</p>
<p>Use ReactDOM.render to customize rendering here</p>
</div>,
layout.get<HTMLElement>()!,
);
},
);
```
`mention:render`: 自定义渲染列表
```ts
this.engine.on(
'mention:render',
(
root: NodeInterface,
data: Array<MentionItem>,
bindItem: (
node: NodeInterface,
data: { [key: string]: string },
) => NodeInterface,
) => {
return new Promise<void>((resolve) => {
const renderCallback = (items: { [key: string]: Element }) => {
// 遍历每个项的DOM节点
Object.keys(items).forEach((key) => {
const element = items[key];
const item = data.find((d) => d.key === key);
if (!item) return;
// 绑定每个列表项所属的属性、事件,以满足编辑器中上下左右选择的功能需要
bindItem($(element), item);
});
resolve();
};
ReactDOM.render(
<MentionList data={data} callback={renderCallback} />,
root.get<HTMLElement>()!,
);
});
},
);
```
`mention:render-item`: 自定义渲染列表项
```ts
this.engine.on('mention:render-item', (data, root) => {
const item = $(`<div>${data}</div>`);
root.append(item);
return item;
});
```
`mention:loading`: 自定渲染加载状态
```ts
this.engine.on('mention:loading', (data, root) => {
root.html(`<div>${data}</div>`);
// or
ReactDOM.render(
<div className="data-mention-loading">Loading...</div>,
root.get<HTMLElement>()!,
);
});
```
`mention:empty`: 自定渲染空状态
```ts
this.engine.on('mention:empty', (root) => {
root.html('<div>没有查询到数据</div>');
// or
ReactDOM.render(
<div className="data-mention-empty">Empty</div>,
root.get<HTMLElement>()!,
);
});
```

View File

@ -249,7 +249,12 @@ class CollapseComponent implements CollapseComponentInterface {
if (result) body?.append(result);
} else if (
CollapseComponent.render ||
(result = this.engine.trigger('mention:render', this.root))
(result = this.engine.trigger(
'mention:render',
this.root,
data,
this.bindItem,
))
) {
(CollapseComponent.render
? CollapseComponent.render(this.root, data, this.bindItem)

View File

@ -41,6 +41,17 @@ new Engine(...,{
})
```
### 溢出展示
```ts
overflow?: {
// 相对编辑器左侧最大能展示的宽度
maxLeftWidth?: () => number;
// 相对于编辑器右侧最大能展示的宽度
maxRightWidth?: () => number;
};
```
## 命令
```ts

View File

@ -251,7 +251,7 @@ class Helper implements HelperInterface {
const $tr = trs.eq(index);
if (!$tr) return;
let height = parseInt($tr.css('height'));
height = height || 33;
height = height || 35;
$tr.css('height', height + 'px');
});
//补充可编辑器区域
@ -603,7 +603,7 @@ class Helper implements HelperInterface {
const $tr = trs.eq(index);
if (!$tr) return;
let height = parseInt($tr.css('height'));
height = height || 33;
height = height || 35;
$tr.css('height', height + 'px');
});
return table;

View File

@ -5,9 +5,11 @@ import {
CardType,
EDITABLE_SELECTOR,
isEngine,
isMobile,
NodeInterface,
Parser,
RangeInterface,
removeUnit,
Scrollbar,
ToolbarItemOptions,
} from '@aomao/engine';
@ -65,7 +67,7 @@ class TableComponent extends Card<TableValue> implements TableInterface {
selection: TableSelectionInterface = new TableSelection(this.editor, this);
conltrollBar: ControllBarInterface = new ControllBar(this.editor, this, {
col_min_width: 40,
row_min_height: 33,
row_min_height: 35,
});
command: TableCommandInterface = new TableCommand(this.editor, this);
scrollbar?: Scrollbar;
@ -80,6 +82,138 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (isEngine(this.editor)) {
this.editor.on('undo', this.doChange);
this.editor.on('redo', this.doChange);
// tab 键选择
if (!this.editor.event.listeners['keydown:tab'])
this.editor.event.listeners['keydown:tab'] = [];
this.editor.event.listeners['keydown:tab'].unshift(
(event: KeyboardEvent) => {
if (!isEngine(this.editor)) return;
const { change, block, node } = this.editor;
const range = change.range.get();
const td = range.endNode.closest('td');
if (td.length === 0) return;
const closestBlock = block.closest(range.endNode);
if (
td.length > 0 &&
(block.isLastOffset(range, 'end') ||
(closestBlock.name !== 'li' &&
node.isEmptyWidthChild(closestBlock)))
) {
let next = td.next();
if (!next) {
const nextRow = td.parent()?.next();
// 最后一行,最后一列
if (!nextRow) {
// 新建一行
this.command.insertRowDown();
next =
td
.parent()
?.next()
?.find('td:first-child') || null;
} else {
next = nextRow.find('td:first-child') || null;
}
}
if (next) {
event.preventDefault();
this.selection.focusCell(next);
return false;
}
}
return;
},
);
// 下键选择
this.editor.on('keydown:down', (event) => {
if (!isEngine(this.editor)) return;
const { change } = this.editor;
const range = change.range.get();
const td = range.endNode.closest('td');
if (td.length === 0) return;
const contentElement = td.find('.table-main-content');
if (!contentElement) return;
const tdRect = contentElement
.get<HTMLElement>()!
.getBoundingClientRect();
const rangeRect = range.getBoundingClientRect();
if (
td.length > 0 &&
(rangeRect.bottom === 0 ||
tdRect.bottom - rangeRect.bottom < 10)
) {
const index = td.index();
const nextRow = td.parent()?.next();
if (nextRow) {
let nextIndex = 0;
let nextTd = nextRow.find('td:last-child');
this.selection.tableModel?.table[nextRow.index()].some(
(cell) => {
if (
!this.helper.isEmptyModelCol(cell) &&
nextIndex >= index &&
cell.element
) {
nextTd = $(cell.element);
return true;
}
nextIndex++;
},
);
if (nextTd) {
event.preventDefault();
this.selection.focusCell(nextTd, true);
return false;
}
}
}
});
// 上键选择
this.editor.on('keydown:up', (event) => {
if (!isEngine(this.editor)) return;
const { change } = this.editor;
const range = change.range.get();
const td = range.endNode.closest('td');
if (td.length === 0) return;
const contentElement = td.find('.table-main-content');
if (!contentElement) return;
const tdRect = contentElement
.get<HTMLElement>()!
.getBoundingClientRect();
const rangeRect = range.getBoundingClientRect();
if (
td.length > 0 &&
(rangeRect.top === 0 || rangeRect.top - tdRect.top < 10)
) {
const index = td.index();
const prevRow = td.parent()?.prev();
if (prevRow) {
let prevIndex = 0;
let prevTd = prevRow.find('td:first-child');
this.selection.tableModel?.table[prevRow.index()].some(
(cell) => {
if (
!this.helper.isEmptyModelCol(cell) &&
prevIndex >= index &&
cell.element
) {
prevTd = $(cell.element);
return true;
}
prevIndex++;
},
);
if (prevTd) {
event.preventDefault();
this.selection.focusCell(prevTd);
return false;
}
}
}
});
}
if (this.colorTool) return;
this.colorTool = new ColorTool(this.editor, this.id, {
@ -172,10 +306,7 @@ class TableComponent extends Card<TableValue> implements TableInterface {
},
];
if (this.isMaximize) return funBtns;
return [
{
type: 'dnd',
},
const toolbars: Array<ToolbarItemOptions | CardToolbarItemOptions> = [
{
type: 'maximize',
},
@ -190,6 +321,12 @@ class TableComponent extends Card<TableValue> implements TableInterface {
},
...funBtns,
];
if (removeUnit(this.wrapper?.css('margin-left') || '0') === 0) {
toolbars.unshift({
type: 'dnd',
});
}
return toolbars;
}
updateAlign(event: MouseEvent, align: 'top' | 'middle' | 'bottom' = 'top') {
@ -291,12 +428,10 @@ class TableComponent extends Card<TableValue> implements TableInterface {
super.activate(activated);
if (activated) {
this.wrapper?.addClass('active');
this.scrollbar?.enableScroll();
} else {
this.selection.clearSelect();
this.conltrollBar.hideContextMenu();
this.wrapper?.removeClass('active');
this.scrollbar?.disableScroll();
}
this.scrollbar?.refresh();
}
@ -344,6 +479,33 @@ class TableComponent extends Card<TableValue> implements TableInterface {
return nodes;
}
overflow(max: number) {
// 表格宽度
const tableWidth = this.wrapper?.find('.data-table')?.width() || 0;
const rootWidth = this.getCenter().width();
// 溢出的宽度
const overflowWidth = tableWidth - rootWidth;
if (overflowWidth > 0) {
this.wrapper?.css(
'margin-right',
`-${overflowWidth > max ? max : overflowWidth}px`,
);
} else if (overflowWidth < 0) {
this.wrapper?.css('margin-right', '');
}
}
updateScrollbar = () => {
if (!this.scrollbar) return;
const hideHeight =
(this.wrapper?.getBoundingClientRect()?.bottom || 0) -
(this.wrapper?.getViewport().bottom || 0);
console.log(hideHeight);
this.wrapper?.find('.data-scrollbar-x').css({
bottom: `${hideHeight > 0 ? hideHeight + 2 : 0}px`,
});
};
didRender() {
super.didRender();
this.viewport = isEngine(this.editor)
@ -356,8 +518,49 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (!isEngine(this.editor) || this.editor.readonly)
this.toolbarModel?.setOffset([0, 0]);
else this.toolbarModel?.setOffset([0, -28, 0, -6]);
const tablePlugin = this.editor.plugin.components['table'];
const tableOptions = tablePlugin?.options['overflow'] || {};
if (this.viewport) {
this.scrollbar = new Scrollbar(this.viewport, true, false, true);
const overflowLeftConfig = tableOptions['maxLeftWidth']
? {
onScrollX: (x: number) => {
const max = tableOptions['maxLeftWidth']();
this.wrapper?.css(
'margin-left',
`-${x > max ? max : x}px`,
);
if (x > 0) {
this.editor.root.find('.data-card-dnd').hide();
} else {
this.editor.root.find('.data-card-dnd').show();
}
return x - max;
},
getScrollLeft: (left: number) => {
return (
left -
removeUnit(
this.wrapper?.css('margin-left') || '0',
)
);
},
getOffsetWidth: (width: number) => {
return (
width +
removeUnit(
this.wrapper?.css('margin-left') || '0',
)
);
},
}
: undefined;
this.scrollbar = new Scrollbar(
this.viewport,
true,
false,
true,
overflowLeftConfig,
);
this.scrollbar.setContentNode(this.viewport.find('.data-table')!);
this.scrollbar.on('display', (display: 'node' | 'block') => {
if (display === 'block') {
@ -367,14 +570,18 @@ class TableComponent extends Card<TableValue> implements TableInterface {
}
});
this.scrollbar.disableScroll();
let changeTimeout: NodeJS.Timeout | undefined;
const handleScrollbarChange = () => {
if (changeTimeout) clearTimeout(changeTimeout);
changeTimeout = setTimeout(() => {
if (tableOptions['maxRightWidth'])
this.overflow(tableOptions['maxRightWidth']());
if (isEngine(this.editor)) this.editor.ot.initSelection();
}, 50);
};
this.scrollbar.on('change', handleScrollbarChange);
if (!isMobile)
window.addEventListener('scroll', this.updateScrollbar);
window.addEventListener('resize', this.updateScrollbar);
if (isEngine(this.editor) && !isMobile) {
this.editor.scrollNode?.on('scroll', this.updateScrollbar);
}
}
this.selection.on('select', () => {
this.conltrollBar.refresh();
@ -401,6 +608,8 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (!silence) {
this.onChange();
}
if (tableOptions['maxRightWidth'])
this.overflow(tableOptions['maxRightWidth']());
this.scrollbar?.refresh();
});
@ -412,6 +621,9 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (tableValue) this.setValue(tableValue);
this.onChange();
}
if (tableOptions['maxRightWidth'])
this.overflow(tableOptions['maxRightWidth']());
this.scrollbar?.refresh();
}
render() {

View File

@ -458,7 +458,7 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
this.emit('select', this.selectArea);
}
focusCell(cell: NodeInterface | Node) {
focusCell(cell: NodeInterface | Node, start: boolean = false) {
if (!isEngine(this.editor)) return;
const { change } = this.editor;
if (isNode(cell)) cell = $(cell);
@ -469,7 +469,7 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
.select(editableElement, true)
.shrinkToElementNode()
.shrinkToTextNode()
.collapse(false);
.collapse(start);
setTimeout(() => {
change.range.select(range);
}, 20);
@ -1090,9 +1090,10 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
top += rect.top - (vRect?.top || 0) - 13;
left += rect.left - (vRect?.left || 0);
}
const sLeft = removeUnit(
const sLeft =
removeUnit(
this.table.wrapper?.find('.data-scrollbar')?.css('left') || '0',
);
) + removeUnit(this.table.wrapper?.css('margin-left') || '0');
left += sLeft;
const headerHeight =

View File

@ -13,6 +13,7 @@ import {
} from '../types';
const TABLE_WRAPPER_CLASS_NAME = 'table-wrapper';
const TABLE_OVERFLOW_CLASS_NAME = 'table-overflow';
const TABLE_CLASS_NAME = 'data-table';
const COLS_HEADER_CLASS_NAME = 'table-cols-header';
const COLS_HEADER_ITEM_CLASS_NAME = 'table-cols-header-item';
@ -41,6 +42,7 @@ const TABLE_TD_BG_CLASS_NAME = 'table-main-bg';
class Template implements TemplateInterface {
static readonly TABLE_WRAPPER_CLASS = `.${TABLE_WRAPPER_CLASS_NAME}`;
static readonly TABLE_OVERFLOW_CLASS = `.${TABLE_OVERFLOW_CLASS_NAME}`;
static readonly TABLE_CLASS = `.${TABLE_CLASS_NAME}`;
static readonly COLS_HEADER_CLASS = `.${COLS_HEADER_CLASS_NAME}`;
static readonly COLS_HEADER_ITEM_CLASS = `.${COLS_HEADER_ITEM_CLASS_NAME}`;
@ -122,7 +124,7 @@ class Template implements TemplateInterface {
* @return {string} html
*/
htmlEdit(
{ rows, cols, html, noBorder }: TableValue,
{ rows, cols, html, noBorder, overflow }: TableValue,
menus: TableMenu,
): string {
cols = cols === -Infinity ? 1 : cols;
@ -188,7 +190,9 @@ class Template implements TemplateInterface {
noBorder === true ? " data-table-no-border='true'" : ''
} ${DATA_TRANSIENT_ATTRIBUTES}="class">${colgroup}${trs}</table>`;
return `<div class="${TABLE_WRAPPER_CLASS_NAME}" ${DATA_TRANSIENT_ATTRIBUTES}="*">${tableHeader}<div class="${VIEWPORT}">${this.renderColsHeader(
return `<div class="${TABLE_WRAPPER_CLASS_NAME} ${
overflow !== false ? TABLE_OVERFLOW_CLASS_NAME : ''
}" ${DATA_TRANSIENT_ATTRIBUTES}="*">${tableHeader}<div class="${VIEWPORT}">${this.renderColsHeader(
cols,
)}${table}${placeholder}${tableHighlight}</div>${this.renderRowsHeader(
rows,

View File

@ -27,7 +27,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
}
.data-table tr {
height: 33px;
height: 35px;
}
.data-table tr td {
@ -67,7 +67,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
}
.table-wrapper.scrollbar-show {
margin-bottom: -8px;
margin-bottom: -10px;
}
.table-wrapper.data-table-highlight tr td[table-cell-selection]:after {
@ -98,9 +98,8 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
background-color: #ffffff;
}
.table-wrapper.data-table-highlight-all .table-header {
.table-wrapper.data-table-highlight-all .table-header .table-header-item {
background: rgba(255, 77, 79, 0.4) !important;
border-color: rgba(255, 77, 79, 0.4) !important;
}
.table-wrapper .table-header-item:hover{
@ -113,16 +112,15 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
.table-wrapper .table-header.selected .table-header-item {
background: #4daaff;
border-color: #4daaff;
}
.table-wrapper .table-cols-header {
position: relative;
height: 14px;
height: 13px;
display: none;
width: 100%;
cursor: default;
margin-bottom: -1px;
z-index: 2;
}
.table-wrapper.active .table-cols-header {
@ -131,7 +129,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
.table-wrapper .table-cols-header .table-cols-header-item {
position: relative;
height: 14px;
height: 13px;
width: auto;
border: 1px solid #dfdfdf;
border-bottom: 0 none;
@ -161,7 +159,6 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
background: #fff;
z-index: 1;
border-radius: 0;
height: 14px;
border-bottom: 0;
cursor: move;
}
@ -250,11 +247,11 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
width: 14px;
z-index: 1;
border-right: 0;
visibility: hidden;
display: none;
}
.table-wrapper.active .table-rows-header {
visibility: visible;
display: block;
}
.table-wrapper .table-rows-header .table-rows-header-item {
@ -387,7 +384,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
.table-wrapper .table-viewport .scrollbar-shadow-left {
top: 0;
bottom: 8px;
bottom: 10px;
}
.table-wrapper.active .table-viewport .scrollbar-shadow-left {
@ -397,7 +394,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
.table-wrapper .table-viewport .scrollbar-shadow-right {
top: 0;
bottom: 8px;
bottom: 10px;
}
.table-wrapper.active .table-viewport .scrollbar-shadow-right {
@ -469,6 +466,10 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
z-index: 3;
}
.table-wrapper .table-main-content * {
max-width: 100%;
}
.table-wrapper .table-main-bg {
position: absolute;
top: 0;
@ -573,7 +574,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
}
.table-wrapper.scrollbar-show .data-scrollable.scroll-x {
padding-bottom: 8px;
padding-bottom: 10px;
}
.table-wrapper .data-scrollable.scroll-x {
@ -584,20 +585,17 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
overflow: hidden;
}
.table-wrapper.scrollbar-show .data-scrollable.scroll-x:hover {
/**overflow-x: auto;**/
}
.table-wrapper.scrollbar-show .data-scrollable.scroll-x .data-scrollbar-x{
margin-bottom: 2px;
}
.table-wrapper .data-scrollable .data-scrollbar.data-scrollbar-x {
height: 4px;
height: 6px;
z-index: 5;
}
.table-wrapper .data-scrollable .data-scrollbar.data-scrollbar-x .data-scrollbar-trigger {
height: 4px;
height: 6px;
}
.table-wrapper .table-rows-header .table-row-delete-button,.table-wrapper .table-rows-header .table-row-add-button {
@ -697,11 +695,11 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
}
.data-table-reader.data-scrollable.scroll-x {
padding-bottom: 8px;
padding-bottom: 10px;
}
.data-table-reader .scrollbar-shadow-left, .data-table-reader .scrollbar-shadow-right {
bottom: 8px;
bottom: 10px;
}
.data-table-reader.scrollbar-show.data-scrollable.scroll-x .data-scrollbar-x{
@ -709,14 +707,20 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
}
.data-table-reader.data-scrollable .data-scrollbar.data-scrollbar-x {
height: 4px;
height: 6px;
}
.data-table-reader.data-scrollable .data-scrollbar.data-scrollbar-x .data-scrollbar-trigger {
height: 4px;
height: 6px;
}
[data-card-key="table"].data-card-block-max > [data-card-element="body"] > [data-card-element="center"] {
padding: 48px;
margin-top: 4px;
}
/**
表格可溢出样式
**/
.table-wrapper.table-overflow {
width: auto;
}

View File

@ -21,6 +21,10 @@ import { TableInterface } from './types';
export interface Options extends PluginOptions {
hotkey?: string | Array<string>;
overflow?: {
maxLeftWidth?: () => number;
maxRightWidth?: () => number;
};
markdown?: boolean;
}
@ -265,6 +269,7 @@ class Table extends Plugin<Options> {
this.editor.card.insert(TableComponent.cardName, {
rows: rows || 3,
cols: cols || 3,
overflow: this.options.overflow,
});
}

View File

@ -107,6 +107,7 @@ export type TableValue = {
html?: string;
color?: string;
noBorder?: boolean;
overflow?: boolean;
};
export type TableMenuItem = {
@ -367,7 +368,7 @@ export interface TableSelectionInterface extends EventEmitter2 {
hideHighlight(): void;
focusCell(cell: NodeInterface | Node): void;
focusCell(cell: NodeInterface | Node, start?: boolean): void;
selectCellRange(cell: NodeInterface | Node): void;

View File

@ -40,6 +40,14 @@ new Engine(...,{
})
```
### 是否显示视频标题
默认显示
```ts
showTitle?: boolean
```
### 文件上传
`action`: 上传地址,始终使用 `POST` 请求

View File

@ -1,5 +1,7 @@
[data-card-key="video"] {
outline: 1px solid #ddd;
.data-video {
margin: 0 auto;
position: relative;
cursor: pointer;
}
.data-video-content {
position: relative;
@ -9,6 +11,8 @@
.data-video-content video {
width: 100%;
outline: none;
position: relative;
z-index: 1;
}
.data-video-uploading,
.data-video-uploaded,
@ -23,7 +27,7 @@
line-height: 0;
}
.data-video-active {
outline: 1px solid #d9d9d9;
user-select: none;
}
.data-video-center {
position: absolute;
@ -81,3 +85,20 @@
vertical-align: middle;
margin: -2px 5px 0 0;
}
.data-video-title {
text-align: center;
}
.data-video-title::selection {
background: transparent
}
.data-video .data-resizer {
z-index: inherit;
}
.data-video .data-resizer .data-resizer-holder {
z-index: 2;
}
.data-video .data-resizing {
z-index: 3;
}

View File

@ -1,16 +1,18 @@
import { Tooltip } from '@aomao/engine';
import type {
CardToolbarItemOptions,
ToolbarItemOptions,
NodeInterface,
ResizerInterface,
} from '@aomao/engine';
import {
$,
Card,
CardToolbarItemOptions,
CardType,
escape,
getFileSize,
isEngine,
isMobile,
NodeInterface,
sanitizeUrl,
ToolbarItemOptions,
Resizer,
} from '@aomao/engine';
import './index.css';
@ -49,6 +51,22 @@ export type VideoValue = {
*
*/
size?: number;
/**
*
*/
width?: number;
/**
*
*/
height?: number;
/**
*
*/
naturalWidth?: number;
/**
*
*/
naturalHeight?: number;
/**
*
*/
@ -56,6 +74,14 @@ export type VideoValue = {
};
class VideoComponent extends Card<VideoValue> {
maxWidth: number = 0;
resizer?: ResizerInterface;
video?: NodeInterface;
rate: number = 1;
isLoad: boolean = false;
container?: NodeInterface;
videoContainer?: NodeInterface;
title?: NodeInterface;
static get cardName() {
return 'video';
}
@ -68,7 +94,9 @@ class VideoComponent extends Card<VideoValue> {
return false;
}
private container?: NodeInterface;
static get singleSelectable() {
return false;
}
getLocales() {
return this.editor.language.get<{ [key: string]: string }>('video');
@ -102,7 +130,9 @@ class VideoComponent extends Card<VideoValue> {
}
const fileSize: string = size ? getFileSize(size) : '';
const titleElement = name
? `<div class="data-video-title">${escape(name)}</div>`
: '';
if (status === 'uploading') {
return `
<div class="data-video">
@ -143,10 +173,11 @@ class VideoComponent extends Card<VideoValue> {
</div>
`;
}
const videoPlugin = this.editor.plugin.components['video'];
return `
<div class="data-video">
<div class="data-video-content data-video-done"></div>
${videoPlugin && videoPlugin.options.showTitle !== false ? titleElement : ''}
</div>
`;
}
@ -176,12 +207,18 @@ class VideoComponent extends Card<VideoValue> {
if (cover) {
video.poster = sanitizeUrl(this.onBeforeRender('cover', cover));
}
this.maxWidth = this.getMaxWidth();
if (value.naturalHeight && value.naturalWidth)
this.rate = value.naturalHeight / value.naturalWidth;
this.container?.find('.data-video-content').append(video);
this.videoContainer = this.container?.find('.data-video-content');
video.oncontextmenu = function () {
return false;
};
this.video = $(video);
this.title = this.container?.find('.data-video-title');
this.resetSize();
// 一次渲染时序开启 controls 会触发一次内容为空的 window.onerror疑似 chrome bug
setTimeout(() => {
video.controls = true;
@ -234,9 +271,158 @@ class VideoComponent extends Card<VideoValue> {
this.container?.find('.percent').html(`${percent}%`);
}
getMaxWidth(node: NodeInterface = this.getCenter()) {
const block = this.editor.block.closest(node).get<HTMLElement>();
if (!block) return 0;
return block.clientWidth - 6;
}
/**
*
*/
resetSize() {
if (!this.videoContainer) return;
const value = this.getValue();
if (!value) return;
this.videoContainer.css({
width: '',
//height: '',
});
this.container?.css({
width: '',
});
const video = this.video?.get<HTMLVideoElement>();
if (!video) return;
let { width, height, naturalWidth, naturalHeight } = value;
if (!naturalWidth) {
naturalWidth = video.videoWidth;
}
if (!naturalHeight) {
naturalHeight = video.videoHeight;
}
if (!height) {
width = naturalWidth;
height = Math.round(this.rate * width);
} else if (!width) {
height = naturalHeight;
width = Math.round(height / this.rate);
} else if (width && height) {
// 修正非正常的比例
height = Math.round(this.rate * width);
} else {
width = naturalWidth;
height = naturalHeight;
}
if (width > this.maxWidth) {
width = this.maxWidth;
height = Math.round(width * this.rate);
}
this.container?.css({
width: `${width}px`,
});
this.videoContainer.css('width', `${width}px`);
//this.videoContainer.css('height', `${height}px`);
}
changeSize(width: number, height: number) {
if (width < 24) {
width = 24;
height = width * this.rate;
}
if (width > this.maxWidth) {
width = this.maxWidth;
height = width * this.rate;
}
if (height < 24) {
height = 24;
width = height / this.rate;
}
width = Math.round(width);
height = Math.round(height);
this.videoContainer?.css({
width: `${width}px`,
//height: `${height}px`,
});
this.container?.css({
width: `${width}px`,
});
this.setValue({
width,
height,
});
this.resizer?.destroy();
this.initResizer();
}
onWindowResize = () => {
if (!isEngine(this.editor)) return;
this.maxWidth = this.getMaxWidth();
this.resetSize();
if (this.resizer) {
this.resizer.maxWidth = this.maxWidth;
this.resizer.setSize(
this.videoContainer?.width() || 0,
this.videoContainer?.height() || 0,
);
}
};
initResizer() {
const value = this.getValue();
if (!value) return;
const { naturalHeight, naturalWidth, status } = value;
if (!naturalHeight || !naturalWidth || status !== 'done') return;
const { width, height, cover } = value;
this.maxWidth = this.getMaxWidth();
this.rate = naturalHeight / naturalWidth;
window.removeEventListener('resize', this.onWindowResize);
window.addEventListener('resize', this.onWindowResize);
// 拖动调整视频大小
const resizer = new Resizer({
imgUrl: cover,
width: width || naturalWidth,
height: height || naturalHeight,
rate: this.rate,
maxWidth: this.maxWidth,
onChange: ({ width, height }) => this.changeSize(width, height),
});
this.resizer = resizer;
const resizerNode = resizer.render();
this.videoContainer?.append(resizerNode);
}
onActivate(activated: boolean) {
if (activated) this.container?.addClass('data-video-active');
else this.container?.removeClass('data-video-active');
if (activated) {
this.container?.addClass('data-video-active');
this.initResizer();
} else {
this.container?.removeClass('data-video-active');
this.resizer?.destroy();
}
}
onSelectByOther(
selected: boolean,
value?: {
color: string;
rgb: string;
},
): NodeInterface | void {
this.container?.css(
'outline',
selected ? '2px solid ' + value!.color : '',
);
const className = 'card-selected-other';
if (selected) this.root.addClass(className);
else this.root.removeClass(className);
return this.container;
}
checker(
@ -284,6 +470,8 @@ class VideoComponent extends Card<VideoValue> {
const { command, plugin } = this.editor;
const { video_id, status } = value;
const locales = this.getLocales();
this.maxWidth = this.getMaxWidth();
//阅读模式
if (!isEngine(this.editor)) {
if (status === 'done') {
@ -418,6 +606,7 @@ class VideoComponent extends Card<VideoValue> {
: value.download,
};
this.container = $(this.renderTemplate(newValue));
this.video = this.container.find('video');
center.empty();
center.append(this.container);
this.initPlayer();
@ -436,17 +625,27 @@ class VideoComponent extends Card<VideoValue> {
);
return this.container;
} else {
return $(this.renderTemplate(value));
this.container = $(this.renderTemplate(value));
return this.container;
}
}
didRender() {
super.didRender();
this.container?.on(isMobile ? 'touchstart' : 'click', () => {
handleClick = () => {
if (isEngine(this.editor) && !this.activated) {
this.editor.card.activate(this.root);
}
});
};
didRender() {
super.didRender();
this.toolbarModel?.setDefaultAlign('top');
this.container?.on('click', this.handleClick);
}
destroy() {
super.destroy();
this.container?.off('click', this.handleClick);
window.removeEventListener('resize', this.onWindowResize);
}
}

View File

@ -10,6 +10,7 @@ import {
NodeInterface,
Plugin,
PluginEntry,
PluginOptions,
READY_CARD_KEY,
sanitizeUrl,
SchemaInterface,
@ -18,12 +19,15 @@ import VideoComponent, { VideoValue } from './component';
import VideoUploader from './uploader';
import locales from './locales';
export default class VideoPlugin extends Plugin<{
export interface VideoOptions extends PluginOptions {
onBeforeRender?: (
action: 'download' | 'query' | 'cover',
url: string,
) => string;
}> {
showTitle?: boolean;
}
export default class VideoPlugin extends Plugin<VideoOptions> {
static get pluginName() {
return 'video';
}
@ -46,6 +50,10 @@ export default class VideoPlugin extends Plugin<{
cover?: string,
size?: number,
download?: string,
naturalWidth?: number,
naturalHeight?: number,
width?: number,
height?: number,
): void {
const value: VideoValue = {
status,
@ -55,6 +63,10 @@ export default class VideoPlugin extends Plugin<{
name: name || url,
size,
download,
width,
height,
naturalWidth,
naturalHeight,
};
if (status === 'error') {
value.url = '';

View File

@ -14,7 +14,7 @@ import {
import VideoComponent from './component';
export interface Options extends PluginOptions {
export interface VideoUploaderOptions extends PluginOptions {
/**
*
*/
@ -70,6 +70,9 @@ export interface Options extends PluginOptions {
id?: string;
cover?: string;
status?: string;
name?: string;
width?: number;
height?: number;
}
| string;
};
@ -96,7 +99,7 @@ export interface Options extends PluginOptions {
};
}
export default class extends Plugin<Options> {
export default class extends Plugin<VideoUploaderOptions> {
private cardComponents: { [key: string]: VideoComponent } = {};
static get pluginName() {
@ -219,6 +222,12 @@ export default class extends Plugin<Options> {
const download: string =
response.download ||
(response.data && response.data.download);
const width: number =
response.width ||
(response.data && response.data.width);
const height: number =
response.height ||
(response.data && response.data.height);
let status: string =
response.status ||
(response.data && response.data.status);
@ -232,6 +241,8 @@ export default class extends Plugin<Options> {
cover?: string;
download?: string;
status?: string;
width?: number;
height?: number;
}
| string;
} = {
@ -242,6 +253,8 @@ export default class extends Plugin<Options> {
cover,
download,
status,
width,
height,
},
};
if (parse) {
@ -253,6 +266,9 @@ export default class extends Plugin<Options> {
cover?: string;
download?: string;
status?: string;
name?: string;
width?: number;
height?: number;
};
if (typeof customizeResult.data === 'string')
result.data = {
@ -307,6 +323,8 @@ export default class extends Plugin<Options> {
? { url: result.data }
: {
...result.data,
naturalWidth: result.data.width,
naturalHeight: result.data.height,
},
);
}

View File

@ -1,6 +1,7 @@
const fs = require('fs');
const path = require('path');
const sendToWormhole = require('stream-wormhole');
const ffmpeg = require('fluent-ffmpeg');
const { Controller } = require('egg');
class UploadController extends Controller {
@ -159,10 +160,53 @@ class UploadController extends Controller {
// 监听写入完成事件
remoteFileStrem.on('finish', () => {
if (errFlag) return;
resolve({
const result = {
url,
download: url,
name: sourceName,
};
try {
ffmpeg.setFfmpegPath(
path.join(app.baseDir, './app/ffmpeg/win/ffmpeg.exe'),
);
ffmpeg.setFfprobePath(
path.join(app.baseDir, './app/ffmpeg/win/ffprobe.exe'),
);
ffmpeg.ffprobe(filePath, (err, metadata) => {
const fileName = new Date().getTime() + '-v-image.png'; // stream对象也包含了文件名大小等基本信息
// 创建文件写入路径
const imagePath = path.join(
app.baseDir,
`/app/public/upload/${fileName}`,
);
if (err) {
console.error(err);
reject(err);
return;
} else {
const { width, height } = metadata.streams[0];
result.width = width;
result.height = height;
ffmpeg(filePath)
.screenshots({
timestamps: ['50%'],
filename: fileName,
folder: path.join(
app.baseDir,
'/app/public/upload',
),
})
.on('end', () => {
result.cover = `${this.domain}/upload/${fileName}`;
resolve(result);
});
}
});
} catch {
resolve(result);
}
});
});

View File

@ -52,15 +52,28 @@
]
},
{
"id": "yreo1zOnA0tpLMpO4h",
"title": "g12s",
"status": "true",
"id": "B2apyT5NIgXPe4tRX7",
"title": "hj",
"status": "false",
"children": [
{
"id": 5,
"username": "test",
"username": "Guest-undefined",
"content": "ghjhj",
"createdAt": 1639325623775
}
]
},
{
"id": "kAoP518hzaPYD9Mx9z",
"title": "dsfdf",
"status": "true",
"children": [
{
"id": 6,
"username": "Guest-2",
"content": "sdfdf",
"createdAt": 1639328040653
"createdAt": 1639571978834
}
]
}

View File

@ -1,13 +1,13 @@
{
"id": "demo",
"content": {
"value": "<p data-id=\"peafab28-UeGZGcV7\"><br /></p><card type=\"block\" name=\"table\" editable=\"true\" value=\"data:%7B%22rows%22%3A3%2C%22cols%22%3A3%2C%22id%22%3A%221A2IV%22%2C%22type%22%3A%22block%22%2C%22height%22%3A102%2C%22width%22%3A690%2C%22html%22%3A%22%3Ctable%20class%3D%5C%22data-table%5C%22%20data-id%3D%5C%22t21b6eb9-LlLf0GOG%5C%22%20style%3D%5C%22width%3A%20690px%3B%5C%22%3E%3Ccolgroup%20data-id%3D%5C%22c9d5c669-CVY52f6H%5C%22%3E%3Ccol%20data-id%3D%5C%22c5da60d0-iFijk1EW%5C%22%20width%3D%5C%22230%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22c5da60d0-0D2HF3SP%5C%22%20width%3D%5C%22230%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22c5da60d0-Ni4KSj8K%5C%22%20width%3D%5C%22230%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3C%2Fcolgroup%3E%3Ctbody%20data-id%3D%5C%22t61d509e-Tf2WYKhU%5C%22%3E%3Ctr%20data-id%3D%5C%22t8f11d90-T7IL81Xf%5C%22%20style%3D%5C%22height%3A%2033px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22td18b8d3-2EeOGLQb%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-9N4g8AhO%5C%22%3Esdfdfkk%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-MgP6Ob2g%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-Pl6XNbLN%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-lP0l6F6H%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-nV334EmX%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t8f11d90-Ke7XQKBS%5C%22%20style%3D%5C%22height%3A%2033px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22td18b8d3-p7m1hYhY%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-PJO7hR6Q%5C%22%3Edfgfdgsdf4kk%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-hUPPS4P6%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-30U3kFAE%5C%22%3Edfgfg%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-8mpQLAJH%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-c6mQN6Hb%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t8f11d90-oNKFnQHT%5C%22%20style%3D%5C%22height%3A%2033px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22td18b8d3-l78d0KUc%5C%22%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-V4NEMkLV%5C%22%3Edfggsdfb1kk%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-hLULe0Ql%5C%22%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-8e3tXqmC%5C%22%3Edfgfg%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-9cNNI6bi%5C%22%20class%3D%5C%22table-last-column%20table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-YApPlQd6%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3C%2Ftbody%3E%3C%2Ftable%3E%22%7D\"></card><p data-id=\"peafab28-8nHRgJnn\">sdfabcdefg123dsf1111d1g12s</p><p data-id=\"peafab28-9QE8Ib78\"><strong>12ab12c21123</strong>12345</p><card type=\"block\" name=\"codeblock\" editable=\"false\" value=\"data:%7B%22id%22%3A%222Cwmg%22%2C%22type%22%3A%22block%22%2C%22mode%22%3A%22plain%22%2C%22code%22%3A%22hhhhjhjhjkkk%22%7D\"></card><p data-id=\"peafab28-3d0SPNQb\">1243sffffd</p>",
"value": "<h1 data-id=\"hbc788f1-HCEiiZG4\" id=\"hbc788f1-HCEiiZG4\">sdfsdfsdf</h1><p data-id=\"pd157317-9mX1S9ff\">sdfdsfdsfdfdfg</p><card type=\"block\" name=\"table\" editable=\"true\" value=\"data:%7B%22rows%22%3A3%2C%22cols%22%3A5%2C%22overflow%22%3A%7B%7D%2C%22id%22%3A%22z3W7h%22%2C%22type%22%3A%22block%22%2C%22height%22%3A105%2C%22width%22%3A1295%2C%22html%22%3A%22%3Ctable%20class%3D%5C%22data-table%5C%22%20data-id%3D%5C%22t7216feb-UR4lJETQ%5C%22%20style%3D%5C%22width%3A%201295px%3B%5C%22%3E%3Ccolgroup%20data-id%3D%5C%22c82d01ad-cO2cMESN%5C%22%3E%3Ccol%20data-id%3D%5C%22cac3d390-BEQW2Af8%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-BEQW2Af8%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-5aAaY4PI%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-5aAaY4PI%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-5aAaY4PI%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3C%2Fcolgroup%3E%3Ctbody%20data-id%3D%5C%22tc1e2dd5-ca338IhV%5C%22%3E%3Ctr%20data-id%3D%5C%22t40b42a1-aYJWmUJL%5C%22%20style%3D%5C%22height%3A%2035px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22t5815cab-bCGT82Ui%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-J6C5nVfC%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-faIHF5bL%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-iKc2C558%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-ADmVOlBb%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-B4BVQiFI%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%3E%3Cp%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-Gm3I1IW3%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-A0494K62%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t40b42a1-WRTdh09h%5C%22%20style%3D%5C%22height%3A%2035px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22t5815cab-kTR4C3DT%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-5fDBMYOP%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-rIYUHo3C%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-NJUMh6el%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-h9961ZOB%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-Wa0VLLCM%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%3E%3Cp%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-l7Cj19AZ%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-WAHoEoYg%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t40b42a1-GWMmlanJ%5C%22%20style%3D%5C%22height%3A%2035px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22t5815cab-kjgohUZP%5C%22%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-TJXbZWjs%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20class%3D%5C%22table-last-column%5C%22%20data-id%3D%5C%22te8113ae-FNQhbbZA%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-GT5JJPm6%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20class%3D%5C%22table-last-column%5C%22%20data-id%3D%5C%22te8113ae-iThMaAVQ%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-YnO3LRJI%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-57DBKodH%5C%22%20class%3D%5C%22table-last-column%20table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-Y7so1iqZ%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3C%2Ftbody%3E%3C%2Ftable%3E%22%7D\"></card><p data-id=\"pd157317-kOu29md8\">fgfdgfdg</p>",
"paths": [
{
"id": ["yreo1zOnA0tpLMpO4h"],
"id": ["kAoP518hzaPYD9Mx9z"],
"path": [
[2, 0, 22],
[2, 0, 26]
[1, 0, 6],
[1, 0, 11]
]
}
]

View File

@ -25,6 +25,7 @@
"egg-scripts": "^2.13.0",
"egg-view-assets": "^1.7.0",
"egg-view-nunjucks": "^2.3.0",
"fluent-ffmpeg": "^2.1.2",
"jsdom": "^16.4.0",
"prop-types": "^15.6.2",
"qs": "^6.7.0",