@smartos-lib/components 1.7.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.eslintrc +12 -0
  2. package/.eslintrc-auto-import.json +332 -0
  3. package/Components.code-workspace +143 -0
  4. package/LICENSE +21 -0
  5. package/dist/smart-docx-editor/index.d.ts +2 -0
  6. package/dist/smart-docx-editor/index.js +68 -0
  7. package/dist/smart-file-preview/index.d.ts +18 -0
  8. package/dist/smart-file-preview/index.js +37 -0
  9. package/dist/smart-upload/index.d.ts +2 -0
  10. package/dist/smart-upload/index.js +800 -0
  11. package/index.html +16 -0
  12. package/package.json +23 -0
  13. package/public/favicon.svg +6 -0
  14. package/scripts/components.vite.config.ts +96 -0
  15. package/scripts/shared.ts +9 -0
  16. package/src/App.vue +28 -0
  17. package/src/components/Logo/index.vue +15 -0
  18. package/src/components-private/.gitkeep +0 -0
  19. package/src/composables/useElementStyle.ts +23 -0
  20. package/src/composables/useNaiveStyle.ts +43 -0
  21. package/src/composables/useNaiveTheme.ts +71 -0
  22. package/src/composables/useSmart.ts +36 -0
  23. package/src/layouts/default.vue +3 -0
  24. package/src/main.ts +33 -0
  25. package/src/modules/pinia/index.ts +8 -0
  26. package/src/modules/progress/index.ts +12 -0
  27. package/src/modules/router/install.ts +9 -0
  28. package/src/modules/router/routes.ts +40 -0
  29. package/src/pages/[...all].vue +21 -0
  30. package/src/pages/frame/component/[name].vue +14 -0
  31. package/src/pages/frame/index.vue +81 -0
  32. package/src/pages/index/composables/useTabsManage.ts +46 -0
  33. package/src/pages/index/index.vue +111 -0
  34. package/src/pages/index/type.ts +13 -0
  35. package/src/pages/index/utils/index.ts +41 -0
  36. package/src/settings.ts +9 -0
  37. package/src/shared/components.ts +52 -0
  38. package/src/shared/env.ts +11 -0
  39. package/src/shared/unocss.theme.ts +1600 -0
  40. package/src/stores/theme.ts +29 -0
  41. package/src/styles/element.scss +3 -0
  42. package/src/styles/styles.scss +21 -0
  43. package/src/types.ts +20 -0
  44. package/src/utils/callCustomElementExposed.ts +6 -0
  45. package/src/utils/deepCloneESModule.ts +10 -0
  46. package/src/utils/defineCustomElements.ts +18 -0
  47. package/src/utils/formatComponentsGlob.ts +16 -0
  48. package/src/utils/getFileMD5.ts +31 -0
  49. package/src/utils/getFileNameAndExt.ts +11 -0
  50. package/src/utils/isFileEqual.ts +13 -0
  51. package/src/utils/jsonToFormData.ts +8 -0
  52. package/src/web-components/smart-docx-drive-page/App.vue +37 -0
  53. package/src/web-components/smart-docx-drive-page/apis/doc.ts +85 -0
  54. package/src/web-components/smart-docx-drive-page/apis/file.ts +278 -0
  55. package/src/web-components/smart-docx-drive-page/apis/folder.ts +72 -0
  56. package/src/web-components/smart-docx-drive-page/children/Home.vue +8 -0
  57. package/src/web-components/smart-docx-drive-page/children/Me.vue +47 -0
  58. package/src/web-components/smart-docx-drive-page/components/CustomImage.vue +26 -0
  59. package/src/web-components/smart-docx-drive-page/components/CustomPopover.vue +62 -0
  60. package/src/web-components/smart-docx-drive-page/components/DocxDir.vue +99 -0
  61. package/src/web-components/smart-docx-drive-page/components/DocxDoc.vue +132 -0
  62. package/src/web-components/smart-docx-drive-page/components/DocxDownloadPopoverItem.vue +41 -0
  63. package/src/web-components/smart-docx-drive-page/components/DocxFileList.vue +156 -0
  64. package/src/web-components/smart-docx-drive-page/components/DocxPreview.vue +33 -0
  65. package/src/web-components/smart-docx-drive-page/components/DocxUpload.vue +164 -0
  66. package/src/web-components/smart-docx-drive-page/components/FileIcon.vue +62 -0
  67. package/src/web-components/smart-docx-drive-page/components-private/Header.vue +65 -0
  68. package/src/web-components/smart-docx-drive-page/components-private/Logo.vue +15 -0
  69. package/src/web-components/smart-docx-drive-page/components-private/Menu.vue +34 -0
  70. package/src/web-components/smart-docx-drive-page/components-private/Navbar.vue +36 -0
  71. package/src/web-components/smart-docx-drive-page/composables/useFullscreenElDialog.ts +41 -0
  72. package/src/web-components/smart-docx-drive-page/composables/usePrompt.ts +73 -0
  73. package/src/web-components/smart-docx-drive-page/data.ts +10 -0
  74. package/src/web-components/smart-docx-drive-page/external-style/custom-popover.sass +8 -0
  75. package/src/web-components/smart-docx-drive-page/external-style/index.sass +1 -0
  76. package/src/web-components/smart-docx-drive-page/index.ts +20 -0
  77. package/src/web-components/smart-docx-drive-page/index.vue +39 -0
  78. package/src/web-components/smart-docx-drive-page/info.ts +2 -0
  79. package/src/web-components/smart-docx-drive-page/stores/menu.ts +60 -0
  80. package/src/web-components/smart-docx-drive-page/types.ts +51 -0
  81. package/src/web-components/smart-docx-drive-page/utils/file-actions.ts +63 -0
  82. package/src/web-components/smart-docx-drive-page/utils/file.ts +31 -0
  83. package/src/web-components/smart-docx-editor/App.vue +32 -0
  84. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/Markdown.vue +202 -0
  85. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/Menu.vue +100 -0
  86. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components/types.ts +6 -0
  87. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/Markdown.tsx +71 -0
  88. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/MarkdownElement.tsx +81 -0
  89. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Blockquote/index.sass +6 -0
  90. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Blockquote/index.tsx +12 -0
  91. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Heading/index.sass +14 -0
  92. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/Heading/index.tsx +17 -0
  93. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/List/index.scss +16 -0
  94. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/elements/List/index.tsx +39 -0
  95. package/src/web-components/smart-docx-editor/MarkdownShortcuts/components-react/types/custom-types.d.ts +69 -0
  96. package/src/web-components/smart-docx-editor/MarkdownShortcuts/composables/useTextSelection.ts +50 -0
  97. package/src/web-components/smart-docx-editor/MarkdownShortcuts/index.sass +19 -0
  98. package/src/web-components/smart-docx-editor/MarkdownShortcuts/index.vue +21 -0
  99. package/src/web-components/smart-docx-editor/MarkdownShortcuts/shared/const.ts +23 -0
  100. package/src/web-components/smart-docx-editor/MarkdownShortcuts/utils/slateHelpers.ts +23 -0
  101. package/src/web-components/smart-docx-editor/data.ts +38 -0
  102. package/src/web-components/smart-docx-editor/demo.vue +11 -0
  103. package/src/web-components/smart-docx-editor/index.md +3 -0
  104. package/src/web-components/smart-docx-editor/index.ts +5 -0
  105. package/src/web-components/smart-docx-editor/index.vue +12 -0
  106. package/src/web-components/smart-docx-editor/info.ts +2 -0
  107. package/src/web-components/smart-file-preview/category/Code.vue +171 -0
  108. package/src/web-components/smart-file-preview/category/Image.vue +49 -0
  109. package/src/web-components/smart-file-preview/category/Pdf.vue +14 -0
  110. package/src/web-components/smart-file-preview/category/Video.vue +27 -0
  111. package/src/web-components/smart-file-preview/demo.vue +34 -0
  112. package/src/web-components/smart-file-preview/index.md +5 -0
  113. package/src/web-components/smart-file-preview/index.ts +29 -0
  114. package/src/web-components/smart-file-preview/index.vue +56 -0
  115. package/src/web-components/smart-file-preview/info.ts +2 -0
  116. package/src/web-components/smart-file-preview/shared/const.ts +4 -0
  117. package/src/web-components/smart-file-preview/types.ts +38 -0
  118. package/src/web-components/smart-upload/index.ts +5 -0
  119. package/src/web-components/smart-upload/index.vue +101 -0
  120. package/src/web-components/smart-upload/info.ts +2 -0
  121. package/src/web-components/smart-upload/types.ts +28 -0
  122. package/tsconfig.json +15 -0
  123. package/types/auto-imports.d.ts +975 -0
  124. package/types/components.d.ts +14 -0
  125. package/types/env.d.ts +8 -0
  126. package/types/shims.d.ts +6 -0
  127. package/unocss.config.ts +23 -0
  128. 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,6 @@
1
+ import type { MarkdownShortcutsProps } from '../components-react/Markdown';
2
+
3
+ export interface MarkdownProps {
4
+ /** 初始值 */
5
+ initialValue?: MarkdownShortcutsProps['initialValue']
6
+ }
@@ -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
+ }
@@ -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,6 @@
1
+ docx-blockquote
2
+ @apply block c-neutral-6
3
+ @apply b-l-(2 solid neutral-4)
4
+ @apply my-2 pl-3
5
+ :host-context(.dark) &
6
+ @apply c-neutral-4
@@ -0,0 +1,12 @@
1
+ import type { RenderElementProps } from 'slate-react';
2
+
3
+ /**
4
+ * 引用
5
+ */
6
+ export function Blockquote(props: RenderElementProps) {
7
+ return (
8
+ <docx-blockquote {...props.attributes}>
9
+ {props.children}
10
+ </docx-blockquote>
11
+ );
12
+ }
@@ -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