tiptap-vue-pro-core 0.1.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.
- package/LICENSE +28 -0
- package/README.md +34 -0
- package/dist/extensions/image.d.ts +29 -0
- package/dist/extensions/image.d.ts.map +1 -0
- package/dist/extensions/imageNodeView.d.ts +10 -0
- package/dist/extensions/imageNodeView.d.ts.map +1 -0
- package/dist/extensions.d.ts +30 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/handleImageUpload.d.ts +40 -0
- package/dist/handleImageUpload.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +647 -0
- package/dist/index.umd.cjs +1 -0
- package/dist/markdown.d.ts +29 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/types.d.ts +245 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/useProEditor.d.ts +20 -0
- package/dist/useProEditor.d.ts.map +1 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tiptap-vue-pro contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
本项目基于以下开源项目构建,它们各自遵循其原始许可协议:
|
|
26
|
+
- Tiptap (https://tiptap.dev) — MIT License
|
|
27
|
+
- ProseMirror (https://prosemirror.net) — MIT License
|
|
28
|
+
- Element Plus (https://element-plus.org) — MIT License
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# tiptap-vue-pro-core
|
|
2
|
+
|
|
3
|
+
UI 无关的 Tiptap v3 + Vue 3 富文本编辑器核心层。
|
|
4
|
+
|
|
5
|
+
这个包只提供 `useProEditor()`、默认扩展、命令聚合、图片上传辅助和类型契约,不包含现成 UI。想开箱即用请安装对应 adapter:
|
|
6
|
+
|
|
7
|
+
- Element Plus: `tiptap-vue-pro-element-plus`
|
|
8
|
+
- Naive UI: `tiptap-vue-pro-naive`
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add tiptap-vue-pro-core @tiptap/core @tiptap/pm @tiptap/vue-3
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 用法
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { EditorContent } from '@tiptap/vue-3'
|
|
21
|
+
import { useProEditor } from 'tiptap-vue-pro-core'
|
|
22
|
+
|
|
23
|
+
const ctx = useProEditor({
|
|
24
|
+
content: '<p>hello core</p>',
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<button @click="ctx.commands.bold()">Bold</button>
|
|
30
|
+
<EditorContent :editor="ctx.editor.value" />
|
|
31
|
+
</template>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
完整文档和 Demo: https://github.com/twoer/tiptap-vue-pro
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Extensions } from '@tiptap/core';
|
|
2
|
+
/**
|
|
3
|
+
* 自定义 Image 扩展(基于官方 @tiptap/extension-image)。
|
|
4
|
+
*
|
|
5
|
+
* 在官方基础上增强两点(对标飞书图片):
|
|
6
|
+
* 1. 开启 v3 原生 resize —— 通过 configure.resize 配置 ResizableNodeView,
|
|
7
|
+
* 支持四角拖拽缩放(alwaysPreserveAspectRatio 锁比例,与飞书一致)。
|
|
8
|
+
* 2. 预留 align / caption 两个 schema 属性 —— 对齐与题注的「数据层」。
|
|
9
|
+
* 属性定义本身不依赖任何 UI;具体渲染(NodeView 的对齐样式、题注输入框)
|
|
10
|
+
* 在后续能力里逐步叠加。
|
|
11
|
+
*
|
|
12
|
+
* 架构约束:本文件在 core/,严禁引入 UI 库或 .vue。NodeView 一律用原生 DOM
|
|
13
|
+
* (Tiptap 扩展本身的 addNodeView 用 document.createElement 是框架无关的正常能力,
|
|
14
|
+
* 不算「依赖具体 UI 库」)。
|
|
15
|
+
*/
|
|
16
|
+
/** 图片对齐方式 */
|
|
17
|
+
export type ImageAlign = 'left' | 'center' | 'right';
|
|
18
|
+
/** 图片尺寸预设(相对编辑器内容区宽度的比例;'original' 清除宽度回归自然尺寸) */
|
|
19
|
+
export type ImageSizePreset = 'small' | 'medium' | 'large' | 'original';
|
|
20
|
+
/**
|
|
21
|
+
* 扩展后的 Image。
|
|
22
|
+
*
|
|
23
|
+
* 链式顺序:extend(改 schema/命令) → configure(设 options)。
|
|
24
|
+
* extend 必须在 configure 之前,否则 configure 的 options 不会被 extend 后的
|
|
25
|
+
* 扩展读到。
|
|
26
|
+
*/
|
|
27
|
+
export declare const ImageExtended: import('@tiptap/core').Node<import('@tiptap/extension-image').ImageOptions, any>;
|
|
28
|
+
export type { Extensions };
|
|
29
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/extensions/image.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C;;;;;;;;;;;;;GAaG;AAEH,aAAa;AACb,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEpD,kDAAkD;AAClD,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAA;AAEvE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,kFAsDxB,CAAA;AAGF,YAAY,EAAE,UAAU,EAAE,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NodeViewProps } from '@tiptap/core';
|
|
2
|
+
import { NodeView as PMNodeView } from '@tiptap/pm/view';
|
|
3
|
+
/**
|
|
4
|
+
* 创建图片 NodeView。
|
|
5
|
+
*
|
|
6
|
+
* @param props Tiptap 注入的 NodeViewProps(node/getPos/HTMLAttributes/editor)
|
|
7
|
+
* @param extOptions 扩展自身的 options(HTMLAttributes),用于合并到 img 上
|
|
8
|
+
*/
|
|
9
|
+
export declare function createImageNodeView(props: NodeViewProps, extHTMLAttributes: Record<string, unknown>): PMNodeView;
|
|
10
|
+
//# sourceMappingURL=imageNodeView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageNodeView.d.ts","sourceRoot":"","sources":["../../src/extensions/imageNodeView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmD,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAGlG;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE7D;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,aAAa,EACpB,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACzC,UAAU,CAkKZ"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Extensions } from '@tiptap/core';
|
|
2
|
+
export type { Extensions } from '@tiptap/core';
|
|
3
|
+
/**
|
|
4
|
+
* 默认扩展包。
|
|
5
|
+
*
|
|
6
|
+
* Tiptap v3 的 StarterKit 已经非常完整,自带:
|
|
7
|
+
* Document, Paragraph, Text, History(undo/redo),
|
|
8
|
+
* Bold, Italic, Strike, Underline, Code,
|
|
9
|
+
* Heading(1-6), Blockquote, CodeBlock, HorizontalRule, HardBreak,
|
|
10
|
+
* BulletList, OrderedList, ListItem(来自 @tiptap/extension-list),
|
|
11
|
+
* Link, Dropcursor, TrailingNode。
|
|
12
|
+
*
|
|
13
|
+
* 这里额外补 StarterKit 不含的能力:
|
|
14
|
+
* 1. Placeholder —— 空内容占位
|
|
15
|
+
* 2. CharacterCount —— 字数统计
|
|
16
|
+
* 3. Image —— 图片插入(配合上传)
|
|
17
|
+
* 4. TableKit —— 表格(Table/Row/Cell/Header 一站式,v3 合并包)
|
|
18
|
+
* 5. TextStyle + Color —— 文字颜色(Color 依赖 TextStyle 提供的 mark)
|
|
19
|
+
* 6. Highlight —— 文字背景高亮(multicolor 支持多色)
|
|
20
|
+
* 7. TaskList + TaskItem —— 任务列表(checkbox),来自 @tiptap/extension-list
|
|
21
|
+
* (v3 中 TaskList/TaskItem 都在 extension-list,不是独立的 task-list 包)
|
|
22
|
+
* 8. Markdown —— 官方 @tiptap/markdown,提供导入/导出 MD 能力。
|
|
23
|
+
* 无对应 MD 语法的样式(颜色/高亮/对齐)在导出时会被丢弃——这是
|
|
24
|
+
* Markdown 格式本身的局限,非本组件能力缺失。
|
|
25
|
+
*
|
|
26
|
+
* StarterKit 的 Link 默认新窗口打开、autolink,满足大多数 CMS 场景。
|
|
27
|
+
* 返回类型用 v3 的 Extensions(同时接受 Extension 和 Node),因为 Image 是 Node。
|
|
28
|
+
*/
|
|
29
|
+
export declare function createDefaultExtensions(placeholder?: string): Extensions;
|
|
30
|
+
//# sourceMappingURL=extensions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CAoCxE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import { UploadImage } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* 判断是否为图片文件(基于 MIME 类型,不依赖文件名扩展)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function isImageFile(file: File): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* 同步判断文件列表中是否含图片文件。
|
|
9
|
+
*
|
|
10
|
+
* 用途:paste/drop 事件处理器需要在**同步阶段**决定是否 preventDefault,
|
|
11
|
+
* 否则浏览器会在异步上传完成前执行默认行为(如把图片当链接打开)。
|
|
12
|
+
* handleImageFiles 是 async 不能用于这个判断,故抽出此同步谓词。
|
|
13
|
+
*/
|
|
14
|
+
export declare function hasImageFiles(files: File[] | FileList | null | undefined): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 单张图片上传失败的回调契约。
|
|
17
|
+
*
|
|
18
|
+
* 粘贴/拖拽批量上传时,handleImageFiles 默认对单张失败静默跳过(不中断其余)。
|
|
19
|
+
* adapter 若希望给用户提示,传入此回调;Core 决定「哪张失败了」,adapter 决定
|
|
20
|
+
* 「用什么 UI 显示」(与 NotifyFn 的职责划分一致)。
|
|
21
|
+
*/
|
|
22
|
+
export type ImageUploadErrorFn = (file: File, error: unknown) => void;
|
|
23
|
+
/**
|
|
24
|
+
* 批量处理粘贴/拖拽进来的图片:
|
|
25
|
+
* - 过滤出图片文件
|
|
26
|
+
* - 逐个调用 uploadImage 拿到 url
|
|
27
|
+
* - 调用 commands 插入文档
|
|
28
|
+
*
|
|
29
|
+
* 返回 true 表示"已消费这些文件"(阻止浏览器默认行为,如打开图片)。
|
|
30
|
+
*
|
|
31
|
+
* 设计说明:
|
|
32
|
+
* - 这里只做"图片上传",非图片文件(如 pdf/doc)交还给浏览器默认行为,
|
|
33
|
+
* 符合 MVP 范围(文件附件是第二阶段)。
|
|
34
|
+
* - 上传失败的单张图片跳过,不阻塞其余图片。
|
|
35
|
+
* onError 可选:传入则在单张失败时回调(供 adapter 提示用户),不传则静默。
|
|
36
|
+
* 之前的版本是纯静默(空 catch),粘贴/拖拽失败时用户无感知——这是已知短板,
|
|
37
|
+
* 现通过 onError 暴露给调用方,保持向后兼容(第 4 参可选)。
|
|
38
|
+
*/
|
|
39
|
+
export declare function handleImageFiles(files: File[] | FileList | null | undefined, upload: UploadImage | undefined, editor: Editor, onError?: ImageUploadErrorFn): Promise<boolean>;
|
|
40
|
+
//# sourceMappingURL=handleImageUpload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handleImageUpload.d.ts","sourceRoot":"","sources":["../src/handleImageUpload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,IAAI,EAAE,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAGT;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;AAErE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,IAAI,EAAE,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,EAC3C,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAuBlB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { useProEditor } from './useProEditor';
|
|
2
|
+
export { createDefaultExtensions } from './extensions';
|
|
3
|
+
export { ImageExtended } from './extensions/image';
|
|
4
|
+
export type { ImageAlign, ImageSizePreset } from './extensions/image';
|
|
5
|
+
export { handleImageFiles, hasImageFiles, isImageFile, } from './handleImageUpload';
|
|
6
|
+
export type { ImageUploadErrorFn } from './handleImageUpload';
|
|
7
|
+
export { MarkdownExtension, getMarkdown, importMarkdown, } from './markdown';
|
|
8
|
+
export type { ProEditorOptions, ProEditorContext, ProEditorCommands, OutputFormat, UploadImage, NotifyType, NotifyFn, } from './types';
|
|
9
|
+
export type { Editor } from '@tiptap/vue-3';
|
|
10
|
+
export type { Extension, Extensions } from '@tiptap/core';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,WAAW,GACZ,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAA;AAEnB,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,QAAQ,GACT,MAAM,SAAS,CAAA;AAIhB,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { computed as j, ref as K, watch as F, onBeforeUnmount as it } from "vue";
|
|
2
|
+
import { useEditor as st } from "@tiptap/vue-3";
|
|
3
|
+
import { mergeAttributes as at, ResizableNodeView as ct, isNodeSelection as lt } from "@tiptap/core";
|
|
4
|
+
import { TableMap as ut, CellSelection as _, moveTableColumn as dt, moveTableRow as ft } from "@tiptap/pm/tables";
|
|
5
|
+
import gt from "@tiptap/starter-kit";
|
|
6
|
+
import mt from "@tiptap/extension-character-count";
|
|
7
|
+
import pt from "@tiptap/extension-placeholder";
|
|
8
|
+
import { TableKit as ht } from "@tiptap/extension-table";
|
|
9
|
+
import wt from "@tiptap/extension-image";
|
|
10
|
+
import { TextStyle as vt } from "@tiptap/extension-text-style";
|
|
11
|
+
import { Color as bt } from "@tiptap/extension-color";
|
|
12
|
+
import { Highlight as yt } from "@tiptap/extension-highlight";
|
|
13
|
+
import { TextAlign as Ct } from "@tiptap/extension-text-align";
|
|
14
|
+
import { TaskList as At, TaskItem as kt } from "@tiptap/extension-list";
|
|
15
|
+
import { Markdown as St } from "@tiptap/markdown";
|
|
16
|
+
import { Markdown as Qt } from "@tiptap/markdown";
|
|
17
|
+
function Mt(i, l) {
|
|
18
|
+
const { node: c, getPos: m, HTMLAttributes: I, editor: p } = i, h = document.createElement("div");
|
|
19
|
+
h.className = "tvp-img-node";
|
|
20
|
+
const x = c.attrs.align || "center";
|
|
21
|
+
x !== "center" && h.setAttribute("data-align", x);
|
|
22
|
+
const g = document.createElement("img");
|
|
23
|
+
g.draggable = !1;
|
|
24
|
+
const P = at(l, I);
|
|
25
|
+
for (const [s, d] of Object.entries(P))
|
|
26
|
+
d != null && (s === "width" || s === "height" || s === "align" || s === "caption" || g.setAttribute(s, String(d)));
|
|
27
|
+
P.src != null && (g.src = String(P.src));
|
|
28
|
+
const a = new ct({
|
|
29
|
+
element: g,
|
|
30
|
+
editor: p,
|
|
31
|
+
node: c,
|
|
32
|
+
getPos: m,
|
|
33
|
+
onResize: (s, d) => {
|
|
34
|
+
g.style.width = `${s}px`, g.style.height = `${d}px`;
|
|
35
|
+
},
|
|
36
|
+
onCommit: (s, d) => {
|
|
37
|
+
const k = m();
|
|
38
|
+
k !== void 0 && p.chain().setNodeSelection(k).updateAttributes("image", { width: s, height: d }).run();
|
|
39
|
+
},
|
|
40
|
+
onUpdate: (s) => s.type === c.type,
|
|
41
|
+
options: {
|
|
42
|
+
directions: ["top-left", "top-right", "bottom-left", "bottom-right"],
|
|
43
|
+
min: { width: 60, height: 60 },
|
|
44
|
+
preserveAspectRatio: !0
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
a.dom.style.visibility = "hidden", a.dom.style.pointerEvents = "none";
|
|
48
|
+
const O = () => {
|
|
49
|
+
a.dom.style.visibility = "", a.dom.style.pointerEvents = "";
|
|
50
|
+
const s = p.view;
|
|
51
|
+
s.dispatch(s.state.tr.setMeta("proImageBubbleMenu", "updatePosition"));
|
|
52
|
+
};
|
|
53
|
+
g.addEventListener("load", O, { once: !0 }), g.addEventListener("error", O, { once: !0 }), h.appendChild(a.dom);
|
|
54
|
+
const f = document.createElement("input");
|
|
55
|
+
f.className = "tvp-img-caption", f.type = "text", f.placeholder = "添加题注";
|
|
56
|
+
const R = c.attrs.caption || "";
|
|
57
|
+
f.value = R, R || f.classList.add("tvp-img-caption-empty");
|
|
58
|
+
const H = () => {
|
|
59
|
+
f.readOnly = !p.isEditable;
|
|
60
|
+
};
|
|
61
|
+
H();
|
|
62
|
+
const $ = () => H();
|
|
63
|
+
p.on("update", $);
|
|
64
|
+
const o = () => {
|
|
65
|
+
if (!p.isEditable) return;
|
|
66
|
+
const s = m();
|
|
67
|
+
if (s === void 0) return;
|
|
68
|
+
const d = f.value;
|
|
69
|
+
f.classList.toggle("tvp-img-caption-empty", !d), p.chain().setNodeSelection(s).updateAttributes("image", { caption: d }).run();
|
|
70
|
+
};
|
|
71
|
+
f.addEventListener("input", o);
|
|
72
|
+
const L = (s) => {
|
|
73
|
+
s.stopPropagation();
|
|
74
|
+
};
|
|
75
|
+
return f.addEventListener("keydown", L), h.appendChild(f), {
|
|
76
|
+
dom: h,
|
|
77
|
+
contentDOM: void 0,
|
|
78
|
+
// atom 节点无嵌套可编辑内容
|
|
79
|
+
update(s) {
|
|
80
|
+
if (s.type !== c.type) return !1;
|
|
81
|
+
const d = s.attrs, k = d.align || "center";
|
|
82
|
+
k !== "center" ? h.setAttribute("data-align", k) : h.removeAttribute("data-align"), H();
|
|
83
|
+
const S = d.caption || "";
|
|
84
|
+
S !== f.value && (f.value = S, f.classList.toggle("tvp-img-caption-empty", !S));
|
|
85
|
+
const T = d.src;
|
|
86
|
+
return T && T !== g.getAttribute("src") && (g.src = T), d.width != null ? g.style.width = `${d.width}px` : g.style.width = "", d.height != null ? g.style.height = `${d.height}px` : g.style.height = "", a.update(s, [], void 0);
|
|
87
|
+
},
|
|
88
|
+
destroy() {
|
|
89
|
+
p.off("update", $), f.removeEventListener("input", o), f.removeEventListener("keydown", L), a.destroy();
|
|
90
|
+
},
|
|
91
|
+
// 题注 input / 手柄的 DOM 变化不应被 ProseMirror 视作文档变更
|
|
92
|
+
ignoreMutation: () => !0,
|
|
93
|
+
// 题注 input 内的事件不应被 ProseMirror 编辑视图接管
|
|
94
|
+
stopEvent(s) {
|
|
95
|
+
const d = s.target;
|
|
96
|
+
return !!d && (d === f || f.contains(d));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const Lt = wt.extend({
|
|
101
|
+
addAttributes() {
|
|
102
|
+
var i;
|
|
103
|
+
return {
|
|
104
|
+
...(i = this.parent) == null ? void 0 : i.call(this),
|
|
105
|
+
/**
|
|
106
|
+
* 对齐:data-align 属性。默认 center(与飞书默认居中一致)。
|
|
107
|
+
* 渲染层(CSS)根据该属性控制图片在内容区的水平位置。
|
|
108
|
+
*/
|
|
109
|
+
align: {
|
|
110
|
+
default: "center",
|
|
111
|
+
parseHTML: (l) => l.getAttribute("data-align") || "center",
|
|
112
|
+
renderHTML: (l) => {
|
|
113
|
+
const c = l.align;
|
|
114
|
+
return c && c !== "center" ? { "data-align": c } : {};
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* 题注:data-caption 属性。空串表示无题注。
|
|
119
|
+
* 渲染层(NodeView)把它显示为图片下方的可编辑输入框。
|
|
120
|
+
*/
|
|
121
|
+
caption: {
|
|
122
|
+
default: "",
|
|
123
|
+
parseHTML: (l) => l.getAttribute("data-caption") || "",
|
|
124
|
+
renderHTML: (l) => {
|
|
125
|
+
const c = l.caption;
|
|
126
|
+
return c ? { "data-caption": c } : {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
/**
|
|
132
|
+
* 自定义 NodeView:复用官方 ResizableNodeView(四角手柄) + 追加题注输入框。
|
|
133
|
+
* 完全原生 DOM 实现(core 无 UI 依赖),逻辑见 imageNodeView.ts。
|
|
134
|
+
* 仅在客户端(document 存在)且有 resize 配置时启用;否则回退官方默认。
|
|
135
|
+
*/
|
|
136
|
+
addNodeView() {
|
|
137
|
+
return ({ node: i, getPos: l, HTMLAttributes: c, editor: m }) => Mt(
|
|
138
|
+
{ node: i, getPos: l, HTMLAttributes: c, editor: m },
|
|
139
|
+
this.options.HTMLAttributes
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}).configure({
|
|
143
|
+
inline: !1,
|
|
144
|
+
allowBase64: !1,
|
|
145
|
+
// 强制走上传,避免 base64 撑大文档
|
|
146
|
+
resize: {
|
|
147
|
+
enabled: !0,
|
|
148
|
+
// v3 的方向键值用连字符格式(见 @tiptap/core 的 ResizableNodeViewDirection)
|
|
149
|
+
directions: ["top-left", "top-right", "bottom-left", "bottom-right"],
|
|
150
|
+
minWidth: 60,
|
|
151
|
+
minHeight: 60,
|
|
152
|
+
alwaysPreserveAspectRatio: !0
|
|
153
|
+
// 锁比例,飞书行为
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
function Tt(i) {
|
|
157
|
+
return [
|
|
158
|
+
gt.configure({
|
|
159
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
160
|
+
link: {
|
|
161
|
+
openOnClick: !1,
|
|
162
|
+
autolink: !0,
|
|
163
|
+
defaultProtocol: "https",
|
|
164
|
+
HTMLAttributes: {
|
|
165
|
+
target: "_blank",
|
|
166
|
+
rel: "noopener noreferrer nofollow"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
pt.configure({
|
|
171
|
+
placeholder: i ?? "请输入内容..."
|
|
172
|
+
}),
|
|
173
|
+
mt,
|
|
174
|
+
Lt,
|
|
175
|
+
// ImageExtended:在官方基础上开启 resize + 预留 align/caption 属性(对标飞书)
|
|
176
|
+
ht.configure({
|
|
177
|
+
table: { resizable: !0 }
|
|
178
|
+
}),
|
|
179
|
+
// 文字颜色:Color 依赖 TextStyle,两者成对引入
|
|
180
|
+
vt,
|
|
181
|
+
bt,
|
|
182
|
+
// 背景高亮:multicolor 允许多种颜色共存
|
|
183
|
+
yt.configure({ multicolor: !0 }),
|
|
184
|
+
// 文本对齐:作用于段落和标题
|
|
185
|
+
Ct.configure({ types: ["heading", "paragraph"] }),
|
|
186
|
+
// 任务列表:TaskList 需要 TaskItem 提供 checkbox 行为
|
|
187
|
+
At,
|
|
188
|
+
kt.configure({ nested: !0 }),
|
|
189
|
+
// Markdown 导入/导出:官方扩展,自动接管 setContent(contentType:'markdown')
|
|
190
|
+
// 并提供 editor.storage.markdown.manager 用于序列化
|
|
191
|
+
St
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
function q(i) {
|
|
195
|
+
var c;
|
|
196
|
+
return (c = i.storage.markdown) == null ? void 0 : c.manager;
|
|
197
|
+
}
|
|
198
|
+
function It(i) {
|
|
199
|
+
const l = q(i);
|
|
200
|
+
return l ? l.serialize(i.getJSON()) : "";
|
|
201
|
+
}
|
|
202
|
+
function Rt(i, l) {
|
|
203
|
+
const c = q(i);
|
|
204
|
+
if (c)
|
|
205
|
+
try {
|
|
206
|
+
const m = c.parse(l);
|
|
207
|
+
i.commands.setContent(m);
|
|
208
|
+
return;
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
i.commands.setContent(l);
|
|
212
|
+
}
|
|
213
|
+
function jt(i) {
|
|
214
|
+
const {
|
|
215
|
+
content: l,
|
|
216
|
+
output: c = "html",
|
|
217
|
+
extensions: m,
|
|
218
|
+
placeholder: I,
|
|
219
|
+
uploadImage: p,
|
|
220
|
+
editable: h = !0,
|
|
221
|
+
editorProps: x,
|
|
222
|
+
notify: g
|
|
223
|
+
} = i, P = m ?? Tt(I), a = st({
|
|
224
|
+
extensions: P,
|
|
225
|
+
content: typeof l == "string" ? l : JSON.stringify(l),
|
|
226
|
+
editable: h,
|
|
227
|
+
editorProps: x ?? {},
|
|
228
|
+
onUpdate: ({ editor: t }) => {
|
|
229
|
+
R = !0, $(t), H(t);
|
|
230
|
+
}
|
|
231
|
+
}), O = j(() => !!a.value), f = K({ characters: 0, words: 0 });
|
|
232
|
+
let R = !1;
|
|
233
|
+
function H(t) {
|
|
234
|
+
var r, u;
|
|
235
|
+
const n = t.storage.characterCount;
|
|
236
|
+
n && (f.value = {
|
|
237
|
+
characters: ((r = n.characters) == null ? void 0 : r.call(n)) ?? 0,
|
|
238
|
+
words: ((u = n.words) == null ? void 0 : u.call(n)) ?? 0
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
F(
|
|
242
|
+
() => a.value,
|
|
243
|
+
(t) => {
|
|
244
|
+
t && H(t);
|
|
245
|
+
},
|
|
246
|
+
{ immediate: !0 }
|
|
247
|
+
);
|
|
248
|
+
function $(t) {
|
|
249
|
+
const e = c === "html" ? t.getHTML() : t.getJSON();
|
|
250
|
+
i.content = e;
|
|
251
|
+
}
|
|
252
|
+
F(
|
|
253
|
+
() => i.content,
|
|
254
|
+
(t) => {
|
|
255
|
+
if (R) {
|
|
256
|
+
R = !1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const e = a.value;
|
|
260
|
+
if (!e) return;
|
|
261
|
+
const n = c === "json" ? JSON.stringify(t ?? null) : t, r = c === "json" ? JSON.stringify(e.getJSON()) : e.getHTML();
|
|
262
|
+
n !== r && e.commands.setContent(t ?? "", { emitUpdate: !1 });
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
function o() {
|
|
266
|
+
return a.value;
|
|
267
|
+
}
|
|
268
|
+
function L() {
|
|
269
|
+
var W;
|
|
270
|
+
const t = a.value;
|
|
271
|
+
if (!t) return null;
|
|
272
|
+
const e = t.state.selection, r = ((W = e.$anchorCell) == null ? void 0 : W.pos) ?? e.$from.pos, u = (B) => B.type.name === "tableCell" || B.type.name === "tableHeader";
|
|
273
|
+
function v(B) {
|
|
274
|
+
const D = t.state.doc.resolve(B);
|
|
275
|
+
let V = -1, z = -1;
|
|
276
|
+
for (let N = D.depth; N > 0; N--) {
|
|
277
|
+
const rt = D.node(N).type.name;
|
|
278
|
+
V < 0 && u(D.node(N)) && (V = N), z < 0 && rt === "table" && (z = N);
|
|
279
|
+
}
|
|
280
|
+
return { $pos: D, cellDepth: V, tableDepth: z };
|
|
281
|
+
}
|
|
282
|
+
let { $pos: w, cellDepth: C, tableDepth: M } = v(r);
|
|
283
|
+
if (C < 0 && r > 0 && ({ $pos: w, cellDepth: C, tableDepth: M } = v(r - 1)), C < 0 || M < 0) return null;
|
|
284
|
+
const b = w.node(M), A = w.start(M), y = ut.get(b), E = w.before(C) - A, J = y.findCell(E);
|
|
285
|
+
return {
|
|
286
|
+
map: y,
|
|
287
|
+
tableStart: A,
|
|
288
|
+
rowCount: y.height,
|
|
289
|
+
colCount: y.width,
|
|
290
|
+
row: J.top,
|
|
291
|
+
col: J.left,
|
|
292
|
+
// cell 节点的 doc 绝对 pos(moveTableRow 的 pos 参数需要)
|
|
293
|
+
cellDocPos: A + E
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function s(t) {
|
|
297
|
+
const e = L();
|
|
298
|
+
if (!e) return;
|
|
299
|
+
const n = a.value, r = e.row + t;
|
|
300
|
+
r < 0 || r >= e.rowCount || ft({ from: e.row, to: r, pos: e.cellDocPos })(
|
|
301
|
+
n.state,
|
|
302
|
+
(u) => n.view.dispatch(u)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
function d(t) {
|
|
306
|
+
const e = L();
|
|
307
|
+
if (!e) return;
|
|
308
|
+
const n = a.value, r = e.col + t;
|
|
309
|
+
r < 0 || r >= e.colCount || dt({ from: e.col, to: r, pos: e.cellDocPos })(
|
|
310
|
+
n.state,
|
|
311
|
+
(u) => n.view.dispatch(u)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
function k(t) {
|
|
315
|
+
const e = L();
|
|
316
|
+
if (!e) return;
|
|
317
|
+
const n = a.value, { map: r, tableStart: u, row: v, col: w } = e, C = t === "row" ? r.positionAt(v, 0, n.state.doc.nodeAt(u - 1)) : r.positionAt(0, w, n.state.doc.nodeAt(u - 1)), M = t === "row" ? r.positionAt(v, r.width - 1, n.state.doc.nodeAt(u - 1)) : r.positionAt(r.height - 1, w, n.state.doc.nodeAt(u - 1)), b = n.state.doc.resolve(u + C), A = n.state.doc.resolve(u + M), y = t === "row" ? _.rowSelection(b, A) : _.colSelection(b, A);
|
|
318
|
+
n.view.dispatch(n.state.tr.setSelection(y));
|
|
319
|
+
}
|
|
320
|
+
function S(t) {
|
|
321
|
+
const e = t.state.selection;
|
|
322
|
+
return lt(e) && e.node.type.name === "image";
|
|
323
|
+
}
|
|
324
|
+
const T = g ?? (() => {
|
|
325
|
+
}), Q = {
|
|
326
|
+
undo: () => {
|
|
327
|
+
var t;
|
|
328
|
+
return (t = o()) == null ? void 0 : t.chain().focus().undo().run();
|
|
329
|
+
},
|
|
330
|
+
redo: () => {
|
|
331
|
+
var t;
|
|
332
|
+
return (t = o()) == null ? void 0 : t.chain().focus().redo().run();
|
|
333
|
+
},
|
|
334
|
+
bold: () => {
|
|
335
|
+
var t;
|
|
336
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleBold().run();
|
|
337
|
+
},
|
|
338
|
+
italic: () => {
|
|
339
|
+
var t;
|
|
340
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleItalic().run();
|
|
341
|
+
},
|
|
342
|
+
strike: () => {
|
|
343
|
+
var t;
|
|
344
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleStrike().run();
|
|
345
|
+
},
|
|
346
|
+
toggleHeading: (t) => {
|
|
347
|
+
const e = o();
|
|
348
|
+
e && (t === 0 ? e.chain().focus().setParagraph().run() : e.chain().focus().toggleHeading({ level: t }).run());
|
|
349
|
+
},
|
|
350
|
+
bulletList: () => {
|
|
351
|
+
var t;
|
|
352
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleBulletList().run();
|
|
353
|
+
},
|
|
354
|
+
orderedList: () => {
|
|
355
|
+
var t;
|
|
356
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleOrderedList().run();
|
|
357
|
+
},
|
|
358
|
+
blockquote: () => {
|
|
359
|
+
var t;
|
|
360
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleBlockquote().run();
|
|
361
|
+
},
|
|
362
|
+
codeBlock: () => {
|
|
363
|
+
var t;
|
|
364
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleCodeBlock().run();
|
|
365
|
+
},
|
|
366
|
+
setLink: (t, e) => {
|
|
367
|
+
const n = o();
|
|
368
|
+
if (!n) return;
|
|
369
|
+
const r = e == null ? void 0 : e.range, u = (e == null ? void 0 : e.target) ?? "_blank";
|
|
370
|
+
if (!t) {
|
|
371
|
+
const w = n.chain().focus();
|
|
372
|
+
r && w.setTextSelection(r), w.extendMarkRange("link").unsetLink().scrollIntoView().run();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const v = n.chain().focus();
|
|
376
|
+
r && v.setTextSelection(r), v.extendMarkRange("link").setLink({ href: t, target: u }).scrollIntoView().run();
|
|
377
|
+
},
|
|
378
|
+
/**
|
|
379
|
+
* 在指定位置插入/替换一段带链接的文本。
|
|
380
|
+
*
|
|
381
|
+
* 用显式 from/to + insertContentAt,不依赖 .focus() 恢复 DOM selection——
|
|
382
|
+
* 因为工具栏/弹窗确认时编辑器已失焦,ProseMirror 的 selection 可能已被
|
|
383
|
+
* 扰乱,chain 内部的 focus 不保证回到正确位置。直接按保存的绝对位置写入
|
|
384
|
+
* 最稳妥。
|
|
385
|
+
*
|
|
386
|
+
* - 光标(空 range)处 → 在该位置插入新文本
|
|
387
|
+
* - 有 range → 用 text 替换该 range 内容并套链接
|
|
388
|
+
* text 为空时回退用 href 作显示文字。
|
|
389
|
+
* 末尾 scrollIntoView,确保插入后视口滚到结果处(尤其光标不在编辑器内时)。
|
|
390
|
+
*/
|
|
391
|
+
insertLinkText: (t, e, n) => {
|
|
392
|
+
const r = o();
|
|
393
|
+
if (!r || !t) return;
|
|
394
|
+
const u = (n == null ? void 0 : n.target) ?? "_blank", v = (e == null ? void 0 : e.trim()) || t, w = (n == null ? void 0 : n.range) ?? { from: r.state.selection.from, to: r.state.selection.to };
|
|
395
|
+
r.chain().insertContentAt(w, {
|
|
396
|
+
type: "text",
|
|
397
|
+
text: v,
|
|
398
|
+
marks: [{ type: "link", attrs: { href: t, target: u } }]
|
|
399
|
+
}).scrollIntoView().run();
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* 确保编辑器有可用光标位置。
|
|
403
|
+
*
|
|
404
|
+
* 场景:用户从未点进编辑器,直接点工具栏按钮插入内容。此时 ProseMirror
|
|
405
|
+
* 的 selection 停在文档开头,插入到开头用户看不到 → 误以为「没插入」。
|
|
406
|
+
* 此命令把光标移到文档末尾并聚焦,后续插入自然落在用户可视区。
|
|
407
|
+
*/
|
|
408
|
+
ensureFocusAtEnd: () => {
|
|
409
|
+
const t = o();
|
|
410
|
+
if (!t) return;
|
|
411
|
+
const e = t.state.doc.content.size;
|
|
412
|
+
t.chain().focus().setTextSelection(e).scrollIntoView().run();
|
|
413
|
+
},
|
|
414
|
+
setImage: (t, e) => {
|
|
415
|
+
var n;
|
|
416
|
+
return (n = o()) == null ? void 0 : n.chain().focus().setImage({ src: t, alt: e }).scrollIntoView().run();
|
|
417
|
+
},
|
|
418
|
+
uploadAndInsertImage: async (t) => {
|
|
419
|
+
if (!p) return;
|
|
420
|
+
const e = o();
|
|
421
|
+
if (e)
|
|
422
|
+
try {
|
|
423
|
+
const n = await p(t);
|
|
424
|
+
n ? e.chain().focus().setImage({ src: n }).scrollIntoView().run() : T("图片上传失败", "error");
|
|
425
|
+
} catch {
|
|
426
|
+
T("图片上传失败", "error");
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
/**
|
|
430
|
+
* 图片对齐/尺寸/题注/删除 —— 仅在当前是图片 NodeSelection 时生效。
|
|
431
|
+
*
|
|
432
|
+
* 这些命令不依赖工具条是否打开:NodeSelection 是 ProseMirror 的「整节点选中」
|
|
433
|
+
* 状态(点击图片即进入),此时 updateAttributes 会作用到该图片节点。
|
|
434
|
+
* 非图片选中态调用时静默 no-op,避免抛错干扰调用方。
|
|
435
|
+
*/
|
|
436
|
+
setImageAlign: (t) => {
|
|
437
|
+
const e = o();
|
|
438
|
+
e && S(e) && e.chain().focus().updateAttributes("image", { align: t }).run();
|
|
439
|
+
},
|
|
440
|
+
setImageSize: (t) => {
|
|
441
|
+
const e = o();
|
|
442
|
+
if (!e || !S(e)) return;
|
|
443
|
+
if (t === "original") {
|
|
444
|
+
e.chain().focus().updateAttributes("image", { width: null, height: null }).run();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const n = t === "small" ? 0.25 : t === "medium" ? 0.5 : 0.75, r = e.view.dom.clientWidth || 0;
|
|
448
|
+
r <= 0 || e.chain().focus().updateAttributes("image", { width: Math.round(r * n) }).run();
|
|
449
|
+
},
|
|
450
|
+
setImageCaption: (t) => {
|
|
451
|
+
const e = o();
|
|
452
|
+
!e || !S(e) || e.chain().focus().updateAttributes("image", { caption: t }).run();
|
|
453
|
+
},
|
|
454
|
+
removeImage: () => {
|
|
455
|
+
const t = o();
|
|
456
|
+
!t || !S(t) || t.chain().focus().deleteSelection().run();
|
|
457
|
+
},
|
|
458
|
+
insertTable: (t = 3, e = 3) => {
|
|
459
|
+
var n;
|
|
460
|
+
return (n = o()) == null ? void 0 : n.chain().focus().insertTable({ rows: t, cols: e, withHeaderRow: !0 }).scrollIntoView().run();
|
|
461
|
+
},
|
|
462
|
+
// ---- 表格结构操作 ----
|
|
463
|
+
// Tiptap TableKit 全部内置这些命令,这里只做转发。命令作用在当前选区(光标所在单元格),
|
|
464
|
+
// adapter 的工具栏用 isActive('table') 判定后才显示对应按钮,所以无需在 core 判断场景。
|
|
465
|
+
addRowBefore: () => {
|
|
466
|
+
var t;
|
|
467
|
+
return (t = o()) == null ? void 0 : t.chain().focus().addRowBefore().run();
|
|
468
|
+
},
|
|
469
|
+
addRowAfter: () => {
|
|
470
|
+
var t;
|
|
471
|
+
return (t = o()) == null ? void 0 : t.chain().focus().addRowAfter().run();
|
|
472
|
+
},
|
|
473
|
+
deleteRow: () => {
|
|
474
|
+
var t;
|
|
475
|
+
return (t = o()) == null ? void 0 : t.chain().focus().deleteRow().run();
|
|
476
|
+
},
|
|
477
|
+
addColumnBefore: () => {
|
|
478
|
+
var t;
|
|
479
|
+
return (t = o()) == null ? void 0 : t.chain().focus().addColumnBefore().run();
|
|
480
|
+
},
|
|
481
|
+
addColumnAfter: () => {
|
|
482
|
+
var t;
|
|
483
|
+
return (t = o()) == null ? void 0 : t.chain().focus().addColumnAfter().run();
|
|
484
|
+
},
|
|
485
|
+
deleteColumn: () => {
|
|
486
|
+
var t;
|
|
487
|
+
return (t = o()) == null ? void 0 : t.chain().focus().deleteColumn().run();
|
|
488
|
+
},
|
|
489
|
+
mergeCells: () => {
|
|
490
|
+
var t;
|
|
491
|
+
return (t = o()) == null ? void 0 : t.chain().focus().mergeCells().run();
|
|
492
|
+
},
|
|
493
|
+
splitCell: () => {
|
|
494
|
+
var t;
|
|
495
|
+
return (t = o()) == null ? void 0 : t.chain().focus().splitCell().run();
|
|
496
|
+
},
|
|
497
|
+
toggleHeaderRow: () => {
|
|
498
|
+
var t;
|
|
499
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleHeaderRow().run();
|
|
500
|
+
},
|
|
501
|
+
toggleHeaderColumn: () => {
|
|
502
|
+
var t;
|
|
503
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleHeaderColumn().run();
|
|
504
|
+
},
|
|
505
|
+
deleteTable: () => {
|
|
506
|
+
var t;
|
|
507
|
+
return (t = o()) == null ? void 0 : t.chain().focus().deleteTable().run();
|
|
508
|
+
},
|
|
509
|
+
// ---- 行/列移动(飞书式抓手菜单)----
|
|
510
|
+
// prosemirror-tables 的 moveTableRow/moveTableColumn 接收 {from, to, pos}:
|
|
511
|
+
// from/to 是行/列索引,pos 是任意 cell 的 doc 绝对 pos(用于定位表格)。
|
|
512
|
+
// 这里从当前选区解析行/列号,默认 moveTableRow 会自动选中移动后的行(select:true 默认)。
|
|
513
|
+
moveRowUp: () => s(-1),
|
|
514
|
+
moveRowDown: () => s(1),
|
|
515
|
+
moveColumnLeft: () => d(-1),
|
|
516
|
+
moveColumnRight: () => d(1),
|
|
517
|
+
// ---- 选中整行/整列(飞书式抓手点击)----
|
|
518
|
+
selectRow: () => k("row"),
|
|
519
|
+
selectColumn: () => k("col"),
|
|
520
|
+
hr: () => {
|
|
521
|
+
var t;
|
|
522
|
+
return (t = o()) == null ? void 0 : t.chain().focus().setHorizontalRule().run();
|
|
523
|
+
},
|
|
524
|
+
clearNodes: () => {
|
|
525
|
+
var t;
|
|
526
|
+
return (t = o()) == null ? void 0 : t.chain().focus().clearNodes().run();
|
|
527
|
+
},
|
|
528
|
+
setColor: (t) => {
|
|
529
|
+
var e;
|
|
530
|
+
return (e = o()) == null ? void 0 : e.chain().focus().setColor(t).run();
|
|
531
|
+
},
|
|
532
|
+
toggleHighlight: (t) => {
|
|
533
|
+
var e;
|
|
534
|
+
return (e = o()) == null ? void 0 : e.chain().focus().toggleHighlight({ color: t }).run();
|
|
535
|
+
},
|
|
536
|
+
align: (t) => {
|
|
537
|
+
var e;
|
|
538
|
+
return (e = o()) == null ? void 0 : e.chain().focus().setTextAlign(t).run();
|
|
539
|
+
},
|
|
540
|
+
underline: () => {
|
|
541
|
+
var t;
|
|
542
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleUnderline().run();
|
|
543
|
+
},
|
|
544
|
+
clearFormat: () => {
|
|
545
|
+
var t;
|
|
546
|
+
return (t = o()) == null ? void 0 : t.chain().focus().clearNodes().unsetAllMarks().run();
|
|
547
|
+
},
|
|
548
|
+
taskList: () => {
|
|
549
|
+
var t;
|
|
550
|
+
return (t = o()) == null ? void 0 : t.chain().focus().toggleTaskList().run();
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
function X(t, e) {
|
|
554
|
+
const n = a.value;
|
|
555
|
+
return n ? typeof t == "string" ? n.isActive(t, e) : n.isActive(t) : !1;
|
|
556
|
+
}
|
|
557
|
+
const U = K(0);
|
|
558
|
+
F(
|
|
559
|
+
a,
|
|
560
|
+
(t) => {
|
|
561
|
+
if (!t) return;
|
|
562
|
+
const e = () => U.value++;
|
|
563
|
+
t.on("transaction", e), t.on("selectionUpdate", e);
|
|
564
|
+
},
|
|
565
|
+
{ immediate: !0 }
|
|
566
|
+
);
|
|
567
|
+
const Y = j(() => {
|
|
568
|
+
U.value;
|
|
569
|
+
const t = a.value;
|
|
570
|
+
if (!t) return { inTable: !1, canMerge: !1, canSplit: !1, tablePos: null, rowCount: 0, colCount: 0 };
|
|
571
|
+
const e = t.isActive("table");
|
|
572
|
+
if (!e) return { inTable: !1, canMerge: !1, canSplit: !1, tablePos: null, rowCount: 0, colCount: 0 };
|
|
573
|
+
const n = t.state.selection, r = (A) => {
|
|
574
|
+
for (let y = A.depth; y > 0; y--) {
|
|
575
|
+
const E = A.node(y);
|
|
576
|
+
if (E.type.name === "tableCell" || E.type.name === "tableHeader")
|
|
577
|
+
return E;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}, u = r(n.$from), v = r(n.$to), w = !!u && !!v && u !== v, C = (u == null ? void 0 : u.attrs) ?? { colspan: 1, rowspan: 1 }, M = (C.colspan ?? 1) > 1 || (C.rowspan ?? 1) > 1, b = L();
|
|
581
|
+
return {
|
|
582
|
+
inTable: e,
|
|
583
|
+
canMerge: w,
|
|
584
|
+
canSplit: M,
|
|
585
|
+
tablePos: b ? b.tableStart - 1 : null,
|
|
586
|
+
// tableStart 是内容区 pos,表格节点 pos = start - 1
|
|
587
|
+
rowCount: (b == null ? void 0 : b.rowCount) ?? 0,
|
|
588
|
+
colCount: (b == null ? void 0 : b.colCount) ?? 0
|
|
589
|
+
};
|
|
590
|
+
}), Z = () => {
|
|
591
|
+
var t;
|
|
592
|
+
return ((t = a.value) == null ? void 0 : t.getHTML()) ?? "";
|
|
593
|
+
}, tt = () => {
|
|
594
|
+
var t;
|
|
595
|
+
return ((t = a.value) == null ? void 0 : t.getJSON()) ?? {};
|
|
596
|
+
}, et = () => a.value ? It(a.value) : "", nt = (t) => {
|
|
597
|
+
a.value && Rt(a.value, t);
|
|
598
|
+
}, ot = (t) => {
|
|
599
|
+
var e;
|
|
600
|
+
return (e = a.value) == null ? void 0 : e.setEditable(t);
|
|
601
|
+
};
|
|
602
|
+
return it(() => {
|
|
603
|
+
}), {
|
|
604
|
+
editor: a,
|
|
605
|
+
loaded: O,
|
|
606
|
+
isActive: X,
|
|
607
|
+
commands: Q,
|
|
608
|
+
getHTML: Z,
|
|
609
|
+
getJSON: tt,
|
|
610
|
+
getMarkdown: et,
|
|
611
|
+
importMarkdown: nt,
|
|
612
|
+
wordCount: f,
|
|
613
|
+
setEditable: ot,
|
|
614
|
+
tableState: Y,
|
|
615
|
+
notify: T
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function G(i) {
|
|
619
|
+
return i.type.startsWith("image/");
|
|
620
|
+
}
|
|
621
|
+
function Kt(i) {
|
|
622
|
+
return !i || i.length === 0 ? !1 : Array.from(i).some(G);
|
|
623
|
+
}
|
|
624
|
+
async function _t(i, l, c, m) {
|
|
625
|
+
if (!i || i.length === 0 || !l) return !1;
|
|
626
|
+
const I = Array.from(i).filter(G);
|
|
627
|
+
if (I.length === 0) return !1;
|
|
628
|
+
for (const p of I)
|
|
629
|
+
try {
|
|
630
|
+
const h = await l(p);
|
|
631
|
+
h ? c.chain().focus().setImage({ src: h }).run() : m && m(p, new Error("upload returned null"));
|
|
632
|
+
} catch (h) {
|
|
633
|
+
m == null || m(p, h);
|
|
634
|
+
}
|
|
635
|
+
return !0;
|
|
636
|
+
}
|
|
637
|
+
export {
|
|
638
|
+
Lt as ImageExtended,
|
|
639
|
+
Qt as MarkdownExtension,
|
|
640
|
+
Tt as createDefaultExtensions,
|
|
641
|
+
It as getMarkdown,
|
|
642
|
+
_t as handleImageFiles,
|
|
643
|
+
Kt as hasImageFiles,
|
|
644
|
+
Rt as importMarkdown,
|
|
645
|
+
G as isImageFile,
|
|
646
|
+
jt as useProEditor
|
|
647
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(a,C){typeof exports=="object"&&typeof module<"u"?C(exports,require("vue"),require("@tiptap/vue-3"),require("@tiptap/core"),require("@tiptap/pm/tables"),require("@tiptap/starter-kit"),require("@tiptap/extension-character-count"),require("@tiptap/extension-placeholder"),require("@tiptap/extension-table"),require("@tiptap/extension-image"),require("@tiptap/extension-text-style"),require("@tiptap/extension-color"),require("@tiptap/extension-highlight"),require("@tiptap/extension-text-align"),require("@tiptap/extension-list"),require("@tiptap/markdown")):typeof define=="function"&&define.amd?define(["exports","vue","@tiptap/vue-3","@tiptap/core","@tiptap/pm/tables","@tiptap/starter-kit","@tiptap/extension-character-count","@tiptap/extension-placeholder","@tiptap/extension-table","@tiptap/extension-image","@tiptap/extension-text-style","@tiptap/extension-color","@tiptap/extension-highlight","@tiptap/extension-text-align","@tiptap/extension-list","@tiptap/markdown"],C):(a=typeof globalThis<"u"?globalThis:a||self,C(a.TiptapVueProCore={},a.vue,a.vue3,a.core,a.tables,a.StarterKit,a.CharacterCount,a.Placeholder,a.extensionTable,a.Image,a.extensionTextStyle,a.extensionColor,a.extensionHighlight,a.extensionTextAlign,a.extensionList,a.markdown))})(this,function(a,C,ne,z,q,oe,ie,re,ae,se,ce,le,ue,de,W,K){"use strict";function pe(r,u){const{node:l,getPos:m,HTMLAttributes:H,editor:h}=r,w=document.createElement("div");w.className="tvp-img-node";const D=l.attrs.align||"center";D!=="center"&&w.setAttribute("data-align",D);const g=document.createElement("img");g.draggable=!1;const O=z.mergeAttributes(u,H);for(const[s,p]of Object.entries(O))p!=null&&(s==="width"||s==="height"||s==="align"||s==="caption"||g.setAttribute(s,String(p)));O.src!=null&&(g.src=String(O.src));const c=new z.ResizableNodeView({element:g,editor:h,node:l,getPos:m,onResize:(s,p)=>{g.style.width=`${s}px`,g.style.height=`${p}px`},onCommit:(s,p)=>{const x=m();x!==void 0&&h.chain().setNodeSelection(x).updateAttributes("image",{width:s,height:p}).run()},onUpdate:s=>s.type===l.type,options:{directions:["top-left","top-right","bottom-left","bottom-right"],min:{width:60,height:60},preserveAspectRatio:!0}});c.dom.style.visibility="hidden",c.dom.style.pointerEvents="none";const $=()=>{c.dom.style.visibility="",c.dom.style.pointerEvents="";const s=h.view;s.dispatch(s.state.tr.setMeta("proImageBubbleMenu","updatePosition"))};g.addEventListener("load",$,{once:!0}),g.addEventListener("error",$,{once:!0}),w.appendChild(c.dom);const f=document.createElement("input");f.className="tvp-img-caption",f.type="text",f.placeholder="添加题注";const R=l.attrs.caption||"";f.value=R,R||f.classList.add("tvp-img-caption-empty");const E=()=>{f.readOnly=!h.isEditable};E();const B=()=>E();h.on("update",B);const o=()=>{if(!h.isEditable)return;const s=m();if(s===void 0)return;const p=f.value;f.classList.toggle("tvp-img-caption-empty",!p),h.chain().setNodeSelection(s).updateAttributes("image",{caption:p}).run()};f.addEventListener("input",o);const I=s=>{s.stopPropagation()};return f.addEventListener("keydown",I),w.appendChild(f),{dom:w,contentDOM:void 0,update(s){if(s.type!==l.type)return!1;const p=s.attrs,x=p.align||"center";x!=="center"?w.setAttribute("data-align",x):w.removeAttribute("data-align"),E();const T=p.caption||"";T!==f.value&&(f.value=T,f.classList.toggle("tvp-img-caption-empty",!T));const L=p.src;return L&&L!==g.getAttribute("src")&&(g.src=L),p.width!=null?g.style.width=`${p.width}px`:g.style.width="",p.height!=null?g.style.height=`${p.height}px`:g.style.height="",c.update(s,[],void 0)},destroy(){h.off("update",B),f.removeEventListener("input",o),f.removeEventListener("keydown",I),c.destroy()},ignoreMutation:()=>!0,stopEvent(s){const p=s.target;return!!p&&(p===f||f.contains(p))}}}const _=se.extend({addAttributes(){var r;return{...(r=this.parent)==null?void 0:r.call(this),align:{default:"center",parseHTML:u=>u.getAttribute("data-align")||"center",renderHTML:u=>{const l=u.align;return l&&l!=="center"?{"data-align":l}:{}}},caption:{default:"",parseHTML:u=>u.getAttribute("data-caption")||"",renderHTML:u=>{const l=u.caption;return l?{"data-caption":l}:{}}}}},addNodeView(){return({node:r,getPos:u,HTMLAttributes:l,editor:m})=>pe({node:r,getPos:u,HTMLAttributes:l,editor:m},this.options.HTMLAttributes)}}).configure({inline:!1,allowBase64:!1,resize:{enabled:!0,directions:["top-left","top-right","bottom-left","bottom-right"],minWidth:60,minHeight:60,alwaysPreserveAspectRatio:!0}});function G(r){return[oe.configure({heading:{levels:[1,2,3,4,5,6]},link:{openOnClick:!1,autolink:!0,defaultProtocol:"https",HTMLAttributes:{target:"_blank",rel:"noopener noreferrer nofollow"}}}),re.configure({placeholder:r??"请输入内容..."}),ie,_,ae.TableKit.configure({table:{resizable:!0}}),ce.TextStyle,le.Color,ue.Highlight.configure({multicolor:!0}),de.TextAlign.configure({types:["heading","paragraph"]}),W.TaskList,W.TaskItem.configure({nested:!0}),K.Markdown]}function Q(r){var l;return(l=r.storage.markdown)==null?void 0:l.manager}function X(r){const u=Q(r);return u?u.serialize(r.getJSON()):""}function Y(r,u){const l=Q(r);if(l)try{const m=l.parse(u);r.commands.setContent(m);return}catch{}r.commands.setContent(u)}function fe(r){const{content:u,output:l="html",extensions:m,placeholder:H,uploadImage:h,editable:w=!0,editorProps:D,notify:g}=r,O=m??G(H),c=ne.useEditor({extensions:O,content:typeof u=="string"?u:JSON.stringify(u),editable:w,editorProps:D??{},onUpdate:({editor:e})=>{R=!0,B(e),E(e)}}),$=C.computed(()=>!!c.value),f=C.ref({characters:0,words:0});let R=!1;function E(e){var i,d;const n=e.storage.characterCount;n&&(f.value={characters:((i=n.characters)==null?void 0:i.call(n))??0,words:((d=n.words)==null?void 0:d.call(n))??0})}C.watch(()=>c.value,e=>{e&&E(e)},{immediate:!0});function B(e){const t=l==="html"?e.getHTML():e.getJSON();r.content=t}C.watch(()=>r.content,e=>{if(R){R=!1;return}const t=c.value;if(!t)return;const n=l==="json"?JSON.stringify(e??null):e,i=l==="json"?JSON.stringify(t.getJSON()):t.getHTML();n!==i&&t.commands.setContent(e??"",{emitUpdate:!1})});function o(){return c.value}function I(){var te;const e=c.value;if(!e)return null;const t=e.state.selection,i=((te=t.$anchorCell)==null?void 0:te.pos)??t.$from.pos,d=F=>F.type.name==="tableCell"||F.type.name==="tableHeader";function b(F){const V=e.state.doc.resolve(F);let J=-1,j=-1;for(let N=V.depth;N>0;N--){const Se=V.node(N).type.name;J<0&&d(V.node(N))&&(J=N),j<0&&Se==="table"&&(j=N)}return{$pos:V,cellDepth:J,tableDepth:j}}let{$pos:v,cellDepth:A,tableDepth:M}=b(i);if(A<0&&i>0&&({$pos:v,cellDepth:A,tableDepth:M}=b(i-1)),A<0||M<0)return null;const y=v.node(M),S=v.start(M),k=q.TableMap.get(y),P=v.before(A)-S,ee=k.findCell(P);return{map:k,tableStart:S,rowCount:k.height,colCount:k.width,row:ee.top,col:ee.left,cellDocPos:S+P}}function s(e){const t=I();if(!t)return;const n=c.value,i=t.row+e;i<0||i>=t.rowCount||q.moveTableRow({from:t.row,to:i,pos:t.cellDocPos})(n.state,d=>n.view.dispatch(d))}function p(e){const t=I();if(!t)return;const n=c.value,i=t.col+e;i<0||i>=t.colCount||q.moveTableColumn({from:t.col,to:i,pos:t.cellDocPos})(n.state,d=>n.view.dispatch(d))}function x(e){const t=I();if(!t)return;const n=c.value,{map:i,tableStart:d,row:b,col:v}=t,A=e==="row"?i.positionAt(b,0,n.state.doc.nodeAt(d-1)):i.positionAt(0,v,n.state.doc.nodeAt(d-1)),M=e==="row"?i.positionAt(b,i.width-1,n.state.doc.nodeAt(d-1)):i.positionAt(i.height-1,v,n.state.doc.nodeAt(d-1)),y=n.state.doc.resolve(d+A),S=n.state.doc.resolve(d+M),k=e==="row"?q.CellSelection.rowSelection(y,S):q.CellSelection.colSelection(y,S);n.view.dispatch(n.state.tr.setSelection(k))}function T(e){const t=e.state.selection;return z.isNodeSelection(t)&&t.node.type.name==="image"}const L=g??(()=>{}),he={undo:()=>{var e;return(e=o())==null?void 0:e.chain().focus().undo().run()},redo:()=>{var e;return(e=o())==null?void 0:e.chain().focus().redo().run()},bold:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleBold().run()},italic:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleItalic().run()},strike:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleStrike().run()},toggleHeading:e=>{const t=o();t&&(e===0?t.chain().focus().setParagraph().run():t.chain().focus().toggleHeading({level:e}).run())},bulletList:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleBulletList().run()},orderedList:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleOrderedList().run()},blockquote:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleBlockquote().run()},codeBlock:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleCodeBlock().run()},setLink:(e,t)=>{const n=o();if(!n)return;const i=t==null?void 0:t.range,d=(t==null?void 0:t.target)??"_blank";if(!e){const v=n.chain().focus();i&&v.setTextSelection(i),v.extendMarkRange("link").unsetLink().scrollIntoView().run();return}const b=n.chain().focus();i&&b.setTextSelection(i),b.extendMarkRange("link").setLink({href:e,target:d}).scrollIntoView().run()},insertLinkText:(e,t,n)=>{const i=o();if(!i||!e)return;const d=(n==null?void 0:n.target)??"_blank",b=(t==null?void 0:t.trim())||e,v=(n==null?void 0:n.range)??{from:i.state.selection.from,to:i.state.selection.to};i.chain().insertContentAt(v,{type:"text",text:b,marks:[{type:"link",attrs:{href:e,target:d}}]}).scrollIntoView().run()},ensureFocusAtEnd:()=>{const e=o();if(!e)return;const t=e.state.doc.content.size;e.chain().focus().setTextSelection(t).scrollIntoView().run()},setImage:(e,t)=>{var n;return(n=o())==null?void 0:n.chain().focus().setImage({src:e,alt:t}).scrollIntoView().run()},uploadAndInsertImage:async e=>{if(!h)return;const t=o();if(t)try{const n=await h(e);n?t.chain().focus().setImage({src:n}).scrollIntoView().run():L("图片上传失败","error")}catch{L("图片上传失败","error")}},setImageAlign:e=>{const t=o();t&&T(t)&&t.chain().focus().updateAttributes("image",{align:e}).run()},setImageSize:e=>{const t=o();if(!t||!T(t))return;if(e==="original"){t.chain().focus().updateAttributes("image",{width:null,height:null}).run();return}const n=e==="small"?.25:e==="medium"?.5:.75,i=t.view.dom.clientWidth||0;i<=0||t.chain().focus().updateAttributes("image",{width:Math.round(i*n)}).run()},setImageCaption:e=>{const t=o();!t||!T(t)||t.chain().focus().updateAttributes("image",{caption:e}).run()},removeImage:()=>{const e=o();!e||!T(e)||e.chain().focus().deleteSelection().run()},insertTable:(e=3,t=3)=>{var n;return(n=o())==null?void 0:n.chain().focus().insertTable({rows:e,cols:t,withHeaderRow:!0}).scrollIntoView().run()},addRowBefore:()=>{var e;return(e=o())==null?void 0:e.chain().focus().addRowBefore().run()},addRowAfter:()=>{var e;return(e=o())==null?void 0:e.chain().focus().addRowAfter().run()},deleteRow:()=>{var e;return(e=o())==null?void 0:e.chain().focus().deleteRow().run()},addColumnBefore:()=>{var e;return(e=o())==null?void 0:e.chain().focus().addColumnBefore().run()},addColumnAfter:()=>{var e;return(e=o())==null?void 0:e.chain().focus().addColumnAfter().run()},deleteColumn:()=>{var e;return(e=o())==null?void 0:e.chain().focus().deleteColumn().run()},mergeCells:()=>{var e;return(e=o())==null?void 0:e.chain().focus().mergeCells().run()},splitCell:()=>{var e;return(e=o())==null?void 0:e.chain().focus().splitCell().run()},toggleHeaderRow:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleHeaderRow().run()},toggleHeaderColumn:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleHeaderColumn().run()},deleteTable:()=>{var e;return(e=o())==null?void 0:e.chain().focus().deleteTable().run()},moveRowUp:()=>s(-1),moveRowDown:()=>s(1),moveColumnLeft:()=>p(-1),moveColumnRight:()=>p(1),selectRow:()=>x("row"),selectColumn:()=>x("col"),hr:()=>{var e;return(e=o())==null?void 0:e.chain().focus().setHorizontalRule().run()},clearNodes:()=>{var e;return(e=o())==null?void 0:e.chain().focus().clearNodes().run()},setColor:e=>{var t;return(t=o())==null?void 0:t.chain().focus().setColor(e).run()},toggleHighlight:e=>{var t;return(t=o())==null?void 0:t.chain().focus().toggleHighlight({color:e}).run()},align:e=>{var t;return(t=o())==null?void 0:t.chain().focus().setTextAlign(e).run()},underline:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleUnderline().run()},clearFormat:()=>{var e;return(e=o())==null?void 0:e.chain().focus().clearNodes().unsetAllMarks().run()},taskList:()=>{var e;return(e=o())==null?void 0:e.chain().focus().toggleTaskList().run()}};function we(e,t){const n=c.value;return n?typeof e=="string"?n.isActive(e,t):n.isActive(e):!1}const Z=C.ref(0);C.watch(c,e=>{if(!e)return;const t=()=>Z.value++;e.on("transaction",t),e.on("selectionUpdate",t)},{immediate:!0});const ve=C.computed(()=>{Z.value;const e=c.value;if(!e)return{inTable:!1,canMerge:!1,canSplit:!1,tablePos:null,rowCount:0,colCount:0};const t=e.isActive("table");if(!t)return{inTable:!1,canMerge:!1,canSplit:!1,tablePos:null,rowCount:0,colCount:0};const n=e.state.selection,i=S=>{for(let k=S.depth;k>0;k--){const P=S.node(k);if(P.type.name==="tableCell"||P.type.name==="tableHeader")return P}return null},d=i(n.$from),b=i(n.$to),v=!!d&&!!b&&d!==b,A=(d==null?void 0:d.attrs)??{colspan:1,rowspan:1},M=(A.colspan??1)>1||(A.rowspan??1)>1,y=I();return{inTable:t,canMerge:v,canSplit:M,tablePos:y?y.tableStart-1:null,rowCount:(y==null?void 0:y.rowCount)??0,colCount:(y==null?void 0:y.colCount)??0}}),be=()=>{var e;return((e=c.value)==null?void 0:e.getHTML())??""},ye=()=>{var e;return((e=c.value)==null?void 0:e.getJSON())??{}},Ce=()=>c.value?X(c.value):"",ke=e=>{c.value&&Y(c.value,e)},Ae=e=>{var t;return(t=c.value)==null?void 0:t.setEditable(e)};return C.onBeforeUnmount(()=>{}),{editor:c,loaded:$,isActive:we,commands:he,getHTML:be,getJSON:ye,getMarkdown:Ce,importMarkdown:ke,wordCount:f,setEditable:Ae,tableState:ve,notify:L}}function U(r){return r.type.startsWith("image/")}function ge(r){return!r||r.length===0?!1:Array.from(r).some(U)}async function me(r,u,l,m){if(!r||r.length===0||!u)return!1;const H=Array.from(r).filter(U);if(H.length===0)return!1;for(const h of H)try{const w=await u(h);w?l.chain().focus().setImage({src:w}).run():m&&m(h,new Error("upload returned null"))}catch(w){m==null||m(h,w)}return!0}Object.defineProperty(a,"MarkdownExtension",{enumerable:!0,get:()=>K.Markdown}),a.ImageExtended=_,a.createDefaultExtensions=G,a.getMarkdown=X,a.handleImageFiles=me,a.hasImageFiles=ge,a.importMarkdown=Y,a.isImageFile=U,a.useProEditor=fe,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Markdown, MarkdownManager } from '@tiptap/markdown';
|
|
2
|
+
import { Editor } from '@tiptap/vue-3';
|
|
3
|
+
/**
|
|
4
|
+
* Markdown 导入/导出能力(基于官方 @tiptap/markdown 扩展)。
|
|
5
|
+
*
|
|
6
|
+
* 设计说明:
|
|
7
|
+
* - 扩展会把 MarkdownManager 挂到 editor.storage.markdown.manager,
|
|
8
|
+
* 并给 editor 实例追加 getMarkdown() 方法。这里统一从 storage 取 manager,
|
|
9
|
+
* 避免对 editor 实例做 as any(扩展未在 Editor 类型上声明这两个成员)。
|
|
10
|
+
* - 官方扩展不支持「把无对应 MD 语法的样式降级为内联 HTML」,
|
|
11
|
+
* 因此颜色/高亮/对齐等富样式在导出时会丢失——这是第一档取舍:
|
|
12
|
+
* 覆盖标题/粗斜体/列表/任务/引用/代码块/表格/链接/图片等 MD 原生能力。
|
|
13
|
+
*/
|
|
14
|
+
export { Markdown as MarkdownExtension };
|
|
15
|
+
/** 取到编辑器上的 MarkdownManager(未启用扩展时为 undefined) */
|
|
16
|
+
export declare function getMarkdownManager(editor: Editor): MarkdownManager | undefined;
|
|
17
|
+
/** 编辑器当前内容序列化为 Markdown 字符串。未启用扩展时返回空串。 */
|
|
18
|
+
export declare function getMarkdown(editor: Editor): string;
|
|
19
|
+
/**
|
|
20
|
+
* 把一段 Markdown 写入编辑器(替换全部内容)。
|
|
21
|
+
*
|
|
22
|
+
* - 启用了扩展:用 manager.parse 转 JSON 后 setContent,
|
|
23
|
+
* 解析失败时降级为原文直接塞入(勉强兼容纯文本)。
|
|
24
|
+
* - 未启用扩展:直接 setContent(md)——此时按 HTML/JSON 默认规则处理。
|
|
25
|
+
*
|
|
26
|
+
* 不传 emitUpdate:false,以便新内容通过 onUpdate 流回 v-model。
|
|
27
|
+
*/
|
|
28
|
+
export declare function importMarkdown(editor: Editor, md: string): void;
|
|
29
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,IAAI,iBAAiB,EAAE,CAAA;AAExC,iDAAiD;AACjD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAM9E;AAED,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAY/D"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/vue-3';
|
|
2
|
+
import { Extensions } from '@tiptap/core';
|
|
3
|
+
import { ImageAlign, ImageSizePreset } from './extensions/image';
|
|
4
|
+
import { Ref } from 'vue';
|
|
5
|
+
/**
|
|
6
|
+
* 扩展数组类型,v3 用 Extensions(同时接受 Extension 和 Node)。
|
|
7
|
+
*/
|
|
8
|
+
export type { Extensions } from '@tiptap/core';
|
|
9
|
+
/**
|
|
10
|
+
* 编辑器输出格式
|
|
11
|
+
*/
|
|
12
|
+
export type OutputFormat = 'html' | 'json';
|
|
13
|
+
/**
|
|
14
|
+
* 图片上传函数契约。
|
|
15
|
+
*
|
|
16
|
+
* adapter 接收到上传/粘贴/拖拽的 File,交给 Core 调用此函数拿到 url,
|
|
17
|
+
* 再插入文档。Core 不关心上传到 OSS/COS/本地,只关心返回 url。
|
|
18
|
+
*
|
|
19
|
+
* 实现方应负责:
|
|
20
|
+
* - 上传进度(可选,Core 不强求)
|
|
21
|
+
* - 失败时抛错或返回 null(返回 null 时 Core 跳过插入,Core 会调 notify
|
|
22
|
+
* 提示用户;adapter 负责注入具体的提示 UI,见 NotifyFn)
|
|
23
|
+
*/
|
|
24
|
+
export type UploadImage = (file: File) => Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* 消息提示类型。
|
|
27
|
+
*/
|
|
28
|
+
export type NotifyType = 'success' | 'error' | 'warning' | 'info';
|
|
29
|
+
/**
|
|
30
|
+
* 消息提示回调契约。
|
|
31
|
+
*
|
|
32
|
+
* 这是 Core 与 UI 库之间的「提示」边界:Core 在需要提示用户的时机
|
|
33
|
+
* (上传失败、移除链接、导入结果等)调用它;adapter 注入各自的实现
|
|
34
|
+
* (EP 的 ElMessage、Naive 的 useMessage、或自定义)。
|
|
35
|
+
*
|
|
36
|
+
* 设计动机:让两个 adapter 的「提示时机 + 文案」完全对等——文案集中
|
|
37
|
+
* 在调用处,adapter 只决定「用什么组件显示」。不传时静默(纯 headless)。
|
|
38
|
+
*/
|
|
39
|
+
export type NotifyFn = (message: string, type?: NotifyType) => void;
|
|
40
|
+
/**
|
|
41
|
+
* useProEditor 的配置项
|
|
42
|
+
*/
|
|
43
|
+
export interface ProEditorOptions {
|
|
44
|
+
/** v-model 绑定值,字符串(html/json 文本)或对象(json) */
|
|
45
|
+
content: string | object;
|
|
46
|
+
/** 输出格式,默认 'html' */
|
|
47
|
+
output?: OutputFormat;
|
|
48
|
+
/** 扩展数组。传入则覆盖默认扩展包;不传使用默认包 */
|
|
49
|
+
extensions?: Extensions;
|
|
50
|
+
/** placeholder 文案 */
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
/** 图片上传函数。不传则图片只能以已有 url 插入,粘贴/拖拽上传失效 */
|
|
53
|
+
uploadImage?: UploadImage;
|
|
54
|
+
/** 是否只读 */
|
|
55
|
+
editable?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* 消息提示回调。adapter 注入各自 UI 库的实现(EP 的 ElMessage / Naive 的
|
|
58
|
+
* useMessage)。Core 在上传失败等场景调用它;不传则静默。
|
|
59
|
+
*/
|
|
60
|
+
notify?: NotifyFn;
|
|
61
|
+
/** 额外编辑器属性,透传给 Editor 构造函数(如 autofocus, editorProps) */
|
|
62
|
+
editorProps?: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* useProEditor 返回的结构
|
|
66
|
+
*/
|
|
67
|
+
export interface ProEditorContext {
|
|
68
|
+
/** Tiptap Editor 实例(loaded 后才有值) */
|
|
69
|
+
editor: Ref<Editor | undefined>;
|
|
70
|
+
/** 编辑器是否已挂载 */
|
|
71
|
+
loaded: Ref<boolean>;
|
|
72
|
+
/** 命令是否可用。name 可传扩展名,或传 attrs-only 对象(如 { textAlign: 'center' }) */
|
|
73
|
+
isActive: (name: string | Record<string, unknown>, attrs?: Record<string, unknown>) => boolean;
|
|
74
|
+
/** 聚合命令,工具栏按钮直接调用 */
|
|
75
|
+
commands: ProEditorCommands;
|
|
76
|
+
/** 当前内容字符串(按 output 格式) */
|
|
77
|
+
getHTML: () => string;
|
|
78
|
+
getJSON: () => object;
|
|
79
|
+
/** 当前内容序列化为 Markdown。未启用 Markdown 扩展时返回空串 */
|
|
80
|
+
getMarkdown: () => string;
|
|
81
|
+
/** 把一段 Markdown 写入编辑器(替换全部内容);解析失败则降级直接塞入 */
|
|
82
|
+
importMarkdown: (md: string) => void;
|
|
83
|
+
/** 字数统计 */
|
|
84
|
+
wordCount: Ref<{
|
|
85
|
+
characters: number;
|
|
86
|
+
words: number;
|
|
87
|
+
}>;
|
|
88
|
+
/** 切换只读 */
|
|
89
|
+
setEditable: (editable: boolean) => void;
|
|
90
|
+
/**
|
|
91
|
+
* 表格选区状态(响应式)。驱动表格气泡里「合并/拆分」按钮的可用性,
|
|
92
|
+
* 避免 ProseMirror splitCell 对未合并单元格静默 return false 导致「点了没反应」。
|
|
93
|
+
* 光标不在表格内时,几何字段为默认值(inTable=false)。
|
|
94
|
+
*/
|
|
95
|
+
tableState: Ref<TableState>;
|
|
96
|
+
/**
|
|
97
|
+
* 消息提示。adapter 注入的 UI 库实现(EP 的 ElMessage / Naive 的 useMessage),
|
|
98
|
+
* 供工具栏等组件在「导入成功 / 链接校验失败」等场景统一调用,文案与触发点对齐。
|
|
99
|
+
*/
|
|
100
|
+
notify: NotifyFn;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 表格选区状态。用于驱动「合并/拆分」按钮的禁用态,解释 ProseMirror 的隐式约束:
|
|
104
|
+
* - mergeCells 只在「选中多个单元格」(CellSelection 跨格)时有效
|
|
105
|
+
* - splitCell 只在「光标在被合并的单元格」(colspan/rowspan > 1)时有效
|
|
106
|
+
* 把这两个隐式约束显式化,让 UI 能给出禁用反馈而非静默失败。
|
|
107
|
+
*
|
|
108
|
+
* 几何字段(tablePos/rowCount/colCount)供 adapter 渲染飞书式覆盖层定位:
|
|
109
|
+
* core 不读 DOM,只暴露纯数据;adapter 用 editor.view.nodeDOM(tablePos) 拿 table DOM 算 rect。
|
|
110
|
+
*/
|
|
111
|
+
export interface TableState {
|
|
112
|
+
/** 光标是否在表格内(任意单元格,含单选/多选) */
|
|
113
|
+
inTable: boolean;
|
|
114
|
+
/** 是否选中了多个单元格(CellSelection 且 anchor ≠ head)——合并的前提 */
|
|
115
|
+
canMerge: boolean;
|
|
116
|
+
/** 光标所在单元格是否被合并过(colspan 或 rowspan > 1)——拆分的前提 */
|
|
117
|
+
canSplit: boolean;
|
|
118
|
+
/** 当前表格在文档中的起始 pos(adapter 用 view.nodeDOM(pos) 定位)。不在表格内为 null */
|
|
119
|
+
tablePos: number | null;
|
|
120
|
+
/** 表格行数 */
|
|
121
|
+
rowCount: number;
|
|
122
|
+
/** 表格列数 */
|
|
123
|
+
colCount: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 命令契约。每个方法对应工具栏一个按钮。
|
|
127
|
+
* adapter 的工具栏直接消费这个对象。
|
|
128
|
+
*
|
|
129
|
+
* 命名原则:动词在前,和 Tiptap 原生命名对齐,降低心智成本。
|
|
130
|
+
*/
|
|
131
|
+
export interface ProEditorCommands {
|
|
132
|
+
undo: () => void;
|
|
133
|
+
redo: () => void;
|
|
134
|
+
bold: () => void;
|
|
135
|
+
italic: () => void;
|
|
136
|
+
strike: () => void;
|
|
137
|
+
/** 切换标题级别,level 0 表示取消标题(转普通段落) */
|
|
138
|
+
toggleHeading: (level: 1 | 2 | 3 | 4 | 5 | 6 | 0) => void;
|
|
139
|
+
bulletList: () => void;
|
|
140
|
+
orderedList: () => void;
|
|
141
|
+
blockquote: () => void;
|
|
142
|
+
codeBlock: () => void;
|
|
143
|
+
/** 设置/更新链接;href 为空则移除链接 */
|
|
144
|
+
setLink: (href: string, opts?: {
|
|
145
|
+
target?: string;
|
|
146
|
+
range?: {
|
|
147
|
+
from: number;
|
|
148
|
+
to: number;
|
|
149
|
+
};
|
|
150
|
+
}) => void;
|
|
151
|
+
/**
|
|
152
|
+
* 在指定位置插入/替换一段带链接的文本。
|
|
153
|
+
* 传 range 时按绝对位置写入(绕开弹窗失焦导致的 selection 漂移);
|
|
154
|
+
* 不传则用当前 selection。text 为空时用 href 作显示文字。
|
|
155
|
+
*/
|
|
156
|
+
insertLinkText: (href: string, text?: string, opts?: {
|
|
157
|
+
target?: string;
|
|
158
|
+
range?: {
|
|
159
|
+
from: number;
|
|
160
|
+
to: number;
|
|
161
|
+
};
|
|
162
|
+
}) => void;
|
|
163
|
+
/**
|
|
164
|
+
* 确保编辑器有可用光标位置。
|
|
165
|
+
* 用户从未点进编辑器时,把光标移到文档末尾并聚焦,
|
|
166
|
+
* 供工具栏「插入类」按钮在操作前调用,避免插入到开头看不到。
|
|
167
|
+
*/
|
|
168
|
+
ensureFocusAtEnd: () => void;
|
|
169
|
+
/** 插入图片(已有 url) */
|
|
170
|
+
setImage: (src: string, alt?: string) => void;
|
|
171
|
+
/** 插入图片(从 File,走 uploadImage) */
|
|
172
|
+
uploadAndInsertImage: (file: File) => Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* 设置当前选中图片的对齐方式。
|
|
175
|
+
* 仅在当前选中节点是 image 时生效(NodeSelection)。
|
|
176
|
+
*/
|
|
177
|
+
setImageAlign: (align: ImageAlign) => void;
|
|
178
|
+
/**
|
|
179
|
+
* 按预设设置当前选中图片的宽度。
|
|
180
|
+
* small/medium/large 按编辑器内容区宽度的 25%/50%/75%;
|
|
181
|
+
* original 清除宽度属性,回归自然尺寸。仅 NodeSelection 生效。
|
|
182
|
+
*/
|
|
183
|
+
setImageSize: (preset: ImageSizePreset) => void;
|
|
184
|
+
/**
|
|
185
|
+
* 设置当前选中图片的题注。空串清除题注。仅 NodeSelection 生效。
|
|
186
|
+
*/
|
|
187
|
+
setImageCaption: (caption: string) => void;
|
|
188
|
+
/** 删除当前选中图片。仅 NodeSelection 生效。 */
|
|
189
|
+
removeImage: () => void;
|
|
190
|
+
insertTable: (rows?: number, cols?: number) => void;
|
|
191
|
+
/**
|
|
192
|
+
* 表格结构操作命令(Tiptap TableKit 全部内置,这里只做接线)。
|
|
193
|
+
*
|
|
194
|
+
* 设计为「无参 + 当前选区」:命令作用在光标所在单元格上,与 Tiptap 原生命令
|
|
195
|
+
* 语义一致。adapter 的工具栏在选中表格时才显示这些按钮(用 isActive('table') 判定),
|
|
196
|
+
* 所以 core 不需要在命令里判断「是否在表格内」——adapter 已保证只在有效场景暴露入口。
|
|
197
|
+
*
|
|
198
|
+
* 命名对齐 Tiptap(addRowAfter 等),降低心智成本。
|
|
199
|
+
*/
|
|
200
|
+
addRowBefore: () => void;
|
|
201
|
+
addRowAfter: () => void;
|
|
202
|
+
deleteRow: () => void;
|
|
203
|
+
addColumnBefore: () => void;
|
|
204
|
+
addColumnAfter: () => void;
|
|
205
|
+
deleteColumn: () => void;
|
|
206
|
+
/** 合并选区内单元格(需选中多个单元格,通常用拖拽或键盘选区) */
|
|
207
|
+
mergeCells: () => void;
|
|
208
|
+
/** 拆分已合并的单元格 */
|
|
209
|
+
splitCell: () => void;
|
|
210
|
+
/** 切换首行是否为表头(<th>) */
|
|
211
|
+
toggleHeaderRow: () => void;
|
|
212
|
+
/** 切换首列是否为表头 */
|
|
213
|
+
toggleHeaderColumn: () => void;
|
|
214
|
+
/** 删除整个表格 */
|
|
215
|
+
deleteTable: () => void;
|
|
216
|
+
/**
|
|
217
|
+
* 移动当前行/列(飞书式抓手菜单用)。基于 prosemirror-tables 的 moveTableRow/moveTableColumn。
|
|
218
|
+
* 已是首行/首列时 moveUp/Left 无效,已是末行/末列时 moveDown/Right 无效(命令静默返回)。
|
|
219
|
+
* 作用于当前选区所在行/列,与 addRow/deleteRow 同理。
|
|
220
|
+
*/
|
|
221
|
+
moveRowUp: () => void;
|
|
222
|
+
moveRowDown: () => void;
|
|
223
|
+
moveColumnLeft: () => void;
|
|
224
|
+
moveColumnRight: () => void;
|
|
225
|
+
/** 选中光标所在整行(CellSelection.rowSelection)——飞书式抓手点击行为 */
|
|
226
|
+
selectRow: () => void;
|
|
227
|
+
/** 选中光标所在整列(CellSelection.colSelection) */
|
|
228
|
+
selectColumn: () => void;
|
|
229
|
+
hr: () => void;
|
|
230
|
+
clearNodes: () => void;
|
|
231
|
+
/** 设置文字颜色;color 为空字符串则清除颜色 */
|
|
232
|
+
setColor: (color: string) => void;
|
|
233
|
+
/** 切换背景高亮;color 为空则清除高亮 */
|
|
234
|
+
toggleHighlight: (color: string) => void;
|
|
235
|
+
/** 设置文本对齐 */
|
|
236
|
+
align: (align: 'left' | 'center' | 'right' | 'justify') => void;
|
|
237
|
+
/** 下划线(StarterKit 自带 underline 扩展) */
|
|
238
|
+
underline: () => void;
|
|
239
|
+
/** 清除所有格式(节点类型 + marks) */
|
|
240
|
+
clearFormat: () => void;
|
|
241
|
+
/** 任务列表(toggle) */
|
|
242
|
+
taskList: () => void;
|
|
243
|
+
}
|
|
244
|
+
export type { ImageAlign, ImageSizePreset } from './extensions/image';
|
|
245
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ProEditorOptions, ProEditorContext } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* tiptap-vue-pro 的核心 composable。
|
|
4
|
+
*
|
|
5
|
+
* 职责:
|
|
6
|
+
* 1. 通过官方 useEditor 创建编辑器实例(必须用 useEditor,不能用 new Editor,
|
|
7
|
+
* 否则 vue-3 的 Editor 在构造时 reactiveState 初始化会崩)
|
|
8
|
+
* 2. 实现 v-model 双向绑定(支持 html / json 两种输出)
|
|
9
|
+
* 3. 聚合命令对象 commands,供工具栏按钮直接调用
|
|
10
|
+
* 4. 处理图片上传(从 File 到 url 再到文档插入)
|
|
11
|
+
* 5. 字数统计、只读切换
|
|
12
|
+
*
|
|
13
|
+
* 不做的事(故意下推给 adapter):
|
|
14
|
+
* - 任何 DOM 渲染、UI 组件
|
|
15
|
+
* - toast / message 提示(上传失败等由 adapter 用各自 UI 库展示)
|
|
16
|
+
*
|
|
17
|
+
* 用法:adapter 组件在 setup 里调用此 composable,把返回的 editor 传给 EditorContent。
|
|
18
|
+
*/
|
|
19
|
+
export declare function useProEditor(options: ProEditorOptions): ProEditorContext;
|
|
20
|
+
//# sourceMappingURL=useProEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProEditor.d.ts","sourceRoot":"","sources":["../src/useProEditor.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAIjB,MAAM,SAAS,CAAA;AAEhB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,gBAAgB,CAgfxE"}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tiptap-vue-pro-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "tiptap-vue-pro 的 UI 无关核心层:封装 Tiptap v3,提供 composable 与类型契约",
|
|
5
|
+
"author": "twoer",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"tiptap",
|
|
9
|
+
"vue",
|
|
10
|
+
"vue3",
|
|
11
|
+
"rich-text",
|
|
12
|
+
"editor",
|
|
13
|
+
"wysiwyg",
|
|
14
|
+
"prosemirror",
|
|
15
|
+
"headless"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/twoer/tiptap-vue-pro#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/twoer/tiptap-vue-pro",
|
|
21
|
+
"directory": "packages/core"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/twoer/tiptap-vue-pro/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/index.umd.cjs",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"require": "./dist/index.umd.cjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@tiptap/core": "^3.0.0",
|
|
47
|
+
"@tiptap/pm": "^3.0.0",
|
|
48
|
+
"@tiptap/vue-3": "^3.0.0",
|
|
49
|
+
"vue": "^3.4.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@tiptap/core": "3.27.1",
|
|
53
|
+
"@tiptap/extension-character-count": "3.27.1",
|
|
54
|
+
"@tiptap/extension-color": "^3.27.1",
|
|
55
|
+
"@tiptap/extension-highlight": "^3.27.1",
|
|
56
|
+
"@tiptap/extension-image": "3.27.1",
|
|
57
|
+
"@tiptap/extension-list": "^3.27.1",
|
|
58
|
+
"@tiptap/extension-placeholder": "3.27.1",
|
|
59
|
+
"@tiptap/extension-table": "3.27.1",
|
|
60
|
+
"@tiptap/extension-text-align": "^3.27.1",
|
|
61
|
+
"@tiptap/extension-text-style": "^3.27.1",
|
|
62
|
+
"@tiptap/markdown": "3.27.1",
|
|
63
|
+
"@tiptap/pm": "3.27.1",
|
|
64
|
+
"@tiptap/starter-kit": "3.27.1",
|
|
65
|
+
"@tiptap/vue-3": "3.27.1"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/node": "^26.0.1",
|
|
69
|
+
"@vitejs/plugin-vue": "^5.2.4",
|
|
70
|
+
"@vue/test-utils": "^2.4.11",
|
|
71
|
+
"happy-dom": "^20.10.6",
|
|
72
|
+
"vite": "^5.4.0",
|
|
73
|
+
"vite-plugin-dts": "^4.0.0",
|
|
74
|
+
"vitest": "^1.6.1",
|
|
75
|
+
"vue": "^3.5.0",
|
|
76
|
+
"vue-tsc": "^2.1.0"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "vite build",
|
|
80
|
+
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
|
81
|
+
"test": "vitest run",
|
|
82
|
+
"test:watch": "vitest"
|
|
83
|
+
}
|
|
84
|
+
}
|