- 视频可拖动大小
- 表格可移除编辑区域
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; display: flex;
> span { > span {
position: relative; position: relative;
margin-left: 40px; margin-left: 24px;
display: inline-block; display: inline-block;
color: @c-text; color: @c-text;
height: @s-nav-height; height: @s-nav-height;
@ -113,7 +113,7 @@
} }
+ *:not(a) { + *:not(a) {
margin-left: 40px; margin-left: 24px;
} }
// second nav // second nav

2
.gitignore vendored
View File

@ -23,3 +23,5 @@
# log # log
*.log *.log
.vscode .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 ```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 * look for the address
*/ */
@ -124,3 +84,136 @@ Get all mentions in the document
//Return Array<{ key: string, name: string}> //Return Array<{ key: string, name: string}>
engine.command.executeMethod('mention', 'getList'); 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 ```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; action?: string;
/** /**
@ -124,3 +84,136 @@ parse?: (
//返回 Array<{ key: string, name: string}> //返回 Array<{ key: string, name: string}>
engine.command.executeMethod('mention', 'getList'); 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 ## Command
```ts ```ts

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import {
CardEntry, CardEntry,
PluginOptions, PluginOptions,
NodeInterface, NodeInterface,
$,
} from '@aomao/engine'; } from '@aomao/engine';
//引入插件 begin //引入插件 begin
import Redo from '@aomao/plugin-redo'; 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 Mention, { MentionComponent } from '@aomao/plugin-mention';
import Embed, { EmbedComponent } from '@aomao/plugin-embed'; import Embed, { EmbedComponent } from '@aomao/plugin-embed';
import Test, { TestComponent } from './plugins/test'; import Test, { TestComponent } from './plugins/test';
//import Mind, { MindComponent } from '@aomao/plugin-mind';
import { import {
ToolbarPlugin, ToolbarPlugin,
ToolbarComponent, ToolbarComponent,
@ -98,7 +98,6 @@ export const plugins: Array<PluginEntry> = [
Mention, Mention,
Embed, Embed,
Test, Test,
//Mind
]; ];
export const cards: Array<CardEntry> = [ export const cards: Array<CardEntry> = [
@ -115,10 +114,35 @@ export const cards: Array<CardEntry> = [
MentionComponent, MentionComponent,
TestComponent, TestComponent,
EmbedComponent, EmbedComponent,
//MindComponent
]; ];
export const pluginConfig: { [key: string]: PluginOptions } = { 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]: { [MarkRange.pluginName]: {
//标记类型集合 //标记类型集合
keys: ['comment'], keys: ['comment'],
@ -153,7 +177,7 @@ export const pluginConfig: { [key: string]: PluginOptions } = {
}, },
[Video.pluginName]: { [Video.pluginName]: {
onBeforeRender: (status: string, url: string) => { onBeforeRender: (status: string, url: string) => {
return url + `?token=12323`; return url;
}, },
}, },
[Math.pluginName]: { [Math.pluginName]: {

View File

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

View File

@ -1,9 +1,8 @@
.data-toc-wrapper { .data-toc-wrapper {
position: absolute; position: absolute;
top: 20px; top: 20px;
width: calc(50% - 454px); min-width: 210px;
right: calc(50% + 428px); padding: 0 16px;
padding-right: 24px;
} }
.data-toc-title { .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 Button from 'antd/es/button';
import 'antd/es/space/style'; import 'antd/es/space/style';
import 'antd/es/button/style'; import 'antd/es/button/style';
import './editor.css'; import './editor.less';
const localMember = const localMember =
typeof localStorage === 'undefined' ? null : localStorage.getItem('member'); typeof localStorage === 'undefined' ? null : localStorage.getItem('member');
@ -72,7 +72,7 @@ export default () => {
return ( return (
<Context.Provider value={{ lang }}> <Context.Provider value={{ lang }}>
<Space className="doc-editor-mode"> {/* <Space className="doc-editor-mode">
<Button <Button
size="small" size="small"
disabled={readonly} disabled={readonly}
@ -89,7 +89,7 @@ export default () => {
> >
{lang === 'zh-CN' ? '编辑模式' : 'Edit mode'} {lang === 'zh-CN' ? '编辑模式' : 'Edit mode'}
</Button> </Button>
</Space> </Space> */}
<Editor <Editor
lang={lang} lang={lang}
placeholder="这里是编辑区域哦~" placeholder="这里是编辑区域哦~"

View File

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

View File

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

View File

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

View File

@ -441,6 +441,7 @@ class ChangeModel implements ChangeInterface {
let node: NodeInterface | null = $(childNodes[0]); let node: NodeInterface | null = $(childNodes[0]);
let prev: NodeInterface | null = null; let prev: NodeInterface | null = null;
const appendNodes = []; const appendNodes = [];
let startRangeNodeParent = startRange.node.parent();
while (node && node.length > 0) { while (node && node.length > 0) {
nodeApi.removeSide(node); nodeApi.removeSide(node);
const next: NodeInterface | null = node.next(); const next: NodeInterface | null = node.next();
@ -458,6 +459,22 @@ class ChangeModel implements ChangeInterface {
if (!next) { if (!next) {
range.select(node, true).collapse(false); 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; node = next;
} }
if (mergeNode[0]) { if (mergeNode[0]) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
.data-image-resizer { .data-resizer {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -7,37 +7,10 @@
bottom: 0px; bottom: 0px;
right: 0; right: 0;
z-index: 1; z-index: 1;
outline: 2px solid #1890FF;
max-width: initial !important;
} }
.data-image-resizer-holder { .data-resizer img {
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 {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -46,13 +19,39 @@
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0;
}
.data-image-resizer-bg-active {
opacity: 0.3; 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; position: absolute;
display: inline-block; display: inline-block;
line-height: 24px; line-height: 24px;
@ -68,31 +67,31 @@
transform: scale(0.8); transform: scale(0.8);
} }
.data-image-resizer-number-right-top { .data-resizer-number-right-top {
top: 0px; top: 0px;
right: -6px; right: -6px;
transform: translateX(100%) scale(0.8); transform: translateX(100%) scale(0.8);
} }
.data-image-resizer-number-right-bottom { .data-resizer-number-right-bottom {
right: -6px; right: -6px;
bottom: 0px; bottom: 0px;
transform: translateX(100%) scale(0.8); transform: translateX(100%) scale(0.8);
} }
.data-image-resizer-number-left-bottom { .data-resizer-number-left-bottom {
left: -6px; left: -6px;
bottom: 0px; bottom: 0px;
transform: translateX(-100%) scale(0.8); transform: translateX(-100%) scale(0.8);
} }
.data-image-resizer-number-left-top { .data-resizer-number-left-top {
left: -6px; left: -6px;
top: 0px; top: 0px;
transform: translateX(-100%) scale(0.8); transform: translateX(-100%) scale(0.8);
} }
.data-image-resizer-number-active { .data-resizer-number-active {
opacity: 1; opacity: 1;
visibility: visible; 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'; import './index.css';
export type Options = { class Resizer implements ResizerInterface {
src: string; private options: ResizerOptions;
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;
private root: NodeInterface; private root: NodeInterface;
private image: NodeInterface; private image?: NodeInterface;
private resizerNumber: NodeInterface; private resizerNumber: NodeInterface;
private point: Point = { x: 0, y: 0 }; private point: Point = { x: 0, y: 0 };
private position?: Position; private position?: ResizerPosition;
private size: Size; private size: Size;
maxWidth: number; maxWidth: number;
/** /**
@ -40,11 +24,12 @@ class Resizer {
*/ */
private resizing: boolean = false; private resizing: boolean = false;
constructor(options: Options) { constructor(options: ResizerOptions) {
this.options = options; this.options = options;
this.root = $(this.renderTemplate(options.src)); this.root = $(this.renderTemplate(options.imgUrl));
this.image = this.root.find('img'); if (options.imgUrl) this.image = this.root.find('img');
this.resizerNumber = this.root.find('.data-image-resizer-number'); this.image?.hide();
this.resizerNumber = this.root.find('.data-resizer-number');
const { width, height } = this.options; const { width, height } = this.options;
this.size = { this.size = {
width, width,
@ -53,19 +38,19 @@ class Resizer {
this.maxWidth = this.options.maxWidth; this.maxWidth = this.options.maxWidth;
} }
renderTemplate(src: string) { renderTemplate(imgUrl?: string) {
return ` return `
<div class="data-image-resizer"> <div class="data-resizer">
<img class="data-image-resizer-bg data-image-resizer-bg-active" src="${src}" /> ${imgUrl ? `<img src="${imgUrl}">` : ''}
<div class="data-image-resizer-holder data-image-resizer-holder-right-top"></div> <div class="data-resizer-holder data-resizer-holder-right-top"></div>
<div class="data-image-resizer-holder data-image-resizer-holder-right-bottom"></div> <div class="data-resizer-holder data-resizer-holder-right-bottom"></div>
<div class="data-image-resizer-holder data-image-resizer-holder-left-bottom"></div> <div class="data-resizer-holder data-resizer-holder-left-bottom"></div>
<div class="data-image-resizer-holder data-image-resizer-holder-left-top"></div> <div class="data-resizer-holder data-resizer-holder-left-top"></div>
<span class="data-image-resizer-number"></span> <span class="data-resizer-number"></span>
</div>`; </div>`;
} }
onMouseDown(event: MouseEvent | TouchEvent, position: Position) { onMouseDown(event: MouseEvent | TouchEvent, position: ResizerPosition) {
if (this.resizing) return; if (this.resizing) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -97,11 +82,10 @@ class Resizer {
}; };
this.position = position; this.position = position;
this.resizing = true; this.resizing = true;
this.resizerNumber.addClass( this.root.addClass('data-resizing');
`data-image-resizer-number-${this.position}`, this.resizerNumber.addClass(`data-resizer-number-${this.position}`);
); this.resizerNumber.addClass('data-resizer-number-active');
this.resizerNumber.addClass('data-image-resizer-number-active'); this.image?.show();
this.image.show();
document.addEventListener( document.addEventListener(
isMobile ? 'touchmove' : 'mousemove', isMobile ? 'touchmove' : 'mousemove',
this.onMouseMove, this.onMouseMove,
@ -140,13 +124,11 @@ class Resizer {
width: clientWidth, width: clientWidth,
height: clientHeight, height: clientHeight,
}; };
this.resizerNumber.removeClass( this.resizerNumber.removeClass(`data-resizer-number-${this.position}`);
`data-image-resizer-number-${this.position}`, this.resizerNumber.removeClass('data-resizer-number-active');
);
this.resizerNumber.removeClass('data-image-resizer-number-active');
this.position = undefined; this.position = undefined;
this.resizing = false; this.resizing = false;
this.root.removeClass('data-resizing');
document.removeEventListener( document.removeEventListener(
isMobile ? 'touchmove' : 'mousemove', isMobile ? 'touchmove' : 'mousemove',
this.onMouseMove, this.onMouseMove,
@ -157,7 +139,7 @@ class Resizer {
); );
const { onChange } = this.options; const { onChange } = this.options;
if (onChange) onChange(this.size); if (onChange) onChange(this.size);
this.image.hide(); this.image?.hide();
}; };
updateSize(width: number, height: number) { updateSize(width: number, height: number) {
@ -166,6 +148,10 @@ class Resizer {
} else { } else {
width = this.size.width + width; width = this.size.width + width;
} }
this.setSize(width, height);
}
setSize(width: number, height: number) {
if (width < 24) { if (width < 24) {
width = 24; width = 24;
} }
@ -181,10 +167,6 @@ class Resizer {
} }
width = Math.round(width); width = Math.round(width);
height = Math.round(height); height = Math.round(height);
this.setSize(width, height);
}
setSize(width: number, height: number) {
this.root.css({ this.root.css({
width: width + 'px', width: width + 'px',
height: height + 'px', height: height + 'px',
@ -193,37 +175,33 @@ class Resizer {
} }
on(eventType: string, listener: EventListener) { on(eventType: string, listener: EventListener) {
this.image.on(eventType, listener); this.image?.on(eventType, listener);
} }
off(eventType: string, listener: EventListener) { off(eventType: string, listener: EventListener) {
this.image.off(eventType, listener); this.image?.off(eventType, listener);
} }
render() { render() {
const { width, height } = this.options; const { width, height } = this.options;
this.root.css({ this.setSize(width, height);
width: `${width}px`,
height: `${height}px`,
});
this.root this.root
.find('.data-image-resizer-holder-right-top') .find('.data-resizer-holder-right-top')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => { .on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'right-top'); return this.onMouseDown(event, 'right-top');
}); });
this.root this.root
.find('.data-image-resizer-holder-right-bottom') .find('.data-resizer-holder-right-bottom')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => { .on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'right-bottom'); return this.onMouseDown(event, 'right-bottom');
}); });
this.root this.root
.find('.data-image-resizer-holder-left-bottom') .find('.data-resizer-holder-left-bottom')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => { .on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'left-bottom'); return this.onMouseDown(event, 'left-bottom');
}); });
this.root this.root
.find('.data-image-resizer-holder-left-top') .find('.data-resizer-holder-left-top')
.on(isMobile ? 'touchstart' : 'mousedown', (event) => { .on(isMobile ? 'touchstart' : 'mousedown', (event) => {
return this.onMouseDown(event, 'left-top'); return this.onMouseDown(event, 'left-top');
}); });

View File

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

View File

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

View File

@ -1,18 +1,9 @@
import { DATA_ELEMENT } from '../../constants/root'; import { DATA_ELEMENT } from '../../constants/root';
import { NodeInterface } from '../../types/node'; import { NodeInterface } from '../../types/node';
import { Placement } from '../../types/position';
import { $ } from '../../node'; import { $ } from '../../node';
import './index.css'; import './index.css';
type Placement =
| 'top'
| 'topLeft'
| 'topRight'
| 'bottom'
| 'bottomLeft'
| 'bottomRight'
| 'left'
| 'right';
const template = (options: { placement: Placement }) => { const template = (options: { placement: Placement }) => {
return ` return `
<div ${DATA_ELEMENT}="tooltip" class="data-tooltip data-tooltip-placement-${options.placement} data-tooltip-hidden" style="transform-origin: 50% 45px 0px;"> <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, ToolbarItemOptions,
} from './toolbar'; } from './toolbar';
import { CardActiveTrigger, CardType } from '../card/enum'; import { CardActiveTrigger, CardType } from '../card/enum';
import { Placement } from './position';
export type CardOptions = { export type CardOptions = {
editor: EditorInterface; editor: EditorInterface;
@ -53,6 +54,7 @@ export interface CardToolbarInterface {
* @param offset [tx,ty,bx,by] * @param offset [tx,ty,bx,by]
*/ */
setOffset(offset: Array<number>): void; setOffset(offset: Array<number>): void;
setDefaultAlign(align: Placement): void;
/** /**
* *
*/ */

View File

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

View File

@ -22,8 +22,12 @@ export interface PluginEntry {
readonly pluginName: string; readonly pluginName: string;
} }
export interface PluginInterface { export interface PluginInterface<T extends PluginOptions = {}> {
readonly kind: string; readonly kind: string;
/**
*
**/
options: T;
/** /**
* readonly * 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 React, { useState } from 'react';
import classnames from 'classnames-es-ts'; import classnames from 'classnames-es-ts';
import { EngineInterface } from '@aomao/engine'; import { EngineInterface, Placement } from '@aomao/engine';
import Popover from 'antd/es/popover'; import Popover from 'antd/es/popover';
import 'antd/es/popover/style'; import 'antd/es/popover/style';
@ -17,19 +17,7 @@ export type CollapseItemProps = {
disabled?: boolean; disabled?: boolean;
onDisabled?: () => boolean; onDisabled?: () => boolean;
className?: string; className?: string;
placement?: placement?: Placement;
| 'right'
| 'top'
| 'left'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom';
onClick?: (event: React.MouseEvent, name: string) => void | boolean; onClick?: (event: React.MouseEvent, name: string) => void | boolean;
onMouseDown?: (event: React.MouseEvent) => void; 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 { import {
$, $,
EditorInterface,
isEngine, isEngine,
escape, escape,
NodeInterface,
sanitizeUrl, sanitizeUrl,
Tooltip, Tooltip,
isMobile, isMobile,
Resizer,
CardType, CardType,
} from '@aomao/engine'; } from '@aomao/engine';
import Pswp from '../pswp'; import Pswp from '../pswp';
import Resizer from '../resizer';
import './index.css'; import './index.css';
export type Status = 'uploading' | 'done' | 'error'; export type Status = 'uploading' | 'done' | 'error';
@ -295,12 +294,12 @@ class Image {
this.meta.css({ this.meta.css({
'background-color': '', 'background-color': '',
width: '', width: '',
height: '', //height: '',
}); });
this.image.css({ this.image.css({
width: '', width: '',
height: '', //height: '',
}); });
const img = this.image.get<HTMLImageElement>(); const img = this.image.get<HTMLImageElement>();
@ -334,7 +333,7 @@ class Image {
} }
this.image.css('width', `${width}px`); this.image.css('width', `${width}px`);
this.image.css('height', `${height}px`); //this.image.css('height', `${height}px`);
} }
changeSize(width: number, height: number) { changeSize(width: number, height: number) {
@ -359,7 +358,7 @@ class Image {
this.size.height = height; this.size.height = height;
this.image.css({ this.image.css({
width: `${width}px`, width: `${width}px`,
height: `${height}px`, //height: `${height}px`,
}); });
const { onChange } = this.options; const { onChange } = this.options;
@ -450,7 +449,7 @@ class Image {
if (isMobile || !isEngine(this.editor) || this.editor.readonly) return; if (isMobile || !isEngine(this.editor) || this.editor.readonly) return;
// 拖动调整图片大小 // 拖动调整图片大小
const resizer = new Resizer({ const resizer = new Resizer({
src: this.getSrc(), imgUrl: this.getSrc(),
width: clientWidth, width: clientWidth,
height: clientHeight, height: clientHeight,
rate: this.rate, rate: this.rate,
@ -542,7 +541,7 @@ class Image {
if (this.src) { if (this.src) {
this.image.css({ this.image.css({
width: width + 'px', width: width + 'px',
height: height + 'px', //height: height + 'px',
}); });
const { onChange } = this.options; const { onChange } = this.options;
if (width > 0 && height > 0) { if (width > 0 && height > 0) {

View File

@ -228,6 +228,23 @@ class ImageComponent extends Card<ImageValue> {
else this.image?.blur(); 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 { render(loadingBg?: string): string | void | NodeInterface {
const value = this.getValue(); const value = this.getValue();
if (!value) return; if (!value) return;
@ -253,20 +270,6 @@ class ImageComponent extends Card<ImageValue> {
}, },
onChange: (size) => { onChange: (size) => {
if (size) this.setSize(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: () => { onError: () => {
this.isLocalError = true; this.isLocalError = true;
@ -290,12 +293,8 @@ class ImageComponent extends Card<ImageValue> {
} }
didRender() { didRender() {
if ( super.didRender();
this.type === CardType.INLINE && this.toolbarModel?.setDefaultAlign('top');
this.getValue()?.status === 'done'
) {
this.toolbarModel?.setOffset([-12, 0, -12, 0]);
}
} }
} }

View File

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

View File

@ -42,6 +42,7 @@ class Pswp extends EventEmitter2 implements PswpInterface {
hideAnimationDuration: 0, hideAnimationDuration: 0,
closeOnVerticalDrag: isMobile, closeOnVerticalDrag: isMobile,
tapToClose: true, tapToClose: true,
bgOpacity: 0.8,
barsSize: { barsSize: {
top: 44, top: 44,
bottom: 80, 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 ```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; action?: string;
/** /**
@ -124,3 +84,136 @@ parse?: (
//返回 Array<{ key: string, name: string}> //返回 Array<{ key: string, name: string}>
engine.command.executeMethod('mention', 'getList'); 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); if (result) body?.append(result);
} else if ( } else if (
CollapseComponent.render || 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
? CollapseComponent.render(this.root, data, this.bindItem) ? CollapseComponent.render(this.root, data, this.bindItem)

View File

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

View File

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

View File

@ -5,9 +5,11 @@ import {
CardType, CardType,
EDITABLE_SELECTOR, EDITABLE_SELECTOR,
isEngine, isEngine,
isMobile,
NodeInterface, NodeInterface,
Parser, Parser,
RangeInterface, RangeInterface,
removeUnit,
Scrollbar, Scrollbar,
ToolbarItemOptions, ToolbarItemOptions,
} from '@aomao/engine'; } from '@aomao/engine';
@ -65,7 +67,7 @@ class TableComponent extends Card<TableValue> implements TableInterface {
selection: TableSelectionInterface = new TableSelection(this.editor, this); selection: TableSelectionInterface = new TableSelection(this.editor, this);
conltrollBar: ControllBarInterface = new ControllBar(this.editor, this, { conltrollBar: ControllBarInterface = new ControllBar(this.editor, this, {
col_min_width: 40, col_min_width: 40,
row_min_height: 33, row_min_height: 35,
}); });
command: TableCommandInterface = new TableCommand(this.editor, this); command: TableCommandInterface = new TableCommand(this.editor, this);
scrollbar?: Scrollbar; scrollbar?: Scrollbar;
@ -80,6 +82,138 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (isEngine(this.editor)) { if (isEngine(this.editor)) {
this.editor.on('undo', this.doChange); this.editor.on('undo', this.doChange);
this.editor.on('redo', 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; if (this.colorTool) return;
this.colorTool = new ColorTool(this.editor, this.id, { this.colorTool = new ColorTool(this.editor, this.id, {
@ -172,10 +306,7 @@ class TableComponent extends Card<TableValue> implements TableInterface {
}, },
]; ];
if (this.isMaximize) return funBtns; if (this.isMaximize) return funBtns;
return [ const toolbars: Array<ToolbarItemOptions | CardToolbarItemOptions> = [
{
type: 'dnd',
},
{ {
type: 'maximize', type: 'maximize',
}, },
@ -190,6 +321,12 @@ class TableComponent extends Card<TableValue> implements TableInterface {
}, },
...funBtns, ...funBtns,
]; ];
if (removeUnit(this.wrapper?.css('margin-left') || '0') === 0) {
toolbars.unshift({
type: 'dnd',
});
}
return toolbars;
} }
updateAlign(event: MouseEvent, align: 'top' | 'middle' | 'bottom' = 'top') { updateAlign(event: MouseEvent, align: 'top' | 'middle' | 'bottom' = 'top') {
@ -291,12 +428,10 @@ class TableComponent extends Card<TableValue> implements TableInterface {
super.activate(activated); super.activate(activated);
if (activated) { if (activated) {
this.wrapper?.addClass('active'); this.wrapper?.addClass('active');
this.scrollbar?.enableScroll();
} else { } else {
this.selection.clearSelect(); this.selection.clearSelect();
this.conltrollBar.hideContextMenu(); this.conltrollBar.hideContextMenu();
this.wrapper?.removeClass('active'); this.wrapper?.removeClass('active');
this.scrollbar?.disableScroll();
} }
this.scrollbar?.refresh(); this.scrollbar?.refresh();
} }
@ -344,6 +479,33 @@ class TableComponent extends Card<TableValue> implements TableInterface {
return nodes; 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() { didRender() {
super.didRender(); super.didRender();
this.viewport = isEngine(this.editor) this.viewport = isEngine(this.editor)
@ -356,8 +518,49 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (!isEngine(this.editor) || this.editor.readonly) if (!isEngine(this.editor) || this.editor.readonly)
this.toolbarModel?.setOffset([0, 0]); this.toolbarModel?.setOffset([0, 0]);
else this.toolbarModel?.setOffset([0, -28, 0, -6]); else this.toolbarModel?.setOffset([0, -28, 0, -6]);
const tablePlugin = this.editor.plugin.components['table'];
const tableOptions = tablePlugin?.options['overflow'] || {};
if (this.viewport) { 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.setContentNode(this.viewport.find('.data-table')!);
this.scrollbar.on('display', (display: 'node' | 'block') => { this.scrollbar.on('display', (display: 'node' | 'block') => {
if (display === 'block') { if (display === 'block') {
@ -367,14 +570,18 @@ class TableComponent extends Card<TableValue> implements TableInterface {
} }
}); });
this.scrollbar.disableScroll(); this.scrollbar.disableScroll();
let changeTimeout: NodeJS.Timeout | undefined;
const handleScrollbarChange = () => { const handleScrollbarChange = () => {
if (changeTimeout) clearTimeout(changeTimeout); if (tableOptions['maxRightWidth'])
changeTimeout = setTimeout(() => { this.overflow(tableOptions['maxRightWidth']());
if (isEngine(this.editor)) this.editor.ot.initSelection(); if (isEngine(this.editor)) this.editor.ot.initSelection();
}, 50);
}; };
this.scrollbar.on('change', handleScrollbarChange); 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.selection.on('select', () => {
this.conltrollBar.refresh(); this.conltrollBar.refresh();
@ -401,6 +608,8 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (!silence) { if (!silence) {
this.onChange(); this.onChange();
} }
if (tableOptions['maxRightWidth'])
this.overflow(tableOptions['maxRightWidth']());
this.scrollbar?.refresh(); this.scrollbar?.refresh();
}); });
@ -412,6 +621,9 @@ class TableComponent extends Card<TableValue> implements TableInterface {
if (tableValue) this.setValue(tableValue); if (tableValue) this.setValue(tableValue);
this.onChange(); this.onChange();
} }
if (tableOptions['maxRightWidth'])
this.overflow(tableOptions['maxRightWidth']());
this.scrollbar?.refresh();
} }
render() { render() {

View File

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

View File

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

View File

@ -27,7 +27,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
} }
.data-table tr { .data-table tr {
height: 33px; height: 35px;
} }
.data-table tr td { .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 { .table-wrapper.scrollbar-show {
margin-bottom: -8px; margin-bottom: -10px;
} }
.table-wrapper.data-table-highlight tr td[table-cell-selection]:after { .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; 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; background: rgba(255, 77, 79, 0.4) !important;
border-color: rgba(255, 77, 79, 0.4) !important;
} }
.table-wrapper .table-header-item:hover{ .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 { .table-wrapper .table-header.selected .table-header-item {
background: #4daaff; background: #4daaff;
border-color: #4daaff;
} }
.table-wrapper .table-cols-header { .table-wrapper .table-cols-header {
position: relative; position: relative;
height: 14px; height: 13px;
display: none; display: none;
width: 100%; width: 100%;
cursor: default; cursor: default;
margin-bottom: -1px;
z-index: 2;
} }
.table-wrapper.active .table-cols-header { .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 { .table-wrapper .table-cols-header .table-cols-header-item {
position: relative; position: relative;
height: 14px; height: 13px;
width: auto; width: auto;
border: 1px solid #dfdfdf; border: 1px solid #dfdfdf;
border-bottom: 0 none; border-bottom: 0 none;
@ -161,7 +159,6 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
background: #fff; background: #fff;
z-index: 1; z-index: 1;
border-radius: 0; border-radius: 0;
height: 14px;
border-bottom: 0; border-bottom: 0;
cursor: move; cursor: move;
} }
@ -250,11 +247,11 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
width: 14px; width: 14px;
z-index: 1; z-index: 1;
border-right: 0; border-right: 0;
visibility: hidden; display: none;
} }
.table-wrapper.active .table-rows-header { .table-wrapper.active .table-rows-header {
visibility: visible; display: block;
} }
.table-wrapper .table-rows-header .table-rows-header-item { .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 { .table-wrapper .table-viewport .scrollbar-shadow-left {
top: 0; top: 0;
bottom: 8px; bottom: 10px;
} }
.table-wrapper.active .table-viewport .scrollbar-shadow-left { .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 { .table-wrapper .table-viewport .scrollbar-shadow-right {
top: 0; top: 0;
bottom: 8px; bottom: 10px;
} }
.table-wrapper.active .table-viewport .scrollbar-shadow-right { .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; z-index: 3;
} }
.table-wrapper .table-main-content * {
max-width: 100%;
}
.table-wrapper .table-main-bg { .table-wrapper .table-main-bg {
position: absolute; position: absolute;
top: 0; 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 { .table-wrapper.scrollbar-show .data-scrollable.scroll-x {
padding-bottom: 8px; padding-bottom: 10px;
} }
.table-wrapper .data-scrollable.scroll-x { .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; 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{ .table-wrapper.scrollbar-show .data-scrollable.scroll-x .data-scrollbar-x{
margin-bottom: 2px; margin-bottom: 2px;
} }
.table-wrapper .data-scrollable .data-scrollbar.data-scrollbar-x { .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 { .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 { .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 { .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 { .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{ .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 { .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 { .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"] { [data-card-key="table"].data-card-block-max > [data-card-element="body"] > [data-card-element="center"] {
padding: 48px; padding: 48px;
margin-top: 4px; margin-top: 4px;
} }
/**
表格可溢出样式
**/
.table-wrapper.table-overflow {
width: auto;
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
[data-card-key="video"] { .data-video {
outline: 1px solid #ddd; margin: 0 auto;
position: relative;
cursor: pointer;
} }
.data-video-content { .data-video-content {
position: relative; position: relative;
@ -9,6 +11,8 @@
.data-video-content video { .data-video-content video {
width: 100%; width: 100%;
outline: none; outline: none;
position: relative;
z-index: 1;
} }
.data-video-uploading, .data-video-uploading,
.data-video-uploaded, .data-video-uploaded,
@ -23,7 +27,7 @@
line-height: 0; line-height: 0;
} }
.data-video-active { .data-video-active {
outline: 1px solid #d9d9d9; user-select: none;
} }
.data-video-center { .data-video-center {
position: absolute; position: absolute;
@ -81,3 +85,20 @@
vertical-align: middle; vertical-align: middle;
margin: -2px 5px 0 0; 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 { import {
$, $,
Card, Card,
CardToolbarItemOptions,
CardType, CardType,
escape, escape,
getFileSize, getFileSize,
isEngine, isEngine,
isMobile,
NodeInterface,
sanitizeUrl, sanitizeUrl,
ToolbarItemOptions, Resizer,
} from '@aomao/engine'; } from '@aomao/engine';
import './index.css'; import './index.css';
@ -49,6 +51,22 @@ export type VideoValue = {
* *
*/ */
size?: number; size?: number;
/**
*
*/
width?: number;
/**
*
*/
height?: number;
/**
*
*/
naturalWidth?: number;
/**
*
*/
naturalHeight?: number;
/** /**
* *
*/ */
@ -56,6 +74,14 @@ export type VideoValue = {
}; };
class VideoComponent extends Card<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() { static get cardName() {
return 'video'; return 'video';
} }
@ -68,7 +94,9 @@ class VideoComponent extends Card<VideoValue> {
return false; return false;
} }
private container?: NodeInterface; static get singleSelectable() {
return false;
}
getLocales() { getLocales() {
return this.editor.language.get<{ [key: string]: string }>('video'); 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 fileSize: string = size ? getFileSize(size) : '';
const titleElement = name
? `<div class="data-video-title">${escape(name)}</div>`
: '';
if (status === 'uploading') { if (status === 'uploading') {
return ` return `
<div class="data-video"> <div class="data-video">
@ -143,10 +173,11 @@ class VideoComponent extends Card<VideoValue> {
</div> </div>
`; `;
} }
const videoPlugin = this.editor.plugin.components['video'];
return ` return `
<div class="data-video"> <div class="data-video">
<div class="data-video-content data-video-done"></div> <div class="data-video-content data-video-done"></div>
${videoPlugin && videoPlugin.options.showTitle !== false ? titleElement : ''}
</div> </div>
`; `;
} }
@ -176,12 +207,18 @@ class VideoComponent extends Card<VideoValue> {
if (cover) { if (cover) {
video.poster = sanitizeUrl(this.onBeforeRender('cover', 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.container?.find('.data-video-content').append(video);
this.videoContainer = this.container?.find('.data-video-content');
video.oncontextmenu = function () { video.oncontextmenu = function () {
return false; return false;
}; };
this.video = $(video);
this.title = this.container?.find('.data-video-title');
this.resetSize();
// 一次渲染时序开启 controls 会触发一次内容为空的 window.onerror疑似 chrome bug // 一次渲染时序开启 controls 会触发一次内容为空的 window.onerror疑似 chrome bug
setTimeout(() => { setTimeout(() => {
video.controls = true; video.controls = true;
@ -234,9 +271,158 @@ class VideoComponent extends Card<VideoValue> {
this.container?.find('.percent').html(`${percent}%`); 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) { onActivate(activated: boolean) {
if (activated) this.container?.addClass('data-video-active'); if (activated) {
else this.container?.removeClass('data-video-active'); 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( checker(
@ -284,6 +470,8 @@ class VideoComponent extends Card<VideoValue> {
const { command, plugin } = this.editor; const { command, plugin } = this.editor;
const { video_id, status } = value; const { video_id, status } = value;
const locales = this.getLocales(); const locales = this.getLocales();
this.maxWidth = this.getMaxWidth();
//阅读模式 //阅读模式
if (!isEngine(this.editor)) { if (!isEngine(this.editor)) {
if (status === 'done') { if (status === 'done') {
@ -418,6 +606,7 @@ class VideoComponent extends Card<VideoValue> {
: value.download, : value.download,
}; };
this.container = $(this.renderTemplate(newValue)); this.container = $(this.renderTemplate(newValue));
this.video = this.container.find('video');
center.empty(); center.empty();
center.append(this.container); center.append(this.container);
this.initPlayer(); this.initPlayer();
@ -436,17 +625,27 @@ class VideoComponent extends Card<VideoValue> {
); );
return this.container; return this.container;
} else { } else {
return $(this.renderTemplate(value)); this.container = $(this.renderTemplate(value));
return this.container;
} }
} }
handleClick = () => {
if (isEngine(this.editor) && !this.activated) {
this.editor.card.activate(this.root);
}
};
didRender() { didRender() {
super.didRender(); super.didRender();
this.container?.on(isMobile ? 'touchstart' : 'click', () => { this.toolbarModel?.setDefaultAlign('top');
if (isEngine(this.editor) && !this.activated) { this.container?.on('click', this.handleClick);
this.editor.card.activate(this.root); }
}
}); destroy() {
super.destroy();
this.container?.off('click', this.handleClick);
window.removeEventListener('resize', this.onWindowResize);
} }
} }

View File

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

View File

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

View File

@ -1,6 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const sendToWormhole = require('stream-wormhole'); const sendToWormhole = require('stream-wormhole');
const ffmpeg = require('fluent-ffmpeg');
const { Controller } = require('egg'); const { Controller } = require('egg');
class UploadController extends Controller { class UploadController extends Controller {
@ -159,10 +160,53 @@ class UploadController extends Controller {
// 监听写入完成事件 // 监听写入完成事件
remoteFileStrem.on('finish', () => { remoteFileStrem.on('finish', () => {
if (errFlag) return; if (errFlag) return;
resolve({ const result = {
url, url,
download: 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", "id": "B2apyT5NIgXPe4tRX7",
"title": "g12s", "title": "hj",
"status": "true", "status": "false",
"children": [ "children": [
{ {
"id": 5, "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", "content": "sdfdf",
"createdAt": 1639328040653 "createdAt": 1639571978834
} }
] ]
} }

View File

@ -1,13 +1,13 @@
{ {
"id": "demo", "id": "demo",
"content": { "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": [ "paths": [
{ {
"id": ["yreo1zOnA0tpLMpO4h"], "id": ["kAoP518hzaPYD9Mx9z"],
"path": [ "path": [
[2, 0, 22], [1, 0, 6],
[2, 0, 26] [1, 0, 11]
] ]
} }
] ]

View File

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