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 App from './App.vue'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
|
import 'prosemirror-view/style/prosemirror.css'
|
||||||
|
import '@/assets/styles/prosemirror.scss'
|
||||||
import '@/assets/styles/global.scss'
|
import '@/assets/styles/global.scss'
|
||||||
import 'animate.css'
|
import 'animate.css'
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export const slides: Slide[] = [
|
|||||||
},
|
},
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
lock: false,
|
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',
|
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$/,
|
/^(\d+)\.\s$/,
|
||||||
nodeType,
|
nodeType,
|
||||||
match => ({order: +match[1]}),
|
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 { marks } from 'prosemirror-schema-basic'
|
||||||
import { Node } from 'prosemirror-model'
|
import { MarkSpec } from 'prosemirror-model'
|
||||||
|
|
||||||
const subscript = {
|
const subscript: MarkSpec = {
|
||||||
excludes: 'subscript',
|
excludes: 'subscript',
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'sub' },
|
{ tag: 'sub' },
|
||||||
{
|
{
|
||||||
style: 'vertical-align',
|
style: 'vertical-align',
|
||||||
getAttrs: (value: string) => value === 'sub' && null
|
getAttrs: value => value === 'sub' && null
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: () => ['sub', 0],
|
toDOM: () => ['sub', 0],
|
||||||
}
|
}
|
||||||
|
|
||||||
const superscript = {
|
const superscript: MarkSpec = {
|
||||||
excludes: 'superscript',
|
excludes: 'superscript',
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'sup' },
|
{ tag: 'sup' },
|
||||||
{
|
{
|
||||||
style: 'vertical-align',
|
style: 'vertical-align',
|
||||||
getAttrs: (value: string) => value === 'super' && null
|
getAttrs: value => value === 'super' && null
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: () => ['sup', 0],
|
toDOM: () => ['sup', 0],
|
||||||
}
|
}
|
||||||
|
|
||||||
const strikethrough = {
|
const strikethrough: MarkSpec = {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'strike' },
|
{ tag: 'strike' },
|
||||||
{
|
{
|
||||||
style: 'text-decoration',
|
style: 'text-decoration',
|
||||||
getAttrs: (value: string) => value === 'line-through' && null
|
getAttrs: value => value === 'line-through' && null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
style: 'text-decoration-line',
|
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],
|
toDOM: () => ['span', { style: 'text-decoration-line: line-through' }, 0],
|
||||||
}
|
}
|
||||||
|
|
||||||
const underline = {
|
const underline: MarkSpec = {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'u' },
|
{ tag: 'u' },
|
||||||
{
|
{
|
||||||
style: 'text-decoration',
|
style: 'text-decoration',
|
||||||
getAttrs: (value: string) => value === 'underline' && null
|
getAttrs: value => value === 'underline' && null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
style: 'text-decoration-line',
|
style: 'text-decoration-line',
|
||||||
getAttrs: (value: string) => value === 'underline' && null
|
getAttrs: value => value === 'underline' && null
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: () => ['span', { style: 'text-decoration: underline' }, 0],
|
toDOM: () => ['span', { style: 'text-decoration: underline' }, 0],
|
||||||
}
|
}
|
||||||
|
|
||||||
const forecolor = {
|
const forecolor: MarkSpec = {
|
||||||
attrs: {
|
attrs: {
|
||||||
color: {},
|
color: {},
|
||||||
},
|
},
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
style: 'color',
|
style: 'color',
|
||||||
getAttrs: (color: string) => color ? { color } : {}
|
getAttrs: color => color ? { color } : {}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: (node: Node) => {
|
toDOM: mark => {
|
||||||
const { color } = node.attrs
|
const { color } = mark.attrs
|
||||||
let style = ''
|
let style = ''
|
||||||
if(color) style += `color: ${color};`
|
if(color) style += `color: ${color};`
|
||||||
return ['span', { style }, 0]
|
return ['span', { style }, 0]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const backcolor = {
|
const backcolor: MarkSpec = {
|
||||||
attrs: {
|
attrs: {
|
||||||
backcolor: {},
|
backcolor: {},
|
||||||
},
|
},
|
||||||
@ -82,18 +82,18 @@ const backcolor = {
|
|||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'span[style*=background-color]',
|
tag: 'span[style*=background-color]',
|
||||||
getAttrs: (backcolor: string) => backcolor ? { backcolor } : {}
|
getAttrs: backcolor => backcolor ? { backcolor } : {}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: (node: Node) => {
|
toDOM: mark => {
|
||||||
const { backcolor } = node.attrs
|
const { backcolor } = mark.attrs
|
||||||
let style = ''
|
let style = ''
|
||||||
if(backcolor) style += `background-color: ${backcolor};`
|
if(backcolor) style += `background-color: ${backcolor};`
|
||||||
return ['span', { style }, 0]
|
return ['span', { style }, 0]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontsize = {
|
const fontsize: MarkSpec = {
|
||||||
attrs: {
|
attrs: {
|
||||||
fontsize: {},
|
fontsize: {},
|
||||||
},
|
},
|
||||||
@ -102,31 +102,33 @@ const fontsize = {
|
|||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
style: 'font-size',
|
style: 'font-size',
|
||||||
getAttrs: (fontsize: string) => fontsize ? { fontsize } : {}
|
getAttrs: fontsize => fontsize ? { fontsize } : {}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: (node: Node) => {
|
toDOM: mark => {
|
||||||
const { fontsize } = node.attrs
|
const { fontsize } = mark.attrs
|
||||||
let style = ''
|
let style = ''
|
||||||
if(fontsize) style += `font-size: ${fontsize}`
|
if(fontsize) style += `font-size: ${fontsize}`
|
||||||
return ['span', { style }, 0]
|
return ['span', { style }, 0]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontname = {
|
const fontname: MarkSpec = {
|
||||||
attrs: {
|
attrs: {
|
||||||
fontname: '',
|
fontname: {},
|
||||||
},
|
},
|
||||||
inline: true,
|
inline: true,
|
||||||
group: 'inline',
|
group: 'inline',
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
style: 'font-family',
|
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) => {
|
toDOM: mark => {
|
||||||
const { fontname } = node.attrs
|
const { fontname } = mark.attrs
|
||||||
let style = ''
|
let style = ''
|
||||||
if(fontname) style += `font-family: ${fontname}`
|
if(fontname) style += `font-family: ${fontname}`
|
||||||
return ['span', { style }, 0]
|
return ['span', { style }, 0]
|
||||||
|
@ -1,55 +1,58 @@
|
|||||||
import { nodes } from 'prosemirror-schema-basic'
|
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'
|
import { orderedList, bulletList, listItem } from 'prosemirror-schema-list'
|
||||||
|
|
||||||
const listNodes = {
|
const _orderedList: NodeSpec = {
|
||||||
ordered_list: {
|
...orderedList,
|
||||||
...orderedList,
|
content: 'list_item+',
|
||||||
content: 'list_item+',
|
group: 'block',
|
||||||
group: 'block',
|
}
|
||||||
},
|
|
||||||
bullet_list: {
|
|
||||||
...bulletList,
|
|
||||||
content: 'list_item+',
|
|
||||||
group: 'block',
|
|
||||||
},
|
|
||||||
list_item: {
|
|
||||||
...listItem,
|
|
||||||
content: 'paragraph block*',
|
|
||||||
group: 'block',
|
|
||||||
},
|
|
||||||
|
|
||||||
paragraph: {
|
const _bulletList: NodeSpec = {
|
||||||
attrs: {
|
...bulletList,
|
||||||
align: {
|
content: 'list_item+',
|
||||||
default: '',
|
group: 'block',
|
||||||
},
|
}
|
||||||
|
|
||||||
|
const _listItem: NodeSpec = {
|
||||||
|
...listItem,
|
||||||
|
content: 'paragraph block*',
|
||||||
|
group: 'block',
|
||||||
|
}
|
||||||
|
|
||||||
|
const paragraph: NodeSpec = {
|
||||||
|
attrs: {
|
||||||
|
align: {
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
content: 'inline*',
|
},
|
||||||
group: 'block',
|
content: 'inline*',
|
||||||
parseDOM: [
|
group: 'block',
|
||||||
{
|
parseDOM: [
|
||||||
tag: 'p',
|
{
|
||||||
getAttrs: (dom: HTMLElement) => {
|
tag: 'p',
|
||||||
const { textAlign } = dom.style
|
getAttrs: dom => {
|
||||||
let align = dom.getAttribute('align') || textAlign || ''
|
const { textAlign } = (dom as HTMLElement).style
|
||||||
align = /(left|right|center|justify)/.test(align) ? align : ''
|
let align = (dom as HTMLElement).getAttribute('align') || textAlign || ''
|
||||||
|
align = /(left|right|center|justify)/.test(align) ? align : ''
|
||||||
return { align }
|
|
||||||
}
|
return { align }
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
toDOM: (node: Node) => {
|
],
|
||||||
const { align } = node.attrs
|
toDOM: (node: Node) => {
|
||||||
let style = ''
|
const { align } = node.attrs
|
||||||
if(align && align !== 'left') style += `text-align: ${align};`
|
let style = ''
|
||||||
|
if(align && align !== 'left') style += `text-align: ${align};`
|
||||||
|
|
||||||
return ['p', { style }, 0]
|
return ['p', { style }, 0]
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...nodes,
|
...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 { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
import { PPTElement, Slide } from '@/types/slides'
|
import { PPTElement, Slide } from '@/types/slides'
|
||||||
import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit'
|
import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit'
|
||||||
|
import { removeAllRanges } from '@/utils/selection'
|
||||||
|
|
||||||
import useViewportSize from './hooks/useViewportSize'
|
import useViewportSize from './hooks/useViewportSize'
|
||||||
import useMouseSelection from './hooks/useMouseSelection'
|
import useMouseSelection from './hooks/useMouseSelection'
|
||||||
@ -161,6 +162,7 @@ export default defineComponent({
|
|||||||
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
|
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, [])
|
||||||
if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
|
if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
|
||||||
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
|
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
|
||||||
|
removeAllRanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEditorAreaFocus = () => {
|
const removeEditorAreaFocus = () => {
|
||||||
|
@ -41,7 +41,6 @@ export default () => {
|
|||||||
const { redo, undo } = useHistorySnapshot()
|
const { redo, undo } = useHistorySnapshot()
|
||||||
|
|
||||||
const copy = () => {
|
const copy = () => {
|
||||||
if(disableHotkeys.value) return
|
|
||||||
if(activeElementIdList.value.length) copyElement()
|
if(activeElementIdList.value.length) copyElement()
|
||||||
else if(thumbnailsFocus.value) copySlide()
|
else if(thumbnailsFocus.value) copySlide()
|
||||||
}
|
}
|
||||||
@ -53,38 +52,36 @@ export default () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectAll = () => {
|
const selectAll = () => {
|
||||||
if(!editorAreaFocus.value && disableHotkeys.value) return
|
if(!editorAreaFocus.value) return
|
||||||
selectAllElement()
|
selectAllElement()
|
||||||
}
|
}
|
||||||
|
|
||||||
const lock = () => {
|
const lock = () => {
|
||||||
if(!editorAreaFocus.value && disableHotkeys.value) return
|
if(!editorAreaFocus.value) return
|
||||||
lockElement()
|
lockElement()
|
||||||
}
|
}
|
||||||
const combine = () => {
|
const combine = () => {
|
||||||
if(!editorAreaFocus.value && disableHotkeys.value) return
|
if(!editorAreaFocus.value) return
|
||||||
combineElements()
|
combineElements()
|
||||||
}
|
}
|
||||||
|
|
||||||
const uncombine = () => {
|
const uncombine = () => {
|
||||||
if(!editorAreaFocus.value && disableHotkeys.value) return
|
if(!editorAreaFocus.value) return
|
||||||
uncombineElements()
|
uncombineElements()
|
||||||
}
|
}
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
if(disableHotkeys.value) return
|
|
||||||
if(activeElementIdList.value.length) deleteElement()
|
if(activeElementIdList.value.length) deleteElement()
|
||||||
else if(thumbnailsFocus.value) deleteSlide()
|
else if(thumbnailsFocus.value) deleteSlide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const move = (key: string) => {
|
const move = (key: string) => {
|
||||||
if(disableHotkeys.value) return
|
|
||||||
if(activeElementIdList.value.length) moveElement(key)
|
if(activeElementIdList.value.length) moveElement(key)
|
||||||
else if(key === KEYS.UP || key === KEYS.DOWN) updateSlideIndex(key)
|
else if(key === KEYS.UP || key === KEYS.DOWN) updateSlideIndex(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = () => {
|
const create = () => {
|
||||||
if(!thumbnailsFocus.value || disableHotkeys.value) return
|
if(!thumbnailsFocus.value) return
|
||||||
createSlide()
|
createSlide()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,58 +106,72 @@ export default () => {
|
|||||||
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
|
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
|
||||||
|
|
||||||
if(ctrlKey && key === KEYS.C) {
|
if(ctrlKey && key === KEYS.C) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
copy()
|
copy()
|
||||||
}
|
}
|
||||||
if(ctrlKey && key === KEYS.X) {
|
if(ctrlKey && key === KEYS.X) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
cut()
|
cut()
|
||||||
}
|
}
|
||||||
if(ctrlKey && key === KEYS.Z) {
|
if(ctrlKey && key === KEYS.Z) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
undo()
|
undo()
|
||||||
}
|
}
|
||||||
if(ctrlKey && key === KEYS.Y) {
|
if(ctrlKey && key === KEYS.Y) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
redo()
|
redo()
|
||||||
}
|
}
|
||||||
if(ctrlKey && key === KEYS.A) {
|
if(ctrlKey && key === KEYS.A) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
selectAll()
|
selectAll()
|
||||||
}
|
}
|
||||||
if(ctrlKey && key === KEYS.L) {
|
if(ctrlKey && key === KEYS.L) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
lock()
|
lock()
|
||||||
}
|
}
|
||||||
if(!shiftKey && ctrlKey && key === KEYS.G) {
|
if(!shiftKey && ctrlKey && key === KEYS.G) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
combine()
|
combine()
|
||||||
}
|
}
|
||||||
if(shiftKey && ctrlKey && key === KEYS.G) {
|
if(shiftKey && ctrlKey && key === KEYS.G) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
uncombine()
|
uncombine()
|
||||||
}
|
}
|
||||||
if(key === KEYS.DELETE) {
|
if(key === KEYS.DELETE) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
remove()
|
remove()
|
||||||
}
|
}
|
||||||
if(key === KEYS.UP) {
|
if(key === KEYS.UP) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
move(KEYS.UP)
|
move(KEYS.UP)
|
||||||
}
|
}
|
||||||
if(key === KEYS.DOWN) {
|
if(key === KEYS.DOWN) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
move(KEYS.DOWN)
|
move(KEYS.DOWN)
|
||||||
}
|
}
|
||||||
if(key === KEYS.LEFT) {
|
if(key === KEYS.LEFT) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
move(KEYS.LEFT)
|
move(KEYS.LEFT)
|
||||||
}
|
}
|
||||||
if(key === KEYS.RIGHT) {
|
if(key === KEYS.RIGHT) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
move(KEYS.RIGHT)
|
move(KEYS.RIGHT)
|
||||||
}
|
}
|
||||||
if(key === KEYS.ENTER) {
|
if(key === KEYS.ENTER) {
|
||||||
|
if(disableHotkeys.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
create()
|
create()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editable-element-shape"
|
<div
|
||||||
|
class="editable-element-shape"
|
||||||
:style="{
|
:style="{
|
||||||
top: elementInfo.top + 'px',
|
top: elementInfo.top + 'px',
|
||||||
left: elementInfo.left + 'px',
|
left: elementInfo.left + 'px',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editable-element-shape"
|
<div
|
||||||
|
class="editable-element-shape"
|
||||||
:class="{ 'lock': elementInfo.lock }"
|
:class="{ 'lock': elementInfo.lock }"
|
||||||
:style="{
|
:style="{
|
||||||
top: elementInfo.top + 'px',
|
top: elementInfo.top + 'px',
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<SvgWrapper overflow="visible"
|
<SvgWrapper
|
||||||
|
overflow="visible"
|
||||||
:width="elementInfo.width"
|
:width="elementInfo.width"
|
||||||
:height="elementInfo.height"
|
:height="elementInfo.height"
|
||||||
>
|
>
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
filter: shadowStyle ? `drop-shadow(${shadowStyle})` : '',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<SvgWrapper overflow="visible"
|
<SvgWrapper
|
||||||
|
overflow="visible"
|
||||||
:width="elementInfo.width"
|
:width="elementInfo.width"
|
||||||
:height="elementInfo.height"
|
:height="elementInfo.height"
|
||||||
>
|
>
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
transform: `rotate(${elementInfo.rotate}deg)`,
|
transform: `rotate(${elementInfo.rotate}deg)`,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="element-content"
|
<div
|
||||||
|
class="element-content"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: elementInfo.fill,
|
backgroundColor: elementInfo.fill,
|
||||||
opacity: elementInfo.opacity,
|
opacity: elementInfo.opacity,
|
||||||
@ -20,7 +21,7 @@
|
|||||||
:height="elementInfo.height"
|
:height="elementInfo.height"
|
||||||
:outline="elementInfo.outline"
|
:outline="elementInfo.outline"
|
||||||
/>
|
/>
|
||||||
<div class="text" v-html="elementInfo.content"></div>
|
<div class="text ProseMirror-static" v-html="elementInfo.content"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -68,15 +69,4 @@ export default defineComponent({
|
|||||||
position: relative;
|
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>
|
</style>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="editable-element-text"
|
class="editable-element-text"
|
||||||
|
ref="elementRef"
|
||||||
:class="{ 'lock': elementInfo.lock }"
|
:class="{ 'lock': elementInfo.lock }"
|
||||||
:style="{
|
:style="{
|
||||||
top: elementInfo.top + 'px',
|
top: elementInfo.top + 'px',
|
||||||
@ -10,7 +11,8 @@
|
|||||||
}"
|
}"
|
||||||
@mousedown="$event => handleSelectElement($event)"
|
@mousedown="$event => handleSelectElement($event)"
|
||||||
>
|
>
|
||||||
<div class="element-content"
|
<div
|
||||||
|
class="element-content"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: elementInfo.fill,
|
backgroundColor: elementInfo.fill,
|
||||||
opacity: elementInfo.opacity,
|
opacity: elementInfo.opacity,
|
||||||
@ -23,9 +25,9 @@
|
|||||||
:height="elementInfo.height"
|
:height="elementInfo.height"
|
||||||
:outline="elementInfo.outline"
|
:outline="elementInfo.outline"
|
||||||
/>
|
/>
|
||||||
<div class="text"
|
<div
|
||||||
v-html="elementInfo.content"
|
class="text"
|
||||||
:contenteditable="!elementInfo.lock"
|
ref="editorViewRef"
|
||||||
@mousedown="$event => handleSelectElement($event, false)"
|
@mousedown="$event => handleSelectElement($event, false)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@ -33,10 +35,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 { PPTTextElement } from '@/types/slides'
|
||||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
import { initProsemirrorEditor, createDocument } from '@/prosemirror/'
|
||||||
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
import useElementShadow from '@/views/components/element/hooks/useElementShadow'
|
||||||
|
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||||
|
|
||||||
import ElementOutline from '@/views/components/element/ElementOutline.vue'
|
import ElementOutline from '@/views/components/element/ElementOutline.vue'
|
||||||
|
|
||||||
@ -59,6 +67,72 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props) {
|
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) => {
|
const handleSelectElement = (e: MouseEvent, canMove = true) => {
|
||||||
if(props.elementInfo.lock) return
|
if(props.elementInfo.lock) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -70,6 +144,8 @@ export default defineComponent({
|
|||||||
const { shadowStyle } = useElementShadow(shadow)
|
const { shadowStyle } = useElementShadow(shadow)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
elementRef,
|
||||||
|
editorViewRef,
|
||||||
handleSelectElement,
|
handleSelectElement,
|
||||||
shadowStyle,
|
shadowStyle,
|
||||||
}
|
}
|
||||||
@ -97,15 +173,4 @@ export default defineComponent({
|
|||||||
cursor: text;
|
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>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user