diff --git a/src/assets/styles/prosemirror.scss b/src/assets/styles/prosemirror.scss new file mode 100644 index 00000000..3867cbde --- /dev/null +++ b/src/assets/styles/prosemirror.scss @@ -0,0 +1,53 @@ +.ProseMirror, .ProseMirror-static { + outline: 0; + border: 0; + font-size: 20px; + line-height: 1.5; + word-break: break-word; + font-family: '微软雅黑'; + + ::selection { + background-color: rgba(27, 110, 232, 0.3); + color: inherit; + } + + p + p { + margin-top: 5px; + } + + ul { + list-style-type: disc; + padding-inline-start: 20px; + + li { + list-style-type: disc; + } + } + + ol { + list-style-type: decimal; + padding-inline-start: 20px; + + li { + list-style-type: decimal; + } + } + + code { + background-color: #eee; + padding: 1px 3px; + margin: 0 1px; + border-radius: 2px; + font-family: inherit; + } + + blockquote { + overflow: hidden; + padding-right: 1.2em; + padding-left: 1.2em; + margin-left: 0; + margin-right: 0; + font-style: italic; + border-left: 5px solid #ccc; + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index fe8e7f8e..3814f646 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,8 @@ import { createApp } from 'vue' import App from './App.vue' import store from './store' +import 'prosemirror-view/style/prosemirror.css' +import '@/assets/styles/prosemirror.scss' import '@/assets/styles/global.scss' import 'animate.css' diff --git a/src/mocks/index.ts b/src/mocks/index.ts index 306bb751..62741730 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -24,7 +24,7 @@ export const slides: Slide[] = [ }, opacity: 1, lock: false, - content: '
一段测试文字,字号固定为28px
', + content: '

一段测试文字,字号固定为28px

', }, { id: 'xxx3', diff --git a/src/prosemirror/index.ts b/src/prosemirror/index.ts new file mode 100644 index 00000000..42a75bb9 --- /dev/null +++ b/src/prosemirror/index.ts @@ -0,0 +1,28 @@ +import { EditorState } from 'prosemirror-state' +import { EditorView } from 'prosemirror-view' +import { Schema, DOMParser } from 'prosemirror-model' + +import { buildPlugins } from './plugins/index' +import { schemaNodes, schemaMarks } from './schema/index' + +const schema = new Schema({ + nodes: schemaNodes, + marks: schemaMarks, +}) + +export const createDocument = (content: string) => { + const htmlString = `
${content}
` + const parser = new window.DOMParser() + const element = parser.parseFromString(htmlString, 'text/html').body.firstElementChild + return DOMParser.fromSchema(schema).parse(element as Element) +} + +export const initProsemirrorEditor = (dom: Element, content: string, props = {}) => { + return new EditorView(dom, { + state: EditorState.create({ + doc: createDocument(content), + plugins: buildPlugins(schema), + }), + ...props, + }) +} \ No newline at end of file diff --git a/src/prosemirror/plugins/inputrules.ts b/src/prosemirror/plugins/inputrules.ts index 21ea6311..cf8c558d 100644 --- a/src/prosemirror/plugins/inputrules.ts +++ b/src/prosemirror/plugins/inputrules.ts @@ -15,7 +15,7 @@ const orderedListRule = (nodeType: NodeType) => ( /^(\d+)\.\s$/, nodeType, match => ({order: +match[1]}), - (match, node) => node.childCount + node.attrs.order == +match[1], + (match, node) => node.childCount + node.attrs.order === +match[1], ) ) diff --git a/src/prosemirror/schema/marks.ts b/src/prosemirror/schema/marks.ts index 18687ac0..128ba274 100644 --- a/src/prosemirror/schema/marks.ts +++ b/src/prosemirror/schema/marks.ts @@ -1,79 +1,79 @@ import { marks } from 'prosemirror-schema-basic' -import { Node } from 'prosemirror-model' +import { MarkSpec } from 'prosemirror-model' -const subscript = { +const subscript: MarkSpec = { excludes: 'subscript', parseDOM: [ { tag: 'sub' }, { style: 'vertical-align', - getAttrs: (value: string) => value === 'sub' && null + getAttrs: value => value === 'sub' && null }, ], toDOM: () => ['sub', 0], } -const superscript = { +const superscript: MarkSpec = { excludes: 'superscript', parseDOM: [ { tag: 'sup' }, { style: 'vertical-align', - getAttrs: (value: string) => value === 'super' && null + getAttrs: value => value === 'super' && null }, ], toDOM: () => ['sup', 0], } -const strikethrough = { +const strikethrough: MarkSpec = { parseDOM: [ { tag: 'strike' }, { style: 'text-decoration', - getAttrs: (value: string) => value === 'line-through' && null + getAttrs: value => value === 'line-through' && null }, { style: 'text-decoration-line', - getAttrs: (value: string) => value === 'line-through' && null + getAttrs: value => value === 'line-through' && null }, ], toDOM: () => ['span', { style: 'text-decoration-line: line-through' }, 0], } -const underline = { +const underline: MarkSpec = { parseDOM: [ { tag: 'u' }, { style: 'text-decoration', - getAttrs: (value: string) => value === 'underline' && null + getAttrs: value => value === 'underline' && null }, { style: 'text-decoration-line', - getAttrs: (value: string) => value === 'underline' && null + getAttrs: value => value === 'underline' && null }, ], toDOM: () => ['span', { style: 'text-decoration: underline' }, 0], } -const forecolor = { +const forecolor: MarkSpec = { attrs: { color: {}, }, parseDOM: [ { style: 'color', - getAttrs: (color: string) => color ? { color } : {} + getAttrs: color => color ? { color } : {} }, ], - toDOM: (node: Node) => { - const { color } = node.attrs + toDOM: mark => { + const { color } = mark.attrs let style = '' if(color) style += `color: ${color};` return ['span', { style }, 0] }, } -const backcolor = { +const backcolor: MarkSpec = { attrs: { backcolor: {}, }, @@ -82,18 +82,18 @@ const backcolor = { parseDOM: [ { tag: 'span[style*=background-color]', - getAttrs: (backcolor: string) => backcolor ? { backcolor } : {} + getAttrs: backcolor => backcolor ? { backcolor } : {} }, ], - toDOM: (node: Node) => { - const { backcolor } = node.attrs + toDOM: mark => { + const { backcolor } = mark.attrs let style = '' if(backcolor) style += `background-color: ${backcolor};` return ['span', { style }, 0] }, } -const fontsize = { +const fontsize: MarkSpec = { attrs: { fontsize: {}, }, @@ -102,31 +102,33 @@ const fontsize = { parseDOM: [ { style: 'font-size', - getAttrs: (fontsize: string) => fontsize ? { fontsize } : {} + getAttrs: fontsize => fontsize ? { fontsize } : {} }, ], - toDOM: (node: Node) => { - const { fontsize } = node.attrs + toDOM: mark => { + const { fontsize } = mark.attrs let style = '' if(fontsize) style += `font-size: ${fontsize}` return ['span', { style }, 0] }, } -const fontname = { +const fontname: MarkSpec = { attrs: { - fontname: '', + fontname: {}, }, inline: true, group: 'inline', parseDOM: [ { style: 'font-family', - getAttrs: (fontname: string) => ({ fontname: fontname ? fontname.replace(/[\"\']/g, '') : '' }) + getAttrs: fontname => { + return { fontname: fontname && typeof fontname === 'string' ? fontname.replace(/[\"\']/g, '') : '' } + } }, ], - toDOM: (node: Node) => { - const { fontname } = node.attrs + toDOM: mark => { + const { fontname } = mark.attrs let style = '' if(fontname) style += `font-family: ${fontname}` return ['span', { style }, 0] diff --git a/src/prosemirror/schema/nodes.ts b/src/prosemirror/schema/nodes.ts index b7e4d0eb..16c5a1d2 100644 --- a/src/prosemirror/schema/nodes.ts +++ b/src/prosemirror/schema/nodes.ts @@ -1,55 +1,58 @@ import { nodes } from 'prosemirror-schema-basic' -import { Node } from 'prosemirror-model' +import { Node, NodeSpec } from 'prosemirror-model' import { orderedList, bulletList, listItem } from 'prosemirror-schema-list' -const listNodes = { - ordered_list: { - ...orderedList, - content: 'list_item+', - group: 'block', - }, - bullet_list: { - ...bulletList, - content: 'list_item+', - group: 'block', - }, - list_item: { - ...listItem, - content: 'paragraph block*', - group: 'block', - }, +const _orderedList: NodeSpec = { + ...orderedList, + content: 'list_item+', + group: 'block', +} - paragraph: { - attrs: { - align: { - default: '', - }, +const _bulletList: NodeSpec = { + ...bulletList, + content: 'list_item+', + group: 'block', +} + +const _listItem: NodeSpec = { + ...listItem, + content: 'paragraph block*', + group: 'block', +} + +const paragraph: NodeSpec = { + attrs: { + align: { + default: '', }, - content: 'inline*', - group: 'block', - parseDOM: [ - { - tag: 'p', - getAttrs: (dom: HTMLElement) => { - const { textAlign } = dom.style - let align = dom.getAttribute('align') || textAlign || '' - align = /(left|right|center|justify)/.test(align) ? align : '' - - return { align } - } + }, + content: 'inline*', + group: 'block', + parseDOM: [ + { + tag: 'p', + getAttrs: dom => { + const { textAlign } = (dom as HTMLElement).style + let align = (dom as HTMLElement).getAttribute('align') || textAlign || '' + align = /(left|right|center|justify)/.test(align) ? align : '' + + return { align } } - ], - toDOM: (node: Node) => { - const { align } = node.attrs - let style = '' - if(align && align !== 'left') style += `text-align: ${align};` + } + ], + toDOM: (node: Node) => { + const { align } = node.attrs + let style = '' + if(align && align !== 'left') style += `text-align: ${align};` - return ['p', { style }, 0] - }, + return ['p', { style }, 0] }, } export default { ...nodes, - ...listNodes, + 'ordered_list': _orderedList, + 'bullet_list': _bulletList, + 'list_item': _listItem, + paragraph, } diff --git a/src/utils/selection.ts b/src/utils/selection.ts new file mode 100644 index 00000000..01b2de1f --- /dev/null +++ b/src/utils/selection.ts @@ -0,0 +1,4 @@ +export const removeAllRanges = () => { + const selection = window.getSelection() + selection && selection.removeAllRanges() +} \ No newline at end of file diff --git a/src/views/Editor/Canvas/index.vue b/src/views/Editor/Canvas/index.vue index 318b2d52..e0eba62e 100644 --- a/src/views/Editor/Canvas/index.vue +++ b/src/views/Editor/Canvas/index.vue @@ -82,6 +82,7 @@ import { State, MutationTypes } from '@/store' import { ContextmenuItem } from '@/components/Contextmenu/types' import { PPTElement, Slide } from '@/types/slides' import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit' +import { removeAllRanges } from '@/utils/selection' import useViewportSize from './hooks/useViewportSize' import useMouseSelection from './hooks/useMouseSelection' @@ -161,6 +162,7 @@ export default defineComponent({ store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, []) if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e) if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true) + removeAllRanges() } const removeEditorAreaFocus = () => { diff --git a/src/views/Editor/useHotkey.ts b/src/views/Editor/useHotkey.ts index eda6eecb..905cc756 100644 --- a/src/views/Editor/useHotkey.ts +++ b/src/views/Editor/useHotkey.ts @@ -41,7 +41,6 @@ export default () => { const { redo, undo } = useHistorySnapshot() const copy = () => { - if(disableHotkeys.value) return if(activeElementIdList.value.length) copyElement() else if(thumbnailsFocus.value) copySlide() } @@ -53,38 +52,36 @@ export default () => { } const selectAll = () => { - if(!editorAreaFocus.value && disableHotkeys.value) return + if(!editorAreaFocus.value) return selectAllElement() } const lock = () => { - if(!editorAreaFocus.value && disableHotkeys.value) return + if(!editorAreaFocus.value) return lockElement() } const combine = () => { - if(!editorAreaFocus.value && disableHotkeys.value) return + if(!editorAreaFocus.value) return combineElements() } const uncombine = () => { - if(!editorAreaFocus.value && disableHotkeys.value) return + if(!editorAreaFocus.value) return uncombineElements() } const remove = () => { - if(disableHotkeys.value) return if(activeElementIdList.value.length) deleteElement() else if(thumbnailsFocus.value) deleteSlide() } const move = (key: string) => { - if(disableHotkeys.value) return if(activeElementIdList.value.length) moveElement(key) else if(key === KEYS.UP || key === KEYS.DOWN) updateSlideIndex(key) } const create = () => { - if(!thumbnailsFocus.value || disableHotkeys.value) return + if(!thumbnailsFocus.value) return createSlide() } @@ -109,58 +106,72 @@ export default () => { if(!editorAreaFocus.value && !thumbnailsFocus.value) return if(ctrlKey && key === KEYS.C) { + if(disableHotkeys.value) return e.preventDefault() copy() } if(ctrlKey && key === KEYS.X) { + if(disableHotkeys.value) return e.preventDefault() cut() } if(ctrlKey && key === KEYS.Z) { + if(disableHotkeys.value) return e.preventDefault() undo() } if(ctrlKey && key === KEYS.Y) { + if(disableHotkeys.value) return e.preventDefault() redo() } if(ctrlKey && key === KEYS.A) { + if(disableHotkeys.value) return e.preventDefault() selectAll() } if(ctrlKey && key === KEYS.L) { + if(disableHotkeys.value) return e.preventDefault() lock() } if(!shiftKey && ctrlKey && key === KEYS.G) { + if(disableHotkeys.value) return e.preventDefault() combine() } if(shiftKey && ctrlKey && key === KEYS.G) { + if(disableHotkeys.value) return e.preventDefault() uncombine() } if(key === KEYS.DELETE) { + if(disableHotkeys.value) return e.preventDefault() remove() } if(key === KEYS.UP) { + if(disableHotkeys.value) return e.preventDefault() move(KEYS.UP) } if(key === KEYS.DOWN) { + if(disableHotkeys.value) return e.preventDefault() move(KEYS.DOWN) } if(key === KEYS.LEFT) { + if(disableHotkeys.value) return e.preventDefault() move(KEYS.LEFT) } if(key === KEYS.RIGHT) { + if(disableHotkeys.value) return e.preventDefault() move(KEYS.RIGHT) } if(key === KEYS.ENTER) { + if(disableHotkeys.value) return e.preventDefault() create() } diff --git a/src/views/components/element/LineElement/BaseLineElement.vue b/src/views/components/element/LineElement/BaseLineElement.vue index 1d75e260..e472eca6 100644 --- a/src/views/components/element/LineElement/BaseLineElement.vue +++ b/src/views/components/element/LineElement/BaseLineElement.vue @@ -1,5 +1,6 @@ @@ -68,15 +69,4 @@ export default defineComponent({ position: relative; } } - -::v-deep(.text) { - word-break: break-word; - font-family: '微软雅黑'; - outline: 0; - - ::selection { - background-color: rgba(27, 110, 232, 0.3); - color: inherit; - } -} diff --git a/src/views/components/element/TextElement/index.vue b/src/views/components/element/TextElement/index.vue index 279d732a..05b28ff2 100644 --- a/src/views/components/element/TextElement/index.vue +++ b/src/views/components/element/TextElement/index.vue @@ -1,6 +1,7 @@