diff --git a/examples/sites/demos/app/rich-text-editor/webdoc/rich-text-editor.js b/examples/sites/demos/app/rich-text-editor/webdoc/rich-text-editor.js index c33ffc524..cfa1e86af 100644 --- a/examples/sites/demos/app/rich-text-editor/webdoc/rich-text-editor.js +++ b/examples/sites/demos/app/rich-text-editor/webdoc/rich-text-editor.js @@ -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' + }, + ] } ] } diff --git a/packages/renderless/src/rich-text-edtior/index.ts b/packages/renderless/src/rich-text-edtior/index.ts index 73a0e134c..ec659d46e 100644 --- a/packages/renderless/src/rich-text-edtior/index.ts +++ b/packages/renderless/src/rich-text-edtior/index.ts @@ -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' } -} \ No newline at end of file +} + +// 处理参数实现自定义展示 +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 +} diff --git a/packages/renderless/src/rich-text-edtior/vue.ts b/packages/renderless/src/rich-text-edtior/vue.ts index 97e93710e..2d3621aa2 100644 --- a/packages/renderless/src/rich-text-edtior/vue.ts +++ b/packages/renderless/src/rich-text-edtior/vue.ts @@ -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() diff --git a/packages/theme/src/rich-text-editor/index.less b/packages/theme/src/rich-text-editor/index.less index 2405e869e..ccd7882c0 100644 --- a/packages/theme/src/rich-text-editor/index.less +++ b/packages/theme/src/rich-text-editor/index.less @@ -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; + } } \ No newline at end of file diff --git a/packages/vue/src/rich-text-editor/package.json b/packages/vue/src/rich-text-editor/package.json index 96ec0f818..cce0144e6 100644 --- a/packages/vue/src/rich-text-editor/package.json +++ b/packages/vue/src/rich-text-editor/package.json @@ -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:~" diff --git a/packages/vue/src/rich-text-editor/src/pc.vue b/packages/vue/src/rich-text-editor/src/pc.vue index a9d1cb457..8f6b28ddd 100644 --- a/packages/vue/src/rich-text-editor/src/pc.vue +++ b/packages/vue/src/rich-text-editor/src/pc.vue @@ -2,170 +2,102 @@