Skip to content

Commit c8b43ee

Browse files
authored
feat(rich-text-editor): tiny-tiptap and some extension (#1764)
* feat(rich-text-editor): tiny-tiptap and some extension * fix(rich-text-editor): remove spread in reduce
1 parent 4a863b9 commit c8b43ee

File tree

32 files changed

+1040
-0
lines changed

32 files changed

+1040
-0
lines changed

packages/tiny-tiptap/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# 使用文档
2+
3+
## 引入
4+
5+
```js
6+
import TinyTiptap from '@opentiny/tiny-tiptap'
7+
```
8+
9+
## 初始化实例
10+
11+
```js
12+
new TinyTiptap(editorClass, options, viewMap, viewRender)
13+
```
14+
15+
## 参数配置
16+
17+
### TinyTiptap
18+
19+
| 名称 | 类型 | 默认值 | 说明 |
20+
| ------------ | ------ | ------ | ------------------------------------------------------------------- |
21+
| editorClass | Editor | Editor | 根据使用环境传递的 Editor 类,默认值为 Tiptap 的 core 包中的 Editor |
22+
| options | Object | {} | 参见 Tiptap 扩展说明 |
23+
| viewOptions | Object | {} | viewMap 与 menuMap |
24+
| otherOptions | Object | {} | 包括其他的一些配置比如 placeholder 待后续补充 |
25+
26+
#### viewMap
27+
28+
Map 类型,主要包括 key(扩展名) -> view(视图) 的映射
29+
30+
#### menuMap
31+
32+
| 名称 | 类型 | 默认值 | 说明 |
33+
| --------- | ------ | ------ | ---------------------------- |
34+
| renderer | Object | - | 根据使用环境传递的视图渲染器 |
35+
| slashView | View | {} | 定制化的斜杠菜单视图 |
36+
37+
## 使用示例
38+
39+
```js
40+
import { VueNodeViewRenderer, VueRenderer } from '@tiptap/vue'
41+
import ImageView from './components/image-view.vue'
42+
43+
// 设置扩展到视图的映射
44+
const viewMap = new Map([['image', VueNodeViewRenderer(ImageView)]])
45+
const menuMap = {
46+
renderer: VueRenderer,
47+
slashView
48+
}
49+
50+
const viewOptions = {
51+
viewMap,
52+
menuMap
53+
}
54+
55+
const otherOptions = {
56+
placeholder: '自定义占位符'
57+
}
58+
59+
const editorInstance = new TinyTiptap(Editor, options, viewOptions, otherOptions)
60+
```

packages/tiny-tiptap/package.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@opentiny/tiny-tiptap",
3+
"private": true,
4+
"version": "1.0.0",
5+
"main": "src/index.ts",
6+
"license": "MIT",
7+
"sideEffects": false,
8+
"type": "module",
9+
"dependencies": {
10+
"@tiptap/core": "~2.1.16",
11+
"@tiptap/extension-blockquote": "~2.1.16",
12+
"@tiptap/extension-bold": "~2.1.16",
13+
"@tiptap/extension-bullet-list": "~2.1.16",
14+
"@tiptap/extension-code": "~2.1.16",
15+
"@tiptap/extension-code-block-lowlight": "~2.1.16",
16+
"@tiptap/extension-collaboration": "~2.1.16",
17+
"@tiptap/extension-color": "~2.1.16",
18+
"@tiptap/extension-document": "~2.1.16",
19+
"@tiptap/extension-heading": "~2.1.16",
20+
"@tiptap/extension-highlight": "~2.1.16",
21+
"@tiptap/extension-history": "~2.1.16",
22+
"@tiptap/extension-image": "~2.1.16",
23+
"@tiptap/extension-italic": "~2.1.16",
24+
"@tiptap/extension-link": "~2.1.16",
25+
"@tiptap/extension-list-item": "~2.1.16",
26+
"@tiptap/extension-ordered-list": "~2.1.16",
27+
"@tiptap/extension-paragraph": "~2.1.16",
28+
"@tiptap/extension-placeholder": "~2.1.16",
29+
"@tiptap/extension-strike": "~2.1.16",
30+
"@tiptap/extension-subscript": "~2.1.16",
31+
"@tiptap/extension-superscript": "~2.1.16",
32+
"@tiptap/extension-table": "~2.1.16",
33+
"@tiptap/extension-table-cell": "~2.1.16",
34+
"@tiptap/extension-table-header": "~2.1.16",
35+
"@tiptap/extension-table-row": "~2.1.16",
36+
"@tiptap/extension-task-item": "~2.1.16",
37+
"@tiptap/extension-task-list": "~2.1.16",
38+
"@tiptap/extension-text": "~2.1.16",
39+
"@tiptap/extension-text-align": "~2.1.16",
40+
"@tiptap/extension-text-style": "~2.1.16",
41+
"@tiptap/extension-underline": "~2.1.16",
42+
"@tiptap/pm": "~2.1.16",
43+
"@tiptap/starter-kit": "~2.1.16",
44+
"@tiptap/suggestion": "~2.1.16",
45+
"highlight.js": "^11.8.0",
46+
"lowlight": "^2.9.0",
47+
"tippy.js": "^6.3.7"
48+
},
49+
"scripts": {
50+
"build": "pnpm -w build:ui $npm_package_name",
51+
"//postversion": "pnpm build"
52+
}
53+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Content, Editor, Extension, Node, NodeViewRenderer } from '@tiptap/core'
2+
import { extensions } from './extension'
3+
4+
import { generateSlashMenuExtension } from './components/slash'
5+
6+
export default class TiptapEditor {
7+
options: any
8+
editor: Editor
9+
extensions: any[]
10+
/**
11+
* @param editorClass Editor 的构造类
12+
* @param options Editor 的配置项
13+
* @param viewOptions
14+
* @param viewOptions.viewMap 需要进行视图映射的扩展组件 由 key 和 view 组成
15+
* @param viewOptions.menuMap 包括映射方法以及菜单等组件
16+
* @param otherOptions
17+
*/
18+
constructor(editorClass, options = {}, viewOptions, otherOptions) {
19+
this.extensions = extensions
20+
const { viewMap, menuMap } = viewOptions
21+
22+
this.initExtensionViews(viewMap)
23+
24+
this.initMenuViews(menuMap)
25+
26+
this.initOtherOptions(otherOptions)
27+
28+
// TODO 合并用户传入的 options
29+
this.options = {
30+
...options,
31+
...{
32+
extensions: this.extensions
33+
}
34+
}
35+
36+
this.editor = this.createEditor(editorClass, this.options)
37+
}
38+
39+
createEditor(editorClass, options) {
40+
return new editorClass(options)
41+
}
42+
43+
getContent() {
44+
return this.editor.getHTML()
45+
}
46+
47+
setContent(content: Content) {
48+
this.editor.commands.setContent(content)
49+
}
50+
51+
destroy() {
52+
this.editor.destroy()
53+
}
54+
55+
private initExtensionViews(viewMap: Map<string, any>) {
56+
// 根据传入的视图构建 重新继承扩展并重新 configure
57+
viewMap.forEach((view, key) => {
58+
const extensionIndex = this.extensions.findIndex((extension) => extension.name === key)
59+
const extension = this.extensions.at(extensionIndex)
60+
if (extension) {
61+
const originOption = extension.options
62+
const newExtension = extension
63+
.extend({
64+
addNodeView() {
65+
return view
66+
// return () => ({ dom: view })
67+
}
68+
})
69+
.configure({ ...originOption })
70+
this.extensions[extensionIndex] = newExtension
71+
}
72+
})
73+
}
74+
75+
private initMenuViews(menuMap) {
76+
const { renderer: Renderer, slashView } = menuMap
77+
78+
const slashMenu = generateSlashMenuExtension(Renderer, slashView)
79+
this.extensions.push(slashMenu)
80+
}
81+
82+
private initOtherOptions(otherOptions) {
83+
const { placeholder } = otherOptions
84+
const placeholderIndex = this.extensions.findIndex((extension) => extension.name === 'placeholder')
85+
const placeholderExtension = this.extensions[placeholderIndex]
86+
if (placeholderExtension) {
87+
this.extensions[placeholderIndex] = placeholderExtension.configure({
88+
placeholder
89+
})
90+
}
91+
}
92+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { AnyExtension, Editor, Extension, Range } from '@tiptap/core'
2+
import Suggestion from '@tiptap/suggestion'
3+
import { SlashMenuItem } from '@/types'
4+
import type { Instance } from 'tippy.js'
5+
import tippy from 'tippy.js'
6+
7+
export const generateSlashMenuExtension = (Renderer, view) => {
8+
return Extension.create({
9+
name: 'slashMenu',
10+
11+
addProseMirrorPlugins() {
12+
const slashMenuItems = getSlashMenuItemsFromExtensions(this.editor)
13+
14+
return [
15+
Suggestion({
16+
editor: this.editor,
17+
char: '/',
18+
command: ({ editor, range, props }: { editor: Editor; range: Range; props: SlashMenuItem }) => {
19+
props.command({ editor, range })
20+
},
21+
items: ({ query }: { query: string }) => {
22+
return slashMenuItems.filter((item) =>
23+
[...item.keywords, item.title].some((keyword) => keyword.includes(query.toLocaleLowerCase()))
24+
)
25+
},
26+
render: () => {
27+
let popup: Instance[]
28+
let viewComponent
29+
30+
return {
31+
onStart: (props: Record<string, any>) => {
32+
viewComponent = new Renderer(view, {
33+
props,
34+
editor: props.editor
35+
})
36+
37+
if (!props.clientRect) return
38+
39+
popup = tippy('body', {
40+
getReferenceClientRect: props.clientRect,
41+
appendTo: () => document.body,
42+
content: viewComponent.element,
43+
showOnCreate: true,
44+
interactive: true,
45+
trigger: 'manual',
46+
placement: 'bottom-start'
47+
})
48+
},
49+
50+
onUpdate: (props: Record<string, any>) => {
51+
viewComponent.updateProps(props)
52+
53+
if (!props.clientRect) return
54+
55+
popup[0].setProps({
56+
getReferenceClientRect: props.clientRect
57+
})
58+
},
59+
60+
onKeyDown: (props: Record<string, any>) => {
61+
if (props.event.key === 'Escape') {
62+
popup[0].hide()
63+
64+
return true
65+
}
66+
67+
return viewComponent.ref?.onKeyDown(props)
68+
},
69+
70+
onExit() {
71+
popup[0].destroy()
72+
viewComponent.destroy()
73+
}
74+
}
75+
}
76+
})
77+
]
78+
}
79+
})
80+
}
81+
82+
function getSlashMenuItemsFromExtensions(editor: Editor) {
83+
const extensionManager = editor.extensionManager
84+
return (extensionManager?.extensions ?? [])
85+
.reduce((prev: SlashMenuItem[], curr: AnyExtension) => {
86+
const { getSlashMenus } = curr.options
87+
88+
if (!getSlashMenus) return prev
89+
90+
const menus = getSlashMenus()
91+
92+
prev.push(...menus)
93+
return prev
94+
}, [])
95+
.sort((a, b) => a.priority - b.priority)
96+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ExtensionOptions } from '@/types'
2+
import { Editor, Range } from '@tiptap/core'
3+
import type { BlockquoteOptions } from '@tiptap/extension-blockquote'
4+
import TiptapBlockquote from '@tiptap/extension-blockquote'
5+
import { iconRichTextQuoteText } from '@opentiny/vue-icon'
6+
7+
const Blockquote = TiptapBlockquote.extend<ExtensionOptions & BlockquoteOptions>({
8+
addOptions() {
9+
return {
10+
...this.parent?.(),
11+
getSlashMenus() {
12+
return [
13+
{
14+
priority: 50,
15+
icon: iconRichTextQuoteText(),
16+
title: '内容引用',
17+
keywords: ['quote', 'neirongyinyong'],
18+
command: ({ editor, range }: { editor: Editor; range: Range }) => {
19+
editor.chain().focus().deleteRange(range).toggleBlockquote().run()
20+
}
21+
}
22+
]
23+
}
24+
}
25+
}
26+
})
27+
28+
export default Blockquote
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ExtensionOptions } from '@/types'
2+
import type { BoldOptions } from '@tiptap/extension-bold'
3+
import TiptapBold from '@tiptap/extension-bold'
4+
5+
const Bold = TiptapBold.extend<ExtensionOptions & BoldOptions>({
6+
addOptions() {
7+
return {
8+
...this.parent?.()
9+
}
10+
}
11+
})
12+
13+
export default Bold
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { ExtensionOptions } from '@/types'
2+
import type { Editor, Range } from '@tiptap/core'
3+
import type { BulletListOptions } from '@tiptap/extension-bullet-list'
4+
import TiptapBulletList from '@tiptap/extension-bullet-list'
5+
import ListItem from '@tiptap/extension-list-item'
6+
import { iconRichTextListUnordered } from '@opentiny/vue-icon'
7+
8+
const BulletList = TiptapBulletList.extend<ExtensionOptions & BulletListOptions>({
9+
addOptions() {
10+
return {
11+
...this.parent?.(),
12+
getSlashMenus() {
13+
return [
14+
{
15+
priority: 20,
16+
icon: iconRichTextListUnordered(),
17+
title: '无序列表',
18+
keywords: ['bulletlist', 'wuxuliebiao'],
19+
command: ({ editor, range }: { editor: Editor; range: Range }) => {
20+
editor.chain().focus().deleteRange(range).toggleBulletList().run()
21+
}
22+
}
23+
]
24+
}
25+
}
26+
},
27+
addExtensions() {
28+
return [ListItem]
29+
}
30+
})
31+
32+
export default BulletList

0 commit comments

Comments
 (0)