@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.
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