forked from opentiny/tiny-vue
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:
parent
7d6120ed1d
commit
51c46cbd42
|
@ -23,11 +23,140 @@ export default {
|
|||
'en-US': 'default rich text content'
|
||||
},
|
||||
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': [],
|
||||
'slots': []
|
||||
'slots': [
|
||||
{
|
||||
'name': 'toolBar',
|
||||
'type': '',
|
||||
'defaultValue': '',
|
||||
'desc': { 'zh-CN': 'toolBar添加按钮,会传出editor实例,详情见tiptap', 'en-US': 'toolBar Add Button' },
|
||||
'demoId': 'custom-search-types'
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,57 +1,50 @@
|
|||
export const handleChange = (editor) => {
|
||||
return (event) => {
|
||||
const file = event.target.files[0]
|
||||
if (!file.type.match("image.*")) {
|
||||
console.log("请选择图片文件!")
|
||||
if (!file.type.match('image.*')) {
|
||||
console.log('请选择图片文件!')
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
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)
|
||||
}
|
||||
}
|
||||
export const setLink = (editor) => {
|
||||
return () => {
|
||||
const previousUrl = editor.value.getAttributes('link').href
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
if (url === '') {
|
||||
editor.value
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.unsetLink()
|
||||
.run()
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
return
|
||||
}
|
||||
editor.value
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({ href: url })
|
||||
.run()
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
}
|
||||
// table 处理逻辑
|
||||
export const handleMove = (state, box) => {
|
||||
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.flagY = Math.ceil((e.y - y) / 30)
|
||||
}
|
||||
}
|
||||
export const handleClickOutside = (state, box) => {
|
||||
return (e) => {
|
||||
if (!box.value?.contains(e.target)) {
|
||||
if (!box.value[0]?.contains(e.target)) {
|
||||
state.isShow = false
|
||||
removeClickOutside(state, box)()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export const removeClickOutside = (state, box) => {
|
||||
return () => {
|
||||
|
@ -60,7 +53,7 @@ export const removeClickOutside = (state, box) => {
|
|||
}
|
||||
export const handleClick = (state, box) => {
|
||||
return (e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
if (state.isShow) {
|
||||
if (state.flagX && state.flagY) {
|
||||
state.editor.chain().focus().insertTable({ rows: state.flagX, cols: state.flagY, withHeaderRow: true }).run()
|
||||
|
@ -77,13 +70,165 @@ export const handleClick = (state, box) => {
|
|||
// bubble菜单
|
||||
export const shouldShow = ({ editor, view, state, oldState, from, to }) => {
|
||||
// 仅在无序列表选中的时候才显示 气泡菜单
|
||||
return editor.isActive("table");
|
||||
};
|
||||
return editor.isActive('table')
|
||||
}
|
||||
// font-size 设置
|
||||
export const handleFontSize = (fontSize) => {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
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 = (
|
||||
props,
|
||||
{ computed, onMounted, onBeforeUnmount, reactive, ref },
|
||||
{ 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 CustomImage = Image.extend({
|
||||
|
@ -18,46 +112,48 @@ export const renderless = (
|
|||
const CustomParagraph = Paragraph.extend({
|
||||
addOptions() {
|
||||
return {
|
||||
levels: [1, 1.5, 2, 2.5, 3],
|
||||
levels: [1, 1.5, 2, 2.5, 3]
|
||||
}
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
level: {
|
||||
default: 1,
|
||||
},
|
||||
default: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
const hasLevel = this.options.levels.includes(node.attrs.level)
|
||||
const level = hasLevel
|
||||
? node.attrs.level
|
||||
: this.options.levels[0]
|
||||
console.log('2', node, HTMLAttributes, this.options);
|
||||
const level = hasLevel ? node.attrs.level : this.options.levels[0]
|
||||
return ['p', mergeAttributes({ style: `line-height: ${level}` }, HTMLAttributes), 0]
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setP: attributes => ({ commands }) => {
|
||||
return commands.setNode(this.name, attributes)
|
||||
},
|
||||
setP:
|
||||
(attributes) =>
|
||||
({ commands }) => {
|
||||
return commands.setNode(this.name, attributes)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit?.configure({
|
||||
// 开启多人协作功能要关闭默认的history模式
|
||||
history: false,
|
||||
history: false
|
||||
}),
|
||||
Collaboration?.configure({
|
||||
document: ydoc,
|
||||
document: ydoc
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
resizable: true
|
||||
}),
|
||||
TableCell, TableHeader, TableRow,
|
||||
Color, TextStyle,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Color,
|
||||
TextStyle,
|
||||
CustomImage,
|
||||
Highlight,
|
||||
Link,
|
||||
|
@ -66,24 +162,65 @@ export const renderless = (
|
|||
Superscript,
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
nested: true
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
types: ['heading', 'paragraph']
|
||||
}),
|
||||
CustomParagraph,
|
||||
CodeBlockLowlight.extend({
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(Codehighlight(NodeViewContent, nodeViewProps, NodeViewWrapper))
|
||||
},
|
||||
}
|
||||
}).configure({ lowlight }),
|
||||
Placeholder.configure({
|
||||
placeholder: props.placeholder ?? 'Write something …'
|
||||
})
|
||||
.configure({ lowlight }),
|
||||
],
|
||||
content: 'Example Tesxt',
|
||||
content: '',
|
||||
autofocus: true,
|
||||
editable: true,
|
||||
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)
|
||||
|
@ -93,10 +230,11 @@ export const renderless = (
|
|||
// table 变量
|
||||
isShow: false,
|
||||
flagX: 0,
|
||||
flagY: 0,
|
||||
flagY: 0
|
||||
})
|
||||
state.editor = editor
|
||||
const api = {
|
||||
toolBar,
|
||||
state,
|
||||
setLink: setLink(editor),
|
||||
handleChange: handleChange(editor),
|
||||
|
@ -111,6 +249,9 @@ export const renderless = (
|
|||
//
|
||||
fontSize,
|
||||
handleFontSize: handleFontSize(fontSize),
|
||||
eventImg,
|
||||
eventClick,
|
||||
Active
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
state.editor.destroy()
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
&:not(:disabled):hover,
|
||||
&.is-active {
|
||||
background-color: #d2e4ff;
|
||||
|
||||
svg,
|
||||
path {
|
||||
|
@ -103,7 +104,7 @@
|
|||
background: white;
|
||||
display: flex;
|
||||
left: 0;
|
||||
top: 1.00rem;
|
||||
top: 1.25rem;
|
||||
z-index: 999;
|
||||
width: 90px;
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -36,6 +36,7 @@
|
|||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lowlight": "^2.9.0",
|
||||
"@tiptap/extension-placeholder": "^2.1.7",
|
||||
"@opentiny/vue-common": "workspace:~",
|
||||
"@opentiny/vue-renderless": "workspace:~",
|
||||
"@opentiny/vue-theme": "workspace:~"
|
||||
|
|
|
@ -2,170 +2,102 @@
|
|||
<div class="tiny-rich-text-editor">
|
||||
<div class="tiny-rich-text-editor__toolbar">
|
||||
<!-- starter-kit功能区 -->
|
||||
<button title="bold" @click="state.editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'is-active': state.editor?.isActive('bold') }">
|
||||
<TinyIconRichTextBold></TinyIconRichTextBold>
|
||||
</button>
|
||||
<button class="font-size-box">
|
||||
<TinyIconRichTextFontSize></TinyIconRichTextFontSize>
|
||||
<div class="font-size-options">
|
||||
<button @click="handleFontSize(12)">12px</button>
|
||||
<button @click="handleFontSize(14)">14px</button>
|
||||
<button @click="handleFontSize(16)">16px</button>
|
||||
<button @click="handleFontSize(18)">18px</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>
|
||||
<template v-for="item in toolBar">
|
||||
<button v-if="(item.name ?? item) === 'font-size'" class="font-size-box">
|
||||
<TinyIconRichTextFontSize></TinyIconRichTextFontSize>
|
||||
<div class="font-size-options">
|
||||
<button @click="handleFontSize(12)">12px</button>
|
||||
<button @click="handleFontSize(14)">14px</button>
|
||||
<button @click="handleFontSize(16)">16px</button>
|
||||
<button @click="handleFontSize(18)">18px</button>
|
||||
<button @click="handleFontSize(20)">20px</button>
|
||||
<button @click="handleFontSize(24)">24px</button>
|
||||
<button @click="handleFontSize(30)">30px</button>
|
||||
</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>
|
||||
</button>
|
||||
<button v-else-if="(item.name ?? item) === 'line-height'" class="line-height-button" title="line height">
|
||||
<div class="line-height-icon">
|
||||
<TinyIconRichTextLineHeight></TinyIconRichTextLineHeight>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<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 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"
|
||||
class="bubble-menu">
|
||||
<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('js', js)
|
||||
lowlight.registerLanguage('ts', ts)
|
||||
// Placeholder
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
// collaboration 包
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
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'
|
||||
|
||||
export default defineComponent({
|
||||
// emits: ['click', 'hook-updated'],
|
||||
// props: [...props, 'type', 'text', 'size', 'icon', 'resetTime', 'nativeType', 'loading', 'disabled', 'plain', 'autofocus', 'round', 'circle', 'tabindex'],
|
||||
emits: ['beforeCreate', 'create', 'update:modelValue', 'focus', 'blur', 'selectionUpdate', 'transaction', 'destroy', 'update'],
|
||||
props: [...props, 'modelValue', 'collaboration', 'placeholder', 'customToolBar', 'options'],
|
||||
components: {
|
||||
EditorContent,
|
||||
BubbleMenu,
|
||||
|
@ -405,7 +339,8 @@ export default defineComponent({
|
|||
VueNodeViewRenderer,
|
||||
NodeViewContent,
|
||||
nodeViewProps,
|
||||
NodeViewWrapper
|
||||
NodeViewWrapper,
|
||||
Placeholder
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue