@smartos-lib/components 1.7.0-beta.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintrc +12 -0
- package/.eslintrc-auto-import.json +332 -0
- package/Components.code-workspace +143 -0
- package/LICENSE +21 -0
- package/dist/smart-docx-editor/index.d.ts +2 -0
- package/dist/smart-docx-editor/index.js +68 -0
- package/dist/smart-file-preview/index.d.ts +18 -0
- package/dist/smart-file-preview/index.js +37 -0
- package/dist/smart-upload/index.d.ts +2 -0
- package/dist/smart-upload/index.js +800 -0
- package/index.html +16 -0
- package/package.json +23 -0
- package/public/favicon.svg +6 -0
- package/scripts/components.vite.config.ts +96 -0
- package/scripts/shared.ts +9 -0
- package/src/App.vue +28 -0
- package/src/components/Logo/index.vue +15 -0
- package/src/components-private/.gitkeep +0 -0
- package/src/composables/useElementStyle.ts +23 -0
- package/src/composables/useNaiveStyle.ts +43 -0
- package/src/composables/useNaiveTheme.ts +71 -0
- package/src/composables/useSmart.ts +36 -0
- package/src/layouts/default.vue +3 -0
- package/src/main.ts +33 -0
- package/src/modules/pinia/index.ts +8 -0
- package/src/modules/progress/index.ts +12 -0
- package/src/modules/router/install.ts +9 -0
- package/src/modules/router/routes.ts +40 -0
- package/src/pages/[...all].vue +21 -0
- package/src/pages/frame/component/[name].vue +14 -0
- package/src/pages/frame/index.vue +81 -0
- package/src/pages/index/composables/useTabsManage.ts +46 -0
- package/src/pages/index/index.vue +111 -0
- package/src/pages/index/type.ts +13 -0
- package/src/pages/index/utils/index.ts +41 -0
- package/src/settings.ts +9 -0
- package/src/shared/components.ts +52 -0
- package/src/shared/env.ts +11 -0
- package/src/shared/unocss.theme.ts +1600 -0
- package/src/stores/theme.ts +29 -0
- package/src/styles/element.scss +3 -0
- package/src/styles/styles.scss +21 -0
- package/src/types.ts +20 -0
- package/src/utils/callCustomElementExposed.ts +6 -0
- package/src/utils/deepCloneESModule.ts +10 -0
- package/src/utils/defineCustomElements.ts +18 -0
- package/src/utils/formatComponentsGlob.ts +16 -0
- package/src/utils/getFileMD5.ts +31 -0
- package/src/utils/getFileNameAndExt.ts +11 -0
- package/src/utils/isFileEqual.ts +13 -0
- package/src/utils/jsonToFormData.ts +8 -0
- package/src/web-components/smart-docx-drive-page/App.vue +37 -0
- package/src/web-components/smart-docx-drive-page/apis/doc.ts +85 -0
- package/src/web-components/smart-docx-drive-page/apis/file.ts +278 -0
- package/src/web-components/smart-docx-drive-page/apis/folder.ts +72 -0
- package/src/web-components/smart-docx-drive-page/children/Home.vue +8 -0
- package/src/web-components/smart-docx-drive-page/children/Me.vue +47 -0
- package/src/web-components/smart-docx-drive-page/components/CustomImage.vue +26 -0
- package/src/web-components/smart-docx-drive-page/components/CustomPopover.vue +62 -0
- package/src/web-components/smart-docx-drive-page/components/DocxDir.vue +99 -0
- package/src/web-components/smart-docx-drive-page/components/DocxDoc.vue +132 -0
- package/src/web-components/smart-docx-drive-page/components/DocxDownloadPopoverItem.vue +41 -0
- package/src/web-components/smart-docx-drive-page/components/DocxFileList.vue +156 -0
- package/src/web-components/smart-docx-drive-page/components/DocxPreview.vue +33 -0
- package/src/web-components/smart-docx-drive-page/components/DocxUpload.vue +164 -0
- package/src/web-components/smart-docx-drive-page/components/FileIcon.vue +62 -0
- package/src/web-components/smart-docx-drive-page/components-private/Header.vue +65 -0
- package/src/web-components/smart-docx-drive-page/components-private/Logo.vue +15 -0
- package/src/web-components/smart-docx-drive-page/components-private/Menu.vue +34 -0
- package/src/web-components/smart-docx-drive-page/components-private/Navbar.vue +36 -0
- package/src/web-components/smart-docx-drive-page/composables/useFullscreenElDialog.ts +41 -0
- package/src/web-components/smart-docx-drive-page/composables/usePrompt.ts +73 -0
- package/src/web-components/smart-docx-drive-page/data.ts +10 -0
- package/src/web-components/smart-docx-drive-page/external-style/custom-popover.sass +8 -0
- package/src/web-components/smart-docx-drive-page/external-style/index.sass +1 -0
- package/src/web-components/smart-docx-drive-page/index.ts +20 -0
- package/src/web-components/smart-docx-drive-page/index.vue +39 -0
- package/src/web-components/smart-docx-drive-page/info.ts +2 -0
- package/src/web-components/smart-docx-drive-page/stores/menu.ts +60 -0
- package/src/web-components/smart-docx-drive-page/types.ts +51 -0
- package/src/web-components/smart-docx-drive-page/utils/file-actions.ts +63 -0
- package/src/web-components/smart-docx-drive-page/utils/file.ts +31 -0
- package/src/web-components/smart-docx-editor/App.vue +32 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/Markdown.vue +202 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/Menu.vue +100 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/types.ts +6 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/Markdown.tsx +71 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/MarkdownElement.tsx +81 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Blockquote/index.sass +6 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Blockquote/index.tsx +12 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Heading/index.sass +14 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Heading/index.tsx +17 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/List/index.scss +16 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/List/index.tsx +39 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/types/custom-types.d.ts +69 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/composables/useTextSelection.ts +50 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/index.sass +19 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/index.vue +21 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/shared/const.ts +23 -0
- package/src/web-components/smart-docx-editor/MarkdownShortcuts/utils/slateHelpers.ts +23 -0
- package/src/web-components/smart-docx-editor/data.ts +38 -0
- package/src/web-components/smart-docx-editor/demo.vue +11 -0
- package/src/web-components/smart-docx-editor/index.md +3 -0
- package/src/web-components/smart-docx-editor/index.ts +5 -0
- package/src/web-components/smart-docx-editor/index.vue +12 -0
- package/src/web-components/smart-docx-editor/info.ts +2 -0
- package/src/web-components/smart-file-preview/category/Code.vue +171 -0
- package/src/web-components/smart-file-preview/category/Image.vue +49 -0
- package/src/web-components/smart-file-preview/category/Pdf.vue +14 -0
- package/src/web-components/smart-file-preview/category/Video.vue +27 -0
- package/src/web-components/smart-file-preview/demo.vue +34 -0
- package/src/web-components/smart-file-preview/index.md +5 -0
- package/src/web-components/smart-file-preview/index.ts +29 -0
- package/src/web-components/smart-file-preview/index.vue +56 -0
- package/src/web-components/smart-file-preview/info.ts +2 -0
- package/src/web-components/smart-file-preview/shared/const.ts +4 -0
- package/src/web-components/smart-file-preview/types.ts +38 -0
- package/src/web-components/smart-upload/index.ts +5 -0
- package/src/web-components/smart-upload/index.vue +101 -0
- package/src/web-components/smart-upload/info.ts +2 -0
- package/src/web-components/smart-upload/types.ts +28 -0
- package/tsconfig.json +15 -0
- package/types/auto-imports.d.ts +975 -0
- package/types/components.d.ts +14 -0
- package/types/env.d.ts +8 -0
- package/types/shims.d.ts +6 -0
- package/unocss.config.ts +23 -0
- package/vite.config.ts +60 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
/**
|
2
|
+
* 云文档云服务页类型定义
|
3
|
+
*/
|
4
|
+
export interface SmartDocxDrivePageProps {
|
5
|
+
/**
|
6
|
+
* Logo 图标地址
|
7
|
+
* @default <i-system-uicons-document-stack />
|
8
|
+
**/
|
9
|
+
icon?: string
|
10
|
+
/**
|
11
|
+
* 标题
|
12
|
+
* @default 'SmartOS 云文档'
|
13
|
+
*/
|
14
|
+
title?: string
|
15
|
+
|
16
|
+
/**
|
17
|
+
* 当前激活的菜单项
|
18
|
+
* - home: 首页
|
19
|
+
* - me: 我的空间
|
20
|
+
* @default 'home'
|
21
|
+
*/
|
22
|
+
activeMenu?: 'home' | 'me'
|
23
|
+
}
|
24
|
+
|
25
|
+
/** 上传文件信息 */
|
26
|
+
export interface FileItem {
|
27
|
+
id: string
|
28
|
+
|
29
|
+
ref?: Element | ComponentPublicInstance | HTMLDivElement | null
|
30
|
+
|
31
|
+
/** 文件 */
|
32
|
+
file: File
|
33
|
+
/** 文件名 */
|
34
|
+
fileName: string
|
35
|
+
/** 文件名后缀 */
|
36
|
+
fileExt: string
|
37
|
+
|
38
|
+
/** 读取文件中 */
|
39
|
+
isReading?: boolean
|
40
|
+
|
41
|
+
/** 上传进度 */
|
42
|
+
progress?: number
|
43
|
+
/** 上传中 */
|
44
|
+
isUploading?: boolean
|
45
|
+
/** 上传成功 */
|
46
|
+
isUploadSuccess?: boolean
|
47
|
+
/** 上传失败 */
|
48
|
+
isUploadError?: boolean
|
49
|
+
/** 是否是极速上传 */
|
50
|
+
isQuickUpload?: boolean
|
51
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { deleteFile, renameFile } from '../apis/file';
|
2
|
+
import type { FileInfo } from '../apis/file';
|
3
|
+
import { type DirInfo, getFolderList } from '../apis/folder';
|
4
|
+
import { useConfirm, usePrompt } from '../composables/usePrompt';
|
5
|
+
import { menuStore } from '../stores/menu';
|
6
|
+
|
7
|
+
const { flatDirTree } = menuStore();
|
8
|
+
|
9
|
+
/** 重命名文件 */
|
10
|
+
export function toRenameFile(file: FileInfo) {
|
11
|
+
return usePrompt('', '文件名称', {
|
12
|
+
inputValue: file.fileName ?? '',
|
13
|
+
}, async (instance) => {
|
14
|
+
await renameFile({ userFileId: file.userFileId, fileName: instance.inputValue }).execute();
|
15
|
+
});
|
16
|
+
}
|
17
|
+
|
18
|
+
/** 删除文件 */
|
19
|
+
export function toDeleteFile(file: FileInfo) {
|
20
|
+
return useConfirm(`是否删除文件:${file.fileFullName}`, '提示', {
|
21
|
+
confirmButtonText: '删除',
|
22
|
+
cancelButtonText: '取消',
|
23
|
+
type: 'error',
|
24
|
+
confirmButtonClass: 'smart-button--danger',
|
25
|
+
}, async () => {
|
26
|
+
await deleteFile({ userFileId: file.userFileId }).execute();
|
27
|
+
});
|
28
|
+
}
|
29
|
+
|
30
|
+
/** 重命名文件夹 */
|
31
|
+
export function toRenameFolder(info: DirInfo) {
|
32
|
+
return usePrompt('', '文件夹名称', {
|
33
|
+
inputValue: info.label ?? '',
|
34
|
+
}, async (instance) => {
|
35
|
+
const parentFilePath = info.filePath.split('/').slice(0, -1).join('/') || '/';
|
36
|
+
const folderList = getFolderList({ filePath: parentFilePath });
|
37
|
+
|
38
|
+
await folderList.execute();
|
39
|
+
|
40
|
+
const folderFileInfo = folderList.data!.data.records.find(item => item.fileName === info.label)!;
|
41
|
+
|
42
|
+
await renameFile({ userFileId: folderFileInfo.userFileId, fileName: instance.inputValue }).execute();
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
/** 删除文件夹 */
|
47
|
+
export function toDeleteFolder(info: DirInfo) {
|
48
|
+
return useConfirm(`是否删除文件夹:${info.label}`, '提示', {
|
49
|
+
confirmButtonText: '删除',
|
50
|
+
cancelButtonText: '取消',
|
51
|
+
type: 'error',
|
52
|
+
confirmButtonClass: 'smart-button--danger',
|
53
|
+
}, async () => {
|
54
|
+
const parentFilePath = info.filePath.split('/').slice(0, -1).join('/') || '/';
|
55
|
+
const folderList = getFolderList({ filePath: parentFilePath });
|
56
|
+
|
57
|
+
await folderList.execute();
|
58
|
+
|
59
|
+
const folderFileInfo = folderList.data!.data.records.find(item => item.fileName === info.label)!;
|
60
|
+
|
61
|
+
await deleteFile({ userFileId: folderFileInfo.userFileId }).execute();
|
62
|
+
});
|
63
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import SparkMd5 from 'spark-md5';
|
2
|
+
|
3
|
+
const FileSlice = File.prototype.slice || (File.prototype as any).mozSlice || (File.prototype as any).webkitSlice;
|
4
|
+
const FileChunkSize = 1024 * 1024 * 2; // 2MB
|
5
|
+
|
6
|
+
export function getFileMd5(file: File) {
|
7
|
+
return new Promise<string>((resolve) => {
|
8
|
+
const chunks = Math.ceil(file.size / FileChunkSize);
|
9
|
+
const spark = new SparkMd5.ArrayBuffer();
|
10
|
+
const fileReader = new FileReader();
|
11
|
+
let currentChunk = 0;
|
12
|
+
|
13
|
+
fileReader.onload = function (e) {
|
14
|
+
spark.append(e.target!.result as ArrayBuffer); // Append array buffer
|
15
|
+
currentChunk++;
|
16
|
+
|
17
|
+
if (currentChunk < chunks)
|
18
|
+
loadNext();
|
19
|
+
else
|
20
|
+
resolve(spark.end());
|
21
|
+
};
|
22
|
+
|
23
|
+
function loadNext() {
|
24
|
+
const start = currentChunk * FileChunkSize;
|
25
|
+
const end = start + FileChunkSize >= file.size ? file.size : start + FileChunkSize;
|
26
|
+
fileReader.readAsArrayBuffer(FileSlice.call(file, start, end));
|
27
|
+
}
|
28
|
+
|
29
|
+
loadNext();
|
30
|
+
});
|
31
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<!-- Markdown 对外层 -->
|
2
|
+
|
3
|
+
<template>
|
4
|
+
<ElConfigProvider namespace="smart">
|
5
|
+
<Index
|
6
|
+
:initial-value="initialValue"
|
7
|
+
/>
|
8
|
+
</ElConfigProvider>
|
9
|
+
</template>
|
10
|
+
|
11
|
+
<script lang="ts" setup>
|
12
|
+
import type { Descendant } from 'slate';
|
13
|
+
import type { MarkdownProps } from './MarkdownShortcuts/components/types';
|
14
|
+
import Index from './index.vue';
|
15
|
+
import { shadowRoot, valueChange } from './MarkdownShortcuts/shared/const';
|
16
|
+
|
17
|
+
defineProps<Pick<MarkdownProps, 'initialValue'>>();
|
18
|
+
|
19
|
+
const emit = defineEmits<{
|
20
|
+
change: [value: Descendant[]]
|
21
|
+
}>();
|
22
|
+
|
23
|
+
useSmart();
|
24
|
+
|
25
|
+
valueChange.on((value) => {
|
26
|
+
emit('change', value);
|
27
|
+
});
|
28
|
+
|
29
|
+
onMounted(() => {
|
30
|
+
shadowRoot.value = useCurrentElement().value!.parentNode as typeof shadowRoot.value;
|
31
|
+
});
|
32
|
+
</script>
|
@@ -0,0 +1,202 @@
|
|
1
|
+
<!-- Markdown 逻辑层 -->
|
2
|
+
|
3
|
+
<template>
|
4
|
+
<MarkdownShortcuts
|
5
|
+
v-bind="props"
|
6
|
+
:withOverride="withOverride"
|
7
|
+
/>
|
8
|
+
</template>
|
9
|
+
|
10
|
+
<script lang="ts" setup>
|
11
|
+
import type { Node } from 'slate';
|
12
|
+
import { Editor, Element, Path, Point, Range, Text, Transforms } from 'slate';
|
13
|
+
import { applyPureReactInVue } from 'veaury';
|
14
|
+
import MarkdownShortcutsReact from '../components-react/Markdown';
|
15
|
+
import { MARKS, SHORTCUTS } from '../components-react/MarkdownElement';
|
16
|
+
import { useTextSelection } from '../composables/useTextSelection';
|
17
|
+
import type { MarkdownShortcutsProps } from '../components-react/Markdown';
|
18
|
+
import type { HeadingElement } from '../components-react/types/custom-types';
|
19
|
+
import type { MarkdownProps } from './types';
|
20
|
+
|
21
|
+
const props = defineProps<MarkdownProps>();
|
22
|
+
|
23
|
+
const MarkdownShortcuts = applyPureReactInVue(MarkdownShortcutsReact);
|
24
|
+
|
25
|
+
/** 匹配当前焦点所在的块级节点 */
|
26
|
+
function matchBlockElement(editor: Editor) {
|
27
|
+
return {
|
28
|
+
match: (n: Node) => Element.isElement(n) && Editor.isBlock(editor, n),
|
29
|
+
};
|
30
|
+
}
|
31
|
+
/** 匹配当前焦点所在的节点 */
|
32
|
+
const matchElement = {
|
33
|
+
match: (n: Node) => Element.isElement(n),
|
34
|
+
};
|
35
|
+
|
36
|
+
/** 编辑器功能重写 */
|
37
|
+
const withOverride: MarkdownShortcutsProps['withOverride'] = (editor) => {
|
38
|
+
const { insertText, deleteBackward, insertBreak } = editor;
|
39
|
+
|
40
|
+
/** 插入回车 */
|
41
|
+
editor.insertBreak = () => {
|
42
|
+
const { selection } = editor;
|
43
|
+
|
44
|
+
if (selection && Range.isCollapsed(selection)) {
|
45
|
+
/** 当前节点 */
|
46
|
+
const [block, path] = Editor.above(editor, matchBlockElement(editor)) || [];
|
47
|
+
|
48
|
+
// 在块级节点末尾按下回车键
|
49
|
+
if (block && Element.isElement(block) && Point.equals(selection.anchor, Editor.end(editor, path!))) {
|
50
|
+
// 在列表项节点末尾按下回车键, 创建一个新的列表项
|
51
|
+
if (block.type === 'list-item')
|
52
|
+
return Transforms.insertNodes(editor, { type: 'list-item', children: [{ text: '' }] }, matchBlockElement(editor));
|
53
|
+
// 在非段落节点末尾按下回车键, 创建一个新的段落
|
54
|
+
else if (block.type !== 'paragraph')
|
55
|
+
return Transforms.insertNodes(editor, { type: 'paragraph', children: [{ text: '' }] }, matchBlockElement(editor));
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
insertBreak();
|
60
|
+
};
|
61
|
+
|
62
|
+
/** 插入文本 */
|
63
|
+
editor.insertText = (text) => {
|
64
|
+
const { selection } = editor;
|
65
|
+
|
66
|
+
if (text.endsWith(' ') && selection && Range.isCollapsed(selection)) {
|
67
|
+
/** 父级容器节点 */
|
68
|
+
const match = Editor.above(editor, matchBlockElement(editor));
|
69
|
+
/** 父级容器节点在根节点中的位置 */
|
70
|
+
const path = match ? match[1] : [];
|
71
|
+
/** 当前节点的起点位置 */
|
72
|
+
const start = Editor.start(editor, path);
|
73
|
+
/** 当前节点的范围 */
|
74
|
+
const range = { anchor: selection.anchor, focus: start };
|
75
|
+
/** 当前节点焦点前面的文本 */
|
76
|
+
const beforeText = Editor.string(editor, range) + text.slice(0, -1);
|
77
|
+
/** 当前节点类型 */
|
78
|
+
const type = SHORTCUTS[beforeText as keyof typeof SHORTCUTS];
|
79
|
+
|
80
|
+
// 使用前缀匹配的节点类型
|
81
|
+
if (type) {
|
82
|
+
Transforms.select(editor, range);
|
83
|
+
|
84
|
+
if (!Range.isCollapsed(range))
|
85
|
+
Transforms.delete(editor);
|
86
|
+
|
87
|
+
const newProperties: Partial<Element> = {
|
88
|
+
type,
|
89
|
+
};
|
90
|
+
|
91
|
+
// 标题
|
92
|
+
if (type === 'heading')
|
93
|
+
(newProperties as HeadingElement).level = `${beforeText.length}`;
|
94
|
+
// 引用
|
95
|
+
else if (type === 'blockquote')
|
96
|
+
newProperties.type = 'paragraph'; // 先设为段落, 再包裹为引用
|
97
|
+
|
98
|
+
Transforms.setNodes<Element>(editor, newProperties, matchBlockElement(editor));
|
99
|
+
|
100
|
+
// 引用
|
101
|
+
if (type === 'blockquote')
|
102
|
+
Transforms.wrapNodes(editor, { type: 'blockquote', children: [] }, matchBlockElement(editor));
|
103
|
+
// 无序列表
|
104
|
+
else if (type === 'list-item')
|
105
|
+
Transforms.wrapNodes(editor, { type: 'list', children: [] }, matchBlockElement(editor));
|
106
|
+
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
// 加粗 / 斜线 / 删除线
|
110
|
+
else {
|
111
|
+
for (const [matchReg, leaf] of MARKS) {
|
112
|
+
const [mdText, text] = beforeText.match(matchReg) || [];
|
113
|
+
|
114
|
+
if (mdText) {
|
115
|
+
const point = Editor.before(editor, selection, { unit: 'character', distance: mdText.length })!;
|
116
|
+
const range = Editor.range(editor, point, selection);
|
117
|
+
|
118
|
+
// 删除前后缀
|
119
|
+
Transforms.insertText(editor, text, { at: range });
|
120
|
+
// 设置节点属性
|
121
|
+
Transforms.setNodes(editor, { [leaf]: true }, {
|
122
|
+
at: range,
|
123
|
+
match: node => Text.isText(node),
|
124
|
+
split: true,
|
125
|
+
});
|
126
|
+
// 插入空格
|
127
|
+
return Transforms.insertNodes(editor, { text: ' ' });
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
insertText(text);
|
134
|
+
};
|
135
|
+
|
136
|
+
/** 删除 */
|
137
|
+
editor.deleteBackward = (...args) => {
|
138
|
+
const { selection } = editor;
|
139
|
+
|
140
|
+
if (selection && Range.isCollapsed(selection)) {
|
141
|
+
/** 当前节点 */
|
142
|
+
const [node, path] = Editor.above<Element>(editor, matchBlockElement(editor)) || [];
|
143
|
+
|
144
|
+
// 当前光标在当前节点的起点位置
|
145
|
+
if (node && Point.equals(selection.anchor, Editor.start(editor, path!))) {
|
146
|
+
const [parent, parentPath] = Editor.parent(editor, path!);
|
147
|
+
|
148
|
+
if (parent) {
|
149
|
+
// 判断父级节点也是块级节点, 删除操作会将当前节点脱离父级节点
|
150
|
+
// 并转为段落节点
|
151
|
+
if (Element.isElement(parent)) {
|
152
|
+
const isFirst = parent.children[0] === node;
|
153
|
+
const isLast = parent.children.at(-1) === node;
|
154
|
+
let isUnwrap = false;
|
155
|
+
|
156
|
+
// 当前节点是父级节点的唯一子节点
|
157
|
+
if (isFirst && isLast) {
|
158
|
+
isUnwrap = true;
|
159
|
+
Transforms.unwrapNodes(editor, { at: parentPath });
|
160
|
+
}
|
161
|
+
// 当前节点是父级节点的第一个子节点
|
162
|
+
else if (isFirst) {
|
163
|
+
isUnwrap = true;
|
164
|
+
Transforms.moveNodes(editor, { to: parentPath });
|
165
|
+
}
|
166
|
+
// 当前节点是父级节点的最后一个子节点
|
167
|
+
else if (isLast) {
|
168
|
+
isUnwrap = true;
|
169
|
+
Transforms.moveNodes(editor, { to: Path.next(parentPath) });
|
170
|
+
}
|
171
|
+
|
172
|
+
// 若当前节点已脱离父级节点
|
173
|
+
if (isUnwrap) {
|
174
|
+
// 转为段落节点
|
175
|
+
Transforms.setNodes(editor, { type: 'paragraph' });
|
176
|
+
return;
|
177
|
+
}
|
178
|
+
}
|
179
|
+
// 父级节点是编辑器根节点, 且当前节点是唯一子节点, 转为段落节点
|
180
|
+
else if (Editor.isEditor(parent) && parent.children.length === 1) {
|
181
|
+
Transforms.setNodes(editor, { type: 'paragraph' });
|
182
|
+
}
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
deleteBackward(...args);
|
187
|
+
}
|
188
|
+
};
|
189
|
+
|
190
|
+
return editor;
|
191
|
+
};
|
192
|
+
|
193
|
+
// 防止拖拽选中文本
|
194
|
+
{
|
195
|
+
const { isValidSelection } = useTextSelection();
|
196
|
+
|
197
|
+
useEventListener(useCurrentElement(), 'dragstart', (event: DragEvent) => {
|
198
|
+
if (isValidSelection.value)
|
199
|
+
event.preventDefault();
|
200
|
+
});
|
201
|
+
}
|
202
|
+
</script>
|
@@ -0,0 +1,100 @@
|
|
1
|
+
<!-- 菜单栏 -->
|
2
|
+
|
3
|
+
<template>
|
4
|
+
<div
|
5
|
+
v-if="isMenuShow"
|
6
|
+
ref="menuRef" :style="floatingStyles"
|
7
|
+
class="docx-menu"
|
8
|
+
flex="~ items-center gap-2"
|
9
|
+
text="lg black" select-none
|
10
|
+
b="1 solid gray-3 rounded-1.5" bg-white el="6 op-36" p="y2 x3"
|
11
|
+
@mousedown.prevent.stop @mouseup.prevent.stop @click.prevent.stop @dblclick.prevent.stop @contextmenu.prevent.stop
|
12
|
+
>
|
13
|
+
<!-- 加粗 -->
|
14
|
+
<el-tooltip placement="top" content="粗体<br>Markdown: **文本** 空格" raw-content :offset="15" :hide-after="0">
|
15
|
+
<button class="docx-menu-item" :class="{ active: isBoldActive }" @click="toToggleMark('bold')">
|
16
|
+
<i-iconoir-bold class="size-5.5" />
|
17
|
+
</button>
|
18
|
+
</el-tooltip>
|
19
|
+
<!-- 删除线 -->
|
20
|
+
<el-tooltip placement="top" content="删除线<br>Markdown: ~~文本~~ 空格" raw-content :offset="15" :hide-after="0">
|
21
|
+
<button class="docx-menu-item" :class="{ active: isDelActive }" @click="toToggleMark('del')">
|
22
|
+
<i-fluent-text-strikethrough-20-filled class="size-4.7 relative top-.03" />
|
23
|
+
</button>
|
24
|
+
</el-tooltip>
|
25
|
+
<!-- 斜体 -->
|
26
|
+
<el-tooltip placement="top" content="斜体<br>Markdown: *文本* 空格" raw-content :offset="15" :hide-after="0">
|
27
|
+
<button class="docx-menu-item" :class="{ active: isItalicActive }" @click="toToggleMark('italic')">
|
28
|
+
<i-ri-italic class="size-5 relative top-.1" />
|
29
|
+
</button>
|
30
|
+
</el-tooltip>
|
31
|
+
<!-- 下划线 -->
|
32
|
+
<el-tooltip placement="top" content="下划线<br>Markdown: ~文本~ 空格" raw-content :offset="15" :hide-after="0">
|
33
|
+
<button class="docx-menu-item" :class="{ active: isUnderlineActive }" @click="toToggleMark('underline')">
|
34
|
+
<i-iconoir-underline class="size-6 relative top-.1" />
|
35
|
+
</button>
|
36
|
+
</el-tooltip>
|
37
|
+
</div>
|
38
|
+
</template>
|
39
|
+
|
40
|
+
<script lang="ts" setup>
|
41
|
+
import { flip, offset, shift, useFloating } from '@floating-ui/vue';
|
42
|
+
import { useTextSelection } from '../composables/useTextSelection';
|
43
|
+
import { isMarkActive, toggleMark } from '../utils/slateHelpers';
|
44
|
+
import { shadowRoot } from '../shared/const';
|
45
|
+
import type { CustomText } from '../components-react/types/custom-types';
|
46
|
+
|
47
|
+
const menuRef = ref<HTMLElement>();
|
48
|
+
|
49
|
+
const isMousePressed = useMousePressed().pressed;
|
50
|
+
|
51
|
+
const isMenuHover = useElementHover(menuRef);
|
52
|
+
const isMenuFocusWithin = useFocusWithin(menuRef).focused;
|
53
|
+
|
54
|
+
const { range, isValidSelection, onValidSelectionUpdate } = useTextSelection();
|
55
|
+
const { floatingStyles } = useFloating(range, menuRef, {
|
56
|
+
placement: 'top',
|
57
|
+
middleware: [offset(6), shift(), flip()],
|
58
|
+
});
|
59
|
+
|
60
|
+
const isMenuShowManual = ref(false);
|
61
|
+
const isMenuShow = computed(() => {
|
62
|
+
return isMenuShowManual.value && (isMenuHover.value || isMenuFocusWithin.value || (!isMousePressed.value && isValidSelection.value));
|
63
|
+
});
|
64
|
+
|
65
|
+
const isBoldActive = ref(false);
|
66
|
+
const isDelActive = ref(false);
|
67
|
+
const isItalicActive = ref(false);
|
68
|
+
const isUnderlineActive = ref(false);
|
69
|
+
|
70
|
+
const { trigger } = watchTriggerable(isMenuShow, () => {
|
71
|
+
isBoldActive.value = isMarkActive('bold');
|
72
|
+
isDelActive.value = isMarkActive('del');
|
73
|
+
isItalicActive.value = isMarkActive('italic');
|
74
|
+
isUnderlineActive.value = isMarkActive('underline');
|
75
|
+
});
|
76
|
+
|
77
|
+
function toToggleMark(format: keyof Omit<CustomText, 'text'>) {
|
78
|
+
toggleMark(format);
|
79
|
+
trigger();
|
80
|
+
}
|
81
|
+
|
82
|
+
useEventListener(shadowRoot, 'keydown', (event: KeyboardEvent) => {
|
83
|
+
if (event.key === 'Escape' && isMenuShow.value)
|
84
|
+
isMenuShowManual.value = false;
|
85
|
+
});
|
86
|
+
onValidSelectionUpdate(() => {
|
87
|
+
isMenuShowManual.value = true;
|
88
|
+
});
|
89
|
+
</script>
|
90
|
+
|
91
|
+
<style lang="sass" scoped>
|
92
|
+
:host-context(.dark) .docx-menu
|
93
|
+
@apply filter-invert
|
94
|
+
.docx-menu-item
|
95
|
+
@apply size-6 flex-(~ justify-center items-center)
|
96
|
+
@apply cursor-pointer rounded
|
97
|
+
@apply hover:bg-gray-2
|
98
|
+
&.active
|
99
|
+
@apply c-blue-6 bg-blue-2 bg-op-60 hover:bg-op-90
|
100
|
+
</style>
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import type { RenderElementProps, RenderLeafProps } from 'slate-react';
|
2
|
+
import type { BaseSelection, Descendant, Editor } from 'slate';
|
3
|
+
import { useCallback, useMemo } from 'react';
|
4
|
+
import { createEditor } from 'slate';
|
5
|
+
import { Editable, Slate, withReact } from 'slate-react';
|
6
|
+
import { withHistory } from 'slate-history';
|
7
|
+
import { editor as editorStore, selectionChange, valueChange } from '../shared/const';
|
8
|
+
import { RenderElement, RenderLeaf } from './MarkdownElement';
|
9
|
+
|
10
|
+
export interface MarkdownShortcutsProps {
|
11
|
+
/** 初始值 */
|
12
|
+
initialValue?: Descendant[]
|
13
|
+
/** 编辑器功能重写 */
|
14
|
+
withOverride?: (editor: Editor) => Editor
|
15
|
+
/** 用户输入前的事件回调 */
|
16
|
+
onBeforeInput?: (event: InputEvent) => void
|
17
|
+
}
|
18
|
+
|
19
|
+
export default function MarkdownShortcuts(props: MarkdownShortcutsProps) {
|
20
|
+
const {
|
21
|
+
initialValue = [{
|
22
|
+
type: 'paragraph', children: [{ text: '' }],
|
23
|
+
}],
|
24
|
+
onBeforeInput,
|
25
|
+
withOverride = (editor: Editor) => editor,
|
26
|
+
} = props;
|
27
|
+
|
28
|
+
const editor = useMemo(
|
29
|
+
() => withOverride(withReact(withHistory(createEditor()))),
|
30
|
+
[],
|
31
|
+
);
|
32
|
+
|
33
|
+
const renderElement = useCallback((props: RenderElementProps) => <RenderElement {...props} />, []);
|
34
|
+
const renderLeaf = useCallback((props: RenderLeafProps) => <RenderLeaf {...props} />, []);
|
35
|
+
|
36
|
+
const handleDOMBeforeInput = useCallback(
|
37
|
+
onBeforeInput
|
38
|
+
? (event: InputEvent) => {
|
39
|
+
queueMicrotask(() => onBeforeInput!(event));
|
40
|
+
}
|
41
|
+
: () => {},
|
42
|
+
[editor],
|
43
|
+
);
|
44
|
+
|
45
|
+
const onSelectionChange = useCallback(
|
46
|
+
(selection: BaseSelection) => selectionChange.trigger(selection),
|
47
|
+
[editor],
|
48
|
+
);
|
49
|
+
const onValueChange = useCallback(
|
50
|
+
(value: Descendant[]) => valueChange.trigger(value),
|
51
|
+
[editor],
|
52
|
+
);
|
53
|
+
|
54
|
+
editorStore.value = editor;
|
55
|
+
|
56
|
+
return (
|
57
|
+
<Slate
|
58
|
+
editor={editor}
|
59
|
+
initialValue={initialValue}
|
60
|
+
onSelectionChange={onSelectionChange}
|
61
|
+
onValueChange={onValueChange}
|
62
|
+
>
|
63
|
+
<Editable
|
64
|
+
className='smart-docx-editor'
|
65
|
+
renderElement={renderElement}
|
66
|
+
renderLeaf={renderLeaf}
|
67
|
+
onDOMBeforeInput={handleDOMBeforeInput}
|
68
|
+
/>
|
69
|
+
</Slate>
|
70
|
+
);
|
71
|
+
}
|
package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/MarkdownElement.tsx
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
import type { RenderElementProps, RenderLeafProps } from 'slate-react';
|
2
|
+
import type { Element } from 'slate';
|
3
|
+
import { Heading } from './elements/Heading/index';
|
4
|
+
import { Blockquote } from './elements/Blockquote/index';
|
5
|
+
import { List, ListItem } from './elements/List';
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 使用前缀匹配的 Markdown 节点类型
|
9
|
+
*/
|
10
|
+
export const SHORTCUTS: Record<string, Element['type']> = {
|
11
|
+
// 标题
|
12
|
+
'#': 'heading',
|
13
|
+
'##': 'heading',
|
14
|
+
'###': 'heading',
|
15
|
+
'####': 'heading',
|
16
|
+
'#####': 'heading',
|
17
|
+
'######': 'heading',
|
18
|
+
// 引用
|
19
|
+
'>': 'blockquote',
|
20
|
+
// 无序列表
|
21
|
+
'*': 'list-item',
|
22
|
+
'-': 'list-item',
|
23
|
+
'+': 'list-item',
|
24
|
+
};
|
25
|
+
|
26
|
+
/**
|
27
|
+
* 加粗 / 斜线 / 删除线 / 下划线
|
28
|
+
*/
|
29
|
+
export const MARKS: [RegExp, keyof RenderLeafProps['leaf']][] = [
|
30
|
+
[/\*\*(.+?)\*\*$/, 'bold'],
|
31
|
+
[/\*(.+?)\*$/, 'italic'],
|
32
|
+
[/\~\~(.+?)\~\~$/, 'del'],
|
33
|
+
[/\~(.+?)\~$/, 'underline'],
|
34
|
+
];
|
35
|
+
|
36
|
+
/**
|
37
|
+
* 渲染元素节点
|
38
|
+
*/
|
39
|
+
export function RenderElement(props: RenderElementProps) {
|
40
|
+
// 标题
|
41
|
+
if (props.element.type === 'heading')
|
42
|
+
return <Heading {...props} />;
|
43
|
+
// 引用
|
44
|
+
if (props.element.type === 'blockquote')
|
45
|
+
return <Blockquote {...props} />;
|
46
|
+
// 列表
|
47
|
+
if (props.element.type === 'list')
|
48
|
+
return <List {...props} />;
|
49
|
+
// 列表项
|
50
|
+
if (props.element.type === 'list-item')
|
51
|
+
return <ListItem {...props} />;
|
52
|
+
|
53
|
+
// 段落
|
54
|
+
return (
|
55
|
+
<p {...props.attributes}>
|
56
|
+
{props.children}
|
57
|
+
</p>
|
58
|
+
);
|
59
|
+
};
|
60
|
+
|
61
|
+
/**
|
62
|
+
* 渲染叶子节点
|
63
|
+
*/
|
64
|
+
export function RenderLeaf({ attributes, children, leaf }: RenderLeafProps) {
|
65
|
+
if (leaf.bold)
|
66
|
+
children = <strong>{children}</strong>;
|
67
|
+
|
68
|
+
if (leaf.code)
|
69
|
+
children = <code>{children}</code>;
|
70
|
+
|
71
|
+
if (leaf.italic)
|
72
|
+
children = <em>{children}</em>;
|
73
|
+
|
74
|
+
if (leaf.del)
|
75
|
+
children = <del>{children}</del>;
|
76
|
+
|
77
|
+
if (leaf.underline)
|
78
|
+
children = <u>{children}</u>;
|
79
|
+
|
80
|
+
return <span {...attributes}>{children}</span>;
|
81
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
docx-h1, docx-h2, docx-h3, docx-h4, docx-h5, docx-h6
|
2
|
+
@apply block font-bold
|
3
|
+
docx-h1
|
4
|
+
@apply text-26px mt-26px mb-10px
|
5
|
+
docx-h2
|
6
|
+
@apply text-22px mt-22px mb-8px
|
7
|
+
docx-h3
|
8
|
+
@apply text-20px mt-20px mb-8px
|
9
|
+
docx-h4
|
10
|
+
@apply text-18px mt-18px mb-8px
|
11
|
+
docx-h5
|
12
|
+
@apply text-16px mt-18px mb-8px
|
13
|
+
docx-h6
|
14
|
+
@apply text-16px mt-16px mb-8px
|