parent
c46845d865
commit
f84824aba8
|
@ -72,7 +72,7 @@
|
|||
display: flex;
|
||||
> span {
|
||||
position: relative;
|
||||
margin-left: 40px;
|
||||
margin-left: 24px;
|
||||
display: inline-block;
|
||||
color: @c-text;
|
||||
height: @s-nav-height;
|
||||
|
@ -113,7 +113,7 @@
|
|||
}
|
||||
|
||||
+ *:not(a) {
|
||||
margin-left: 40px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
// second nav
|
||||
|
|
|
@ -23,3 +23,5 @@
|
|||
# log
|
||||
*.log
|
||||
.vscode
|
||||
|
||||
**/ffmpeg
|
|
@ -31,47 +31,7 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
`defaultData`: Default drop-down query list display data
|
||||
|
||||
`onSearch`: the method to query, or configure the action, choose one of the two
|
||||
|
||||
`onSelect`: Call back after selecting an item in the list, here you can return a custom value combined with key and name to form a new value and store it in cardValue. And it will return together after executing the getList command
|
||||
|
||||
`onClick`: Triggered when clicking on the "mention"
|
||||
|
||||
`onMouseEnter`: Triggered when the mouse moves over the "mention"
|
||||
|
||||
`onRender`: custom rendering list
|
||||
|
||||
`onRenderItem`: custom rendering list item
|
||||
|
||||
`onLoading`: custom rendering loading status
|
||||
|
||||
`onEmpty`: custom render empty state
|
||||
|
||||
`action`: query address, always use `GET` request, parameter `keyword`
|
||||
|
||||
`data`: When querying, these data will be sent to the server at the same time
|
||||
|
||||
```ts
|
||||
//List data displayed by default
|
||||
defaultData?: Array<{ key: string, name: string, avatar?: string}>
|
||||
//Method for query, or configure action, choose one of the two
|
||||
onSearch?:(keyword: string) => Promise<Array<{ key: string, name: string, avatar?: string}>>
|
||||
//Call back after selecting an item in the list, here you can return a custom value combined with key and name to form a new value and store it in cardValue. And it will return together after executing the getList command
|
||||
onSelect?: (data: {[key:string]: string}) => void | {[key: string]: string}
|
||||
//Click event on "mention"
|
||||
onClick?:(data: {[key:string]: string}) => void
|
||||
// Triggered when the mouse moves over the "mention"
|
||||
onMouseEnter?:(node: NodeInterface, data: {[key:string]: string}) => void
|
||||
//Customize the rendering list, bindItem can bind the required properties and events for the list item
|
||||
onRender?: (data: MentionItem, root: NodeInterface, bindItem: (node: NodeInterface, data: {[key:string]: string}) => NodeInterface) => Promise<string | NodeInterface | void>;
|
||||
//Custom rendering list items
|
||||
onRenderItem?: (item: MentionItem, root: NodeInterface) => string | NodeInterface | void
|
||||
// Customize the rendering loading status
|
||||
onLoading?: (root: NodeInterface) => string | NodeInterface | void
|
||||
// Custom render empty state
|
||||
onEmpty?: (root: NodeInterface) => string | NodeInterface | void
|
||||
/**
|
||||
* look for the address
|
||||
*/
|
||||
|
@ -124,3 +84,136 @@ Get all mentions in the document
|
|||
//Return Array<{ key: string, name: string}>
|
||||
engine.command.executeMethod('mention', 'getList');
|
||||
```
|
||||
|
||||
## Plug-in events
|
||||
|
||||
`mention:default`: default drop-down query list to display data
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:default', () => {
|
||||
return [];
|
||||
});
|
||||
```
|
||||
|
||||
`mention:search`: Method of query, or configure action, choose one of the two
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:search', (keyword) => {
|
||||
return new Promise((resolve) => {
|
||||
query({ keyword })
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch(() => resolve([]));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
`mention:select`: Call back after selecting an item in the list, here you can return a custom value combined with key and name to form a new value and store it in cardValue. And will return together after executing the getList command
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:select', (data) => {
|
||||
data['test'] = 'test';
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
`mention:item-click`: triggered when clicking on "mention"
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:item-click',
|
||||
(root: NodeInterface, { key, name }: { key: string; name: string }) => {
|
||||
console.log('mention click:', key, '-', name);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:enter`: Triggered when the mouse moves over the "mention"
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:enter',
|
||||
(layout: NodeInterface, { name }: { key: string; name: string }) => {
|
||||
ReactDOM.render(
|
||||
<div style={{ padding: 5 }}>
|
||||
<p>This is name: {name}</p>
|
||||
<p>Configure the mention:enter event of the mention plugin</p>
|
||||
<p>Use ReactDOM.render to customize rendering here</p>
|
||||
<p>Use ReactDOM.render to customize rendering here</p>
|
||||
</div>,
|
||||
layout.get<HTMLElement>()!,
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:render`: custom rendering list
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:render',
|
||||
(
|
||||
root: NodeInterface,
|
||||
data: Array<MentionItem>,
|
||||
bindItem: (
|
||||
node: NodeInterface,
|
||||
data: { [key: string]: string },
|
||||
) => NodeInterface,
|
||||
) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const renderCallback = (items: { [key: string]: Element }) => {
|
||||
// Traverse the DOM node of each item
|
||||
Object.keys(items).forEach((key) => {
|
||||
const element = items[key];
|
||||
const item = data.find((d) => d.key === key);
|
||||
if (!item) return;
|
||||
// Bind the attributes and events of each list item to meet the functional needs of the up, down, left, and right selection in the editor
|
||||
bindItem($(element), item);
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
ReactDOM.render(
|
||||
<MentionList data={data} callback={renderCallback} />,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:render-item`: custom rendering list item
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:render-item', (data, root) => {
|
||||
const item = $(`<div>${data}</div>`);
|
||||
root.append(item);
|
||||
return item;
|
||||
});
|
||||
```
|
||||
|
||||
`mention:loading`: custom rendering loading status
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:loading', (data, root) => {
|
||||
root.html(`<div>${data}</div>`);
|
||||
// or
|
||||
ReactDOM.render(
|
||||
<div className="data-mention-loading">Loading...</div>,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
`mention:empty`: custom render empty state
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:empty', (root) => {
|
||||
root.html('<div>No data found</div>');
|
||||
// or
|
||||
ReactDOM.render(
|
||||
<div className="data-mention-empty">Empty</div>,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
|
|
@ -31,49 +31,9 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
`defaultData`: 默认下拉查询列表展示数据
|
||||
|
||||
`onSearch`: 查询时的方法,或者配置 action,二选其一
|
||||
|
||||
`onSelect`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
|
||||
|
||||
`onClick`: 在“提及”上单击时触发
|
||||
|
||||
`onMouseEnter`: 鼠标移入“提及”上时触发
|
||||
|
||||
`onRender`: 自定义渲染列表
|
||||
|
||||
`onRenderItem`: 自定义渲染列表项
|
||||
|
||||
`onLoading`: 自定渲染加载状态
|
||||
|
||||
`onEmpty`: 自定渲染空状态
|
||||
|
||||
`action`: 查询地址,始终使用 `GET` 请求,参数 `keyword`
|
||||
|
||||
`data`: 查询时同时将这些数据一起传到到服务端
|
||||
|
||||
```ts
|
||||
//默认展示的列表数据
|
||||
defaultData?: Array<{ key: string, name: string, avatar?: string}>
|
||||
//查询时的方法,或者配置 action,二选其一
|
||||
onSearch?:(keyword: string) => Promise<Array<{ key: string, name: string, avatar?: string}>>
|
||||
//选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
|
||||
onSelect?: (data: {[key:string]: string}) => void | {[key: string]: string}
|
||||
//在“提及”上单击事件
|
||||
onClick?:(data: {[key:string]: string}) => void
|
||||
//鼠标移入“提及”上时触发
|
||||
onMouseEnter?:(node: NodeInterface, data: {[key:string]: string}) => void
|
||||
//自定义渲染列表,bindItem 可以为列表项绑定需要的属性和事件
|
||||
onRender?: (data: MentionItem, root: NodeInterface, bindItem: (node: NodeInterface, data: {[key:string]: string}) => NodeInterface) => Promise<string | NodeInterface | void>;
|
||||
//自定义渲染列表项
|
||||
onRenderItem?: (item: MentionItem, root: NodeInterface) => string | NodeInterface | void
|
||||
// 自定渲染加载状态
|
||||
onLoading?: (root: NodeInterface) => string | NodeInterface | void
|
||||
// 自定渲染空状态
|
||||
onEmpty?: (root: NodeInterface) => string | NodeInterface | void
|
||||
/**
|
||||
* 查询地址
|
||||
* 查询地址,或者监听 mention:search 事件执行查询
|
||||
*/
|
||||
action?: string;
|
||||
/**
|
||||
|
@ -124,3 +84,136 @@ parse?: (
|
|||
//返回 Array<{ key: string, name: string}>
|
||||
engine.command.executeMethod('mention', 'getList');
|
||||
```
|
||||
|
||||
## 插件事件
|
||||
|
||||
`mention:default`: 默认下拉查询列表展示数据
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:default', () => {
|
||||
return [];
|
||||
});
|
||||
```
|
||||
|
||||
`mention:search`: 查询时的方法,或者配置 action,二选其一
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:search', (keyword) => {
|
||||
return new Promise((resolve) => {
|
||||
query({ keyword })
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch(() => resolve([]));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
`mention:select`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:select', (data) => {
|
||||
data['test'] = 'test';
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
`mention:item-click`: 在“提及”上单击时触发
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:item-click',
|
||||
(root: NodeInterface, { key, name }: { key: string; name: string }) => {
|
||||
console.log('mention click:', key, '-', name);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:enter`: 鼠标移入“提及”上时触发
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:enter',
|
||||
(layout: NodeInterface, { name }: { key: string; name: string }) => {
|
||||
ReactDOM.render(
|
||||
<div style={{ padding: 5 }}>
|
||||
<p>This is name: {name}</p>
|
||||
<p>配置 mention 插件的 mention:enter 事件</p>
|
||||
<p>此处使用 ReactDOM.render 自定义渲染</p>
|
||||
<p>Use ReactDOM.render to customize rendering here</p>
|
||||
</div>,
|
||||
layout.get<HTMLElement>()!,
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:render`: 自定义渲染列表
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:render',
|
||||
(
|
||||
root: NodeInterface,
|
||||
data: Array<MentionItem>,
|
||||
bindItem: (
|
||||
node: NodeInterface,
|
||||
data: { [key: string]: string },
|
||||
) => NodeInterface,
|
||||
) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const renderCallback = (items: { [key: string]: Element }) => {
|
||||
// 遍历每个项的DOM节点
|
||||
Object.keys(items).forEach((key) => {
|
||||
const element = items[key];
|
||||
const item = data.find((d) => d.key === key);
|
||||
if (!item) return;
|
||||
// 绑定每个列表项所属的属性、事件,以满足编辑器中上下左右选择的功能需要
|
||||
bindItem($(element), item);
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
ReactDOM.render(
|
||||
<MentionList data={data} callback={renderCallback} />,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:render-item`: 自定义渲染列表项
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:render-item', (data, root) => {
|
||||
const item = $(`<div>${data}</div>`);
|
||||
root.append(item);
|
||||
return item;
|
||||
});
|
||||
```
|
||||
|
||||
`mention:loading`: 自定渲染加载状态
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:loading', (data, root) => {
|
||||
root.html(`<div>${data}</div>`);
|
||||
// or
|
||||
ReactDOM.render(
|
||||
<div className="data-mention-loading">Loading...</div>,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
`mention:empty`: 自定渲染空状态
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:empty', (root) => {
|
||||
root.html('<div>没有查询到数据</div>');
|
||||
// or
|
||||
ReactDOM.render(
|
||||
<div className="data-mention-empty">Empty</div>,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
|
|
@ -41,6 +41,17 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
### Overflow display
|
||||
|
||||
```ts
|
||||
overflow?: {
|
||||
// Relative to the maximum displayable width on the left side of the editor
|
||||
maxLeftWidth?: () => number;
|
||||
// Relative to the maximum displayable width on the right side of the editor
|
||||
maxRightWidth?: () => number;
|
||||
};
|
||||
```
|
||||
|
||||
## Command
|
||||
|
||||
```ts
|
||||
|
|
|
@ -41,6 +41,17 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
### 溢出展示
|
||||
|
||||
```ts
|
||||
overflow?: {
|
||||
// 相对编辑器左侧最大能展示的宽度
|
||||
maxLeftWidth?: () => number;
|
||||
// 相对于编辑器右侧最大能展示的宽度
|
||||
maxRightWidth?: () => number;
|
||||
};
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
```ts
|
||||
|
|
|
@ -40,6 +40,14 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
### Whether to display the video title
|
||||
|
||||
Default Display
|
||||
|
||||
```ts
|
||||
showTitle?: boolean
|
||||
```
|
||||
|
||||
### File Upload
|
||||
|
||||
`action`: upload address, always use `POST` request
|
||||
|
|
|
@ -40,6 +40,14 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
### 是否显示视频标题
|
||||
|
||||
默认显示
|
||||
|
||||
```ts
|
||||
showTitle?: boolean
|
||||
```
|
||||
|
||||
### 文件上传
|
||||
|
||||
`action`: 上传地址,始终使用 `POST` 请求
|
||||
|
|
|
@ -44,9 +44,9 @@
|
|||
.doc-comment-layer {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: calc(50% - 454px);
|
||||
left: calc(50% + 428px);
|
||||
padding-left: 24px;
|
||||
padding-left: 16px;
|
||||
min-width: 260px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.doc-comment-title {
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
CardEntry,
|
||||
PluginOptions,
|
||||
NodeInterface,
|
||||
$,
|
||||
} from '@aomao/engine';
|
||||
//引入插件 begin
|
||||
import Redo from '@aomao/plugin-redo';
|
||||
|
@ -43,7 +44,6 @@ import LineHeight from '@aomao/plugin-line-height';
|
|||
import Mention, { MentionComponent } from '@aomao/plugin-mention';
|
||||
import Embed, { EmbedComponent } from '@aomao/plugin-embed';
|
||||
import Test, { TestComponent } from './plugins/test';
|
||||
//import Mind, { MindComponent } from '@aomao/plugin-mind';
|
||||
import {
|
||||
ToolbarPlugin,
|
||||
ToolbarComponent,
|
||||
|
@ -98,7 +98,6 @@ export const plugins: Array<PluginEntry> = [
|
|||
Mention,
|
||||
Embed,
|
||||
Test,
|
||||
//Mind
|
||||
];
|
||||
|
||||
export const cards: Array<CardEntry> = [
|
||||
|
@ -115,10 +114,35 @@ export const cards: Array<CardEntry> = [
|
|||
MentionComponent,
|
||||
TestComponent,
|
||||
EmbedComponent,
|
||||
//MindComponent
|
||||
];
|
||||
|
||||
export const pluginConfig: { [key: string]: PluginOptions } = {
|
||||
[Table.pluginName]: {
|
||||
overflow: {
|
||||
maxLeftWidth: () => {
|
||||
// 编辑区域位置
|
||||
const rect = $('.editor-content')
|
||||
.get<HTMLElement>()
|
||||
?.getBoundingClientRect();
|
||||
const editorLeft = rect?.left || 0;
|
||||
// 减去大纲的宽度
|
||||
const width = editorLeft - $('.data-toc-wrapper').width();
|
||||
// 留 16px 的间隔
|
||||
return width <= 0 ? 100 : width - 16;
|
||||
},
|
||||
maxRightWidth: () => {
|
||||
// 编辑区域位置
|
||||
const rect = $('.editor-content')
|
||||
.get<HTMLElement>()
|
||||
?.getBoundingClientRect();
|
||||
const editorRigth = (rect?.right || 0) - (rect?.width || 0);
|
||||
// 减去评论区域的宽度
|
||||
const width = editorRigth - $('.doc-comment-layer').width();
|
||||
// 留 16px 的间隔
|
||||
return width <= 0 ? 100 : width - 16;
|
||||
},
|
||||
},
|
||||
},
|
||||
[MarkRange.pluginName]: {
|
||||
//标记类型集合
|
||||
keys: ['comment'],
|
||||
|
@ -153,7 +177,7 @@ export const pluginConfig: { [key: string]: PluginOptions } = {
|
|||
},
|
||||
[Video.pluginName]: {
|
||||
onBeforeRender: (status: string, url: string) => {
|
||||
return url + `?token=12323`;
|
||||
return url;
|
||||
},
|
||||
},
|
||||
[Math.pluginName]: {
|
||||
|
|
|
@ -90,8 +90,6 @@
|
|||
}
|
||||
|
||||
.editor-container {
|
||||
background: #fafafa;
|
||||
background-color: #fafafa;
|
||||
padding: 24px 0 64px;
|
||||
height: calc(100vh - 138px);
|
||||
width: 100%;
|
||||
|
@ -111,7 +109,6 @@
|
|||
width: 812px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
min-height: 800px;
|
||||
@media @mobile {
|
||||
width: auto;
|
||||
|
@ -121,7 +118,7 @@
|
|||
}
|
||||
|
||||
.editor-content .am-engine {
|
||||
padding: 40px 60px 60px;
|
||||
padding: 40px 0 60px;
|
||||
|
||||
@media @mobile {
|
||||
padding: 18px 0 0 0;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
.data-toc-wrapper {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: calc(50% - 454px);
|
||||
right: calc(50% + 428px);
|
||||
padding-right: 24px;
|
||||
min-width: 210px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.data-toc-title {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
.doc-editor-mode {
|
||||
font-size: 12px;
|
||||
background: #ffffff;
|
||||
padding: 0;
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
top: 68px;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import Space from 'antd/es/space';
|
|||
import Button from 'antd/es/button';
|
||||
import 'antd/es/space/style';
|
||||
import 'antd/es/button/style';
|
||||
import './editor.css';
|
||||
import './editor.less';
|
||||
|
||||
const localMember =
|
||||
typeof localStorage === 'undefined' ? null : localStorage.getItem('member');
|
||||
|
@ -72,7 +72,7 @@ export default () => {
|
|||
|
||||
return (
|
||||
<Context.Provider value={{ lang }}>
|
||||
<Space className="doc-editor-mode">
|
||||
{/* <Space className="doc-editor-mode">
|
||||
<Button
|
||||
size="small"
|
||||
disabled={readonly}
|
||||
|
@ -89,7 +89,7 @@ export default () => {
|
|||
>
|
||||
{lang === 'zh-CN' ? '编辑模式' : 'Edit mode'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space> */}
|
||||
<Editor
|
||||
lang={lang}
|
||||
placeholder="这里是编辑区域哦~"
|
||||
|
|
|
@ -52,7 +52,7 @@ function startServer() {
|
|||
if (action === 'ready') {
|
||||
client.add(ws, data.doc_id, {
|
||||
id: getId(data.doc_id, uid),
|
||||
name: `Guest-${uid}`,
|
||||
name: `G-${uid}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
|
|
|
@ -322,6 +322,7 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface {
|
|||
const className = 'card-selected-other';
|
||||
if (selected) this.root.addClass(className);
|
||||
else this.root.removeClass(className);
|
||||
return center;
|
||||
}
|
||||
onActivate(activated: boolean) {
|
||||
if (!this.resize) return;
|
||||
|
@ -335,7 +336,7 @@ abstract class CardEntry<T extends CardValue = {}> implements CardInterface {
|
|||
rgb: string;
|
||||
},
|
||||
): NodeInterface | void {
|
||||
this.onSelectByOther(activated, value);
|
||||
return this.onSelectByOther(activated, value);
|
||||
}
|
||||
onChange?(trigger: 'remote' | 'local', node: NodeInterface): void;
|
||||
destroy() {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { DATA_ELEMENT, TRIGGER_CARD_ID, UI } from '../../constants';
|
|||
import { $ } from '../../node';
|
||||
import { isEngine, isMobile } from '../../utils';
|
||||
import Position from '../../position';
|
||||
import placements from '../../position/placements';
|
||||
import './index.css';
|
||||
|
||||
export const isCardToolbarItemOptions = (
|
||||
|
@ -30,6 +31,7 @@ class CardToolbar implements CardToolbarInterface {
|
|||
private position: Position;
|
||||
#hideTimeout: NodeJS.Timeout | null = null;
|
||||
#showTimeout: NodeJS.Timeout | null = null;
|
||||
#defaultAlign: keyof typeof placements = 'topLeft';
|
||||
|
||||
constructor(editor: EditorInterface, card: CardInterface) {
|
||||
this.editor = editor;
|
||||
|
@ -41,6 +43,10 @@ class CardToolbar implements CardToolbarInterface {
|
|||
}
|
||||
}
|
||||
|
||||
setDefaultAlign(align: keyof typeof placements) {
|
||||
this.#defaultAlign = align;
|
||||
}
|
||||
|
||||
clearHide = () => {
|
||||
if (this.#hideTimeout) clearTimeout(this.#hideTimeout);
|
||||
this.#hideTimeout = null;
|
||||
|
@ -287,14 +293,14 @@ class CardToolbar implements CardToolbarInterface {
|
|||
(this.card.constructor as CardEntry).cardName,
|
||||
);
|
||||
if (this.toolbar) this.toolbar.show();
|
||||
let prevAlign = 'topLeft';
|
||||
let prevAlign = this.#defaultAlign;
|
||||
setTimeout(() => {
|
||||
this.position.bind(
|
||||
container,
|
||||
this.card.isMaximize
|
||||
? this.card.getCenter().first()!
|
||||
: this.card.root,
|
||||
'topLeft',
|
||||
this.#defaultAlign,
|
||||
this.offset,
|
||||
(rect) => {
|
||||
if (
|
||||
|
@ -311,7 +317,7 @@ class CardToolbar implements CardToolbarInterface {
|
|||
this.position.update(false);
|
||||
} else if (
|
||||
this.offset &&
|
||||
rect.align === 'topLeft' &&
|
||||
rect.align === this.#defaultAlign &&
|
||||
rect.align !== prevAlign
|
||||
) {
|
||||
this.position.setOffset(this.offset);
|
||||
|
|
|
@ -441,6 +441,7 @@ class ChangeModel implements ChangeInterface {
|
|||
let node: NodeInterface | null = $(childNodes[0]);
|
||||
let prev: NodeInterface | null = null;
|
||||
const appendNodes = [];
|
||||
let startRangeNodeParent = startRange.node.parent();
|
||||
while (node && node.length > 0) {
|
||||
nodeApi.removeSide(node);
|
||||
const next: NodeInterface | null = node.next();
|
||||
|
@ -458,6 +459,22 @@ class ChangeModel implements ChangeInterface {
|
|||
if (!next) {
|
||||
range.select(node, true).collapse(false);
|
||||
}
|
||||
// 被删除了重新设置开始节点位置
|
||||
if (
|
||||
startRange &&
|
||||
(!startRangeNodeParent || startRangeNodeParent.length === 0)
|
||||
) {
|
||||
const children = node.children();
|
||||
startRangeNodeParent = node.parent();
|
||||
startRange = {
|
||||
node: node,
|
||||
offset:
|
||||
children.length === 1 &&
|
||||
children[0].nodeName === 'BR'
|
||||
? 0
|
||||
: range.startOffset,
|
||||
};
|
||||
}
|
||||
node = next;
|
||||
}
|
||||
if (mergeNode[0]) {
|
||||
|
|
|
@ -490,11 +490,11 @@ class NativeEvent {
|
|||
return;
|
||||
if (files.length === 0) {
|
||||
change.cacheRangeBeforeCommand();
|
||||
this.paste(source);
|
||||
setTimeout(() => {
|
||||
// 如果 text 和 html 都有,就解析 text
|
||||
pasteMarkdown(source, text || '');
|
||||
}, 200);
|
||||
this.paste(source);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import Request, {
|
|||
import Scrollbar from './scrollbar';
|
||||
import Position from './position';
|
||||
import { $, getHashId, uuid } from './node';
|
||||
import Resizer from './resizer';
|
||||
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
|
@ -69,4 +70,5 @@ export {
|
|||
isRangeInterface,
|
||||
isRange,
|
||||
isSelection,
|
||||
Resizer,
|
||||
};
|
||||
|
|
|
@ -105,12 +105,6 @@ class RangeColoring implements RangeColoringInterface {
|
|||
child = $(
|
||||
`<div class="${USER_BACKGROUND_CLASS}" ${DATA_UUID}="${uuid}" ${DATA_COLOR}="${color}" />`,
|
||||
);
|
||||
child.css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
'pointer-events': 'none',
|
||||
});
|
||||
this.root.append(child);
|
||||
targetCanvas = new TinyCanvas({
|
||||
container: child.get<HTMLElement>()!,
|
||||
|
@ -118,6 +112,12 @@ class RangeColoring implements RangeColoringInterface {
|
|||
|
||||
child[0]['__canvas'] = targetCanvas;
|
||||
}
|
||||
child.css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
'pointer-events': 'none',
|
||||
});
|
||||
child[0]['__range'] = range.cloneRange();
|
||||
const parentWidth = this.root.width();
|
||||
const parentHeight = this.root.height();
|
||||
|
@ -140,6 +140,14 @@ class RangeColoring implements RangeColoringInterface {
|
|||
if (!!result) {
|
||||
if (Array.isArray(result)) subRanges = result;
|
||||
else {
|
||||
if (result.x < 0) {
|
||||
targetCanvas.resize(
|
||||
parentWidth - result.x,
|
||||
parentHeight,
|
||||
);
|
||||
child.css('left', `${result.x}px`);
|
||||
result.x = 0;
|
||||
}
|
||||
targetCanvas.clearRect(result);
|
||||
targetCanvas.drawRect({ ...result.toJSON(), ...fill });
|
||||
return [range];
|
||||
|
|
|
@ -3,10 +3,10 @@ import { EditorInterface } from '../types/engine';
|
|||
import { PluginOptions, PluginInterface } from '../types/plugin';
|
||||
|
||||
abstract class PluginEntry<T extends PluginOptions = {}>
|
||||
implements PluginInterface
|
||||
implements PluginInterface<T>
|
||||
{
|
||||
protected readonly editor: EditorInterface;
|
||||
protected options: T;
|
||||
options: T;
|
||||
constructor(editor: EditorInterface, options: PluginOptions) {
|
||||
this.editor = editor;
|
||||
this.options = (options || {}) as T;
|
||||
|
|
|
@ -14,7 +14,7 @@ class Position {
|
|||
#root?: NodeInterface;
|
||||
#onUpdate?: (rect: any) => void;
|
||||
#updateTimeout?: NodeJS.Timeout;
|
||||
#observer?: MutationObserver;
|
||||
#observer?: ResizeObserver;
|
||||
|
||||
constructor(editor: EditorInterface) {
|
||||
this.#editor = editor;
|
||||
|
@ -43,7 +43,7 @@ class Position {
|
|||
this.#editor.scrollNode?.on('scroll', this.updateListener);
|
||||
}
|
||||
let size = { width: target.width(), height: target.height() };
|
||||
this.#observer = new MutationObserver(() => {
|
||||
this.#observer = new ResizeObserver(() => {
|
||||
const width = target.width();
|
||||
const height = target.height();
|
||||
|
||||
|
@ -54,13 +54,7 @@ class Position {
|
|||
};
|
||||
this.updateListener();
|
||||
});
|
||||
this.#observer.observe(target.get<HTMLElement>()!, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
attributeOldValue: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
this.#observer.observe(target.get<HTMLElement>()!);
|
||||
this.update();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.data-image-resizer {
|
||||
.data-resizer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -7,37 +7,10 @@
|
|||
bottom: 0px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
outline: 2px solid #1890FF;
|
||||
max-width: initial !important;
|
||||
}
|
||||
.data-image-resizer-holder {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #fff;
|
||||
background: #1890FF;
|
||||
display: inline-block;
|
||||
}
|
||||
.data-image-resizer-holder-right-top {
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
.data-image-resizer-holder-right-bottom {
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.data-image-resizer-holder-left-bottom {
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
.data-image-resizer-holder-left-top {
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.data-image-resizer-bg {
|
||||
.data-resizer img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
@ -46,13 +19,39 @@
|
|||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
.data-image-resizer-bg-active {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.data-resizer-holder {
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
background: #1890FF;
|
||||
display: inline-block;
|
||||
}
|
||||
.data-resizer-holder-right-top {
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
.data-resizer-holder-right-bottom {
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.data-resizer-holder-left-bottom {
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
.data-resizer-holder-left-top {
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.data-image-resizer-number {
|
||||
.data-resizer-number {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
line-height: 24px;
|
||||
|
@ -68,31 +67,31 @@
|
|||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.data-image-resizer-number-right-top {
|
||||
.data-resizer-number-right-top {
|
||||
top: 0px;
|
||||
right: -6px;
|
||||
transform: translateX(100%) scale(0.8);
|
||||
}
|
||||
|
||||
.data-image-resizer-number-right-bottom {
|
||||
.data-resizer-number-right-bottom {
|
||||
right: -6px;
|
||||
bottom: 0px;
|
||||
transform: translateX(100%) scale(0.8);
|
||||
}
|
||||
|
||||
.data-image-resizer-number-left-bottom {
|
||||
.data-resizer-number-left-bottom {
|
||||
left: -6px;
|
||||
bottom: 0px;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.data-image-resizer-number-left-top {
|
||||
.data-resizer-number-left-top {
|
||||
left: -6px;
|
||||
top: 0px;
|
||||
transform: translateX(-100%) scale(0.8);
|
||||
}
|
||||
|
||||
.data-image-resizer-number-active {
|
||||
.data-resizer-number-active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
|
@ -1,38 +1,22 @@
|
|||
import { $, NodeInterface, EventListener, isMobile } from '@aomao/engine';
|
||||
import type { NodeInterface, EventListener } from '../types';
|
||||
import type {
|
||||
ResizerInterface,
|
||||
ResizerOptions,
|
||||
Point,
|
||||
ResizerPosition,
|
||||
Size,
|
||||
} from '../types';
|
||||
import { $ } from '../node';
|
||||
import { isMobile } from '../utils';
|
||||
import './index.css';
|
||||
|
||||
export type Options = {
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
maxWidth: number;
|
||||
rate: number;
|
||||
onChange?: (size: Size) => void;
|
||||
};
|
||||
|
||||
export type Position =
|
||||
| 'right-top'
|
||||
| 'left-top'
|
||||
| 'right-bottom'
|
||||
| 'left-bottom';
|
||||
|
||||
export type Point = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Size = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
class Resizer {
|
||||
private options: Options;
|
||||
class Resizer implements ResizerInterface {
|
||||
private options: ResizerOptions;
|
||||
private root: NodeInterface;
|
||||
private image: NodeInterface;
|
||||
private image?: NodeInterface;
|
||||
private resizerNumber: NodeInterface;
|
||||
private point: Point = { x: 0, y: 0 };
|
||||
private position?: Position;
|
||||
private position?: ResizerPosition;
|
||||
private size: Size;
|
||||
maxWidth: number;
|
||||
/**
|
||||
|
@ -40,11 +24,12 @@ class Resizer {
|
|||
*/
|
||||
private resizing: boolean = false;
|
||||
|
||||
constructor(options: Options) {
|
||||
constructor(options: ResizerOptions) {
|
||||
this.options = options;
|
||||
this.root = $(this.renderTemplate(options.src));
|
||||
this.image = this.root.find('img');
|
||||
this.resizerNumber = this.root.find('.data-image-resizer-number');
|
||||
this.root = $(this.renderTemplate(options.imgUrl));
|
||||
if (options.imgUrl) this.image = this.root.find('img');
|
||||
this.image?.hide();
|
||||
this.resizerNumber = this.root.find('.data-resizer-number');
|
||||
const { width, height } = this.options;
|
||||
this.size = {
|
||||
width,
|
||||
|
@ -53,19 +38,19 @@ class Resizer {
|
|||
this.maxWidth = this.options.maxWidth;
|
||||
}
|
||||
|
||||
renderTemplate(src: string) {
|
||||
renderTemplate(imgUrl?: string) {
|
||||
return `
|
||||
<div class="data-image-resizer">
|
||||
<img class="data-image-resizer-bg data-image-resizer-bg-active" src="${src}" />
|
||||
<div class="data-image-resizer-holder data-image-resizer-holder-right-top"></div>
|
||||
<div class="data-image-resizer-holder data-image-resizer-holder-right-bottom"></div>
|
||||
<div class="data-image-resizer-holder data-image-resizer-holder-left-bottom"></div>
|
||||
<div class="data-image-resizer-holder data-image-resizer-holder-left-top"></div>
|
||||
<span class="data-image-resizer-number"></span>
|
||||
<div class="data-resizer">
|
||||
${imgUrl ? `<img src="${imgUrl}">` : ''}
|
||||
<div class="data-resizer-holder data-resizer-holder-right-top"></div>
|
||||
<div class="data-resizer-holder data-resizer-holder-right-bottom"></div>
|
||||
<div class="data-resizer-holder data-resizer-holder-left-bottom"></div>
|
||||
<div class="data-resizer-holder data-resizer-holder-left-top"></div>
|
||||
<span class="data-resizer-number"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent | TouchEvent, position: Position) {
|
||||
onMouseDown(event: MouseEvent | TouchEvent, position: ResizerPosition) {
|
||||
if (this.resizing) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -97,11 +82,10 @@ class Resizer {
|
|||
};
|
||||
this.position = position;
|
||||
this.resizing = true;
|
||||
this.resizerNumber.addClass(
|
||||
`data-image-resizer-number-${this.position}`,
|
||||
);
|
||||
this.resizerNumber.addClass('data-image-resizer-number-active');
|
||||
this.image.show();
|
||||
this.root.addClass('data-resizing');
|
||||
this.resizerNumber.addClass(`data-resizer-number-${this.position}`);
|
||||
this.resizerNumber.addClass('data-resizer-number-active');
|
||||
this.image?.show();
|
||||
document.addEventListener(
|
||||
isMobile ? 'touchmove' : 'mousemove',
|
||||
this.onMouseMove,
|
||||
|
@ -140,13 +124,11 @@ class Resizer {
|
|||
width: clientWidth,
|
||||
height: clientHeight,
|
||||
};
|
||||
this.resizerNumber.removeClass(
|
||||
`data-image-resizer-number-${this.position}`,
|
||||
);
|
||||
this.resizerNumber.removeClass('data-image-resizer-number-active');
|
||||
this.resizerNumber.removeClass(`data-resizer-number-${this.position}`);
|
||||
this.resizerNumber.removeClass('data-resizer-number-active');
|
||||
this.position = undefined;
|
||||
this.resizing = false;
|
||||
|
||||
this.root.removeClass('data-resizing');
|
||||
document.removeEventListener(
|
||||
isMobile ? 'touchmove' : 'mousemove',
|
||||
this.onMouseMove,
|
||||
|
@ -157,7 +139,7 @@ class Resizer {
|
|||
);
|
||||
const { onChange } = this.options;
|
||||
if (onChange) onChange(this.size);
|
||||
this.image.hide();
|
||||
this.image?.hide();
|
||||
};
|
||||
|
||||
updateSize(width: number, height: number) {
|
||||
|
@ -166,6 +148,10 @@ class Resizer {
|
|||
} else {
|
||||
width = this.size.width + width;
|
||||
}
|
||||
this.setSize(width, height);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
if (width < 24) {
|
||||
width = 24;
|
||||
}
|
||||
|
@ -181,10 +167,6 @@ class Resizer {
|
|||
}
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
this.setSize(width, height);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.root.css({
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
|
@ -193,37 +175,33 @@ class Resizer {
|
|||
}
|
||||
|
||||
on(eventType: string, listener: EventListener) {
|
||||
this.image.on(eventType, listener);
|
||||
this.image?.on(eventType, listener);
|
||||
}
|
||||
|
||||
off(eventType: string, listener: EventListener) {
|
||||
this.image.off(eventType, listener);
|
||||
this.image?.off(eventType, listener);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height } = this.options;
|
||||
this.root.css({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
});
|
||||
|
||||
this.setSize(width, height);
|
||||
this.root
|
||||
.find('.data-image-resizer-holder-right-top')
|
||||
.find('.data-resizer-holder-right-top')
|
||||
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
|
||||
return this.onMouseDown(event, 'right-top');
|
||||
});
|
||||
this.root
|
||||
.find('.data-image-resizer-holder-right-bottom')
|
||||
.find('.data-resizer-holder-right-bottom')
|
||||
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
|
||||
return this.onMouseDown(event, 'right-bottom');
|
||||
});
|
||||
this.root
|
||||
.find('.data-image-resizer-holder-left-bottom')
|
||||
.find('.data-resizer-holder-left-bottom')
|
||||
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
|
||||
return this.onMouseDown(event, 'left-bottom');
|
||||
});
|
||||
this.root
|
||||
.find('.data-image-resizer-holder-left-top')
|
||||
.find('.data-resizer-holder-left-top')
|
||||
.on(isMobile ? 'touchstart' : 'mousedown', (event) => {
|
||||
return this.onMouseDown(event, 'left-top');
|
||||
});
|
|
@ -40,7 +40,6 @@
|
|||
background: #888;
|
||||
}
|
||||
.data-scrollable .data-scrollbar.data-scrollbar-x {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { EventEmitter2 } from 'eventemitter2';
|
||||
import domAlign from 'dom-align';
|
||||
import { DATA_ELEMENT, UI } from '../constants';
|
||||
import { NodeInterface } from '../types';
|
||||
import { $ } from '../node';
|
||||
|
@ -11,6 +12,12 @@ export type ScrollbarDragging = {
|
|||
position: number;
|
||||
};
|
||||
|
||||
export type ScrollbarCustomeOptions = {
|
||||
onScrollX?: (x: number) => number;
|
||||
getOffsetWidth?: (width: number) => number;
|
||||
getScrollLeft?: (left: number) => number;
|
||||
};
|
||||
|
||||
class Scrollbar extends EventEmitter2 {
|
||||
private container: NodeInterface;
|
||||
private x: boolean;
|
||||
|
@ -36,6 +43,7 @@ class Scrollbar extends EventEmitter2 {
|
|||
#content?: NodeInterface;
|
||||
shadowTimer?: NodeJS.Timeout;
|
||||
#enableScroll: boolean = true;
|
||||
#scroll?: ScrollbarCustomeOptions;
|
||||
/**
|
||||
* @param {nativeNode} container 需要添加滚动条的元素
|
||||
* @param {boolean} x 横向滚动条
|
||||
|
@ -47,12 +55,14 @@ class Scrollbar extends EventEmitter2 {
|
|||
x: boolean = true,
|
||||
y: boolean = false,
|
||||
shadow: boolean = true,
|
||||
scroll?: ScrollbarCustomeOptions,
|
||||
) {
|
||||
super();
|
||||
this.container = isNode(container) ? $(container) : container;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.shadow = shadow;
|
||||
this.#scroll = scroll;
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
@ -72,7 +82,7 @@ class Scrollbar extends EventEmitter2 {
|
|||
const children = this.container.children();
|
||||
let hasScrollbar = false;
|
||||
children.each((child) => {
|
||||
if ($(child).hasClass('data-scrollbar')) {
|
||||
if (!hasScrollbar && $(child).hasClass('data-scrollbar')) {
|
||||
hasScrollbar = true;
|
||||
}
|
||||
});
|
||||
|
@ -81,7 +91,7 @@ class Scrollbar extends EventEmitter2 {
|
|||
this.container.addClass('data-scrollable');
|
||||
if (this.x) {
|
||||
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.container.append(this.scrollBarX);
|
||||
|
@ -89,7 +99,7 @@ class Scrollbar extends EventEmitter2 {
|
|||
}
|
||||
if (this.y) {
|
||||
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.container.append(this.scrollBarY);
|
||||
|
@ -110,12 +120,14 @@ class Scrollbar extends EventEmitter2 {
|
|||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
refresh = () => {
|
||||
const element = this.container.get<HTMLElement>();
|
||||
if (element) {
|
||||
const { offsetWidth, offsetHeight, scrollLeft, scrollTop } =
|
||||
element;
|
||||
const offsetWidth = this.#scroll?.getOffsetWidth
|
||||
? this.#scroll.getOffsetWidth(element.offsetWidth)
|
||||
: element.offsetWidth;
|
||||
|
||||
const { offsetHeight, scrollTop } = element;
|
||||
const contentElement = this.#content?.get<HTMLElement>();
|
||||
const sPLeft = removeUnit(this.container.css('padding-left'));
|
||||
const sPRight = removeUnit(this.container.css('padding-right'));
|
||||
|
@ -170,14 +182,30 @@ class Scrollbar extends EventEmitter2 {
|
|||
if (
|
||||
this.x &&
|
||||
contentElement &&
|
||||
element.scrollWidth - sPLeft - sPRight !==
|
||||
element.scrollWidth - sPLeft - sPRight >
|
||||
contentElement.offsetWidth
|
||||
) {
|
||||
element.scrollLeft -=
|
||||
let left =
|
||||
element.scrollWidth -
|
||||
sPLeft -
|
||||
sPRight -
|
||||
contentElement.offsetWidth;
|
||||
if (this.#scroll) {
|
||||
const { onScrollX, getScrollLeft } = this.#scroll;
|
||||
|
||||
left = getScrollLeft
|
||||
? getScrollLeft(-0) + element.scrollLeft - left
|
||||
: element.scrollLeft - left;
|
||||
|
||||
if (onScrollX) {
|
||||
const result = onScrollX(left);
|
||||
if (result > 0) element.scrollLeft = result;
|
||||
else element.scrollLeft = 0;
|
||||
}
|
||||
this.scroll({ left });
|
||||
} else {
|
||||
element.scrollLeft -= left;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 实际内容高度小于容器滚动高度(有内容删除了)
|
||||
|
@ -194,10 +222,13 @@ class Scrollbar extends EventEmitter2 {
|
|||
contentElement.offsetHeight;
|
||||
return;
|
||||
}
|
||||
this.reRenderX(scrollLeft);
|
||||
const left = this.#scroll?.getScrollLeft
|
||||
? this.#scroll.getScrollLeft(element.scrollLeft)
|
||||
: element.scrollLeft;
|
||||
this.reRenderX(left);
|
||||
this.reRenderY(scrollTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 启用鼠标在内容节点上滚动或在移动设备使用手指滑动
|
||||
|
@ -212,13 +243,26 @@ class Scrollbar extends EventEmitter2 {
|
|||
this.#enableScroll = false;
|
||||
}
|
||||
|
||||
scroll = (event: Event) => {
|
||||
const { target } = event;
|
||||
if (!target) return;
|
||||
scroll = (event: Event | { top?: number; left?: number }) => {
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
if (!this.#scroll && event instanceof Event) {
|
||||
const { scrollTop, scrollLeft } = event.target as HTMLElement;
|
||||
top = scrollTop;
|
||||
left = scrollLeft;
|
||||
} else if (!(event instanceof Event)) {
|
||||
if (event.top === undefined) {
|
||||
event.top = this.container.get<HTMLElement>()?.scrollTop || 0;
|
||||
}
|
||||
if (event.left === undefined) {
|
||||
event.left = this.container.get<HTMLElement>()?.scrollLeft || 0;
|
||||
}
|
||||
top = event.top;
|
||||
left = event.left;
|
||||
} else return;
|
||||
|
||||
const { scrollTop, scrollLeft } = target as HTMLElement;
|
||||
this.reRenderX(scrollLeft);
|
||||
this.reRenderY(scrollTop);
|
||||
this.reRenderX(left);
|
||||
this.reRenderY(top);
|
||||
};
|
||||
|
||||
wheelXScroll = (event: any) => {
|
||||
|
@ -227,12 +271,29 @@ class Scrollbar extends EventEmitter2 {
|
|||
const dir = wheelValue > 0 ? 'up' : 'down';
|
||||
const containerElement = this.container.get<HTMLElement>();
|
||||
if (!containerElement) return;
|
||||
let left = containerElement.scrollLeft + (dir === 'up' ? -20 : 20);
|
||||
const containerWidth = this.#scroll?.getOffsetWidth
|
||||
? this.#scroll.getOffsetWidth(containerElement.offsetWidth)
|
||||
: containerElement.offsetWidth;
|
||||
const step = Math.max(containerWidth / 8, 20);
|
||||
let left =
|
||||
(this.#scroll?.getScrollLeft
|
||||
? this.#scroll.getScrollLeft(containerElement.scrollLeft)
|
||||
: containerElement.scrollLeft) + (dir === 'up' ? -step : step);
|
||||
left =
|
||||
dir === 'up'
|
||||
? Math.max(0, left)
|
||||
: Math.min(left, this.sWidth - this.oWidth);
|
||||
if (this.#scroll) {
|
||||
const { onScrollX } = this.#scroll;
|
||||
if (onScrollX) {
|
||||
const result = onScrollX(left);
|
||||
if (result > 0) containerElement.scrollLeft = result;
|
||||
else containerElement.scrollLeft = 0;
|
||||
}
|
||||
this.scroll({ left });
|
||||
} else {
|
||||
containerElement.scrollLeft = left;
|
||||
}
|
||||
};
|
||||
|
||||
wheelYScroll = (event: any) => {
|
||||
|
@ -241,7 +302,9 @@ class Scrollbar extends EventEmitter2 {
|
|||
const dir = wheelValue > 0 ? 'up' : 'down';
|
||||
const containerElement = this.container.get<HTMLElement>();
|
||||
if (!containerElement) return;
|
||||
let top = containerElement.scrollTop + (dir === 'up' ? -20 : 20);
|
||||
const containerHeight = containerElement.offsetHeight;
|
||||
const step = Math.max(containerHeight / 8, 20);
|
||||
let top = containerElement.scrollTop + (dir === 'up' ? -step : step);
|
||||
top =
|
||||
dir === 'up'
|
||||
? Math.max(0, top)
|
||||
|
@ -325,6 +388,7 @@ class Scrollbar extends EventEmitter2 {
|
|||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
window.addEventListener('resize', this.refresh);
|
||||
// 绑定滚动条事件
|
||||
this.bindXScrollEvent();
|
||||
this.bindYScrollEvent();
|
||||
|
@ -361,8 +425,19 @@ class Scrollbar extends EventEmitter2 {
|
|||
this.slideX?.css('left', left + 'px');
|
||||
let min = left / (this.oWidth - this.xWidth);
|
||||
min = Math.min(1, min);
|
||||
this.container.get<HTMLElement>()!.scrollLeft =
|
||||
(this.sWidth - this.oWidth) * min;
|
||||
const containerElement = this.container.get<HTMLElement>()!;
|
||||
const x = (this.sWidth - this.oWidth) * min;
|
||||
if (this.#scroll) {
|
||||
const { onScrollX } = this.#scroll;
|
||||
if (onScrollX) {
|
||||
const result = onScrollX(x);
|
||||
if (result > 0) containerElement.scrollLeft = result;
|
||||
else containerElement.scrollLeft = 0;
|
||||
}
|
||||
this.scroll({ left: x });
|
||||
} else {
|
||||
containerElement.scrollLeft = x;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -463,7 +538,13 @@ class Scrollbar extends EventEmitter2 {
|
|||
|
||||
reRenderShadow = (width: number) => {
|
||||
if (this.shadow) {
|
||||
this.shadowLeft?.css('left', width + 'px');
|
||||
const element = this.container.get<HTMLElement>();
|
||||
if (element) {
|
||||
this.shadowLeft?.css(
|
||||
'left',
|
||||
(this.#scroll ? element.scrollLeft : width) + 'px',
|
||||
);
|
||||
}
|
||||
this.shadowRight?.css('left', width + this.oWidth - 4 + 'px');
|
||||
}
|
||||
};
|
||||
|
@ -476,7 +557,12 @@ class Scrollbar extends EventEmitter2 {
|
|||
min = Math.min(1, min);
|
||||
this.slideX?.css('left', (this.oWidth - this.xWidth) * min + 'px');
|
||||
this.reRenderShadow(left);
|
||||
this.emit('change');
|
||||
if (left === removeUnit(this.scrollBarX?.css('left') || '0'))
|
||||
return;
|
||||
this.emit('change', {
|
||||
x: left,
|
||||
y: removeUnit(this.scrollBarY?.css('top') || '0'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -487,7 +573,11 @@ class Scrollbar extends EventEmitter2 {
|
|||
let min = value <= 0 ? 0 : top / value;
|
||||
min = Math.min(1, min);
|
||||
this.slideY?.css('top', (this.oHeight - this.yHeight) * min + 'px');
|
||||
this.emit('change');
|
||||
if (top === removeUnit(this.scrollBarX?.css('top') || '0')) return;
|
||||
this.emit('change', {
|
||||
x: removeUnit(this.scrollBarX?.css('left') || '0'),
|
||||
y: top,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -526,6 +616,7 @@ class Scrollbar extends EventEmitter2 {
|
|||
this.shadowRight?.remove();
|
||||
}
|
||||
this.#observer?.disconnect();
|
||||
window.removeEventListener('resize', this.refresh);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
import { DATA_ELEMENT } from '../../constants/root';
|
||||
import { NodeInterface } from '../../types/node';
|
||||
import { Placement } from '../../types/position';
|
||||
import { $ } from '../../node';
|
||||
import './index.css';
|
||||
|
||||
type Placement =
|
||||
| 'top'
|
||||
| 'topLeft'
|
||||
| 'topRight'
|
||||
| 'bottom'
|
||||
| 'bottomLeft'
|
||||
| 'bottomRight'
|
||||
| 'left'
|
||||
| 'right';
|
||||
|
||||
const template = (options: { placement: Placement }) => {
|
||||
return `
|
||||
<div ${DATA_ELEMENT}="tooltip" class="data-tooltip data-tooltip-placement-${options.placement} data-tooltip-hidden" style="transform-origin: 50% 45px 0px;">
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
ToolbarItemOptions,
|
||||
} from './toolbar';
|
||||
import { CardActiveTrigger, CardType } from '../card/enum';
|
||||
import { Placement } from './position';
|
||||
|
||||
export type CardOptions = {
|
||||
editor: EditorInterface;
|
||||
|
@ -53,6 +54,7 @@ export interface CardToolbarInterface {
|
|||
* @param offset 偏移量 [tx,ty,bx,by]
|
||||
*/
|
||||
setOffset(offset: Array<number>): void;
|
||||
setDefaultAlign(align: Placement): void;
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
|
|
|
@ -21,3 +21,5 @@ export * from './block';
|
|||
export * from './request';
|
||||
export * from './tiny-canvas';
|
||||
export * from './parser';
|
||||
export * from './resizer';
|
||||
export * from './position';
|
||||
|
|
|
@ -22,8 +22,12 @@ export interface PluginEntry {
|
|||
readonly pluginName: string;
|
||||
}
|
||||
|
||||
export interface PluginInterface {
|
||||
export interface PluginInterface<T extends PluginOptions = {}> {
|
||||
readonly kind: string;
|
||||
/**
|
||||
* 可选项
|
||||
**/
|
||||
options: T;
|
||||
/**
|
||||
* 是否禁用,默认不禁用。在默认不指定的情况下,编辑器为 readonly 的时候全部禁用
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export type Placement =
|
||||
| 'top'
|
||||
| 'topLeft'
|
||||
| 'topRight'
|
||||
| 'bottom'
|
||||
| 'bottomLeft'
|
||||
| 'bottomRight'
|
||||
| 'left'
|
||||
| 'leftTop'
|
||||
| 'leftBottom'
|
||||
| 'right'
|
||||
| 'rightTop'
|
||||
| 'rightBottom';
|
|
@ -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;
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import classnames from 'classnames-es-ts';
|
||||
import { EngineInterface } from '@aomao/engine';
|
||||
import { EngineInterface, Placement } from '@aomao/engine';
|
||||
import Popover from 'antd/es/popover';
|
||||
import 'antd/es/popover/style';
|
||||
|
||||
|
@ -17,19 +17,7 @@ export type CollapseItemProps = {
|
|||
disabled?: boolean;
|
||||
onDisabled?: () => boolean;
|
||||
className?: string;
|
||||
placement?:
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'left'
|
||||
| 'bottom'
|
||||
| 'topLeft'
|
||||
| 'topRight'
|
||||
| 'bottomLeft'
|
||||
| 'bottomRight'
|
||||
| 'leftTop'
|
||||
| 'leftBottom'
|
||||
| 'rightTop'
|
||||
| 'rightBottom';
|
||||
placement?: Placement;
|
||||
onClick?: (event: React.MouseEvent, name: string) => void | boolean;
|
||||
onMouseDown?: (event: React.MouseEvent) => void;
|
||||
};
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { PswpInterface } from '@/types';
|
||||
import type { PswpInterface } from '@/types';
|
||||
import type { EditorInterface, NodeInterface } from '@aomao/engine';
|
||||
import {
|
||||
$,
|
||||
EditorInterface,
|
||||
isEngine,
|
||||
escape,
|
||||
NodeInterface,
|
||||
sanitizeUrl,
|
||||
Tooltip,
|
||||
isMobile,
|
||||
Resizer,
|
||||
CardType,
|
||||
} from '@aomao/engine';
|
||||
import Pswp from '../pswp';
|
||||
import Resizer from '../resizer';
|
||||
import './index.css';
|
||||
|
||||
export type Status = 'uploading' | 'done' | 'error';
|
||||
|
@ -295,12 +294,12 @@ class Image {
|
|||
this.meta.css({
|
||||
'background-color': '',
|
||||
width: '',
|
||||
height: '',
|
||||
//height: '',
|
||||
});
|
||||
|
||||
this.image.css({
|
||||
width: '',
|
||||
height: '',
|
||||
//height: '',
|
||||
});
|
||||
|
||||
const img = this.image.get<HTMLImageElement>();
|
||||
|
@ -334,7 +333,7 @@ class Image {
|
|||
}
|
||||
|
||||
this.image.css('width', `${width}px`);
|
||||
this.image.css('height', `${height}px`);
|
||||
//this.image.css('height', `${height}px`);
|
||||
}
|
||||
|
||||
changeSize(width: number, height: number) {
|
||||
|
@ -359,7 +358,7 @@ class Image {
|
|||
this.size.height = height;
|
||||
this.image.css({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
//height: `${height}px`,
|
||||
});
|
||||
|
||||
const { onChange } = this.options;
|
||||
|
@ -450,7 +449,7 @@ class Image {
|
|||
if (isMobile || !isEngine(this.editor) || this.editor.readonly) return;
|
||||
// 拖动调整图片大小
|
||||
const resizer = new Resizer({
|
||||
src: this.getSrc(),
|
||||
imgUrl: this.getSrc(),
|
||||
width: clientWidth,
|
||||
height: clientHeight,
|
||||
rate: this.rate,
|
||||
|
@ -542,7 +541,7 @@ class Image {
|
|||
if (this.src) {
|
||||
this.image.css({
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
//height: height + 'px',
|
||||
});
|
||||
const { onChange } = this.options;
|
||||
if (width > 0 && height > 0) {
|
||||
|
|
|
@ -228,6 +228,23 @@ class ImageComponent extends Card<ImageValue> {
|
|||
else this.image?.blur();
|
||||
}
|
||||
|
||||
onSelectByOther(
|
||||
selected: boolean,
|
||||
value?: {
|
||||
color: string;
|
||||
rgb: string;
|
||||
},
|
||||
): NodeInterface | void {
|
||||
this.image?.root?.css(
|
||||
'outline',
|
||||
selected ? '2px solid ' + value!.color : '',
|
||||
);
|
||||
const className = 'card-selected-other';
|
||||
if (selected) this.root.addClass(className);
|
||||
else this.root.removeClass(className);
|
||||
return this.image?.root;
|
||||
}
|
||||
|
||||
render(loadingBg?: string): string | void | NodeInterface {
|
||||
const value = this.getValue();
|
||||
if (!value) return;
|
||||
|
@ -253,20 +270,6 @@ class ImageComponent extends Card<ImageValue> {
|
|||
},
|
||||
onChange: (size) => {
|
||||
if (size) this.setSize(size);
|
||||
if (this.type === CardType.BLOCK && this.image) {
|
||||
const maxWidth = this.image.getMaxWidth();
|
||||
const offset = (maxWidth - this.image.root.width()) / 2;
|
||||
if (value.status === 'done') {
|
||||
this.toolbarModel?.setOffset([
|
||||
-offset - 12,
|
||||
0,
|
||||
-offset - 12,
|
||||
0,
|
||||
]);
|
||||
}
|
||||
if (this.activated)
|
||||
this.toolbarModel?.showCardToolbar();
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
this.isLocalError = true;
|
||||
|
@ -290,12 +293,8 @@ class ImageComponent extends Card<ImageValue> {
|
|||
}
|
||||
|
||||
didRender() {
|
||||
if (
|
||||
this.type === CardType.INLINE &&
|
||||
this.getValue()?.status === 'done'
|
||||
) {
|
||||
this.toolbarModel?.setOffset([-12, 0, -12, 0]);
|
||||
}
|
||||
super.didRender();
|
||||
this.toolbarModel?.setDefaultAlign('top');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
}
|
||||
|
||||
.pswp .data-pswp-tool-bar .btn {
|
||||
color: #D9D9D9;
|
||||
color: #f8f9fa;
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
|
|
@ -42,6 +42,7 @@ class Pswp extends EventEmitter2 implements PswpInterface {
|
|||
hideAnimationDuration: 0,
|
||||
closeOnVerticalDrag: isMobile,
|
||||
tapToClose: true,
|
||||
bgOpacity: 0.8,
|
||||
barsSize: {
|
||||
top: 44,
|
||||
bottom: 80,
|
||||
|
|
|
@ -31,49 +31,9 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
`defaultData`: 默认下拉查询列表展示数据
|
||||
|
||||
`onSearch`: 查询时的方法,或者配置 action,二选其一
|
||||
|
||||
`onSelect`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
|
||||
|
||||
`onClick`: 在“提及”上单击时触发
|
||||
|
||||
`onMouseEnter`: 鼠标移入“提及”上时触发
|
||||
|
||||
`onRender`: 自定义渲染列表
|
||||
|
||||
`onRenderItem`: 自定义渲染列表项
|
||||
|
||||
`onLoading`: 自定渲染加载状态
|
||||
|
||||
`onEmpty`: 自定渲染空状态
|
||||
|
||||
`action`: 查询地址,始终使用 `GET` 请求,参数 `keyword`
|
||||
|
||||
`data`: 查询时同时将这些数据一起传到到服务端
|
||||
|
||||
```ts
|
||||
//默认展示的列表数据
|
||||
defaultData?: Array<{ key: string, name: string, avatar?: string}>
|
||||
//查询时的方法,或者配置 action,二选其一
|
||||
onSearch?:(keyword: string) => Promise<Array<{ key: string, name: string, avatar?: string}>>
|
||||
//选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
|
||||
onSelect?: (data: {[key:string]: string}) => void | {[key: string]: string}
|
||||
//在“提及”上单击事件
|
||||
onClick?:(data: {[key:string]: string}) => void
|
||||
//鼠标移入“提及”上时触发
|
||||
onMouseEnter?:(node: NodeInterface, data: {[key:string]: string}) => void
|
||||
//自定义渲染列表,bindItem 可以为列表项绑定需要的属性和事件
|
||||
onRender?: (data: MentionItem, root: NodeInterface, bindItem: (node: NodeInterface, data: {[key:string]: string}) => NodeInterface) => Promise<string | NodeInterface | void>;
|
||||
//自定义渲染列表项
|
||||
onRenderItem?: (item: MentionItem, root: NodeInterface) => string | NodeInterface | void
|
||||
// 自定渲染加载状态
|
||||
onLoading?: (root: NodeInterface) => string | NodeInterface | void
|
||||
// 自定渲染空状态
|
||||
onEmpty?: (root: NodeInterface) => string | NodeInterface | void
|
||||
/**
|
||||
* 查询地址
|
||||
* 查询地址,或者监听 mention:search 事件执行查询
|
||||
*/
|
||||
action?: string;
|
||||
/**
|
||||
|
@ -124,3 +84,136 @@ parse?: (
|
|||
//返回 Array<{ key: string, name: string}>
|
||||
engine.command.executeMethod('mention', 'getList');
|
||||
```
|
||||
|
||||
## 插件事件
|
||||
|
||||
`mention:default`: 默认下拉查询列表展示数据
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:default', () => {
|
||||
return [];
|
||||
});
|
||||
```
|
||||
|
||||
`mention:search`: 查询时的方法,或者配置 action,二选其一
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:search', (keyword) => {
|
||||
return new Promise((resolve) => {
|
||||
query({ keyword })
|
||||
.then((result) => {
|
||||
resolve(result);
|
||||
})
|
||||
.catch(() => resolve([]));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
`mention:select`: 选中列表中的一项后回调,这里可以返回一个自定义值与 key、name 一起组合成新的值存在 cardValue 里面。并且执行 getList 命令后会一起返回来
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:select', (data) => {
|
||||
data['test'] = 'test';
|
||||
return data;
|
||||
});
|
||||
```
|
||||
|
||||
`mention:item-click`: 在“提及”上单击时触发
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:item-click',
|
||||
(root: NodeInterface, { key, name }: { key: string; name: string }) => {
|
||||
console.log('mention click:', key, '-', name);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:enter`: 鼠标移入“提及”上时触发
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:enter',
|
||||
(layout: NodeInterface, { name }: { key: string; name: string }) => {
|
||||
ReactDOM.render(
|
||||
<div style={{ padding: 5 }}>
|
||||
<p>This is name: {name}</p>
|
||||
<p>配置 mention 插件的 mention:enter 事件</p>
|
||||
<p>此处使用 ReactDOM.render 自定义渲染</p>
|
||||
<p>Use ReactDOM.render to customize rendering here</p>
|
||||
</div>,
|
||||
layout.get<HTMLElement>()!,
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:render`: 自定义渲染列表
|
||||
|
||||
```ts
|
||||
this.engine.on(
|
||||
'mention:render',
|
||||
(
|
||||
root: NodeInterface,
|
||||
data: Array<MentionItem>,
|
||||
bindItem: (
|
||||
node: NodeInterface,
|
||||
data: { [key: string]: string },
|
||||
) => NodeInterface,
|
||||
) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const renderCallback = (items: { [key: string]: Element }) => {
|
||||
// 遍历每个项的DOM节点
|
||||
Object.keys(items).forEach((key) => {
|
||||
const element = items[key];
|
||||
const item = data.find((d) => d.key === key);
|
||||
if (!item) return;
|
||||
// 绑定每个列表项所属的属性、事件,以满足编辑器中上下左右选择的功能需要
|
||||
bindItem($(element), item);
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
ReactDOM.render(
|
||||
<MentionList data={data} callback={renderCallback} />,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
`mention:render-item`: 自定义渲染列表项
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:render-item', (data, root) => {
|
||||
const item = $(`<div>${data}</div>`);
|
||||
root.append(item);
|
||||
return item;
|
||||
});
|
||||
```
|
||||
|
||||
`mention:loading`: 自定渲染加载状态
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:loading', (data, root) => {
|
||||
root.html(`<div>${data}</div>`);
|
||||
// or
|
||||
ReactDOM.render(
|
||||
<div className="data-mention-loading">Loading...</div>,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
`mention:empty`: 自定渲染空状态
|
||||
|
||||
```ts
|
||||
this.engine.on('mention:empty', (root) => {
|
||||
root.html('<div>没有查询到数据</div>');
|
||||
// or
|
||||
ReactDOM.render(
|
||||
<div className="data-mention-empty">Empty</div>,
|
||||
root.get<HTMLElement>()!,
|
||||
);
|
||||
});
|
||||
```
|
||||
|
|
|
@ -249,7 +249,12 @@ class CollapseComponent implements CollapseComponentInterface {
|
|||
if (result) body?.append(result);
|
||||
} else if (
|
||||
CollapseComponent.render ||
|
||||
(result = this.engine.trigger('mention:render', this.root))
|
||||
(result = this.engine.trigger(
|
||||
'mention:render',
|
||||
this.root,
|
||||
data,
|
||||
this.bindItem,
|
||||
))
|
||||
) {
|
||||
(CollapseComponent.render
|
||||
? CollapseComponent.render(this.root, data, this.bindItem)
|
||||
|
|
|
@ -41,6 +41,17 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
### 溢出展示
|
||||
|
||||
```ts
|
||||
overflow?: {
|
||||
// 相对编辑器左侧最大能展示的宽度
|
||||
maxLeftWidth?: () => number;
|
||||
// 相对于编辑器右侧最大能展示的宽度
|
||||
maxRightWidth?: () => number;
|
||||
};
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
```ts
|
||||
|
|
|
@ -251,7 +251,7 @@ class Helper implements HelperInterface {
|
|||
const $tr = trs.eq(index);
|
||||
if (!$tr) return;
|
||||
let height = parseInt($tr.css('height'));
|
||||
height = height || 33;
|
||||
height = height || 35;
|
||||
$tr.css('height', height + 'px');
|
||||
});
|
||||
//补充可编辑器区域
|
||||
|
@ -603,7 +603,7 @@ class Helper implements HelperInterface {
|
|||
const $tr = trs.eq(index);
|
||||
if (!$tr) return;
|
||||
let height = parseInt($tr.css('height'));
|
||||
height = height || 33;
|
||||
height = height || 35;
|
||||
$tr.css('height', height + 'px');
|
||||
});
|
||||
return table;
|
||||
|
|
|
@ -5,9 +5,11 @@ import {
|
|||
CardType,
|
||||
EDITABLE_SELECTOR,
|
||||
isEngine,
|
||||
isMobile,
|
||||
NodeInterface,
|
||||
Parser,
|
||||
RangeInterface,
|
||||
removeUnit,
|
||||
Scrollbar,
|
||||
ToolbarItemOptions,
|
||||
} from '@aomao/engine';
|
||||
|
@ -65,7 +67,7 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
selection: TableSelectionInterface = new TableSelection(this.editor, this);
|
||||
conltrollBar: ControllBarInterface = new ControllBar(this.editor, this, {
|
||||
col_min_width: 40,
|
||||
row_min_height: 33,
|
||||
row_min_height: 35,
|
||||
});
|
||||
command: TableCommandInterface = new TableCommand(this.editor, this);
|
||||
scrollbar?: Scrollbar;
|
||||
|
@ -80,6 +82,138 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
if (isEngine(this.editor)) {
|
||||
this.editor.on('undo', this.doChange);
|
||||
this.editor.on('redo', this.doChange);
|
||||
// tab 键选择
|
||||
if (!this.editor.event.listeners['keydown:tab'])
|
||||
this.editor.event.listeners['keydown:tab'] = [];
|
||||
this.editor.event.listeners['keydown:tab'].unshift(
|
||||
(event: KeyboardEvent) => {
|
||||
if (!isEngine(this.editor)) return;
|
||||
const { change, block, node } = this.editor;
|
||||
|
||||
const range = change.range.get();
|
||||
const td = range.endNode.closest('td');
|
||||
if (td.length === 0) return;
|
||||
const closestBlock = block.closest(range.endNode);
|
||||
if (
|
||||
td.length > 0 &&
|
||||
(block.isLastOffset(range, 'end') ||
|
||||
(closestBlock.name !== 'li' &&
|
||||
node.isEmptyWidthChild(closestBlock)))
|
||||
) {
|
||||
let next = td.next();
|
||||
if (!next) {
|
||||
const nextRow = td.parent()?.next();
|
||||
// 最后一行,最后一列
|
||||
if (!nextRow) {
|
||||
// 新建一行
|
||||
this.command.insertRowDown();
|
||||
next =
|
||||
td
|
||||
.parent()
|
||||
?.next()
|
||||
?.find('td:first-child') || null;
|
||||
} else {
|
||||
next = nextRow.find('td:first-child') || null;
|
||||
}
|
||||
}
|
||||
if (next) {
|
||||
event.preventDefault();
|
||||
this.selection.focusCell(next);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
);
|
||||
// 下键选择
|
||||
this.editor.on('keydown:down', (event) => {
|
||||
if (!isEngine(this.editor)) return;
|
||||
const { change } = this.editor;
|
||||
|
||||
const range = change.range.get();
|
||||
const td = range.endNode.closest('td');
|
||||
if (td.length === 0) return;
|
||||
const contentElement = td.find('.table-main-content');
|
||||
if (!contentElement) return;
|
||||
const tdRect = contentElement
|
||||
.get<HTMLElement>()!
|
||||
.getBoundingClientRect();
|
||||
const rangeRect = range.getBoundingClientRect();
|
||||
if (
|
||||
td.length > 0 &&
|
||||
(rangeRect.bottom === 0 ||
|
||||
tdRect.bottom - rangeRect.bottom < 10)
|
||||
) {
|
||||
const index = td.index();
|
||||
const nextRow = td.parent()?.next();
|
||||
if (nextRow) {
|
||||
let nextIndex = 0;
|
||||
let nextTd = nextRow.find('td:last-child');
|
||||
this.selection.tableModel?.table[nextRow.index()].some(
|
||||
(cell) => {
|
||||
if (
|
||||
!this.helper.isEmptyModelCol(cell) &&
|
||||
nextIndex >= index &&
|
||||
cell.element
|
||||
) {
|
||||
nextTd = $(cell.element);
|
||||
return true;
|
||||
}
|
||||
nextIndex++;
|
||||
},
|
||||
);
|
||||
if (nextTd) {
|
||||
event.preventDefault();
|
||||
this.selection.focusCell(nextTd, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// 上键选择
|
||||
this.editor.on('keydown:up', (event) => {
|
||||
if (!isEngine(this.editor)) return;
|
||||
const { change } = this.editor;
|
||||
|
||||
const range = change.range.get();
|
||||
const td = range.endNode.closest('td');
|
||||
if (td.length === 0) return;
|
||||
const contentElement = td.find('.table-main-content');
|
||||
if (!contentElement) return;
|
||||
const tdRect = contentElement
|
||||
.get<HTMLElement>()!
|
||||
.getBoundingClientRect();
|
||||
const rangeRect = range.getBoundingClientRect();
|
||||
if (
|
||||
td.length > 0 &&
|
||||
(rangeRect.top === 0 || rangeRect.top - tdRect.top < 10)
|
||||
) {
|
||||
const index = td.index();
|
||||
const prevRow = td.parent()?.prev();
|
||||
if (prevRow) {
|
||||
let prevIndex = 0;
|
||||
let prevTd = prevRow.find('td:first-child');
|
||||
this.selection.tableModel?.table[prevRow.index()].some(
|
||||
(cell) => {
|
||||
if (
|
||||
!this.helper.isEmptyModelCol(cell) &&
|
||||
prevIndex >= index &&
|
||||
cell.element
|
||||
) {
|
||||
prevTd = $(cell.element);
|
||||
return true;
|
||||
}
|
||||
prevIndex++;
|
||||
},
|
||||
);
|
||||
if (prevTd) {
|
||||
event.preventDefault();
|
||||
this.selection.focusCell(prevTd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.colorTool) return;
|
||||
this.colorTool = new ColorTool(this.editor, this.id, {
|
||||
|
@ -172,10 +306,7 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
},
|
||||
];
|
||||
if (this.isMaximize) return funBtns;
|
||||
return [
|
||||
{
|
||||
type: 'dnd',
|
||||
},
|
||||
const toolbars: Array<ToolbarItemOptions | CardToolbarItemOptions> = [
|
||||
{
|
||||
type: 'maximize',
|
||||
},
|
||||
|
@ -190,6 +321,12 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
},
|
||||
...funBtns,
|
||||
];
|
||||
if (removeUnit(this.wrapper?.css('margin-left') || '0') === 0) {
|
||||
toolbars.unshift({
|
||||
type: 'dnd',
|
||||
});
|
||||
}
|
||||
return toolbars;
|
||||
}
|
||||
|
||||
updateAlign(event: MouseEvent, align: 'top' | 'middle' | 'bottom' = 'top') {
|
||||
|
@ -291,12 +428,10 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
super.activate(activated);
|
||||
if (activated) {
|
||||
this.wrapper?.addClass('active');
|
||||
this.scrollbar?.enableScroll();
|
||||
} else {
|
||||
this.selection.clearSelect();
|
||||
this.conltrollBar.hideContextMenu();
|
||||
this.wrapper?.removeClass('active');
|
||||
this.scrollbar?.disableScroll();
|
||||
}
|
||||
this.scrollbar?.refresh();
|
||||
}
|
||||
|
@ -344,6 +479,33 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
return nodes;
|
||||
}
|
||||
|
||||
overflow(max: number) {
|
||||
// 表格宽度
|
||||
const tableWidth = this.wrapper?.find('.data-table')?.width() || 0;
|
||||
const rootWidth = this.getCenter().width();
|
||||
// 溢出的宽度
|
||||
const overflowWidth = tableWidth - rootWidth;
|
||||
if (overflowWidth > 0) {
|
||||
this.wrapper?.css(
|
||||
'margin-right',
|
||||
`-${overflowWidth > max ? max : overflowWidth}px`,
|
||||
);
|
||||
} else if (overflowWidth < 0) {
|
||||
this.wrapper?.css('margin-right', '');
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollbar = () => {
|
||||
if (!this.scrollbar) return;
|
||||
const hideHeight =
|
||||
(this.wrapper?.getBoundingClientRect()?.bottom || 0) -
|
||||
(this.wrapper?.getViewport().bottom || 0);
|
||||
console.log(hideHeight);
|
||||
this.wrapper?.find('.data-scrollbar-x').css({
|
||||
bottom: `${hideHeight > 0 ? hideHeight + 2 : 0}px`,
|
||||
});
|
||||
};
|
||||
|
||||
didRender() {
|
||||
super.didRender();
|
||||
this.viewport = isEngine(this.editor)
|
||||
|
@ -356,8 +518,49 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
if (!isEngine(this.editor) || this.editor.readonly)
|
||||
this.toolbarModel?.setOffset([0, 0]);
|
||||
else this.toolbarModel?.setOffset([0, -28, 0, -6]);
|
||||
const tablePlugin = this.editor.plugin.components['table'];
|
||||
const tableOptions = tablePlugin?.options['overflow'] || {};
|
||||
if (this.viewport) {
|
||||
this.scrollbar = new Scrollbar(this.viewport, true, false, true);
|
||||
const overflowLeftConfig = tableOptions['maxLeftWidth']
|
||||
? {
|
||||
onScrollX: (x: number) => {
|
||||
const max = tableOptions['maxLeftWidth']();
|
||||
this.wrapper?.css(
|
||||
'margin-left',
|
||||
`-${x > max ? max : x}px`,
|
||||
);
|
||||
if (x > 0) {
|
||||
this.editor.root.find('.data-card-dnd').hide();
|
||||
} else {
|
||||
this.editor.root.find('.data-card-dnd').show();
|
||||
}
|
||||
return x - max;
|
||||
},
|
||||
getScrollLeft: (left: number) => {
|
||||
return (
|
||||
left -
|
||||
removeUnit(
|
||||
this.wrapper?.css('margin-left') || '0',
|
||||
)
|
||||
);
|
||||
},
|
||||
getOffsetWidth: (width: number) => {
|
||||
return (
|
||||
width +
|
||||
removeUnit(
|
||||
this.wrapper?.css('margin-left') || '0',
|
||||
)
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
this.scrollbar = new Scrollbar(
|
||||
this.viewport,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
overflowLeftConfig,
|
||||
);
|
||||
this.scrollbar.setContentNode(this.viewport.find('.data-table')!);
|
||||
this.scrollbar.on('display', (display: 'node' | 'block') => {
|
||||
if (display === 'block') {
|
||||
|
@ -367,14 +570,18 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
}
|
||||
});
|
||||
this.scrollbar.disableScroll();
|
||||
let changeTimeout: NodeJS.Timeout | undefined;
|
||||
const handleScrollbarChange = () => {
|
||||
if (changeTimeout) clearTimeout(changeTimeout);
|
||||
changeTimeout = setTimeout(() => {
|
||||
if (tableOptions['maxRightWidth'])
|
||||
this.overflow(tableOptions['maxRightWidth']());
|
||||
if (isEngine(this.editor)) this.editor.ot.initSelection();
|
||||
}, 50);
|
||||
};
|
||||
this.scrollbar.on('change', handleScrollbarChange);
|
||||
if (!isMobile)
|
||||
window.addEventListener('scroll', this.updateScrollbar);
|
||||
window.addEventListener('resize', this.updateScrollbar);
|
||||
if (isEngine(this.editor) && !isMobile) {
|
||||
this.editor.scrollNode?.on('scroll', this.updateScrollbar);
|
||||
}
|
||||
}
|
||||
this.selection.on('select', () => {
|
||||
this.conltrollBar.refresh();
|
||||
|
@ -401,6 +608,8 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
if (!silence) {
|
||||
this.onChange();
|
||||
}
|
||||
if (tableOptions['maxRightWidth'])
|
||||
this.overflow(tableOptions['maxRightWidth']());
|
||||
this.scrollbar?.refresh();
|
||||
});
|
||||
|
||||
|
@ -412,6 +621,9 @@ class TableComponent extends Card<TableValue> implements TableInterface {
|
|||
if (tableValue) this.setValue(tableValue);
|
||||
this.onChange();
|
||||
}
|
||||
if (tableOptions['maxRightWidth'])
|
||||
this.overflow(tableOptions['maxRightWidth']());
|
||||
this.scrollbar?.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -458,7 +458,7 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
|
|||
this.emit('select', this.selectArea);
|
||||
}
|
||||
|
||||
focusCell(cell: NodeInterface | Node) {
|
||||
focusCell(cell: NodeInterface | Node, start: boolean = false) {
|
||||
if (!isEngine(this.editor)) return;
|
||||
const { change } = this.editor;
|
||||
if (isNode(cell)) cell = $(cell);
|
||||
|
@ -469,7 +469,7 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
|
|||
.select(editableElement, true)
|
||||
.shrinkToElementNode()
|
||||
.shrinkToTextNode()
|
||||
.collapse(false);
|
||||
.collapse(start);
|
||||
setTimeout(() => {
|
||||
change.range.select(range);
|
||||
}, 20);
|
||||
|
@ -1090,9 +1090,10 @@ class TableSelection extends EventEmitter2 implements TableSelectionInterface {
|
|||
top += rect.top - (vRect?.top || 0) - 13;
|
||||
left += rect.left - (vRect?.left || 0);
|
||||
}
|
||||
const sLeft = removeUnit(
|
||||
const sLeft =
|
||||
removeUnit(
|
||||
this.table.wrapper?.find('.data-scrollbar')?.css('left') || '0',
|
||||
);
|
||||
) + removeUnit(this.table.wrapper?.css('margin-left') || '0');
|
||||
left += sLeft;
|
||||
|
||||
const headerHeight =
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '../types';
|
||||
|
||||
const TABLE_WRAPPER_CLASS_NAME = 'table-wrapper';
|
||||
const TABLE_OVERFLOW_CLASS_NAME = 'table-overflow';
|
||||
const TABLE_CLASS_NAME = 'data-table';
|
||||
const COLS_HEADER_CLASS_NAME = 'table-cols-header';
|
||||
const COLS_HEADER_ITEM_CLASS_NAME = 'table-cols-header-item';
|
||||
|
@ -41,6 +42,7 @@ const TABLE_TD_BG_CLASS_NAME = 'table-main-bg';
|
|||
|
||||
class Template implements TemplateInterface {
|
||||
static readonly TABLE_WRAPPER_CLASS = `.${TABLE_WRAPPER_CLASS_NAME}`;
|
||||
static readonly TABLE_OVERFLOW_CLASS = `.${TABLE_OVERFLOW_CLASS_NAME}`;
|
||||
static readonly TABLE_CLASS = `.${TABLE_CLASS_NAME}`;
|
||||
static readonly COLS_HEADER_CLASS = `.${COLS_HEADER_CLASS_NAME}`;
|
||||
static readonly COLS_HEADER_ITEM_CLASS = `.${COLS_HEADER_ITEM_CLASS_NAME}`;
|
||||
|
@ -122,7 +124,7 @@ class Template implements TemplateInterface {
|
|||
* @return {string} 返回 html 字符串
|
||||
*/
|
||||
htmlEdit(
|
||||
{ rows, cols, html, noBorder }: TableValue,
|
||||
{ rows, cols, html, noBorder, overflow }: TableValue,
|
||||
menus: TableMenu,
|
||||
): string {
|
||||
cols = cols === -Infinity ? 1 : cols;
|
||||
|
@ -188,7 +190,9 @@ class Template implements TemplateInterface {
|
|||
noBorder === true ? " data-table-no-border='true'" : ''
|
||||
} ${DATA_TRANSIENT_ATTRIBUTES}="class">${colgroup}${trs}</table>`;
|
||||
|
||||
return `<div class="${TABLE_WRAPPER_CLASS_NAME}" ${DATA_TRANSIENT_ATTRIBUTES}="*">${tableHeader}<div class="${VIEWPORT}">${this.renderColsHeader(
|
||||
return `<div class="${TABLE_WRAPPER_CLASS_NAME} ${
|
||||
overflow !== false ? TABLE_OVERFLOW_CLASS_NAME : ''
|
||||
}" ${DATA_TRANSIENT_ATTRIBUTES}="*">${tableHeader}<div class="${VIEWPORT}">${this.renderColsHeader(
|
||||
cols,
|
||||
)}${table}${placeholder}${tableHighlight}</div>${this.renderRowsHeader(
|
||||
rows,
|
||||
|
|
|
@ -27,7 +27,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
}
|
||||
|
||||
.data-table tr {
|
||||
height: 33px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.data-table tr td {
|
||||
|
@ -67,7 +67,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
}
|
||||
|
||||
.table-wrapper.scrollbar-show {
|
||||
margin-bottom: -8px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.table-wrapper.data-table-highlight tr td[table-cell-selection]:after {
|
||||
|
@ -98,9 +98,8 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.table-wrapper.data-table-highlight-all .table-header {
|
||||
.table-wrapper.data-table-highlight-all .table-header .table-header-item {
|
||||
background: rgba(255, 77, 79, 0.4) !important;
|
||||
border-color: rgba(255, 77, 79, 0.4) !important;
|
||||
}
|
||||
|
||||
.table-wrapper .table-header-item:hover{
|
||||
|
@ -113,16 +112,15 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
|
||||
.table-wrapper .table-header.selected .table-header-item {
|
||||
background: #4daaff;
|
||||
border-color: #4daaff;
|
||||
}
|
||||
|
||||
.table-wrapper .table-cols-header {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
height: 13px;
|
||||
display: none;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
margin-bottom: -1px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.table-wrapper.active .table-cols-header {
|
||||
|
@ -131,7 +129,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
|
||||
.table-wrapper .table-cols-header .table-cols-header-item {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
height: 13px;
|
||||
width: auto;
|
||||
border: 1px solid #dfdfdf;
|
||||
border-bottom: 0 none;
|
||||
|
@ -161,7 +159,6 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
background: #fff;
|
||||
z-index: 1;
|
||||
border-radius: 0;
|
||||
height: 14px;
|
||||
border-bottom: 0;
|
||||
cursor: move;
|
||||
}
|
||||
|
@ -250,11 +247,11 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
width: 14px;
|
||||
z-index: 1;
|
||||
border-right: 0;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-wrapper.active .table-rows-header {
|
||||
visibility: visible;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.table-wrapper .table-rows-header .table-rows-header-item {
|
||||
|
@ -387,7 +384,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
|
||||
.table-wrapper .table-viewport .scrollbar-shadow-left {
|
||||
top: 0;
|
||||
bottom: 8px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.table-wrapper.active .table-viewport .scrollbar-shadow-left {
|
||||
|
@ -397,7 +394,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
|
||||
.table-wrapper .table-viewport .scrollbar-shadow-right {
|
||||
top: 0;
|
||||
bottom: 8px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.table-wrapper.active .table-viewport .scrollbar-shadow-right {
|
||||
|
@ -469,6 +466,10 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
z-index: 3;
|
||||
}
|
||||
|
||||
.table-wrapper .table-main-content * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-wrapper .table-main-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -573,7 +574,7 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
}
|
||||
|
||||
.table-wrapper.scrollbar-show .data-scrollable.scroll-x {
|
||||
padding-bottom: 8px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.table-wrapper .data-scrollable.scroll-x {
|
||||
|
@ -584,20 +585,17 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-wrapper.scrollbar-show .data-scrollable.scroll-x:hover {
|
||||
/**overflow-x: auto;**/
|
||||
}
|
||||
|
||||
.table-wrapper.scrollbar-show .data-scrollable.scroll-x .data-scrollbar-x{
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.table-wrapper .data-scrollable .data-scrollbar.data-scrollbar-x {
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.table-wrapper .data-scrollable .data-scrollbar.data-scrollbar-x .data-scrollbar-trigger {
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.table-wrapper .table-rows-header .table-row-delete-button,.table-wrapper .table-rows-header .table-row-add-button {
|
||||
|
@ -697,11 +695,11 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
}
|
||||
|
||||
.data-table-reader.data-scrollable.scroll-x {
|
||||
padding-bottom: 8px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.data-table-reader .scrollbar-shadow-left, .data-table-reader .scrollbar-shadow-right {
|
||||
bottom: 8px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.data-table-reader.scrollbar-show.data-scrollable.scroll-x .data-scrollbar-x{
|
||||
|
@ -709,14 +707,20 @@ div[data-card-key="table"].card-selected .data-table, div[data-card-key="table"]
|
|||
}
|
||||
|
||||
.data-table-reader.data-scrollable .data-scrollbar.data-scrollbar-x {
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.data-table-reader.data-scrollable .data-scrollbar.data-scrollbar-x .data-scrollbar-trigger {
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
[data-card-key="table"].data-card-block-max > [data-card-element="body"] > [data-card-element="center"] {
|
||||
padding: 48px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
/**
|
||||
表格可溢出样式
|
||||
**/
|
||||
.table-wrapper.table-overflow {
|
||||
width: auto;
|
||||
}
|
|
@ -21,6 +21,10 @@ import { TableInterface } from './types';
|
|||
|
||||
export interface Options extends PluginOptions {
|
||||
hotkey?: string | Array<string>;
|
||||
overflow?: {
|
||||
maxLeftWidth?: () => number;
|
||||
maxRightWidth?: () => number;
|
||||
};
|
||||
markdown?: boolean;
|
||||
}
|
||||
|
||||
|
@ -265,6 +269,7 @@ class Table extends Plugin<Options> {
|
|||
this.editor.card.insert(TableComponent.cardName, {
|
||||
rows: rows || 3,
|
||||
cols: cols || 3,
|
||||
overflow: this.options.overflow,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -107,6 +107,7 @@ export type TableValue = {
|
|||
html?: string;
|
||||
color?: string;
|
||||
noBorder?: boolean;
|
||||
overflow?: boolean;
|
||||
};
|
||||
|
||||
export type TableMenuItem = {
|
||||
|
@ -367,7 +368,7 @@ export interface TableSelectionInterface extends EventEmitter2 {
|
|||
|
||||
hideHighlight(): void;
|
||||
|
||||
focusCell(cell: NodeInterface | Node): void;
|
||||
focusCell(cell: NodeInterface | Node, start?: boolean): void;
|
||||
|
||||
selectCellRange(cell: NodeInterface | Node): void;
|
||||
|
||||
|
|
|
@ -40,6 +40,14 @@ new Engine(...,{
|
|||
})
|
||||
```
|
||||
|
||||
### 是否显示视频标题
|
||||
|
||||
默认显示
|
||||
|
||||
```ts
|
||||
showTitle?: boolean
|
||||
```
|
||||
|
||||
### 文件上传
|
||||
|
||||
`action`: 上传地址,始终使用 `POST` 请求
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
[data-card-key="video"] {
|
||||
outline: 1px solid #ddd;
|
||||
.data-video {
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
.data-video-content {
|
||||
position: relative;
|
||||
|
@ -9,6 +11,8 @@
|
|||
.data-video-content video {
|
||||
width: 100%;
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.data-video-uploading,
|
||||
.data-video-uploaded,
|
||||
|
@ -23,7 +27,7 @@
|
|||
line-height: 0;
|
||||
}
|
||||
.data-video-active {
|
||||
outline: 1px solid #d9d9d9;
|
||||
user-select: none;
|
||||
}
|
||||
.data-video-center {
|
||||
position: absolute;
|
||||
|
@ -81,3 +85,20 @@
|
|||
vertical-align: middle;
|
||||
margin: -2px 5px 0 0;
|
||||
}
|
||||
|
||||
.data-video-title {
|
||||
text-align: center;
|
||||
}
|
||||
.data-video-title::selection {
|
||||
background: transparent
|
||||
}
|
||||
.data-video .data-resizer {
|
||||
z-index: inherit;
|
||||
}
|
||||
.data-video .data-resizer .data-resizer-holder {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.data-video .data-resizing {
|
||||
z-index: 3;
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
import { Tooltip } from '@aomao/engine';
|
||||
import type {
|
||||
CardToolbarItemOptions,
|
||||
ToolbarItemOptions,
|
||||
NodeInterface,
|
||||
ResizerInterface,
|
||||
} from '@aomao/engine';
|
||||
import {
|
||||
$,
|
||||
Card,
|
||||
CardToolbarItemOptions,
|
||||
CardType,
|
||||
escape,
|
||||
getFileSize,
|
||||
isEngine,
|
||||
isMobile,
|
||||
NodeInterface,
|
||||
sanitizeUrl,
|
||||
ToolbarItemOptions,
|
||||
Resizer,
|
||||
} from '@aomao/engine';
|
||||
import './index.css';
|
||||
|
||||
|
@ -49,6 +51,22 @@ export type VideoValue = {
|
|||
* 视频大小
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* 真实宽度
|
||||
*/
|
||||
naturalWidth?: number;
|
||||
/**
|
||||
* 真实高度
|
||||
*/
|
||||
naturalHeight?: number;
|
||||
/**
|
||||
* 错误状态下的错误信息
|
||||
*/
|
||||
|
@ -56,6 +74,14 @@ export type VideoValue = {
|
|||
};
|
||||
|
||||
class VideoComponent extends Card<VideoValue> {
|
||||
maxWidth: number = 0;
|
||||
resizer?: ResizerInterface;
|
||||
video?: NodeInterface;
|
||||
rate: number = 1;
|
||||
isLoad: boolean = false;
|
||||
container?: NodeInterface;
|
||||
videoContainer?: NodeInterface;
|
||||
title?: NodeInterface;
|
||||
static get cardName() {
|
||||
return 'video';
|
||||
}
|
||||
|
@ -68,7 +94,9 @@ class VideoComponent extends Card<VideoValue> {
|
|||
return false;
|
||||
}
|
||||
|
||||
private container?: NodeInterface;
|
||||
static get singleSelectable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getLocales() {
|
||||
return this.editor.language.get<{ [key: string]: string }>('video');
|
||||
|
@ -102,7 +130,9 @@ class VideoComponent extends Card<VideoValue> {
|
|||
}
|
||||
|
||||
const fileSize: string = size ? getFileSize(size) : '';
|
||||
|
||||
const titleElement = name
|
||||
? `<div class="data-video-title">${escape(name)}</div>`
|
||||
: '';
|
||||
if (status === 'uploading') {
|
||||
return `
|
||||
<div class="data-video">
|
||||
|
@ -143,10 +173,11 @@ class VideoComponent extends Card<VideoValue> {
|
|||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const videoPlugin = this.editor.plugin.components['video'];
|
||||
return `
|
||||
<div class="data-video">
|
||||
<div class="data-video-content data-video-done"></div>
|
||||
${videoPlugin && videoPlugin.options.showTitle !== false ? titleElement : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -176,12 +207,18 @@ class VideoComponent extends Card<VideoValue> {
|
|||
if (cover) {
|
||||
video.poster = sanitizeUrl(this.onBeforeRender('cover', cover));
|
||||
}
|
||||
|
||||
this.maxWidth = this.getMaxWidth();
|
||||
if (value.naturalHeight && value.naturalWidth)
|
||||
this.rate = value.naturalHeight / value.naturalWidth;
|
||||
this.container?.find('.data-video-content').append(video);
|
||||
|
||||
this.videoContainer = this.container?.find('.data-video-content');
|
||||
video.oncontextmenu = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
this.video = $(video);
|
||||
this.title = this.container?.find('.data-video-title');
|
||||
this.resetSize();
|
||||
// 一次渲染时序开启 controls 会触发一次内容为空的 window.onerror,疑似 chrome bug
|
||||
setTimeout(() => {
|
||||
video.controls = true;
|
||||
|
@ -234,9 +271,158 @@ class VideoComponent extends Card<VideoValue> {
|
|||
this.container?.find('.percent').html(`${percent}%`);
|
||||
}
|
||||
|
||||
getMaxWidth(node: NodeInterface = this.getCenter()) {
|
||||
const block = this.editor.block.closest(node).get<HTMLElement>();
|
||||
if (!block) return 0;
|
||||
return block.clientWidth - 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置大小
|
||||
*/
|
||||
resetSize() {
|
||||
if (!this.videoContainer) return;
|
||||
const value = this.getValue();
|
||||
if (!value) return;
|
||||
this.videoContainer.css({
|
||||
width: '',
|
||||
//height: '',
|
||||
});
|
||||
this.container?.css({
|
||||
width: '',
|
||||
});
|
||||
|
||||
const video = this.video?.get<HTMLVideoElement>();
|
||||
if (!video) return;
|
||||
let { width, height, naturalWidth, naturalHeight } = value;
|
||||
if (!naturalWidth) {
|
||||
naturalWidth = video.videoWidth;
|
||||
}
|
||||
if (!naturalHeight) {
|
||||
naturalHeight = video.videoHeight;
|
||||
}
|
||||
|
||||
if (!height) {
|
||||
width = naturalWidth;
|
||||
height = Math.round(this.rate * width);
|
||||
} else if (!width) {
|
||||
height = naturalHeight;
|
||||
width = Math.round(height / this.rate);
|
||||
} else if (width && height) {
|
||||
// 修正非正常的比例
|
||||
height = Math.round(this.rate * width);
|
||||
} else {
|
||||
width = naturalWidth;
|
||||
height = naturalHeight;
|
||||
}
|
||||
|
||||
if (width > this.maxWidth) {
|
||||
width = this.maxWidth;
|
||||
height = Math.round(width * this.rate);
|
||||
}
|
||||
this.container?.css({
|
||||
width: `${width}px`,
|
||||
});
|
||||
this.videoContainer.css('width', `${width}px`);
|
||||
//this.videoContainer.css('height', `${height}px`);
|
||||
}
|
||||
|
||||
changeSize(width: number, height: number) {
|
||||
if (width < 24) {
|
||||
width = 24;
|
||||
height = width * this.rate;
|
||||
}
|
||||
|
||||
if (width > this.maxWidth) {
|
||||
width = this.maxWidth;
|
||||
height = width * this.rate;
|
||||
}
|
||||
|
||||
if (height < 24) {
|
||||
height = 24;
|
||||
width = height / this.rate;
|
||||
}
|
||||
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
this.videoContainer?.css({
|
||||
width: `${width}px`,
|
||||
//height: `${height}px`,
|
||||
});
|
||||
this.container?.css({
|
||||
width: `${width}px`,
|
||||
});
|
||||
this.setValue({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
this.resizer?.destroy();
|
||||
this.initResizer();
|
||||
}
|
||||
|
||||
onWindowResize = () => {
|
||||
if (!isEngine(this.editor)) return;
|
||||
this.maxWidth = this.getMaxWidth();
|
||||
this.resetSize();
|
||||
|
||||
if (this.resizer) {
|
||||
this.resizer.maxWidth = this.maxWidth;
|
||||
this.resizer.setSize(
|
||||
this.videoContainer?.width() || 0,
|
||||
this.videoContainer?.height() || 0,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
initResizer() {
|
||||
const value = this.getValue();
|
||||
if (!value) return;
|
||||
const { naturalHeight, naturalWidth, status } = value;
|
||||
if (!naturalHeight || !naturalWidth || status !== 'done') return;
|
||||
const { width, height, cover } = value;
|
||||
this.maxWidth = this.getMaxWidth();
|
||||
this.rate = naturalHeight / naturalWidth;
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
// 拖动调整视频大小
|
||||
const resizer = new Resizer({
|
||||
imgUrl: cover,
|
||||
width: width || naturalWidth,
|
||||
height: height || naturalHeight,
|
||||
rate: this.rate,
|
||||
maxWidth: this.maxWidth,
|
||||
onChange: ({ width, height }) => this.changeSize(width, height),
|
||||
});
|
||||
this.resizer = resizer;
|
||||
const resizerNode = resizer.render();
|
||||
this.videoContainer?.append(resizerNode);
|
||||
}
|
||||
|
||||
onActivate(activated: boolean) {
|
||||
if (activated) this.container?.addClass('data-video-active');
|
||||
else this.container?.removeClass('data-video-active');
|
||||
if (activated) {
|
||||
this.container?.addClass('data-video-active');
|
||||
this.initResizer();
|
||||
} else {
|
||||
this.container?.removeClass('data-video-active');
|
||||
this.resizer?.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onSelectByOther(
|
||||
selected: boolean,
|
||||
value?: {
|
||||
color: string;
|
||||
rgb: string;
|
||||
},
|
||||
): NodeInterface | void {
|
||||
this.container?.css(
|
||||
'outline',
|
||||
selected ? '2px solid ' + value!.color : '',
|
||||
);
|
||||
const className = 'card-selected-other';
|
||||
if (selected) this.root.addClass(className);
|
||||
else this.root.removeClass(className);
|
||||
return this.container;
|
||||
}
|
||||
|
||||
checker(
|
||||
|
@ -284,6 +470,8 @@ class VideoComponent extends Card<VideoValue> {
|
|||
const { command, plugin } = this.editor;
|
||||
const { video_id, status } = value;
|
||||
const locales = this.getLocales();
|
||||
|
||||
this.maxWidth = this.getMaxWidth();
|
||||
//阅读模式
|
||||
if (!isEngine(this.editor)) {
|
||||
if (status === 'done') {
|
||||
|
@ -418,6 +606,7 @@ class VideoComponent extends Card<VideoValue> {
|
|||
: value.download,
|
||||
};
|
||||
this.container = $(this.renderTemplate(newValue));
|
||||
this.video = this.container.find('video');
|
||||
center.empty();
|
||||
center.append(this.container);
|
||||
this.initPlayer();
|
||||
|
@ -436,17 +625,27 @@ class VideoComponent extends Card<VideoValue> {
|
|||
);
|
||||
return this.container;
|
||||
} else {
|
||||
return $(this.renderTemplate(value));
|
||||
this.container = $(this.renderTemplate(value));
|
||||
return this.container;
|
||||
}
|
||||
}
|
||||
|
||||
didRender() {
|
||||
super.didRender();
|
||||
this.container?.on(isMobile ? 'touchstart' : 'click', () => {
|
||||
handleClick = () => {
|
||||
if (isEngine(this.editor) && !this.activated) {
|
||||
this.editor.card.activate(this.root);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
didRender() {
|
||||
super.didRender();
|
||||
this.toolbarModel?.setDefaultAlign('top');
|
||||
this.container?.on('click', this.handleClick);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.container?.off('click', this.handleClick);
|
||||
window.removeEventListener('resize', this.onWindowResize);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
NodeInterface,
|
||||
Plugin,
|
||||
PluginEntry,
|
||||
PluginOptions,
|
||||
READY_CARD_KEY,
|
||||
sanitizeUrl,
|
||||
SchemaInterface,
|
||||
|
@ -18,12 +19,15 @@ import VideoComponent, { VideoValue } from './component';
|
|||
import VideoUploader from './uploader';
|
||||
import locales from './locales';
|
||||
|
||||
export default class VideoPlugin extends Plugin<{
|
||||
export interface VideoOptions extends PluginOptions {
|
||||
onBeforeRender?: (
|
||||
action: 'download' | 'query' | 'cover',
|
||||
url: string,
|
||||
) => string;
|
||||
}> {
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
export default class VideoPlugin extends Plugin<VideoOptions> {
|
||||
static get pluginName() {
|
||||
return 'video';
|
||||
}
|
||||
|
@ -46,6 +50,10 @@ export default class VideoPlugin extends Plugin<{
|
|||
cover?: string,
|
||||
size?: number,
|
||||
download?: string,
|
||||
naturalWidth?: number,
|
||||
naturalHeight?: number,
|
||||
width?: number,
|
||||
height?: number,
|
||||
): void {
|
||||
const value: VideoValue = {
|
||||
status,
|
||||
|
@ -55,6 +63,10 @@ export default class VideoPlugin extends Plugin<{
|
|||
name: name || url,
|
||||
size,
|
||||
download,
|
||||
width,
|
||||
height,
|
||||
naturalWidth,
|
||||
naturalHeight,
|
||||
};
|
||||
if (status === 'error') {
|
||||
value.url = '';
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
|
||||
import VideoComponent from './component';
|
||||
|
||||
export interface Options extends PluginOptions {
|
||||
export interface VideoUploaderOptions extends PluginOptions {
|
||||
/**
|
||||
* 视频上传地址
|
||||
*/
|
||||
|
@ -70,6 +70,9 @@ export interface Options extends PluginOptions {
|
|||
id?: string;
|
||||
cover?: string;
|
||||
status?: string;
|
||||
name?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
| string;
|
||||
};
|
||||
|
@ -96,7 +99,7 @@ export interface Options extends PluginOptions {
|
|||
};
|
||||
}
|
||||
|
||||
export default class extends Plugin<Options> {
|
||||
export default class extends Plugin<VideoUploaderOptions> {
|
||||
private cardComponents: { [key: string]: VideoComponent } = {};
|
||||
|
||||
static get pluginName() {
|
||||
|
@ -219,6 +222,12 @@ export default class extends Plugin<Options> {
|
|||
const download: string =
|
||||
response.download ||
|
||||
(response.data && response.data.download);
|
||||
const width: number =
|
||||
response.width ||
|
||||
(response.data && response.data.width);
|
||||
const height: number =
|
||||
response.height ||
|
||||
(response.data && response.data.height);
|
||||
let status: string =
|
||||
response.status ||
|
||||
(response.data && response.data.status);
|
||||
|
@ -232,6 +241,8 @@ export default class extends Plugin<Options> {
|
|||
cover?: string;
|
||||
download?: string;
|
||||
status?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
| string;
|
||||
} = {
|
||||
|
@ -242,6 +253,8 @@ export default class extends Plugin<Options> {
|
|||
cover,
|
||||
download,
|
||||
status,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
};
|
||||
if (parse) {
|
||||
|
@ -253,6 +266,9 @@ export default class extends Plugin<Options> {
|
|||
cover?: string;
|
||||
download?: string;
|
||||
status?: string;
|
||||
name?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
if (typeof customizeResult.data === 'string')
|
||||
result.data = {
|
||||
|
@ -307,6 +323,8 @@ export default class extends Plugin<Options> {
|
|||
? { url: result.data }
|
||||
: {
|
||||
...result.data,
|
||||
naturalWidth: result.data.width,
|
||||
naturalHeight: result.data.height,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sendToWormhole = require('stream-wormhole');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const { Controller } = require('egg');
|
||||
|
||||
class UploadController extends Controller {
|
||||
|
@ -159,10 +160,53 @@ class UploadController extends Controller {
|
|||
// 监听写入完成事件
|
||||
remoteFileStrem.on('finish', () => {
|
||||
if (errFlag) return;
|
||||
resolve({
|
||||
const result = {
|
||||
url,
|
||||
download: url,
|
||||
name: sourceName,
|
||||
};
|
||||
try {
|
||||
ffmpeg.setFfmpegPath(
|
||||
path.join(app.baseDir, './app/ffmpeg/win/ffmpeg.exe'),
|
||||
);
|
||||
ffmpeg.setFfprobePath(
|
||||
path.join(app.baseDir, './app/ffmpeg/win/ffprobe.exe'),
|
||||
);
|
||||
ffmpeg.ffprobe(filePath, (err, metadata) => {
|
||||
const fileName = new Date().getTime() + '-v-image.png'; // stream对象也包含了文件名,大小等基本信息
|
||||
|
||||
// 创建文件写入路径
|
||||
const imagePath = path.join(
|
||||
app.baseDir,
|
||||
`/app/public/upload/${fileName}`,
|
||||
);
|
||||
|
||||
if (err) {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
return;
|
||||
} else {
|
||||
const { width, height } = metadata.streams[0];
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
ffmpeg(filePath)
|
||||
.screenshots({
|
||||
timestamps: ['50%'],
|
||||
filename: fileName,
|
||||
folder: path.join(
|
||||
app.baseDir,
|
||||
'/app/public/upload',
|
||||
),
|
||||
})
|
||||
.on('end', () => {
|
||||
result.cover = `${this.domain}/upload/${fileName}`;
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -52,15 +52,28 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"id": "yreo1zOnA0tpLMpO4h",
|
||||
"title": "g12s",
|
||||
"status": "true",
|
||||
"id": "B2apyT5NIgXPe4tRX7",
|
||||
"title": "hj",
|
||||
"status": "false",
|
||||
"children": [
|
||||
{
|
||||
"id": 5,
|
||||
"username": "test",
|
||||
"username": "Guest-undefined",
|
||||
"content": "ghjhj",
|
||||
"createdAt": 1639325623775
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kAoP518hzaPYD9Mx9z",
|
||||
"title": "dsfdf",
|
||||
"status": "true",
|
||||
"children": [
|
||||
{
|
||||
"id": 6,
|
||||
"username": "Guest-2",
|
||||
"content": "sdfdf",
|
||||
"createdAt": 1639328040653
|
||||
"createdAt": 1639571978834
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"id": "demo",
|
||||
"content": {
|
||||
"value": "<p data-id=\"peafab28-UeGZGcV7\"><br /></p><card type=\"block\" name=\"table\" editable=\"true\" value=\"data:%7B%22rows%22%3A3%2C%22cols%22%3A3%2C%22id%22%3A%221A2IV%22%2C%22type%22%3A%22block%22%2C%22height%22%3A102%2C%22width%22%3A690%2C%22html%22%3A%22%3Ctable%20class%3D%5C%22data-table%5C%22%20data-id%3D%5C%22t21b6eb9-LlLf0GOG%5C%22%20style%3D%5C%22width%3A%20690px%3B%5C%22%3E%3Ccolgroup%20data-id%3D%5C%22c9d5c669-CVY52f6H%5C%22%3E%3Ccol%20data-id%3D%5C%22c5da60d0-iFijk1EW%5C%22%20width%3D%5C%22230%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22c5da60d0-0D2HF3SP%5C%22%20width%3D%5C%22230%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22c5da60d0-Ni4KSj8K%5C%22%20width%3D%5C%22230%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3C%2Fcolgroup%3E%3Ctbody%20data-id%3D%5C%22t61d509e-Tf2WYKhU%5C%22%3E%3Ctr%20data-id%3D%5C%22t8f11d90-T7IL81Xf%5C%22%20style%3D%5C%22height%3A%2033px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22td18b8d3-2EeOGLQb%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-9N4g8AhO%5C%22%3Esdfdfkk%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-MgP6Ob2g%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-Pl6XNbLN%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-lP0l6F6H%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-nV334EmX%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t8f11d90-Ke7XQKBS%5C%22%20style%3D%5C%22height%3A%2033px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22td18b8d3-p7m1hYhY%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-PJO7hR6Q%5C%22%3Edfgfdgsdf4kk%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-hUPPS4P6%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-30U3kFAE%5C%22%3Edfgfg%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-8mpQLAJH%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-c6mQN6Hb%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t8f11d90-oNKFnQHT%5C%22%20style%3D%5C%22height%3A%2033px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22td18b8d3-l78d0KUc%5C%22%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-V4NEMkLV%5C%22%3Edfggsdfb1kk%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-hLULe0Ql%5C%22%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-8e3tXqmC%5C%22%3Edfgfg%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22td18b8d3-9cNNI6bi%5C%22%20class%3D%5C%22table-last-column%20table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22peafab28-YApPlQd6%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3C%2Ftbody%3E%3C%2Ftable%3E%22%7D\"></card><p data-id=\"peafab28-8nHRgJnn\">sdfabcdefg123dsf1111d1g12s</p><p data-id=\"peafab28-9QE8Ib78\"><strong>12ab12c21123</strong>12345</p><card type=\"block\" name=\"codeblock\" editable=\"false\" value=\"data:%7B%22id%22%3A%222Cwmg%22%2C%22type%22%3A%22block%22%2C%22mode%22%3A%22plain%22%2C%22code%22%3A%22hhhhjhjhjkkk%22%7D\"></card><p data-id=\"peafab28-3d0SPNQb\">1243sffffd</p>",
|
||||
"value": "<h1 data-id=\"hbc788f1-HCEiiZG4\" id=\"hbc788f1-HCEiiZG4\">sdfsdfsdf</h1><p data-id=\"pd157317-9mX1S9ff\">sdfdsfdsfdfdfg</p><card type=\"block\" name=\"table\" editable=\"true\" value=\"data:%7B%22rows%22%3A3%2C%22cols%22%3A5%2C%22overflow%22%3A%7B%7D%2C%22id%22%3A%22z3W7h%22%2C%22type%22%3A%22block%22%2C%22height%22%3A105%2C%22width%22%3A1295%2C%22html%22%3A%22%3Ctable%20class%3D%5C%22data-table%5C%22%20data-id%3D%5C%22t7216feb-UR4lJETQ%5C%22%20style%3D%5C%22width%3A%201295px%3B%5C%22%3E%3Ccolgroup%20data-id%3D%5C%22c82d01ad-cO2cMESN%5C%22%3E%3Ccol%20data-id%3D%5C%22cac3d390-BEQW2Af8%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-BEQW2Af8%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-5aAaY4PI%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-5aAaY4PI%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3Ccol%20data-id%3D%5C%22cac3d390-5aAaY4PI%5C%22%20width%3D%5C%22259%5C%22%20span%3D%5C%221%5C%22%20%2F%3E%3C%2Fcolgroup%3E%3Ctbody%20data-id%3D%5C%22tc1e2dd5-ca338IhV%5C%22%3E%3Ctr%20data-id%3D%5C%22t40b42a1-aYJWmUJL%5C%22%20style%3D%5C%22height%3A%2035px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22t5815cab-bCGT82Ui%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-J6C5nVfC%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-faIHF5bL%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-iKc2C558%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-ADmVOlBb%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-B4BVQiFI%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%3E%3Cp%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-Gm3I1IW3%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-A0494K62%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t40b42a1-WRTdh09h%5C%22%20style%3D%5C%22height%3A%2035px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22t5815cab-kTR4C3DT%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-5fDBMYOP%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-rIYUHo3C%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-NJUMh6el%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-h9961ZOB%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-Wa0VLLCM%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%3E%3Cp%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-l7Cj19AZ%5C%22%20class%3D%5C%22table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-WAHoEoYg%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3Ctr%20data-id%3D%5C%22t40b42a1-GWMmlanJ%5C%22%20style%3D%5C%22height%3A%2035px%3B%5C%22%3E%3Ctd%20data-id%3D%5C%22t5815cab-kjgohUZP%5C%22%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-TJXbZWjs%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20class%3D%5C%22table-last-column%5C%22%20data-id%3D%5C%22te8113ae-FNQhbbZA%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-GT5JJPm6%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20class%3D%5C%22table-last-column%5C%22%20data-id%3D%5C%22te8113ae-iThMaAVQ%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-YnO3LRJI%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20class%3D%5C%22table-last-column%5C%22%3E%3Cp%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3Ctd%20data-id%3D%5C%22t5815cab-57DBKodH%5C%22%20class%3D%5C%22table-last-column%20table-last-row%5C%22%3E%3Cp%20data-id%3D%5C%22pd157317-Y7so1iqZ%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3C%2Ftd%3E%3C%2Ftr%3E%3C%2Ftbody%3E%3C%2Ftable%3E%22%7D\"></card><p data-id=\"pd157317-kOu29md8\">fgfdgfdg</p>",
|
||||
"paths": [
|
||||
{
|
||||
"id": ["yreo1zOnA0tpLMpO4h"],
|
||||
"id": ["kAoP518hzaPYD9Mx9z"],
|
||||
"path": [
|
||||
[2, 0, 22],
|
||||
[2, 0, 26]
|
||||
[1, 0, 6],
|
||||
[1, 0, 11]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"egg-scripts": "^2.13.0",
|
||||
"egg-view-assets": "^1.7.0",
|
||||
"egg-view-nunjucks": "^2.3.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"jsdom": "^16.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"qs": "^6.7.0",
|
||||
|
|
Loading…
Reference in New Issue