Browse Source

添加prosemirror封装

pipipi-pikachu 5 years ago
parent
commit
52d5a6ba47

File diff suppressed because it is too large
+ 4055 - 313
package-lock.json


+ 20 - 0
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 - 0
src/prosemirror/commands/applyMark.ts

@@ -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 - 0
src/prosemirror/commands/setTextAlign.ts

@@ -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 - 0
src/prosemirror/commands/toggleList.ts

@@ -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 - 0
src/prosemirror/plugins/index.ts

@@ -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 - 0
src/prosemirror/plugins/inputrules.ts

@@ -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 - 0
src/prosemirror/plugins/keymap.ts

@@ -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 - 0
src/prosemirror/schema/index.ts

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

+ 146 - 0
src/prosemirror/schema/marks.ts

@@ -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 - 0
src/prosemirror/schema/nodes.ts

@@ -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 - 0
src/prosemirror/utils.ts

@@ -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
+}

+ 0 - 1
src/store/actions.ts

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