feat(rich-text-editor): Add api design (#475)

* feat(rich-text-editor): Add api design

* feat(rich-text-editor): change code

* feat(rich-text-editor): delete log

---------

Co-authored-by: 常浩-BJS21325 <changhao01@youdao>
This commit is contained in:
Caesar-ch 2023-09-14 10:17:44 +08:00 committed by GitHub
parent 7d6120ed1d
commit 51c46cbd42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 581 additions and 221 deletions

View File

@ -23,11 +23,140 @@ export default {
'en-US': 'default rich text content' 'en-US': 'default rich text content'
}, },
demoId: 'basic-usage' demoId: 'basic-usage'
} },
{
'name': 'collaboration',
'type': 'Boolean',
'defaultValue': 'false',
desc: {
'zh-CN': '是否开启协同编辑,默认不开启',
'en-US': 'Whether to enable collaborative editing. It is disabled by default'
},
demoId: 'basic-usage'
},
{
'name': 'customToolBar',
'type': 'Array',
'defaultValue': '[]',
desc: {
'zh-CN': '传入需要展示的工具栏按钮配置,自定义使用',
'en-US': 'Pass in the toolbar button configuration that needs to be displayed, and customize the use'
},
demoId: 'basic-usage'
},
{
'name': 'placeholder',
'type': 'Stirng',
'defaultValue': 'Write soming ...',
desc: {
'zh-CN': '占位符在v-model为空时展示',
'en-US': 'Placeholder, displayed when v-model is empty'
},
demoId: 'basic-usage'
},
{
'name': 'options',
'type': 'Object',
'defaultValue': '{}',
desc: {
'zh-CN': '参见tiptap扩展说明会覆盖useEditor配置项',
'en-US': 'See tiptap extension notes to overwrite the useEditor configuration item'
},
demoId: 'basic-usage'
},
],
'events': [
{
'name': 'update',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器状态改变完成后将会触发该事件,
'en-US': 'When the content is updated.'
},
'demoId': 'base'
},
{
'name': 'beforeCreate',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器视图创造之前,将会触发该事件',
'en-US': 'Before view creation.'
},
'demoId': 'base'
},
{
'name': 'create',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器已经挂载好,将会触发该事件',
'en-US': 'The editor is mounted.'
},
'demoId': 'base'
},
{
'name': 'focus',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器获得焦点,将会触发该事件',
'en-US': 'The editor gets focus.'
},
'demoId': 'base'
},
{
'name': 'blur',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器失去焦点,将会触发该事件',
'en-US': 'The editor loses focus.'
},
'demoId': 'base'
},
{
'name': 'selectionUpdate',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器选区改变,将会触发该事件',
'en-US': 'The selection has changed.'
},
'demoId': 'base'
},
{
'name': 'transaction',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器状态改变,将会触发该事件。',
'en-US': 'The editor state has changed.'
},
'demoId': 'base'
},
{
'name': 'destroy',
'type': '',
'defaultValue': '',
'desc': {
'zh-CN': '当编辑器编辑器销毁了,将会触发该事件',
'en-US': 'The editor is being destroyed.'
},
'demoId': 'base'
},
], ],
'events': [],
'methods': [], 'methods': [],
'slots': [] 'slots': [
{
'name': 'toolBar',
'type': '',
'defaultValue': '',
'desc': { 'zh-CN': 'toolBar添加按钮,会传出editor实例详情见tiptap', 'en-US': 'toolBar Add Button' },
'demoId': 'custom-search-types'
},
]
} }
] ]
} }

View File

@ -1,57 +1,50 @@
export const handleChange = (editor) => { export const handleChange = (editor) => {
return (event) => { return (event) => {
const file = event.target.files[0] const file = event.target.files[0]
if (!file.type.match("image.*")) { if (!file.type.match('image.*')) {
console.log("请选择图片文件!") console.log('请选择图片文件!')
return return
} }
const reader = new FileReader() const reader = new FileReader()
reader.onload = function (e) { reader.onload = function (e) {
editor.value.chain().focus().setImage({ src: e.target?.result }).run() editor.value
.chain()
.focus()
.setImage({ src: e.target?.result })
.run()
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
} }
} }
export const setLink = (editor) => { export const setLink = (editor) => {
return () => { return () => {
const previousUrl = editor.value.getAttributes('link').href const previousUrl = editor.getAttributes('link').href
const url = window.prompt('URL', previousUrl) const url = window.prompt('URL', previousUrl)
if (url === null) { if (url === null) {
return return
} }
if (url === '') { if (url === '') {
editor.value editor.chain().focus().extendMarkRange('link').unsetLink().run()
.chain()
.focus()
.extendMarkRange('link')
.unsetLink()
.run()
return return
} }
editor.value editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: url })
.run()
} }
} }
// table 处理逻辑 // table 处理逻辑
export const handleMove = (state, box) => { export const handleMove = (state, box) => {
return (e) => { return (e) => {
let { x, y } = box.value.getBoundingClientRect() let { x, y } = box.value[0].getBoundingClientRect()
state.flagX = Math.ceil((e.x - x) / 30) // 后期改变30就可以 state.flagX = Math.ceil((e.x - x) / 30) // 后期改变30就可以
state.flagY = Math.ceil((e.y - y) / 30) state.flagY = Math.ceil((e.y - y) / 30)
} }
} }
export const handleClickOutside = (state, box) => { export const handleClickOutside = (state, box) => {
return (e) => { return (e) => {
if (!box.value?.contains(e.target)) { if (!box.value[0]?.contains(e.target)) {
state.isShow = false state.isShow = false
removeClickOutside(state, box)() removeClickOutside(state, box)()
} }
} }
} }
export const removeClickOutside = (state, box) => { export const removeClickOutside = (state, box) => {
return () => { return () => {
@ -60,7 +53,7 @@ export const removeClickOutside = (state, box) => {
} }
export const handleClick = (state, box) => { export const handleClick = (state, box) => {
return (e) => { return (e) => {
e.stopPropagation(); e.stopPropagation()
if (state.isShow) { if (state.isShow) {
if (state.flagX && state.flagY) { if (state.flagX && state.flagY) {
state.editor.chain().focus().insertTable({ rows: state.flagX, cols: state.flagY, withHeaderRow: true }).run() state.editor.chain().focus().insertTable({ rows: state.flagX, cols: state.flagY, withHeaderRow: true }).run()
@ -77,13 +70,165 @@ export const handleClick = (state, box) => {
// bubble菜单 // bubble菜单
export const shouldShow = ({ editor, view, state, oldState, from, to }) => { export const shouldShow = ({ editor, view, state, oldState, from, to }) => {
// 仅在无序列表选中的时候才显示 气泡菜单 // 仅在无序列表选中的时候才显示 气泡菜单
return editor.isActive("table"); return editor.isActive('table')
}; }
// font-size 设置 // font-size 设置
export const handleFontSize = (fontSize) => { export const handleFontSize = (fontSize) => {
return (value) => { return (value) => {
console.log('123', value); fontSize.value = value + 'px'
fontSize.value = value + "px";
} }
} }
// 处理参数实现自定义展示
const eventMap = new Map()
eventMap.set('bold', (editor) => {
editor.chain().focus().toggleBold().run()
})
eventMap.set('italic', (editor) => {
editor.chain().focus().toggleItalic().run()
})
eventMap.set('link', (editor) => {
setLink(editor)()
})
eventMap.set('unlink', (editor) => {
editor.chain().focus().unsetLink().run()
})
eventMap.set('highlight', (editor) => {
editor.chain().focus().toggleHighlight().run()
})
eventMap.set('underline', (editor) => {
editor.chain().focus().toggleUnderline().run()
})
eventMap.set('strike', (editor) => {
editor.chain().focus().toggleStrike().run()
})
eventMap.set('subscript', (editor) => {
editor.chain().focus().toggleSubscript().run()
})
eventMap.set('superscript', (editor) => {
editor.chain().focus().toggleSuperscript().run()
})
eventMap.set('code', (editor) => {
editor.chain().focus().toggleCode().run()
})
eventMap.set('unorderedlist', (editor) => {
editor.chain().focus().toggleBulletList().run()
})
eventMap.set('orderedlist', (editor) => {
editor.chain().focus().toggleOrderedList().run()
})
eventMap.set('taskList', (editor) => {
editor.chain().focus().toggleTaskList().run()
})
eventMap.set('quote', (editor) => {
editor.chain().focus().toggleBlockquote().run()
})
eventMap.set('code-block', (editor) => {
editor.chain().focus().toggleCodeBlock().run()
})
eventMap.set('format-clear', (editor) => {
editor.chain().focus().unsetAllMarks().run()
})
eventMap.set('node-delete', (editor) => {
editor.chain().focus().clearNodes().run()
})
eventMap.set('undo', (editor) => {
editor.chain().focus().undo().run()
})
eventMap.set('redo', (editor) => {
editor.chain().focus().redo().run()
})
eventMap.set('left', (editor) => {
editor.chain().focus().setTextAlign('left').run()
})
eventMap.set('center', (editor) => {
editor.chain().focus().setTextAlign('center').run()
})
eventMap.set('right', (editor) => {
editor.chain().focus().setTextAlign('right').run()
})
export const eventClick = (editor, item) => {
if (typeof item === 'string') {
eventMap.get(item)(editor)
} else {
eventMap.get(item.name)(editor)
}
}
const imgMap = new Map()
imgMap.set('bold', 'TinyIconRichTextBold')
imgMap.set('italic', 'TinyIconRichTextItalic')
imgMap.set('link', 'TinyIconRichTextLink')
imgMap.set('unlink', 'TinyIconRichTextLinkUnlink')
imgMap.set('highlight', 'TinyIconRichTextHighLight')
imgMap.set('underline', 'TinyIconRichTextUnderline')
imgMap.set('strike', 'TinyIconRichTextStrikeThrough')
imgMap.set('subscript', 'TinyIconRichTextSubscript')
imgMap.set('superscript', 'TinyIconRichTextSuperscript')
imgMap.set('code', 'TinyIconRichTextCodeView')
imgMap.set('unorderedlist', 'TinyIconRichTextListUnordered')
imgMap.set('orderedlist', 'TinyIconRichTextListOrdered')
imgMap.set('taskList', 'TinyIconRichTextTaskList')
imgMap.set('quote', 'TinyIconRichTextQuoteText')
imgMap.set('code-block', 'TinyIconRichTextCodeBlock')
imgMap.set('format-clear', 'TinyIconRichTextFormatClear')
imgMap.set('node-delete', 'TinyIconRichTextNodeDelete')
imgMap.set('undo', 'TinyIconRichTextUndo')
imgMap.set('redo', 'TinyIconRichTextRedo')
imgMap.set('left', 'TinyIconRichTextAlignLeft')
imgMap.set('center', 'TinyIconRichTextAlignCenter')
imgMap.set('right', 'TinyIconRichTextAlignRight')
export const eventImg = (item) => {
// 判断是否有图片
// 有: 直接返回
if (typeof item === 'string') {
return imgMap.get(item)
} else if (item.img) {
return item.img
} else {
return imgMap.get(item.name)
}
}
export const Active = (item) => {
let result = ''
if (item.name) {
switch (item.name) {
case 'unlink':
result = 'link'
break
case 'left':
result = { textAlign: 'left' }
break
case 'center':
result = { textAlign: 'center' }
break
case 'right':
result = { textAlign: 'right' }
break
default:
result = item.name
break
}
} else {
switch (item) {
case 'unlink':
result = 'link'
break
case 'left':
result = { textAlign: 'left' }
break
case 'center':
result = { textAlign: 'center' }
break
case 'right':
result = { textAlign: 'right' }
break
default:
result = item
break
}
}
return result
}

View File

@ -1,13 +1,107 @@
import { handleChange, setLink, handleMove, handleClickOutside, removeClickOutside, handleClick, shouldShow, handleFontSize } from './index' import {
handleChange,
setLink,
handleMove,
handleClickOutside,
removeClickOutside,
handleClick,
shouldShow,
handleFontSize,
eventImg,
eventClick,
Active
} from './index'
import Codehighlight from './code-highlight' import Codehighlight from './code-highlight'
export const api = ['state', 'setLink', 'handleChange', 'box', 'handleMove', 'handleClickOutside', 'removeClickOutside', 'handleClick', 'shouldShow', 'handleFontSize', 'fontSize'] export const api = [
'toolBar',
'state',
'setLink',
'handleChange',
'box',
'handleMove',
'handleClickOutside',
'removeClickOutside',
'handleClick',
'shouldShow',
'handleFontSize',
'fontSize',
'eventImg',
'eventClick',
'Active'
]
export const renderless = ( export const renderless = (
props, props,
{ computed, onMounted, onBeforeUnmount, reactive, ref }, { computed, onMounted, onBeforeUnmount, reactive, ref },
{ vm, emit, parent }, { vm, emit, parent },
{ useEditor, Collaboration, Y, WebrtcProvider, StarterKit, Table, TableCell, TableHeader, TableRow, Color, TextStyle, Image, Highlight, Link, Underline, Subscript, Superscript, TaskItem, TaskList, TextAlign, Paragraph, mergeAttributes, CodeBlockLowlight, lowlight, VueNodeViewRenderer, NodeViewContent, nodeViewProps, NodeViewWrapper } {
useEditor,
Collaboration,
Y,
WebrtcProvider,
StarterKit,
Table,
TableCell,
TableHeader,
TableRow,
Color,
TextStyle,
Image,
Highlight,
Link,
Underline,
Subscript,
Superscript,
TaskItem,
TaskList,
TextAlign,
Paragraph,
mergeAttributes,
CodeBlockLowlight,
lowlight,
VueNodeViewRenderer,
NodeViewContent,
nodeViewProps,
NodeViewWrapper,
Placeholder
}
) => { ) => {
const ydoc = new Y.Doc() let toolBar = [
'bold',
'italic',
'link',
'unlink',
'highlight',
'underline',
'strike',
'subscript',
'superscript',
'code',
'unorderedlist',
'orderedlist',
'taskList',
'quote',
'code-block',
'format-clear',
'node-delete',
'undo',
'redo',
'left',
'center',
'right',
'font-size',
'line-height',
'h-box',
'img',
'color',
'table'
]
if (props.customToolBar) {
toolBar = props.customToolBar
}
if (!window._yDoc) {
window._yDoc = new Y.Doc()
}
const ydoc = window._yDoc
const provider = new WebrtcProvider('tiny-examsple-document', ydoc) const provider = new WebrtcProvider('tiny-examsple-document', ydoc)
// 自定义图片 // 自定义图片
const CustomImage = Image.extend({ const CustomImage = Image.extend({
@ -18,46 +112,48 @@ export const renderless = (
const CustomParagraph = Paragraph.extend({ const CustomParagraph = Paragraph.extend({
addOptions() { addOptions() {
return { return {
levels: [1, 1.5, 2, 2.5, 3], levels: [1, 1.5, 2, 2.5, 3]
} }
}, },
addAttributes() { addAttributes() {
return { return {
level: { level: {
default: 1, default: 1
}, }
} }
}, },
renderHTML({ node, HTMLAttributes }) { renderHTML({ node, HTMLAttributes }) {
const hasLevel = this.options.levels.includes(node.attrs.level) const hasLevel = this.options.levels.includes(node.attrs.level)
const level = hasLevel const level = hasLevel ? node.attrs.level : this.options.levels[0]
? node.attrs.level
: this.options.levels[0]
console.log('2', node, HTMLAttributes, this.options);
return ['p', mergeAttributes({ style: `line-height: ${level}` }, HTMLAttributes), 0] return ['p', mergeAttributes({ style: `line-height: ${level}` }, HTMLAttributes), 0]
}, },
addCommands() { addCommands() {
return { return {
setP: attributes => ({ commands }) => { setP:
return commands.setNode(this.name, attributes) (attributes) =>
}, ({ commands }) => {
return commands.setNode(this.name, attributes)
}
} }
}, }
}) })
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
StarterKit?.configure({ StarterKit?.configure({
// 开启多人协作功能要关闭默认的history模式 // 开启多人协作功能要关闭默认的history模式
history: false, history: false
}), }),
Collaboration?.configure({ Collaboration?.configure({
document: ydoc, document: ydoc
}), }),
Table.configure({ Table.configure({
resizable: true, resizable: true
}), }),
TableCell, TableHeader, TableRow, TableCell,
Color, TextStyle, TableHeader,
TableRow,
Color,
TextStyle,
CustomImage, CustomImage,
Highlight, Highlight,
Link, Link,
@ -66,24 +162,65 @@ export const renderless = (
Superscript, Superscript,
TaskList, TaskList,
TaskItem.configure({ TaskItem.configure({
nested: true, nested: true
}), }),
TextAlign.configure({ TextAlign.configure({
types: ['heading', 'paragraph'], types: ['heading', 'paragraph']
}), }),
CustomParagraph, CustomParagraph,
CodeBlockLowlight.extend({ CodeBlockLowlight.extend({
addNodeView() { addNodeView() {
return VueNodeViewRenderer(Codehighlight(NodeViewContent, nodeViewProps, NodeViewWrapper)) return VueNodeViewRenderer(Codehighlight(NodeViewContent, nodeViewProps, NodeViewWrapper))
}, }
}).configure({ lowlight }),
Placeholder.configure({
placeholder: props.placeholder ?? 'Write something …'
}) })
.configure({ lowlight }),
], ],
content: 'Example Tesxt', content: '',
autofocus: true, autofocus: true,
editable: true, editable: true,
injectCSS: false, injectCSS: false,
// 事件
onBeforeCreate({ editor }) {
emit('beforeCreate', { editor })
},
onCreate({ editor }) {
emit('create', { editor })
},
onUpdate({ editor }) {
const json = editor.getJSON()
const html = editor.getHTML()
const text = editor.getText()
// 可传入参数 blockSeparator 控制节点之间的连接
const lineText = editor.getText({ blockSeparator: '--' })
// console.log(json)
// console.log(html)
// console.log(text)
// console.log(lineText) // 文本一行内展示,可设置连接符,只能获得文本
emit('update', { editor })
emit('update:modelValue', html)
},
onFocus({ editor, event }) {
emit('focus', { editor, event })
},
onBlur({ editor, event }) {
emit('blur', { editor, event })
},
onSelectionUpdate({ editor }) {
// The selection has changed.
emit('selectionUpdate', { editor })
},
onTransaction({ editor, transaction }) {
// The editor state has changed.
emit('transaction', { editor, transaction })
},
onDestroy() {
// The editor is being destroyed.
emit('destroy')
},
...props.options
}) })
const box = ref(null) const box = ref(null)
@ -93,10 +230,11 @@ export const renderless = (
// table 变量 // table 变量
isShow: false, isShow: false,
flagX: 0, flagX: 0,
flagY: 0, flagY: 0
}) })
state.editor = editor state.editor = editor
const api = { const api = {
toolBar,
state, state,
setLink: setLink(editor), setLink: setLink(editor),
handleChange: handleChange(editor), handleChange: handleChange(editor),
@ -111,6 +249,9 @@ export const renderless = (
// //
fontSize, fontSize,
handleFontSize: handleFontSize(fontSize), handleFontSize: handleFontSize(fontSize),
eventImg,
eventClick,
Active
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
state.editor.destroy() state.editor.destroy()

View File

@ -21,6 +21,7 @@
&:not(:disabled):hover, &:not(:disabled):hover,
&.is-active { &.is-active {
background-color: #d2e4ff;
svg, svg,
path { path {
@ -103,7 +104,7 @@
background: white; background: white;
display: flex; display: flex;
left: 0; left: 0;
top: 1.00rem; top: 1.25rem;
z-index: 999; z-index: 999;
width: 90px; width: 90px;
flex-wrap: wrap; flex-wrap: wrap;
@ -511,4 +512,12 @@
} }
} }
} }
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #9a9a9a;
pointer-events: none;
height: 0;
}
} }

View File

@ -36,6 +36,7 @@
"@tiptap/extension-code-block-lowlight": "^2.0.4", "@tiptap/extension-code-block-lowlight": "^2.0.4",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"lowlight": "^2.9.0", "lowlight": "^2.9.0",
"@tiptap/extension-placeholder": "^2.1.7",
"@opentiny/vue-common": "workspace:~", "@opentiny/vue-common": "workspace:~",
"@opentiny/vue-renderless": "workspace:~", "@opentiny/vue-renderless": "workspace:~",
"@opentiny/vue-theme": "workspace:~" "@opentiny/vue-theme": "workspace:~"

View File

@ -2,170 +2,102 @@
<div class="tiny-rich-text-editor"> <div class="tiny-rich-text-editor">
<div class="tiny-rich-text-editor__toolbar"> <div class="tiny-rich-text-editor__toolbar">
<!-- starter-kit功能区 --> <!-- starter-kit功能区 -->
<button title="bold" @click="state.editor.chain().focus().toggleBold().run()" <template v-for="item in toolBar">
:class="{ 'is-active': state.editor?.isActive('bold') }"> <button v-if="(item.name ?? item) === 'font-size'" class="font-size-box">
<TinyIconRichTextBold></TinyIconRichTextBold> <TinyIconRichTextFontSize></TinyIconRichTextFontSize>
</button> <div class="font-size-options">
<button class="font-size-box"> <button @click="handleFontSize(12)">12px</button>
<TinyIconRichTextFontSize></TinyIconRichTextFontSize> <button @click="handleFontSize(14)">14px</button>
<div class="font-size-options"> <button @click="handleFontSize(16)">16px</button>
<button @click="handleFontSize(12)">12px</button> <button @click="handleFontSize(18)">18px</button>
<button @click="handleFontSize(14)">14px</button> <button @click="handleFontSize(20)">20px</button>
<button @click="handleFontSize(16)">16px</button> <button @click="handleFontSize(24)">24px</button>
<button @click="handleFontSize(18)">18px</button> <button @click="handleFontSize(30)">30px</button>
<button @click="handleFontSize(20)">20px</button>
<button @click="handleFontSize(24)">24px</button>
<button @click="handleFontSize(30)">30px</button>
</div>
</button>
<button title="link" @click="setLink" :class="{ 'is-active': state.editor?.isActive('link') }">
<TinyIconRichTextLink></TinyIconRichTextLink>
</button>
<button title="unlink" @click="state.editor.chain().focus().unsetLink().run()"
:disabled="!state.editor?.isActive('link')">
<TinyIconRichTextLinkUnlink></TinyIconRichTextLinkUnlink>
</button>
<button title="high light" @click="state.editor.chain().focus().toggleHighlight().run()"
:class="{ 'is-active': state.editor?.isActive('highlight') }">
<TinyIconRichTextHighLight></TinyIconRichTextHighLight>
</button>
<button class="line-height-button" title="line height" @click="state.editor.chain().focus().toggleHeight().run()">
<div class="line-height-icon">
<TinyIconRichTextLineHeight></TinyIconRichTextLineHeight>
</div>
<div class="line-height-options">
<button class="line-1.0" @click.stop="state.editor.chain().focus().setP({ level: 1 }).run()">1.0</button>
<button class="line-1.5" @click.stop="state.editor.chain().focus().setP({ level: 1.5 }).run()">1.5</button>
<button class="line-2.0" @click.stop="state.editor.chain().focus().setP({ level: 2 }).run()">2.0</button>
<button class="line-2.5" @click.stop="state.editor.chain().focus().setP({ level: 2.5 }).run()">2.5</button>
</div>
</button>
<button @click="state.editor.chain().focus().toggleUnderline().run()"
:class="{ 'is-active': state.editor?.isActive('underline') }">
<TinyIconRichTextUnderline></TinyIconRichTextUnderline>
</button>
<button title="strike through" @click="state.editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': state.editor?.isActive('strike') }">
<TinyIconRichTextStrikeThrough></TinyIconRichTextStrikeThrough>
</button>
<button title="italic" @click="state.editor.chain().focus().toggleItalic().run()">
<TinyIconRichTextItalic></TinyIconRichTextItalic>
</button>
<button class="h-box">
<div class="h-ico">
<TinyIconRichTextHeading></TinyIconRichTextHeading>
</div>
<div class="h-options">
<button title="paragraph" @click="state.editor.chain().focus().setParagraph().run()">
<TinyIconRichTextParagraph></TinyIconRichTextParagraph>
</button>
<button title="h1" @click="state.editor.chain().focus().toggleHeading({ level: 1 }).run()">
<TinyIconRichTextH1></TinyIconRichTextH1>
</button>
<button title="h2" @click="state.editor.chain().focus().toggleHeading({ level: 2 }).run()">
<TinyIconRichTextH2></TinyIconRichTextH2>
</button>
<button title="h3" @click="state.editor.chain().focus().toggleHeading({ level: 3 }).run()">
<TinyIconRichTextH3></TinyIconRichTextH3>
</button>
<button title="h4" @click="state.editor.chain().focus().toggleHeading({ level: 4 }).run()">
<TinyIconRichTextH4></TinyIconRichTextH4>
</button>
<button title="h5" @click="state.editor.chain().focus().toggleHeading({ level: 5 }).run()">
<TinyIconRichTextH5></TinyIconRichTextH5>
</button>
<button title="h6" @click="state.editor.chain().focus().toggleHeading({ level: 6 }).run()">
<TinyIconRichTextH6></TinyIconRichTextH6>
</button>
</div>
</button>
<button title="subscript" @click="state.editor.chain().focus().toggleSubscript().run()"
:class="{ 'is-active': state.editor?.isActive('subscript') }">
<TinyIconRichTextSubscript></TinyIconRichTextSubscript>
</button>
<button title="superscript" @click="state.editor.chain().focus().toggleSuperscript().run()"
:class="{ 'is-active': state.editor?.isActive('superscript') }">
<TinyIconRichTextSuperscript></TinyIconRichTextSuperscript>
</button>
<!-- 无序列表 -->
<button title="code" @click="state.editor.chain().focus().toggleCode().run()">
<TinyIconRichTextCodeView></TinyIconRichTextCodeView>
</button>
<button title="unordered list" @click.stop="state.editor.chain().focus().toggleBulletList().run()">
<TinyIconRichTextListUnordered></TinyIconRichTextListUnordered>
</button>
<button title="ordered list" @click="state.editor.chain().focus().toggleOrderedList().run()">
<TinyIconRichTextListOrdered></TinyIconRichTextListOrdered>
</button>
<button @click="state.editor.chain().focus().toggleTaskList().run()"
:class="{ 'is-active': state.editor?.isActive('taskList') }">
<TinyIconRichTextTaskList></TinyIconRichTextTaskList>
</button>
<button title="quote" @click="state.editor.chain().focus().toggleBlockquote().run()">
<TinyIconRichTextQuoteText></TinyIconRichTextQuoteText>
</button>
<button title="code block" @click="state.editor.chain().focus().toggleCodeBlock().run()">
<TinyIconRichTextCodeBlock></TinyIconRichTextCodeBlock>
</button>
<button title="format clear" @click="state.editor.chain().focus().unsetAllMarks().run()">
<TinyIconRichTextFormatClear></TinyIconRichTextFormatClear>
</button>
<button title="node delete" @click="state.editor.chain().focus().clearNodes().run()">
<TinyIconRichTextNodeDelete></TinyIconRichTextNodeDelete>
</button>
<button title="undo" @click="state.editor.chain().focus().undo().run()">
<TinyIconRichTextUndo></TinyIconRichTextUndo>
</button>
<button title="redo" @click="state.editor.chain().focus().redo().run()">
<TinyIconRichTextRedo></TinyIconRichTextRedo>
</button>
<!-- 图片 -->
<button title="img" class="image-button">
<input @change="handleChange" id="img-btn" :placeholder="'啊飒飒'" type="file" accept="image/*" />
<label for="img-btn">
<TinyIconRichTextImage></TinyIconRichTextImage>
</label>
</button>
<!-- 文本对齐 -->
<button @click="state.editor.chain().focus().setTextAlign('left').run()"
:class="{ 'is-active': state.editor?.isActive({ textAlign: 'left' }) }">
<TinyIconRichTextAlignLeft></TinyIconRichTextAlignLeft>
</button>
<button @click="state.editor.chain().focus().setTextAlign('center').run()"
:class="{ 'is-active': state.editor?.isActive({ textAlign: 'center' }) }">
<TinyIconRichTextAlignCenter></TinyIconRichTextAlignCenter>
</button>
<button @click="state.editor.chain().focus().setTextAlign('right').run()"
:class="{ 'is-active': state.editor?.isActive({ textAlign: 'right' }) }">
<TinyIconRichTextAlignRight></TinyIconRichTextAlignRight>
</button>
<!-- 颜色 -->
<button title="color" class="color-button">
<label for="tiny-color">
<TinyIconRichTextColor></TinyIconRichTextColor>
</label>
<input id="tiny-color" type="color" @input="state.editor.chain().focus().setColor($event.target.value).run()"
:value="state.editor?.getAttributes('textStyle').color" />
</button>
<!-- 表格功能按钮 -->
<button title="table" class="table-button">
<div class="table-box" @click="handleClick">
<div class="table-icon">
<TinyIconRichTextTable></TinyIconRichTextTable>
</div> </div>
<div class="table-option" ref="box" v-if="state.isShow" @mousemove="handleMove"> </button>
<div class="item" :class="{ isActive: 1 <= state.flagX && 1 <= state.flagY }"></div> <button v-else-if="(item.name ?? item) === 'line-height'" class="line-height-button" title="line height">
<div class="item" :class="{ isActive: 2 <= state.flagX && 1 <= state.flagY }"></div> <div class="line-height-icon">
<div class="item" :class="{ isActive: 3 <= state.flagX && 1 <= state.flagY }"></div> <TinyIconRichTextLineHeight></TinyIconRichTextLineHeight>
<div class="item" :class="{ isActive: 1 <= state.flagX && 2 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 2 <= state.flagX && 2 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 3 <= state.flagX && 2 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 1 <= state.flagX && 3 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 2 <= state.flagX && 3 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 3 <= state.flagX && 3 <= state.flagY }"></div>
</div> </div>
</div> <div class="line-height-options">
</button> <button class="line-1.0" @click.stop="state.editor.chain().focus().setP({ level: 1 }).run()">1.0</button>
<button class="line-1.5" @click.stop="state.editor.chain().focus().setP({ level: 1.5 }).run()">1.5</button>
<button class="line-2.0" @click.stop="state.editor.chain().focus().setP({ level: 2 }).run()">2.0</button>
<button class="line-2.5" @click.stop="state.editor.chain().focus().setP({ level: 2.5 }).run()">2.5</button>
</div>
</button>
<button v-else-if="(item.name ?? item) === 'h-box'" class="h-box">
<div class="h-ico">
<TinyIconRichTextHeading></TinyIconRichTextHeading>
</div>
<div class="h-options">
<button title="paragraph" @click="state.editor.chain().focus().setParagraph().run()">
<TinyIconRichTextParagraph></TinyIconRichTextParagraph>
</button>
<button title="h1" @click="state.editor.chain().focus().toggleHeading({ level: 1 }).run()">
<TinyIconRichTextH1></TinyIconRichTextH1>
</button>
<button title="h2" @click="state.editor.chain().focus().toggleHeading({ level: 2 }).run()">
<TinyIconRichTextH2></TinyIconRichTextH2>
</button>
<button title="h3" @click="state.editor.chain().focus().toggleHeading({ level: 3 }).run()">
<TinyIconRichTextH3></TinyIconRichTextH3>
</button>
<button title="h4" @click="state.editor.chain().focus().toggleHeading({ level: 4 }).run()">
<TinyIconRichTextH4></TinyIconRichTextH4>
</button>
<button title="h5" @click="state.editor.chain().focus().toggleHeading({ level: 5 }).run()">
<TinyIconRichTextH5></TinyIconRichTextH5>
</button>
<button title="h6" @click="state.editor.chain().focus().toggleHeading({ level: 6 }).run()">
<TinyIconRichTextH6></TinyIconRichTextH6>
</button>
</div>
</button>
<button v-else-if="(item.name ?? item) === 'img'" title="img" class="image-button">
<input @change="handleChange" id="img-btn" type="file" accept="image/*" />
<label for="img-btn">
<TinyIconRichTextImage></TinyIconRichTextImage>
</label>
</button>
<button v-else-if="(item.name ?? item) === 'color'" title="color" class="color-button">
<label for="tiny-color">
<TinyIconRichTextColor></TinyIconRichTextColor>
</label>
<input id="tiny-color" type="color" @input="state.editor.chain().focus().setColor($event.target.value).run()"
:value="state.editor?.getAttributes('textStyle').color" />
</button>
<button v-else-if="(item.name ?? item) === 'table'" title="table" class="table-button">
<div class="table-box" @click="handleClick">
<div class="table-icon">
<TinyIconRichTextTable></TinyIconRichTextTable>
</div>
<div class="table-option" ref="box" v-if="state.isShow" @mousemove="handleMove">
<div class="item" :class="{ isActive: 1 <= state.flagX && 1 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 2 <= state.flagX && 1 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 3 <= state.flagX && 1 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 1 <= state.flagX && 2 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 2 <= state.flagX && 2 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 3 <= state.flagX && 2 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 1 <= state.flagX && 3 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 2 <= state.flagX && 3 <= state.flagY }"></div>
<div class="item" :class="{ isActive: 3 <= state.flagX && 3 <= state.flagY }"></div>
</div>
</div>
</button>
<button v-else-if="(item.name ?? item) === 'unlink'" :title="item.name" @click="eventClick(state.editor, item)"
:disabled="!state.editor?.isActive(Active(item))">
<img v-if="item.img" :src="eventImg(item)" alt="" srcset="" />
<component v-else :is='eventImg(item)'></component>
</button>
<button v-else :title="item.name" @click="eventClick(state.editor, item)"
:class="{ 'is-active': state.editor?.isActive(Active(item)) }">
<img v-if="item.img" :src="eventImg(item)" alt="" srcset="" />
<component v-else :is='eventImg(item)'></component>
</button>
</template>
<!-- 插槽传出editor实例 -->
<slot name="toolBar" :option="state.editor"></slot>
<BubbleMenu :editor="state.editor" :tippy-options="{ duration: 100 }" v-if="state.editor" :should-show="shouldShow" <BubbleMenu :editor="state.editor" :tippy-options="{ duration: 100 }" v-if="state.editor" :should-show="shouldShow"
class="bubble-menu"> class="bubble-menu">
<button title="add column before" @click="state.editor.chain().focus().addColumnBefore().run()" <button title="add column before" @click="state.editor.chain().focus().addColumnBefore().run()"
@ -310,6 +242,8 @@ lowlight.registerLanguage('html', html)
lowlight.registerLanguage('css', css) lowlight.registerLanguage('css', css)
lowlight.registerLanguage('js', js) lowlight.registerLanguage('js', js)
lowlight.registerLanguage('ts', ts) lowlight.registerLanguage('ts', ts)
// Placeholder
import Placeholder from '@tiptap/extension-placeholder'
// collaboration // collaboration
import Collaboration from '@tiptap/extension-collaboration' import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs' import * as Y from 'yjs'
@ -319,8 +253,8 @@ import { props, setup, defineComponent } from '@opentiny/vue-common'
import '@opentiny/vue-theme/rich-text-editor/index.less' import '@opentiny/vue-theme/rich-text-editor/index.less'
export default defineComponent({ export default defineComponent({
// emits: ['click', 'hook-updated'], emits: ['beforeCreate', 'create', 'update:modelValue', 'focus', 'blur', 'selectionUpdate', 'transaction', 'destroy', 'update'],
// props: [...props, 'type', 'text', 'size', 'icon', 'resetTime', 'nativeType', 'loading', 'disabled', 'plain', 'autofocus', 'round', 'circle', 'tabindex'], props: [...props, 'modelValue', 'collaboration', 'placeholder', 'customToolBar', 'options'],
components: { components: {
EditorContent, EditorContent,
BubbleMenu, BubbleMenu,
@ -405,7 +339,8 @@ export default defineComponent({
VueNodeViewRenderer, VueNodeViewRenderer,
NodeViewContent, NodeViewContent,
nodeViewProps, nodeViewProps,
NodeViewWrapper NodeViewWrapper,
Placeholder
} }
}) })
} }