yuque-rich-text 1.0.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.
@@ -0,0 +1,77 @@
1
+ name: Publish npm package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ push:
7
+ branches: [master]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ publish-npm:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+
16
+ # 1. 安装 Node.js
17
+ - uses: actions/setup-node@v3
18
+ with:
19
+ node-version: '22.11.0'
20
+ registry-url: 'https://registry.npmjs.org'
21
+
22
+ # 2. 安装 pnpm(改用官方 pnpm/action-setup 或 npm 安装)
23
+ - name: Install pnpm
24
+ uses: pnpm/action-setup@v2
25
+ with:
26
+ version: latest # 或指定版本,如 "8.15.0"
27
+
28
+ # 3. 缓存 pnpm 依赖
29
+ - name: Cache pnpm dependencies
30
+ uses: actions/cache@v3
31
+ with:
32
+ path: |
33
+ ~/.pnpm-store
34
+ node_modules
35
+ **/node_modules
36
+ key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
37
+ restore-keys: |
38
+ ${{ runner.os }}-pnpm-
39
+
40
+ # 4. 安装依赖并发布到 npm
41
+ - run: pnpm install --frozen-lockfile
42
+ - run: pnpm publish --access public
43
+ env:
44
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
45
+
46
+ publish-github-packages:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v3
50
+
51
+ # 1. 安装 Node.js
52
+ - uses: actions/setup-node@v3
53
+ with:
54
+ node-version: '22.11.0'
55
+ registry-url: 'https://npm.pkg.github.com'
56
+ scope: '@entity-now' # 可选,默认是仓库所有者
57
+
58
+ # 2. 安装 pnpm
59
+ - name: Install pnpm
60
+ uses: pnpm/action-setup@v2
61
+ with:
62
+ version: latest
63
+
64
+ # 3. 缓存 pnpm 依赖
65
+ - name: Cache pnpm dependencies
66
+ uses: actions/cache@v3
67
+ with:
68
+ path: |
69
+ ~/.pnpm-store
70
+ node_modules
71
+ **/node_modules
72
+ key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
73
+ restore-keys: |
74
+ ${{ runner.os }}-pnpm-
75
+
76
+ # 4. 安装依赖并发布到 GitHub Packages
77
+ - run: pnpm install --frozen-lockfile
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["Vue.volar"]
3
+ }
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Yuque Rich Text(语雀富文本编辑器)
2
+
3
+ 由于本人觉得语雀编辑器非常好用,很符合我的使用习惯,然后发现语雀的[Chrome浏览器插件](https://github.com/yuque/yuque-chrome-extension)实现了编辑器的功能,所以将其富文本的功能拆分位一个单独的Vue3组件。
4
+
5
+ ## 截图
6
+ ![组件实例](https://github.com/Entity-Now/yuque-rich-text/blob/master/public/Images/preview.png)
7
+
8
+ ### Props
9
+
10
+ ```js
11
+ export interface EditorProps {
12
+ value: string; // 传递给组件的内容
13
+ children?: any;
14
+ isview?: boolean; // 预览模式,用于在客户端页面展示结果。
15
+ uploadImage?: (params: { data: string | File }) => Promise<{
16
+ url: string;
17
+ size: number;
18
+ filename: string;
19
+ }>;
20
+ uploadVideo?: (params: { data: string | File }) => Promise<{
21
+ url: string;
22
+ size: number;
23
+ filename: string;
24
+ }>;
25
+ }
26
+ ```
27
+
28
+ ### Emit
29
+
30
+ ```js
31
+ export interface EditorEmits{
32
+ onChange?: (value: string) => void;
33
+ onLoad?: () => void;
34
+ onSave?: () => void;
35
+ }
36
+ ```
37
+
38
+ ## Expose
39
+
40
+ ```js
41
+ export interface IEditorRef {
42
+ /**
43
+ * 追加html到文档
44
+ * @param html html内容
45
+ * @param breakLine 是否前置一个换行符
46
+ */
47
+ appendContent: (html: string, breakLine?: boolean) => void;
48
+ /**
49
+ * 设置文档内容,将清空旧的内容
50
+ * @param html html内容
51
+ */
52
+ setContent: (content: string, type?: "text/lake" | "text/html") => void;
53
+ /**
54
+ * 获取文档内容
55
+ * @param type 内容的格式
56
+ * @return 文档内容
57
+ */
58
+ getContent: (type: "lake" | "text/html") => Promise<string>;
59
+ /**
60
+ * 判断当前文档是否是空文档
61
+ * @return true表示当前是空文档
62
+ */
63
+ isEmpty: () => boolean;
64
+
65
+ /**
66
+ * 获取额外信息
67
+ * @return
68
+ */
69
+ getSummaryContent: () => string;
70
+
71
+ /**
72
+ * 统计字数
73
+ * @return
74
+ */
75
+ wordCount: () => number;
76
+
77
+ /**
78
+ * 聚焦到文档开头
79
+ * @param {number} offset 偏移多少个段落,可以将选区落到开头的第offset个段落上, 默认是0
80
+ * @return
81
+ */
82
+ focusToStart: (offset?: number) => void;
83
+
84
+ /**
85
+ * 插入换行符
86
+ * @return
87
+ */
88
+ insertBreakLine: () => void;
89
+ }
90
+ ```
91
+
92
+ ### 编辑器
93
+ > 注意不可在onChange事件中修改value的值,否则会进入无限递归。
94
+ ```js
95
+ <template>
96
+ <YuqueRichText ref="editRef" :value="modelValue"/>
97
+ </template>
98
+
99
+ <script setup lang="ts">
100
+ import { ref, watch, PropType } from "vue";
101
+ import { YuqueRichText } from "yuque-rich-text";
102
+
103
+ const editRef = ref<InstanceType<typeof YuqueRichText>>();
104
+ const modelValue = ref("hello yuque richtext");
105
+ </script>
106
+ ```
107
+
108
+ ## ⚠️ Disclaimer
109
+ This is an **unofficial third-party extension** for `[www.yuque.com]`. It is not affiliated with, maintained by, or endorsed by `[www.yuque.com]`.
110
+
111
+ - **Use at your own risk**. The developers are not responsible for any violations of `[www.yuque.com]`'s terms or damages caused by this project.
112
+ - **Do not use** if `[www.yuque.com]` prohibits third-party modifications.
113
+ - This project does not redistribute any copyrighted materials from `[www.yuque.com]`.
114
+
115
+ [Read `[www.yuque.com]`'s Terms of Service](www.yuque.com) before installation.
package/demo/App.vue ADDED
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <div class="demo">
3
+ <div class="tools">
4
+ <button @click="getContent">getContent</button>
5
+ </div>
6
+ <h2>Yuque Editor</h2>
7
+ <div style="height: 60vh; border: 1px gray solid">
8
+ <YuqueRichText key="1" ref="editRef" :value="modelValue" @onChange="(e) => (output = e)" />
9
+ </div>
10
+ <h2>Yuque Viewer</h2>
11
+ <div style="height: 60vh; border: 1px gray solid;padding: 12px;">
12
+ <YuqueRichText key="2" :isview="true" :value="output" />
13
+ </div>
14
+ <h2>版权说明</h2>
15
+ <div>
16
+ 此项目来自<a href="https://github.com/yuque/yuque-chrome-extension">yuque-chrome-extension</a>,请遵守此项目的用户协议
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { ref, watch, PropType } from "vue";
23
+ import { YuqueRichText } from "yuque-rich-text";
24
+
25
+ const editRef = ref<InstanceType<typeof YuqueRichText>>();
26
+ const modelValue = ref("123");
27
+ const output = ref("");
28
+
29
+ const getContent = () => {
30
+ alert(editRef.value?.getContent("lake"));
31
+ };
32
+ </script>
33
+
34
+ <style>
35
+ .demo{
36
+ position: relative;
37
+ width: 100%;
38
+ height: 100%;
39
+ padding: 2px;
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+ .demo > .tools{
44
+ width: 100%;
45
+ height: 60px;
46
+ display: flex;
47
+ flex-direction: row;
48
+ justify-content: center;
49
+ gap: 10px;
50
+ }
51
+ </style>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
package/demo/main.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue'
2
+ import './style.css'
3
+ import App from './App.vue'
4
+
5
+ createApp(App).mount('#app')
package/demo/style.css ADDED
@@ -0,0 +1,34 @@
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+
9
+ font-synthesis: none;
10
+ text-rendering: optimizeLegibility;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ }
14
+
15
+ a {
16
+ font-weight: 500;
17
+ color: #646cff;
18
+ text-decoration: inherit;
19
+ }
20
+ a:hover {
21
+ color: #535bf2;
22
+ }
23
+
24
+
25
+
26
+ .card {
27
+ padding: 2em;
28
+ }
29
+
30
+ #app {
31
+ margin: 0 auto;
32
+ padding: 2rem;
33
+ text-align: center;
34
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/index.html ADDED
@@ -0,0 +1,21 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <link rel="stylesheet" type="text/css" href="https://gw.alipayobjects.com/render/p/yuyan_npm/@alipay_lakex-doc/1.71.0/umd/doc.css"/>
7
+ <link rel="stylesheet" type="text/css" href="https://gw.alipayobjects.com/os/lib/antd/4.24.13/dist/antd.css"/>
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
+ <title>Vite + Vue + TS</title>
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="/demo/main.ts"></script>
14
+ <script crossorigin src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
15
+ <script crossorigin src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
16
+ <script src="https://gw.alipayobjects.com/render/p/yuyan_v/180020010000005484/7.1.4/CodeMirror.js"></script>
17
+ <script src="https://ur.alipay.com/tracert_a385.js"></script>
18
+ <script src="https://mdn.alipayobjects.com/design_kitchencore/afts/file/ANSZQ7GHQPMAAAAAAAAAAAAADhulAQBr"></script>
19
+ <script src="https://gw.alipayobjects.com/render/p/yuyan_npm/@alipay_lakex-doc/1.71.0/umd/doc.umd.js"></script>
20
+ </body>
21
+ </html>
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "yuque-rich-text",
3
+ "private": false,
4
+ "version": "1.0.0",
5
+ "author": "entity-now",
6
+ "description": "由于本人觉得语雀编辑器非常好用,很符合我的使用习惯,然后发现语雀的浏览器插件实现了编辑器的功能,所以将其富文本的功能拆分位一个单独的Vue3组件。",
7
+ "keywords": [
8
+ "富文本编辑器",
9
+ "Rich Text",
10
+ "yuque",
11
+ "语雀",
12
+ "vue3 rich text"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/Entity-Now/yuque-rich-text"
17
+ },
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "type": "module",
22
+ "main": "./src/index.ts",
23
+ "dependencies": {
24
+ "@types/node": "^22.14.1",
25
+ "vite-plugin-dts": "^4.5.3",
26
+ "vue": "^3.5.13"
27
+ },
28
+ "devDependencies": {
29
+ "@vitejs/plugin-vue": "^5.2.2",
30
+ "@vue/tsconfig": "^0.7.0",
31
+ "typescript": "~5.7.2",
32
+ "vite": "^6.3.0",
33
+ "vue-tsc": "^2.2.8"
34
+ },
35
+ "scripts": {
36
+ "dev": "vite",
37
+ "demo:preview": "vite preview",
38
+ "build:demo": "VITE_BUILD_TARGET=demo vue-tsc -b && vite build",
39
+ "build:lib": "vite build"
40
+ }
41
+ }
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,205 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ declare class IEditorPlugin {
3
+ editor: any;
4
+ init(): void;
5
+ }
6
+
7
+ declare class IKernelPlugin {
8
+ kernel: any;
9
+ init(kernel: any): void;
10
+ }
11
+
12
+ declare class IRendererPlugin {
13
+ editor: any;
14
+ init(): void;
15
+ }
16
+
17
+ declare class CommandClass {
18
+ static UNAVAILABLE: string;
19
+ static EXECUTED: string;
20
+ static NOT_EXECUTED: string;
21
+ static UNKNOWN: string;
22
+
23
+ public readonly kernel: any;
24
+ public readonly editing: any;
25
+
26
+ destroy(): void;
27
+
28
+ execute(editing: any, ...args: any[]): any;
29
+
30
+ getValue(job: any, ...args: any[]): any;
31
+ }
32
+
33
+ export type IEditorPluginCls = new () => IEditorPlugin;
34
+ export type IKernelPluginCls = (new () => IKernelPlugin) & {PluginName: string};
35
+ export type IRendererPluginCls = new () => IRendererPlugin;
36
+ export type ICommandCls = new () => CommandClass;
37
+
38
+ export function InjectEditorPlugin({ EditorPlugin, KernelPlugin, PositionUtil, OpenEditorFactory, toolbarItems, Command, SelectionUtil }: {
39
+ EditorPlugin: IEditorPluginCls;
40
+ KernelPlugin: IKernelPluginCls;
41
+ Plugins: Record<string, any>;
42
+ Command: ICommandCls;
43
+ SelectionUtil: any;
44
+ PositionUtil: any;
45
+ OpenEditorFactory: {
46
+ editorPlugins: IEditorPluginCls[];
47
+ kernelPlugins: IKernelPluginCls[];
48
+ registerEditorPlugin: (plugins: IEditorPluginCls[]) => void;
49
+ registerRenderPlugin: (plugins: IRendererPluginCls[]) => void;
50
+ registerKernelPlugin: (plugins: IKernelPluginCls[]) => void;
51
+ };
52
+ toolbarItems: Record<string, string>
53
+ }, doc: Document) {
54
+
55
+ class CustomEditorPlugin extends EditorPlugin {
56
+ static PluginName = 'CustomEditorPlugin';
57
+
58
+ override init() {
59
+ /**
60
+ * 修改工具栏的按钮
61
+ */
62
+ this.editor.option.toolbar._option.agentConfig = {
63
+ default: {
64
+ items: [
65
+ toolbarItems.bold,
66
+ toolbarItems.italic,
67
+ toolbarItems.strikethrough,
68
+ toolbarItems.underline,
69
+ '|',
70
+ toolbarItems.color,
71
+ toolbarItems.bgColor,
72
+ '|',
73
+ toolbarItems.quote,
74
+ toolbarItems.hr,
75
+ toolbarItems.codeblock
76
+ ],
77
+ },
78
+ table: {
79
+ items: [
80
+ toolbarItems.bold,
81
+ toolbarItems.italic,
82
+ toolbarItems.strikethrough,
83
+ toolbarItems.underline,
84
+ '|',
85
+ toolbarItems.color,
86
+ toolbarItems.bgColor,
87
+ toolbarItems.tableCellBgColor,
88
+ toolbarItems.tableBorderVisible,
89
+ '|',
90
+ toolbarItems.alignment,
91
+ toolbarItems.tableVerticalAlign,
92
+ toolbarItems.tableMergeCell,
93
+ '|',
94
+ toolbarItems.quote,
95
+ toolbarItems.hr,
96
+ toolbarItems.code
97
+ ],
98
+ },
99
+ };
100
+
101
+ this.editor
102
+ .getService({ value: 'IToolbarEditorService' })
103
+ ?.setLayer(doc.getElementById('toolbar') as HTMLDivElement);
104
+ }
105
+ }
106
+
107
+ class InsertHTMLCommand extends Command {
108
+ override execute(editing: any, html: string) {
109
+ const job = editing.newJob();
110
+ const { kernel } = this;
111
+
112
+ let inode = kernel.readData('text/html', html, {
113
+ from: 'insert',
114
+ self: false,
115
+ forceText: false,
116
+ });
117
+ if (!inode) {
118
+ return false;
119
+ }
120
+
121
+ inode = kernel.getExtendMethod('normalizeINodeTree')?.(job, inode) || inode;
122
+
123
+ job.setSelection(SelectionUtil.insertINode(job, job.getSelection(), inode));
124
+
125
+ editing.commitJob(job);
126
+
127
+ return true;
128
+ }
129
+ }
130
+ class ImageInsertAfterCommand extends Command {
131
+
132
+ override execute(editing: any, cardNodeId: string, text: string) {
133
+ const job = editing.newJob();
134
+ const node = job.getNodeById(cardNodeId);
135
+
136
+ if (!node || !node.isConnected || !node.isCardNode() || !node.parentNode) {
137
+ editing.cancelJob(job);
138
+ return false;
139
+ }
140
+
141
+ let position = PositionUtil.breakLine(
142
+ job,
143
+ job.newPosition(node.parentNode, node.offset + 1),
144
+ );
145
+
146
+ position = PositionUtil.insertText(job, position, text);
147
+
148
+ job.setSelection([job.newRange(position)]);
149
+ editing.commitJob(job);
150
+
151
+ return true;
152
+ }
153
+ }
154
+
155
+ class CustomKernelPlugin extends KernelPlugin {
156
+ static PluginName = 'CustomEditorPlugin';
157
+
158
+ override init(kernel: any) {
159
+ kernel.registerCommand('insertHTML', new InsertHTMLCommand());
160
+ // kernel.registerCommand('insertAfterImage', new ImageInsertAfterCommand());
161
+ const htmlService = kernel.getService({ value: 'IHTMLKernelService' });
162
+
163
+ if (htmlService) {
164
+ htmlService.registerHTMLNodeReader(
165
+ ['blockquote'],
166
+ {
167
+ readNode(context: any, node: any) {
168
+ context.setNode({
169
+ id: node.attrs.id || '',
170
+ type: 'element',
171
+ name: 'quote',
172
+ attrs: {},
173
+ });
174
+ },
175
+ leaveNode() {
176
+ // ignore empty
177
+ },
178
+ },
179
+ );
180
+ htmlService.registerHTMLNodeReader(
181
+ ['code'],
182
+ {
183
+ readNode(context: any, node: any) {
184
+ context.setNode({
185
+ id: node.attrs.id || '',
186
+ type: 'element',
187
+ name: 'code',
188
+ attrs: {},
189
+ });
190
+ },
191
+ leaveNode() {
192
+ // ignore empty
193
+ },
194
+ },
195
+ );
196
+ }
197
+ }
198
+ }
199
+
200
+ // 优先加载
201
+ // OpenEditorFactory.editorPlugins.unshift(CustomEditorPlugin);
202
+ // OpenEditorFactory.editorPlugins[CustomEditorPlugin.PluginName] = CustomEditorPlugin;
203
+ OpenEditorFactory.registerKernelPlugin([CustomKernelPlugin]);
204
+ // OpenEditorFactory.registerRenderPlugin([ Plugins.AutoScroll.AutoScrollRenderPlugin ]);
205
+ }
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div>
3
+
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang='ts'>
8
+ import { ref } from 'vue'
9
+
10
+ </script>
11
+
12
+ <style>
13
+
14
+ </style>
@@ -0,0 +1,286 @@
1
+ import { ref, watch, h, onMounted, onUnmounted, defineComponent } from "vue";
2
+ import { templateHtml } from "./template";
3
+ import loadLakeEditor from "./load";
4
+ import { InjectEditorPlugin } from "./editor-plugin";
5
+ import { slash } from './slash-options';
6
+
7
+ const blockquoteID = 'yqextensionblockquoteid';
8
+ export interface EditorProps {
9
+ value: string;
10
+ children?: any;
11
+ isview?: boolean;
12
+ uploadImage?: (params: { data: string | File }) => Promise<{
13
+ url: string;
14
+ size: number;
15
+ filename: string;
16
+ }>;
17
+ uploadVideo?: (params: { data: string | File }) => Promise<{
18
+ url: string;
19
+ size: number;
20
+ filename: string;
21
+ }>;
22
+ }
23
+ export interface EditorEmits{
24
+ onChange?: (value: string) => void;
25
+ onLoad?: () => void;
26
+ onSave?: () => void;
27
+ }
28
+
29
+ function sleep(ms: number) {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+
33
+ export interface IEditorRef {
34
+ /**
35
+ * 追加html到文档
36
+ * @param html html内容
37
+ * @param breakLine 是否前置一个换行符
38
+ */
39
+ appendContent: (html: string, breakLine?: boolean) => void;
40
+ /**
41
+ * 设置文档内容,将清空旧的内容
42
+ * @param html html内容
43
+ */
44
+ setContent: (content: string, type?: "text/lake" | "text/html") => void;
45
+ /**
46
+ * 获取文档内容
47
+ * @param type 内容的格式
48
+ * @return 文档内容
49
+ */
50
+ getContent: (type: "lake" | "text/html") => Promise<string>;
51
+ /**
52
+ * 判断当前文档是否是空文档
53
+ * @return true表示当前是空文档
54
+ */
55
+ isEmpty: () => boolean;
56
+
57
+ /**
58
+ * 获取额外信息
59
+ * @return
60
+ */
61
+ getSummaryContent: () => string;
62
+
63
+ /**
64
+ * 统计字数
65
+ * @return
66
+ */
67
+ wordCount: () => number;
68
+
69
+ /**
70
+ * 聚焦到文档开头
71
+ * @param {number} offset 偏移多少个段落,可以将选区落到开头的第offset个段落上, 默认是0
72
+ * @return
73
+ */
74
+ focusToStart: (offset?: number) => void;
75
+
76
+ /**
77
+ * 插入换行符
78
+ * @return
79
+ */
80
+ insertBreakLine: () => void;
81
+ }
82
+
83
+ export default defineComponent({
84
+ props: {
85
+ value:{
86
+ type: String,
87
+ default: "",
88
+ },
89
+ isview: {
90
+ type: Boolean,
91
+ default: false,
92
+ required: false,
93
+ }
94
+ },
95
+ emits: ['onChange', 'onLoad', 'onSave', 'uploadImage'],
96
+ setup(props: EditorProps, { emit, expose }) {
97
+ const isBrowser = typeof window !== "undefined";
98
+ const iframeRef = ref<HTMLIFrameElement>();
99
+ const editor = ref<any>();
100
+ const unLoad = ref();
101
+ const loadLake = () => {
102
+ function loadFunc() {
103
+ const doc = iframeRef.value?.contentDocument;
104
+ const win = iframeRef.value?.contentWindow;
105
+ if (!doc || !win) {
106
+ return;
107
+ }
108
+ const { createOpenEditor, createOpenViewer } = win.Doc;
109
+ // 注入插件
110
+ InjectEditorPlugin(win.Doc, doc);
111
+ // 加载编辑器
112
+ loadLakeEditor(win).then(() => {
113
+ // 创建编辑器
114
+ let editInstance = props.isview ? createOpenViewer : createOpenEditor;
115
+ const newEditor = editInstance(
116
+ doc.getElementById("root"),
117
+ {
118
+ scrollNode: () => {
119
+ return doc.querySelector(".ne-editor-wrap");
120
+ },
121
+ image: {
122
+ uploadFileURL: "/api/upload/image",
123
+ crawlURL: "/api/upload/image",
124
+ createUploadPromise: props.uploadImage
125
+ },
126
+ video: {
127
+ uploadFileURL: "/api/upload/video",
128
+ createUploadPromise: props.uploadVideo
129
+ },
130
+ placeholder: "输入内容...",
131
+ defaultFontsize: 14,
132
+ }
133
+ );
134
+ newEditor.on("visitLink", (url: string) => {
135
+ window.open(url, "__blank");
136
+ });
137
+ // 监听内容变动
138
+ newEditor.on("contentchange", () => {
139
+ emit("onChange", newEditor.getDocument("lake"));
140
+ });
141
+ // @ts-expect-error 注入调试变量
142
+ win.editor = newEditor;
143
+ // 设置编辑器到状态
144
+ editor.value = newEditor;
145
+ });
146
+ }
147
+ iframeRef.value?.addEventListener("load", loadFunc);
148
+ return () => {
149
+ iframeRef.value?.removeEventListener("load", loadFunc);
150
+ };
151
+ };
152
+ const onKeyDown = (e: KeyboardEvent) => {
153
+ if (e.key === "Enter" && (isBrowser ? e.ctrlKey : e.metaKey)) {
154
+ emit("onSave");
155
+ }
156
+ };
157
+ watch([() => props.value, () => editor.value], ([value, edit], [oldValue, oldEdit]) => {
158
+ if (!editor.value) return;
159
+
160
+ editor.value?.setDocument("lake", props.value);
161
+ editor.value?.execCommand("paragraphSpacing", "relax");
162
+ emit("onLoad");
163
+ });
164
+ watch([() => editor.value, () => iframeRef.value], (input) => {
165
+ if (!editor.value || !iframeRef.value) return;
166
+ iframeRef.value?.contentDocument?.addEventListener(
167
+ "keydown",
168
+ onKeyDown,
169
+ true
170
+ );
171
+ });
172
+ expose<IEditorRef>({
173
+ appendContent: (html: string, breakLine = false) => {
174
+ if (!editor.value) return;
175
+ if (breakLine) {
176
+ editor.value.execCommand('breakLine');
177
+ }
178
+ editor.value.kernel.execCommand('insertHTML', html);
179
+ iframeRef.value?.focus();
180
+ editor.value.execCommand('focus');
181
+ editor.value.renderer.scrollToCurrentSelection();
182
+ },
183
+ setContent: (
184
+ content: string,
185
+ type: 'text/lake' | 'text/html' = 'text/html',
186
+ ) => {
187
+ if (!editor.value) return;
188
+ iframeRef.value?.focus();
189
+ editor.value.setDocument(type, content);
190
+ editor.value.execCommand('focus', 'end');
191
+ // 寻找定位的block 插入到block上方
192
+ const node = editor.value.kernel.model.document.getNodeById(blockquoteID);
193
+ if (node) {
194
+ const rootNode = editor.value.kernel.model.document.rootNode;
195
+ if (rootNode.firstNode === node) {
196
+ return;
197
+ }
198
+ editor.value.kernel.execCommand('selection', {
199
+ ranges: [
200
+ {
201
+ start: {
202
+ node: rootNode.children[node.offset - 1],
203
+ offset: rootNode.children[node.offset - 1].childCount,
204
+ },
205
+ },
206
+ ],
207
+ });
208
+ editor.value.execCommand('focus');
209
+ }
210
+ },
211
+ isEmpty: () => {
212
+ if (!editor) return true;
213
+ return editor.value.queryCommandValue('isEmpty');
214
+ },
215
+ getContent: (type: 'lake' | 'text/html' | 'description') => {
216
+ if (!editor.value) return '';
217
+ // let times = 0;
218
+ // while (!editor.value.canGetDocument()) {
219
+ // // 10s 后返回超时
220
+ // if (times > 100) {
221
+ // throw new Error('文档上传未结束! 请删除未上传成功的图片');
222
+ // }
223
+ // times++;
224
+ // await sleep(100);
225
+ // }
226
+ if (type === 'lake') {
227
+ return editor.value.getDocument('text/lake', { includeMeta: true });
228
+ } else if (type === 'text/html') {
229
+ return editor.value.getDocument('text/html');
230
+ }
231
+ return editor.value.getDocument('description');
232
+ },
233
+ getSummaryContent: () => {
234
+ if (!editor) return '';
235
+ return editor.value.queryCommandValue('getSummary', 'lake');
236
+ },
237
+ wordCount: () => {
238
+ if (!editor) return 0;
239
+ return editor.value.queryCommandValue('wordCount');
240
+ },
241
+ focusToStart: (offset = 0) => {
242
+ if (!editor) return;
243
+ iframeRef.value?.focus();
244
+ if (offset) {
245
+ editor.value.kernel.execCommand('selection', {
246
+ ranges: [
247
+ {
248
+ start: {
249
+ node: editor.value.kernel.model.document.rootNode.children[offset],
250
+ offset: 0,
251
+ },
252
+ },
253
+ ],
254
+ });
255
+ editor.value.execCommand('focus');
256
+ } else {
257
+ editor.value.execCommand('focus', 'start');
258
+ }
259
+ },
260
+ insertBreakLine: () => {
261
+ if (!editor) return;
262
+ editor.value.execCommand('breakLine');
263
+ },
264
+ })
265
+ onMounted(() => {
266
+ unLoad.value = loadLake();
267
+ });
268
+ onUnmounted(() => {
269
+ unLoad.value?.();
270
+ iframeRef.value?.contentDocument?.removeEventListener(
271
+ "keydown",
272
+ onKeyDown,
273
+ true
274
+ );
275
+ });
276
+ return ()=> h("iframe", {
277
+ ref: iframeRef,
278
+ class: "lake-editor",
279
+ height: "100%",
280
+ width: "100%",
281
+ srcDoc: templateHtml,
282
+ allow: "*",
283
+ style: "background: transparent; border: none;",
284
+ });
285
+ },
286
+ });
@@ -0,0 +1,32 @@
1
+ declare global {
2
+ interface Window {
3
+ Doc: any;
4
+ }
5
+ }
6
+
7
+ // 最大等待时间10s
8
+ const LOAD_TIME_OUT = 10_000;
9
+
10
+ function isLakeEditorLoaded(win: Window & {Doc: any}) {
11
+ return !!win.Doc;
12
+ }
13
+
14
+ export default function loadLakeEditor(win: Window & {Doc: any}) {
15
+ const start = Date.now();
16
+ return new Promise((resolve, reject) => {
17
+ if (isLakeEditorLoaded(win)) {
18
+ resolve(win.Doc);
19
+ return;
20
+ }
21
+ const load = () => {
22
+ if (isLakeEditorLoaded(win)) {
23
+ resolve(win.Doc);
24
+ } else if (Date.now() - start > LOAD_TIME_OUT) {
25
+ reject(new Error('load lake editor timeout'));
26
+ } else {
27
+ setTimeout(load, 100);
28
+ }
29
+ };
30
+ setTimeout(load, 100);
31
+ });
32
+ }
@@ -0,0 +1,96 @@
1
+ export const slash = {
2
+ cardSelect: {
3
+ general: {
4
+ groups: [
5
+ {
6
+ type: 'icon',
7
+ show: 'slash', // 只在斜杠面板中出现
8
+ items: [
9
+ 'p',
10
+ 'h1',
11
+ 'h2',
12
+ 'h3',
13
+ 'h4',
14
+ 'h5',
15
+ 'h6',
16
+ 'unorderedList',
17
+ 'orderedList',
18
+ 'taskList',
19
+ 'link',
20
+ 'code',
21
+ 'image'
22
+ ],
23
+ },
24
+ {
25
+ get title() {
26
+ return '基础';
27
+ },
28
+ name: 'group-base',
29
+ type: 'column',
30
+ items: [
31
+ 'label',
32
+ {
33
+ name: 'table',
34
+ allowSelector: true,
35
+ },
36
+ ],
37
+ },
38
+ {
39
+ get title() {
40
+ return '布局和样式';
41
+ },
42
+ name: 'group-layout',
43
+ type: 'normal',
44
+ items: [
45
+ 'quote',
46
+ 'hr',
47
+ 'alert',
48
+ {
49
+ name: 'columns',
50
+ childMenus: ['columns2', 'columns3', 'columns4'],
51
+ },
52
+ 'collapse',
53
+ ],
54
+ },
55
+ {
56
+ get title() {
57
+ return '程序员';
58
+ },
59
+ name: 'group-files',
60
+ type: 'normal',
61
+ items: ['codeblock', 'math'],
62
+ },
63
+ ],
64
+ },
65
+ table: {
66
+ groups: [
67
+ {
68
+ type: 'icon',
69
+ show: 'slash', // 只在斜杠面板中出现
70
+ items: [
71
+ 'p',
72
+ 'h1',
73
+ 'h2',
74
+ 'h3',
75
+ 'h4',
76
+ 'h5',
77
+ 'h6',
78
+ 'unorderedList',
79
+ 'orderedList',
80
+ 'taskList',
81
+ 'link',
82
+ 'code',
83
+ ],
84
+ },
85
+ {
86
+ get title() {
87
+ return '基础';
88
+ },
89
+ name: 'group-base',
90
+ type: 'normal',
91
+ items: ['label', 'math'],
92
+ },
93
+ ],
94
+ },
95
+ },
96
+ };
@@ -0,0 +1,92 @@
1
+
2
+ /**
3
+ * iframe的内容
4
+ */
5
+ export const templateHtml = `
6
+ <!DOCTYPE html>
7
+ <html>
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <title></title>
11
+ <link rel="stylesheet" type="text/css" href="https://gw.alipayobjects.com/render/p/yuyan_npm/@alipay_lakex-doc/1.71.0/umd/doc.css"/>
12
+ <link rel="stylesheet" type="text/css" href="https://gw.alipayobjects.com/os/lib/antd/4.24.13/dist/antd.css"/>
13
+ <style>
14
+ body {
15
+ display: flex;
16
+ flex-direction: column;
17
+ overflow: hidden;
18
+ -webkit-font-smoothing: antialiased;
19
+ }
20
+ .toolbar-container {
21
+ display: none;
22
+ }
23
+ #toolbar {
24
+ flex: 1;
25
+ }
26
+ #root {
27
+ flex: 1;
28
+ overflow: hidden;
29
+ }
30
+ #child {
31
+ display: flex;
32
+ align-items: center;
33
+ padding: 0 16px;
34
+ }
35
+ .ne-layout-mode-fixed .ne-engine, .ne-layout-mode-adapt .ne-engine {
36
+ padding-top: 16px;
37
+ }
38
+ .ne-layout-mode-fixed .ne-editor-body, .ne-layout-mode-adapt .ne-editor-body {
39
+ height: 100%;
40
+ }
41
+ .ne-ui-overlay-button {
42
+ width: 28px !important;
43
+ height: 28px !important;
44
+ padding: 0 !important;;
45
+ border: none !important;;
46
+ }
47
+ ::selection {
48
+ color: #fff !important;
49
+ background: #1677ff !important;
50
+ }
51
+ .continue-button:hover, .continue-button:focus {
52
+ color: #00B96B;
53
+ border-color: #00B96B;
54
+ }
55
+ .ne-layout-mode-fixed .ne-editor-wrap {
56
+ padding: 16px 16px 0;
57
+ height: 100%;
58
+ }
59
+ .ne-layout-mode-fixed .ne-engine, .ne-layout-mode-adapt .ne-engine {
60
+ padding: 16px 24px 0;
61
+ min-height: calc(100vh - 10px)
62
+ }
63
+ .ne-layout-mode-fixed .ne-editor-wrap-content {
64
+ min-width: 317px;
65
+ }
66
+ .ne-layout-mode-fixed .ne-editor-outer-wrap-box {
67
+ min-width: 317px;
68
+ }
69
+ .ne-layout-mode-fixed .ne-editor-outer-wrap-box, .ne-layout-mode-adapt .ne-editor-outer-wrap-box,
70
+ .ne-layout-mode-fixed .ne-editor-wrap-content, .ne-layout-mode-adapt .ne-editor-wrap-content {
71
+ min-width: 317px;
72
+ }
73
+ .ne-editor-wrap {
74
+ overscroll-behavior: contain;
75
+ }
76
+ </style>
77
+ </head>
78
+ <body>
79
+ <div class="toolbar-container">
80
+ <div id="toolbar"></div>
81
+ <div id="child"></div>
82
+ </div>
83
+ <div id="root"></div>
84
+ <script crossorigin src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
85
+ <script crossorigin src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
86
+ <script src="https://gw.alipayobjects.com/render/p/yuyan_v/180020010000005484/7.1.4/CodeMirror.js"></script>
87
+ <script src="https://ur.alipay.com/tracert_a385.js"></script>
88
+ <script src="https://mdn.alipayobjects.com/design_kitchencore/afts/file/ANSZQ7GHQPMAAAAAAAAAAAAADhulAQBr"></script>
89
+ <script src="https://gw.alipayobjects.com/render/p/yuyan_npm/@alipay_lakex-doc/1.71.0/umd/doc.umd.js"></script>
90
+ </body>
91
+ </html>
92
+ `;
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import YuqueRichText from '@/components/lake-rich/lake-rich'
2
+
3
+ export {
4
+ YuqueRichText
5
+ }
6
+
package/tsconfig.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "module": "esnext",
5
+ "strict": true,
6
+ "jsx": "preserve",
7
+ "importHelpers": true,
8
+ "moduleResolution": "node",
9
+ "experimentalDecorators": true,
10
+ "allowJs": true,
11
+ "esModuleInterop": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "sourceMap": true,
14
+ "baseUrl": ".",
15
+ "typeRoots": ["./types", "./node_modules/@types"],
16
+ "types": ["element-plus/global"],
17
+ "paths": {
18
+ "@/*": ["./src/*"],
19
+ "demo/*": ["./demo/*"],
20
+ "yuque-rich-text": ["./src/index.ts"]
21
+ },
22
+ "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
23
+ "isolatedModules": true
24
+ },
25
+ "include": [
26
+ "types/**/*",
27
+ "src/**/*.ts",
28
+ "src/**/*.tsx",
29
+ "src/**/*.vue",
30
+ "demo/**/*.vue",
31
+ "demo/**/*.ts",
32
+ "demo/**/*.tsx",
33
+ "tests/**/*.ts",
34
+ "tests/**/*.tsx"
35
+ ],
36
+ "exclude": ["node_modules"]
37
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,73 @@
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import vue from '@vitejs/plugin-vue'
4
+ import dts from 'vite-plugin-dts';
5
+
6
+ const libDir = path.resolve(__dirname, 'lib');
7
+ const srcDir = path.resolve(__dirname, 'src');
8
+
9
+ // https://vite.dev/config/
10
+ export default ({mode})=>{
11
+ process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
12
+
13
+ const IS_DEMO = process.env.VITE_BUILD_TARGET === 'demo';
14
+
15
+ return defineConfig({
16
+ plugins: [vue(),
17
+ IS_DEMO
18
+ ? null
19
+ : dts({
20
+ include: ['src'],
21
+ insertTypesEntry: true,
22
+ }),
23
+ ],
24
+ resolve: {
25
+ alias: [
26
+ {
27
+ find: '@',
28
+ replacement: path.resolve(__dirname, 'src'),
29
+ },
30
+ {
31
+ find: 'demo',
32
+ replacement: path.resolve(__dirname, 'demo'),
33
+ },
34
+ {
35
+ find: 'yuque-rich-text',
36
+ replacement: path.resolve(__dirname, 'src/index.ts'),
37
+ },
38
+ ],
39
+ },
40
+ build: IS_DEMO
41
+ ? undefined
42
+ : {
43
+ outDir: libDir,
44
+ minify: 'esbuild',
45
+ lib: {
46
+ entry: path.resolve(srcDir, 'index.ts'),
47
+ name: 'YuqueRichText',
48
+ fileName: 'yuque-rich-text',
49
+ },
50
+ // https://rollupjs.org/guide/en/#big-list-of-options
51
+ rollupOptions: {
52
+ // 确保外部化处理那些你不想打包进库的依赖
53
+ external: [
54
+ 'vue',
55
+ ],
56
+ output: {
57
+ exports: 'named',
58
+ // https://github.com/henriquehbr/svelte-typewriter/issues/21#issuecomment-968835822
59
+ inlineDynamicImports: true,
60
+ // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
61
+ globals: {
62
+ vue: 'vue',
63
+
64
+ },
65
+ },
66
+ plugins: [
67
+
68
+ ],
69
+ },
70
+ },
71
+ // publicDir: IS_DEMO ? 'public' : false,
72
+ })
73
+ }