添加prosemirror封装

This commit is contained in:
pipipi-pikachu 2020-12-31 10:56:34 +08:00
parent 0988cbae90
commit 52d5a6ba47
13 changed files with 4473 additions and 198 deletions

4136
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View 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
}

View 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)
}

View 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)
}
}

View 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(),
]
}

View 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 })
}

View 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
}

View File

@ -0,0 +1,5 @@
import nodes from './nodes'
import marks from './marks'
export const schemaNodes = nodes
export const schemaMarks = marks

View 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,
}

View 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
View 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
}

View File

@ -11,7 +11,6 @@ export const actions: ActionTree<State, State> = {
if(lastSnapshot) {
db.snapshots.clear()
// commit(MutationTypes.SET_SLIDES, lastSnapshot.slides)
}
const newFirstSnapshot = {