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
0988cbae90
commit
52d5a6ba47
4136
package-lock.json
generated
4136
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -17,6 +17,16 @@
|
||||
"dexie": "^3.0.3",
|
||||
"lodash": "^4.17.20",
|
||||
"mitt": "^2.1.0",
|
||||
"prosemirror-commands": "^1.1.4",
|
||||
"prosemirror-dropcursor": "^1.3.2",
|
||||
"prosemirror-gapcursor": "^1.1.5",
|
||||
"prosemirror-history": "^1.1.3",
|
||||
"prosemirror-inputrules": "^1.1.3",
|
||||
"prosemirror-model": "^1.13.1",
|
||||
"prosemirror-schema-basic": "^1.1.2",
|
||||
"prosemirror-schema-list": "^1.1.4",
|
||||
"prosemirror-state": "^1.3.3",
|
||||
"prosemirror-view": "^1.16.4",
|
||||
"store2": "^2.12.0",
|
||||
"vue": "^3.0.0",
|
||||
"vuedraggable": "^4.0.1",
|
||||
@ -26,6 +36,14 @@
|
||||
"@types/clipboard": "^2.0.1",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/jest": "^24.0.19",
|
||||
"@types/prosemirror-commands": "^1.0.3",
|
||||
"@types/prosemirror-dropcursor": "^1.0.0",
|
||||
"@types/prosemirror-gapcursor": "^1.0.1",
|
||||
"@types/prosemirror-history": "^1.0.1",
|
||||
"@types/prosemirror-inputrules": "^1.0.3",
|
||||
"@types/prosemirror-keymap": "^1.0.3",
|
||||
"@types/prosemirror-schema-basic": "^1.0.1",
|
||||
"@types/prosemirror-schema-list": "^1.0.1",
|
||||
"@types/resize-observer-browser": "^0.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
@ -41,8 +59,10 @@
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"i": "^0.3.6",
|
||||
"less": "^3.12.2",
|
||||
"less-loader": "^7.1.0",
|
||||
"npm": "^6.14.10",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"stylelint": "^13.8.0",
|
||||
|
40
src/prosemirror/commands/applyMark.ts
Normal file
40
src/prosemirror/commands/applyMark.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import { SelectionRange, Transaction } from 'prosemirror-state'
|
||||
|
||||
const markApplies = (tr: Transaction, ranges: SelectionRange[], type: MarkType) => {
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
let {$from, $to} = ranges[i]
|
||||
let can = $from.depth == 0 ? tr.doc.type.allowsMarkType(type) : false
|
||||
tr.doc.nodesBetween($from.pos, $to.pos, node => {
|
||||
if (can) return false
|
||||
can = node.inlineContent && node.type.allowsMarkType(type)
|
||||
})
|
||||
if (can) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const applyMark = (tr: Transaction, markType: MarkType, attrs: { [key: string]: string; } | undefined) => {
|
||||
if(!tr.selection || !tr.doc || !markType) return tr
|
||||
|
||||
const { empty, $anchor, ranges } = tr.selection
|
||||
if(empty && !$anchor || !markApplies(tr, ranges, markType)) return tr
|
||||
|
||||
if($anchor) {
|
||||
tr = tr.removeStoredMark(markType)
|
||||
return attrs ? tr.addStoredMark(markType.create(attrs)) : tr
|
||||
}
|
||||
|
||||
let has = false
|
||||
for(let i = 0; !has && i < ranges.length; i++) {
|
||||
const { $from, $to } = ranges[i]
|
||||
has = tr.doc.rangeHasMark($from.pos, $to.pos, markType)
|
||||
}
|
||||
for(let j = 0; j < ranges.length; j++) {
|
||||
const { $from, $to } = ranges[j]
|
||||
if(has) tr = tr.removeMark($from.pos, $to.pos, markType)
|
||||
if(attrs) tr = tr.addMark($from.pos, $to.pos, markType.create(attrs))
|
||||
}
|
||||
|
||||
return tr
|
||||
}
|
62
src/prosemirror/commands/setTextAlign.ts
Normal file
62
src/prosemirror/commands/setTextAlign.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Schema, Node, NodeType } from 'prosemirror-model'
|
||||
import { Transaction } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
|
||||
export const setTextAlign = (tr: Transaction, schema: Schema, alignment: string) => {
|
||||
const { selection, doc } = tr
|
||||
if(!selection || !doc) return tr
|
||||
|
||||
const { from, to } = selection
|
||||
const { nodes } = schema
|
||||
|
||||
const blockquote = nodes.blockquote
|
||||
const listItem = nodes.list_item
|
||||
const paragraph = nodes.paragraph
|
||||
|
||||
interface Task {
|
||||
node: Node;
|
||||
pos: number;
|
||||
nodeType: NodeType;
|
||||
}
|
||||
|
||||
const tasks: Task[] = []
|
||||
alignment = alignment || ''
|
||||
|
||||
const allowedNodeTypes = new Set([blockquote, listItem, paragraph])
|
||||
|
||||
doc.nodesBetween(from, to, (node, pos) => {
|
||||
const nodeType = node.type
|
||||
const align = node.attrs.align || ''
|
||||
if(align !== alignment && allowedNodeTypes.has(nodeType)) {
|
||||
tasks.push({
|
||||
node,
|
||||
pos,
|
||||
nodeType,
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if(!tasks.length) return tr
|
||||
|
||||
tasks.forEach(task => {
|
||||
const { node, pos, nodeType } = task
|
||||
let { attrs } = node
|
||||
if(alignment) attrs = { ...attrs, align: alignment }
|
||||
else attrs = { ...attrs, align: null }
|
||||
tr = tr.setNodeMarkup(pos, nodeType, attrs, node.marks);
|
||||
})
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
export const alignmentCommand = (view: EditorView, alignment: string) => {
|
||||
const { state } = view
|
||||
const { schema, selection } = state
|
||||
const tr = setTextAlign(
|
||||
state.tr.setSelection(selection),
|
||||
schema,
|
||||
alignment,
|
||||
)
|
||||
view.dispatch(tr)
|
||||
}
|
40
src/prosemirror/commands/toggleList.ts
Normal file
40
src/prosemirror/commands/toggleList.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { wrapInList, liftListItem } from 'prosemirror-schema-list'
|
||||
import { Schema, Node, NodeType } from 'prosemirror-model'
|
||||
import { Transaction, EditorState } from 'prosemirror-state'
|
||||
import { findParentNode } from '../utils'
|
||||
|
||||
const isList = (node: Node, schema: Schema) => {
|
||||
return (
|
||||
node.type === schema.nodes.bullet_list ||
|
||||
node.type === schema.nodes.ordered_list
|
||||
)
|
||||
}
|
||||
|
||||
export const toggleList = (listType: NodeType, itemType: NodeType) => {
|
||||
return (state: EditorState, dispatch: (tr: Transaction) => void) => {
|
||||
const { schema, selection } = state
|
||||
const { $from, $to } = selection
|
||||
const range = $from.blockRange($to)
|
||||
|
||||
if(!range) return false
|
||||
|
||||
const parentList = findParentNode((node: Node) => isList(node, schema))(selection)
|
||||
|
||||
if(range.depth >= 1 && parentList && range.depth - parentList.depth <= 1) {
|
||||
if(parentList.node.type === listType) {
|
||||
return liftListItem(itemType)(state, dispatch)
|
||||
}
|
||||
|
||||
if(isList(parentList.node, schema) && listType.validContent(parentList.node.content)) {
|
||||
const { tr } = state
|
||||
tr.setNodeMarkup(parentList.pos, listType)
|
||||
|
||||
if(dispatch) dispatch(tr)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return wrapInList(listType)(state, dispatch)
|
||||
}
|
||||
}
|
20
src/prosemirror/plugins/index.ts
Normal file
20
src/prosemirror/plugins/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { Schema } from 'prosemirror-model'
|
||||
import { history } from 'prosemirror-history'
|
||||
import { baseKeymap } from 'prosemirror-commands'
|
||||
import { dropCursor } from 'prosemirror-dropcursor'
|
||||
import { gapCursor } from 'prosemirror-gapcursor'
|
||||
|
||||
import { buildKeymap } from './keymap'
|
||||
import { buildInputRules } from './inputrules'
|
||||
|
||||
export const buildPlugins = (schema: Schema) => {
|
||||
return [
|
||||
buildInputRules(schema),
|
||||
keymap(buildKeymap(schema)),
|
||||
keymap(baseKeymap),
|
||||
dropCursor(),
|
||||
gapCursor(),
|
||||
history(),
|
||||
]
|
||||
}
|
38
src/prosemirror/plugins/inputrules.ts
Normal file
38
src/prosemirror/plugins/inputrules.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { NodeType, Schema } from 'prosemirror-model'
|
||||
import {
|
||||
inputRules,
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
smartQuotes,
|
||||
emDash,
|
||||
ellipsis,
|
||||
} from 'prosemirror-inputrules'
|
||||
|
||||
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
||||
|
||||
const orderedListRule = (nodeType: NodeType) => (
|
||||
wrappingInputRule(
|
||||
/^(\d+)\.\s$/,
|
||||
nodeType,
|
||||
match => ({order: +match[1]}),
|
||||
(match, node) => node.childCount + node.attrs.order == +match[1],
|
||||
)
|
||||
)
|
||||
|
||||
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([-+*])\s$/, nodeType)
|
||||
|
||||
const codeBlockRule = (nodeType: NodeType) => textblockTypeInputRule(/^```$/, nodeType)
|
||||
|
||||
export const buildInputRules = (schema: Schema) => {
|
||||
const rules = [
|
||||
...smartQuotes,
|
||||
ellipsis,
|
||||
emDash,
|
||||
]
|
||||
rules.push(blockQuoteRule(schema.nodes.blockquote))
|
||||
rules.push(orderedListRule(schema.nodes.ordered_list))
|
||||
rules.push(bulletListRule(schema.nodes.bullet_list))
|
||||
rules.push(codeBlockRule(schema.nodes.code_block))
|
||||
|
||||
return inputRules({ rules })
|
||||
}
|
33
src/prosemirror/plugins/keymap.ts
Normal file
33
src/prosemirror/plugins/keymap.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { splitListItem, liftListItem, sinkListItem } from 'prosemirror-schema-list'
|
||||
import { Schema } from 'prosemirror-model'
|
||||
import { undo, redo } from 'prosemirror-history'
|
||||
import { undoInputRule } from 'prosemirror-inputrules'
|
||||
import {
|
||||
toggleMark,
|
||||
selectParentNode,
|
||||
joinUp,
|
||||
joinDown,
|
||||
Command,
|
||||
} from 'prosemirror-commands'
|
||||
|
||||
export const buildKeymap = (schema: Schema) => {
|
||||
const keys = {}
|
||||
const bind = (key: string, cmd: Command) => keys[key] = cmd
|
||||
|
||||
bind('Alt-ArrowUp', joinUp)
|
||||
bind('Alt-ArrowDown', joinDown)
|
||||
bind('Ctrl-z', undo)
|
||||
bind('Ctrl-y', redo)
|
||||
bind('Backspace', undoInputRule)
|
||||
bind('Escape', selectParentNode)
|
||||
bind('Ctrl-b', toggleMark(schema.marks.strong))
|
||||
bind('Ctrl-i', toggleMark(schema.marks.em))
|
||||
bind('Ctrl-u', toggleMark(schema.marks.underline))
|
||||
bind('Ctrl-d', toggleMark(schema.marks.strikethrough))
|
||||
|
||||
bind('Enter', splitListItem(schema.nodes.list_item))
|
||||
bind('Mod-[', liftListItem(schema.nodes.list_item))
|
||||
bind('Mod-]', sinkListItem(schema.nodes.list_item))
|
||||
|
||||
return keys
|
||||
}
|
5
src/prosemirror/schema/index.ts
Normal file
5
src/prosemirror/schema/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import nodes from './nodes'
|
||||
import marks from './marks'
|
||||
|
||||
export const schemaNodes = nodes
|
||||
export const schemaMarks = marks
|
146
src/prosemirror/schema/marks.ts
Normal file
146
src/prosemirror/schema/marks.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { marks } from 'prosemirror-schema-basic'
|
||||
import { Node } from 'prosemirror-model'
|
||||
|
||||
const subscript = {
|
||||
excludes: 'subscript',
|
||||
parseDOM: [
|
||||
{ tag: 'sub' },
|
||||
{
|
||||
style: 'vertical-align',
|
||||
getAttrs: (value: string) => value === 'sub' && null
|
||||
},
|
||||
],
|
||||
toDOM: () => ['sub', 0],
|
||||
}
|
||||
|
||||
const superscript = {
|
||||
excludes: 'superscript',
|
||||
parseDOM: [
|
||||
{ tag: 'sup' },
|
||||
{
|
||||
style: 'vertical-align',
|
||||
getAttrs: (value: string) => value === 'super' && null
|
||||
},
|
||||
],
|
||||
toDOM: () => ['sup', 0],
|
||||
}
|
||||
|
||||
const strikethrough = {
|
||||
parseDOM: [
|
||||
{ tag: 'strike' },
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: (value: string) => value === 'line-through' && null
|
||||
},
|
||||
{
|
||||
style: 'text-decoration-line',
|
||||
getAttrs: (value: string) => value === 'line-through' && null
|
||||
},
|
||||
],
|
||||
toDOM: () => ['span', { style: 'text-decoration-line: line-through' }, 0],
|
||||
}
|
||||
|
||||
const underline = {
|
||||
parseDOM: [
|
||||
{ tag: 'u' },
|
||||
{
|
||||
style: 'text-decoration',
|
||||
getAttrs: (value: string) => value === 'underline' && null
|
||||
},
|
||||
{
|
||||
style: 'text-decoration-line',
|
||||
getAttrs: (value: string) => value === 'underline' && null
|
||||
},
|
||||
],
|
||||
toDOM: () => ['span', { style: 'text-decoration: underline' }, 0],
|
||||
}
|
||||
|
||||
const forecolor = {
|
||||
attrs: {
|
||||
color: {},
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
style: 'color',
|
||||
getAttrs: (color: string) => color ? { color } : {}
|
||||
},
|
||||
],
|
||||
toDOM: (node: Node) => {
|
||||
const { color } = node.attrs
|
||||
let style = ''
|
||||
if(color) style += `color: ${color};`
|
||||
return ['span', { style }, 0]
|
||||
},
|
||||
}
|
||||
|
||||
const backcolor = {
|
||||
attrs: {
|
||||
backcolor: {},
|
||||
},
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'span[style*=background-color]',
|
||||
getAttrs: (backcolor: string) => backcolor ? { backcolor } : {}
|
||||
},
|
||||
],
|
||||
toDOM: (node: Node) => {
|
||||
const { backcolor } = node.attrs
|
||||
let style = ''
|
||||
if(backcolor) style += `background-color: ${backcolor};`
|
||||
return ['span', { style }, 0]
|
||||
},
|
||||
}
|
||||
|
||||
const fontsize = {
|
||||
attrs: {
|
||||
fontsize: {},
|
||||
},
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
parseDOM: [
|
||||
{
|
||||
style: 'font-size',
|
||||
getAttrs: (fontsize: string) => fontsize ? { fontsize } : {}
|
||||
},
|
||||
],
|
||||
toDOM: (node: Node) => {
|
||||
const { fontsize } = node.attrs
|
||||
let style = ''
|
||||
if(fontsize) style += `font-size: ${fontsize}`
|
||||
return ['span', { style }, 0]
|
||||
},
|
||||
}
|
||||
|
||||
const fontname = {
|
||||
attrs: {
|
||||
fontname: '',
|
||||
},
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
parseDOM: [
|
||||
{
|
||||
style: 'font-family',
|
||||
getAttrs: (fontname: string) => ({ fontname: fontname ? fontname.replace(/[\"\']/g, '') : '' })
|
||||
},
|
||||
],
|
||||
toDOM: (node: Node) => {
|
||||
const { fontname } = node.attrs
|
||||
let style = ''
|
||||
if(fontname) style += `font-family: ${fontname}`
|
||||
return ['span', { style }, 0]
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
...marks,
|
||||
subscript,
|
||||
superscript,
|
||||
strikethrough,
|
||||
underline,
|
||||
forecolor,
|
||||
backcolor,
|
||||
fontsize,
|
||||
fontname,
|
||||
}
|
53
src/prosemirror/schema/nodes.ts
Normal file
53
src/prosemirror/schema/nodes.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { nodes } from 'prosemirror-schema-basic'
|
||||
import { Node } 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',
|
||||
},
|
||||
|
||||
paragraph: {
|
||||
attrs: {
|
||||
align: {default: null},
|
||||
},
|
||||
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 }
|
||||
}
|
||||
}
|
||||
],
|
||||
toDOM: (node: Node) => {
|
||||
const { align } = node.attrs
|
||||
let style = ''
|
||||
if(align && align !== 'left') style += `text-align: ${align};`
|
||||
|
||||
return ['p', { style }, 0]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
...nodes,
|
||||
...listNodes,
|
||||
}
|
77
src/prosemirror/utils.ts
Normal file
77
src/prosemirror/utils.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { Node, NodeType, ResolvedPos } from 'prosemirror-model'
|
||||
import { EditorState, Selection } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
|
||||
const equalNodeType = (nodeType: NodeType, node: Node) => {
|
||||
return Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1 || node.type === nodeType
|
||||
}
|
||||
|
||||
const findParentNodeClosestToPos = ($pos: ResolvedPos, predicate: (node: Node) => boolean) => {
|
||||
for(let i = $pos.depth; i > 0; i--) {
|
||||
const node = $pos.node(i)
|
||||
if(predicate(node)) {
|
||||
return {
|
||||
pos: i > 0 ? $pos.before(i) : 0,
|
||||
start: $pos.start(i),
|
||||
depth: i,
|
||||
node,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const findParentNode = (predicate: (node: Node) => boolean) => {
|
||||
return (_ref: Selection) => findParentNodeClosestToPos(_ref.$from, predicate)
|
||||
}
|
||||
|
||||
export const findParentNodeOfType = (nodeType: NodeType) => {
|
||||
return (selection: Selection) => {
|
||||
return findParentNode((node: Node) => {
|
||||
return equalNodeType(nodeType, node)
|
||||
})(selection)
|
||||
}
|
||||
}
|
||||
|
||||
export const isActiveOfParentNodeType = (nodeType: string, state: EditorState) => {
|
||||
const node = state.schema.nodes[nodeType]
|
||||
return !!findParentNodeOfType(node)(state.selection)
|
||||
}
|
||||
|
||||
export const getMarkAttrs = (view: EditorView) => {
|
||||
const { selection, doc } = view.state
|
||||
const { from } = selection
|
||||
const node = doc.nodeAt(from)
|
||||
return node?.marks || []
|
||||
}
|
||||
|
||||
export const getAttrValue = (view: EditorView, markType: string, attr: string) => {
|
||||
const marks = getMarkAttrs(view)
|
||||
for(const mark of marks) {
|
||||
if(mark.type.name === markType && mark.attrs[attr]) return mark.attrs[attr]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const isActiveMark = (view: EditorView, markType: string) => {
|
||||
const marks = getMarkAttrs(view)
|
||||
for(const mark of marks) {
|
||||
if(mark.type.name === markType) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const getAttrValueInSelection = (view: EditorView, attr: string) => {
|
||||
const { selection, doc } = view.state
|
||||
const { from, to } = selection
|
||||
|
||||
let keepChecking = true
|
||||
let value = ''
|
||||
doc.nodesBetween(from, to, node => {
|
||||
if(keepChecking && node.attrs[attr]) {
|
||||
keepChecking = false
|
||||
value = node.attrs[attr]
|
||||
}
|
||||
return keepChecking
|
||||
})
|
||||
return value
|
||||
}
|
@ -11,7 +11,6 @@ export const actions: ActionTree<State, State> = {
|
||||
|
||||
if(lastSnapshot) {
|
||||
db.snapshots.clear()
|
||||
// commit(MutationTypes.SET_SLIDES, lastSnapshot.slides)
|
||||
}
|
||||
|
||||
const newFirstSnapshot = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user