mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
文本元素接入ProseMirror
This commit is contained in:
parent
22887f022e
commit
1c7b2181bc
53
src/assets/styles/prosemirror.scss
Normal file
53
src/assets/styles/prosemirror.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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'
|
||||
|
||||
|
@ -24,7 +24,7 @@ export const slides: Slide[] = [
|
||||
},
|
||||
opacity: 1,
|
||||
lock: false,
|
||||
content: '<div style=\'text-align: center;\'><span style=\'font-size: 28px;\'><span style=\'color: rgb(232, 107, 153); font-weight: bold;\'>一段测试文字</span>,字号固定为<span style=\'font-weight: bold; font-style: italic; text-decoration-line: underline;\'>28px</span></span></div>',
|
||||
content: '<p style=\'text-align: center;\'><span style=\'font-size: 28px;\'><span style=\'color: rgb(232, 107, 153); font-weight: bold;\'>一段测试文字</span>,字号固定为<span style=\'font-weight: bold; font-style: italic; text-decoration-line: underline;\'>28px</span></span></p>',
|
||||
},
|
||||
{
|
||||
id: 'xxx3',
|
||||
|
28
src/prosemirror/index.ts
Normal file
28
src/prosemirror/index.ts
Normal file
@ -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 = `<div>${content}</div>`
|
||||
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,
|
||||
})
|
||||
}
|
@ -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],
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
}
|
||||
|
4
src/utils/selection.ts
Normal file
4
src/utils/selection.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const removeAllRanges = () => {
|
||||
const selection = window.getSelection()
|
||||
selection && selection.removeAllRanges()
|
||||
}
|
@ -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 = () => {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="editable-element-shape"
|
||||
<div
|
||||
class="editable-element-shape"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
left: elementInfo.left + 'px',
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="editable-element-shape"
|
||||
<div
|
||||
class="editable-element-shape"
|
||||
:class="{ 'lock': elementInfo.lock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
|
@ -14,7 +14,8 @@
|
||||
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
||||
}"
|
||||
>
|
||||
<SvgWrapper overflow="visible"
|
||||
<SvgWrapper
|
||||
overflow="visible"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
>
|
||||
|
@ -17,7 +17,8 @@
|
||||
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
||||
}"
|
||||
>
|
||||
<SvgWrapper overflow="visible"
|
||||
<SvgWrapper
|
||||
overflow="visible"
|
||||
:width="elementInfo.width"
|
||||
:height="elementInfo.height"
|
||||
>
|
||||
|
@ -8,7 +8,8 @@
|
||||
transform: `rotate(${elementInfo.rotate}deg)`,
|
||||
}"
|
||||
>
|
||||
<div class="element-content"
|
||||
<div
|
||||
class="element-content"
|
||||
:style="{
|
||||
backgroundColor: elementInfo.fill,
|
||||
opacity: elementInfo.opacity,
|
||||
@ -20,7 +21,7 @@
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<div class="text" v-html="elementInfo.content"></div>
|
||||
<div class="text ProseMirror-static" v-html="elementInfo.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="editable-element-text"
|
||||
ref="elementRef"
|
||||
:class="{ 'lock': elementInfo.lock }"
|
||||
:style="{
|
||||
top: elementInfo.top + 'px',
|
||||
@ -10,7 +11,8 @@
|
||||
}"
|
||||
@mousedown="$event => handleSelectElement($event)"
|
||||
>
|
||||
<div class="element-content"
|
||||
<div
|
||||
class="element-content"
|
||||
:style="{
|
||||
backgroundColor: elementInfo.fill,
|
||||
opacity: elementInfo.opacity,
|
||||
@ -23,9 +25,9 @@
|
||||
:height="elementInfo.height"
|
||||
:outline="elementInfo.outline"
|
||||
/>
|
||||
<div class="text"
|
||||
v-html="elementInfo.content"
|
||||
:contenteditable="!elementInfo.lock"
|
||||
<div
|
||||
class="text"
|
||||
ref="editorViewRef"
|
||||
@mousedown="$event => handleSelectElement($event, false)"
|
||||
></div>
|
||||
</div>
|
||||
@ -33,10 +35,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from 'vue'
|
||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, watch } from 'vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { useStore } from 'vuex'
|
||||
import { MutationTypes, State } from '@/store'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { PPTTextElement } from '@/types/slides'
|
||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import { initProsemirrorEditor, createDocument } from '@/prosemirror/'
|
||||
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import ElementOutline from '@/views/components/element/ElementOutline.vue'
|
||||
|
||||
@ -59,6 +67,72 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore<State>()
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const elementRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const debounceUpdateTextElementHeight = debounce(function(realHeight) {
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
props: { height: realHeight },
|
||||
})
|
||||
}, 500, { trailing: true })
|
||||
|
||||
const updateTextElementHeight = () => {
|
||||
if(!elementRef.value) return
|
||||
|
||||
const realHeight = elementRef.value.clientHeight
|
||||
if(props.elementInfo.height !== realHeight) {
|
||||
debounceUpdateTextElementHeight(realHeight)
|
||||
}
|
||||
}
|
||||
const resizeObserver = new ResizeObserver(updateTextElementHeight)
|
||||
|
||||
onMounted(() => {
|
||||
if(elementRef.value) resizeObserver.observe(elementRef.value)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if(elementRef.value) resizeObserver.unobserve(elementRef.value)
|
||||
})
|
||||
|
||||
const editorViewRef = ref<Element | null>(null)
|
||||
let editorView: EditorView
|
||||
|
||||
const handleFocus = () => {
|
||||
store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, true)
|
||||
}
|
||||
const handleBlur = () => {
|
||||
store.commit(MutationTypes.SET_DISABLE_HOTKEYS_STATE, false)
|
||||
}
|
||||
const handleInput = debounce(function() {
|
||||
store.commit(MutationTypes.UPDATE_ELEMENT, {
|
||||
id: props.elementInfo.id,
|
||||
props: { content: editorView.dom.innerHTML },
|
||||
})
|
||||
addHistorySnapshot()
|
||||
}, 500, { trailing: true })
|
||||
|
||||
const textContent = computed(() => props.elementInfo.content)
|
||||
watch(textContent, () => {
|
||||
if(!editorView) return
|
||||
if(editorView.hasFocus()) return
|
||||
editorView.dom.innerHTML = textContent.value
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
|
||||
handleDOMEvents: {
|
||||
focus: handleFocus,
|
||||
blur: handleBlur,
|
||||
keydown: handleInput,
|
||||
},
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
editorView && editorView.destroy()
|
||||
})
|
||||
|
||||
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
||||
if(props.elementInfo.lock) return
|
||||
e.stopPropagation()
|
||||
@ -70,6 +144,8 @@ export default defineComponent({
|
||||
const { shadowStyle } = useElementShadow(shadow)
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
editorViewRef,
|
||||
handleSelectElement,
|
||||
shadowStyle,
|
||||
}
|
||||
@ -97,15 +173,4 @@ export default defineComponent({
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.text) {
|
||||
word-break: break-word;
|
||||
font-family: '微软雅黑';
|
||||
outline: 0;
|
||||
|
||||
::selection {
|
||||
background-color: rgba(27, 110, 232, 0.3);
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user