initial commit
This commit is contained in:
215
src/components/editor.vue
Normal file
215
src/components/editor.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<script>
|
||||
import { EditorState, Plugin } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { Schema, DOMParser } from 'prosemirror-model'
|
||||
import { dropCursor } from 'prosemirror-dropcursor'
|
||||
import { gapCursor } from 'prosemirror-gapcursor'
|
||||
import { history } from 'prosemirror-history'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { baseKeymap } from 'prosemirror-commands'
|
||||
import { inputRules } from 'prosemirror-inputrules'
|
||||
|
||||
import {
|
||||
buildMenuActions,
|
||||
PluginManager,
|
||||
initNodeViews,
|
||||
menuBubble,
|
||||
builtInKeymap,
|
||||
} from '../utils'
|
||||
import builtInNodes from '../nodes'
|
||||
import builtInMarks from '../marks'
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
doc: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
extensions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const plugins = new PluginManager([
|
||||
...builtInNodes,
|
||||
...builtInMarks,
|
||||
...this.extensions,
|
||||
])
|
||||
const { nodes, marks, views } = plugins
|
||||
|
||||
return {
|
||||
state: null,
|
||||
view: null,
|
||||
pluginplugins: [],
|
||||
plugins,
|
||||
schema: null,
|
||||
nodes,
|
||||
marks,
|
||||
views,
|
||||
keymaps: [],
|
||||
commands: {},
|
||||
menuActions: null,
|
||||
}
|
||||
},
|
||||
|
||||
render(createElement) {
|
||||
const slots = []
|
||||
|
||||
Object
|
||||
.entries(this.$scopedSlots)
|
||||
.forEach(([name, slot]) => {
|
||||
if (name === 'content') {
|
||||
this.contentNode = slot({})
|
||||
slots.push(this.contentNode)
|
||||
} else if (name === 'menubar') {
|
||||
this.menubarNode = slot({
|
||||
nodes: this.menuActions ? this.menuActions.nodes : null,
|
||||
marks: this.menuActions ? this.menuActions.marks : null,
|
||||
focused: this.view ? this.view.focused : false,
|
||||
focus: () => this.view.focus(),
|
||||
})
|
||||
slots.push(this.menubarNode)
|
||||
} else if (name === 'menububble') {
|
||||
this.menububbleNode = slot({
|
||||
nodes: this.menuActions ? this.menuActions.nodes : null,
|
||||
marks: this.menuActions ? this.menuActions.marks : null,
|
||||
focused: this.view ? this.view.focused : false,
|
||||
focus: () => this.view.focus(),
|
||||
})
|
||||
slots.push(this.menububbleNode)
|
||||
}
|
||||
})
|
||||
|
||||
return createElement('div', {
|
||||
class: 'vue-editor',
|
||||
}, slots)
|
||||
},
|
||||
|
||||
methods: {
|
||||
initEditor() {
|
||||
this.schema = this.createSchema()
|
||||
this.pluginplugins = this.createPlugins()
|
||||
this.keymaps = this.createKeymaps()
|
||||
this.inputRules = this.createInputRules()
|
||||
this.state = this.createState()
|
||||
this.clearSlot()
|
||||
this.view = this.createView()
|
||||
this.commands = this.createCommands()
|
||||
this.updateMenuActions()
|
||||
},
|
||||
|
||||
createSchema() {
|
||||
return new Schema({
|
||||
nodes: this.nodes,
|
||||
marks: this.marks,
|
||||
})
|
||||
},
|
||||
|
||||
createPlugins() {
|
||||
return this.plugins.pluginplugins
|
||||
},
|
||||
|
||||
createKeymaps() {
|
||||
return this.plugins.keymaps({
|
||||
schema: this.schema,
|
||||
})
|
||||
},
|
||||
|
||||
createInputRules() {
|
||||
return this.plugins.inputRules({
|
||||
schema: this.schema,
|
||||
})
|
||||
},
|
||||
|
||||
createCommands() {
|
||||
return this.plugins.commands({
|
||||
schema: this.schema,
|
||||
view: this.view,
|
||||
})
|
||||
},
|
||||
|
||||
createState() {
|
||||
return EditorState.create({
|
||||
schema: this.schema,
|
||||
doc: this.getDocument(),
|
||||
plugins: [
|
||||
...this.pluginplugins,
|
||||
...this.getPlugins(),
|
||||
],
|
||||
})
|
||||
},
|
||||
|
||||
getDocument() {
|
||||
if (this.doc) {
|
||||
return this.schema.nodeFromJSON(this.doc)
|
||||
}
|
||||
|
||||
return DOMParser.fromSchema(this.schema).parse(this.contentNode.elm)
|
||||
},
|
||||
|
||||
clearSlot() {
|
||||
this.contentNode.elm.innerHTML = ''
|
||||
},
|
||||
|
||||
getPlugins() {
|
||||
return [
|
||||
menuBubble(this.menububbleNode),
|
||||
inputRules({
|
||||
rules: this.inputRules,
|
||||
}),
|
||||
...this.keymaps,
|
||||
keymap(builtInKeymap),
|
||||
keymap(baseKeymap),
|
||||
dropCursor(),
|
||||
gapCursor(),
|
||||
history(),
|
||||
new Plugin({
|
||||
props: {
|
||||
editable: () => this.editable,
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
createView() {
|
||||
return new EditorView(this.contentNode.elm, {
|
||||
state: this.state,
|
||||
dispatchTransaction: this.dispatchTransaction,
|
||||
nodeViews: initNodeViews({
|
||||
nodes: this.views,
|
||||
editable: this.editable,
|
||||
}),
|
||||
})
|
||||
},
|
||||
|
||||
updateMenuActions() {
|
||||
this.menuActions = buildMenuActions({
|
||||
schema: this.schema,
|
||||
state: this.view.state,
|
||||
commands: this.commands,
|
||||
})
|
||||
},
|
||||
|
||||
dispatchTransaction(transaction) {
|
||||
this.state = this.state.apply(transaction)
|
||||
this.view.updateState(this.state)
|
||||
this.$emit('update', this.state)
|
||||
this.updateMenuActions()
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initEditor()
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
89
src/helpers/index.js
Normal file
89
src/helpers/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
joinBackward,
|
||||
selectNodeBackward,
|
||||
joinForward,
|
||||
selectNodeForward,
|
||||
joinUp,
|
||||
joinDown,
|
||||
lift,
|
||||
newlineInCode,
|
||||
exitCode,
|
||||
createParagraphNear,
|
||||
liftEmptyBlock,
|
||||
splitBlock,
|
||||
splitBlockKeepMarks,
|
||||
selectParentNode,
|
||||
selectAll,
|
||||
wrapIn,
|
||||
setBlockType,
|
||||
toggleMark,
|
||||
autoJoin,
|
||||
baseKeymap,
|
||||
pcBaseKeymap,
|
||||
macBaseKeymap,
|
||||
} from 'prosemirror-commands'
|
||||
|
||||
import {
|
||||
addListNodes,
|
||||
wrapInList,
|
||||
splitListItem,
|
||||
liftListItem,
|
||||
sinkListItem,
|
||||
} from 'prosemirror-schema-list'
|
||||
|
||||
import {
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
} from 'prosemirror-inputrules'
|
||||
|
||||
import removeMark from './removeMark'
|
||||
import toggleBlockType from './toggleBlockType'
|
||||
import toggleList from './toggleList'
|
||||
import updateMark from './updateMark'
|
||||
|
||||
export {
|
||||
// prosemirror-commands
|
||||
chainCommands,
|
||||
deleteSelection,
|
||||
joinBackward,
|
||||
selectNodeBackward,
|
||||
joinForward,
|
||||
selectNodeForward,
|
||||
joinUp,
|
||||
joinDown,
|
||||
lift,
|
||||
newlineInCode,
|
||||
exitCode,
|
||||
createParagraphNear,
|
||||
liftEmptyBlock,
|
||||
splitBlock,
|
||||
splitBlockKeepMarks,
|
||||
selectParentNode,
|
||||
selectAll,
|
||||
wrapIn,
|
||||
setBlockType,
|
||||
toggleMark,
|
||||
autoJoin,
|
||||
baseKeymap,
|
||||
pcBaseKeymap,
|
||||
macBaseKeymap,
|
||||
|
||||
// prosemirror-schema-list
|
||||
addListNodes,
|
||||
wrapInList,
|
||||
splitListItem,
|
||||
liftListItem,
|
||||
sinkListItem,
|
||||
|
||||
// prosemirror-inputrules
|
||||
wrappingInputRule,
|
||||
textblockTypeInputRule,
|
||||
|
||||
// custom
|
||||
removeMark,
|
||||
toggleBlockType,
|
||||
toggleList,
|
||||
updateMark,
|
||||
}
|
||||
6
src/helpers/removeMark.js
Normal file
6
src/helpers/removeMark.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function (type) {
|
||||
return (state, dispatch) => {
|
||||
const { from, to } = state.selection
|
||||
return dispatch(state.tr.removeMark(from, to, type))
|
||||
}
|
||||
}
|
||||
14
src/helpers/toggleBlockType.js
Normal file
14
src/helpers/toggleBlockType.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { setBlockType } from 'prosemirror-commands'
|
||||
import { nodeIsActive } from 'vue-mirror/utils'
|
||||
|
||||
export default function (type, toggletype, attrs = {}) {
|
||||
return (state, dispatch, view) => {
|
||||
const isActive = nodeIsActive(state, type, attrs)
|
||||
|
||||
if (isActive) {
|
||||
return setBlockType(toggletype)(state, dispatch, view)
|
||||
}
|
||||
|
||||
return setBlockType(type, attrs)(state, dispatch, view)
|
||||
}
|
||||
}
|
||||
49
src/helpers/toggleList.js
Normal file
49
src/helpers/toggleList.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { nodeIsActive } from 'vue-mirror/utils'
|
||||
import { wrapInList, liftListItem } from 'vue-mirror/helpers'
|
||||
|
||||
export default function toggleList(type, itemType) {
|
||||
return (state, dispatch, view) => {
|
||||
const isActive = nodeIsActive(state, type)
|
||||
|
||||
if (isActive) {
|
||||
return liftListItem(itemType)(state, dispatch, view)
|
||||
}
|
||||
|
||||
return wrapInList(type)(state, dispatch, view)
|
||||
}
|
||||
}
|
||||
|
||||
// https://discuss.prosemirror.net/t/list-type-toggle/948
|
||||
|
||||
// import { wrapInList, liftListItem } from 'prosemirror-schema-list'
|
||||
|
||||
// function isList(node, schema) {
|
||||
// return (node.type === schema.nodes.bullet_list || node.type === schema.nodes.ordered_list)
|
||||
// }
|
||||
|
||||
// export default function toggleList(listType, schema) {
|
||||
// const lift = liftListItem(schema.nodes.list_item)
|
||||
// const wrap = wrapInList(listType)
|
||||
|
||||
// return (state, dispatch) => {
|
||||
// const { $from, $to } = state.selection
|
||||
// const range = $from.blockRange($to)
|
||||
// if (!range) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// if (range.depth >= 2 && $from.node(range.depth - 1).type === listType) {
|
||||
// return lift(state, dispatch)
|
||||
// } else if (range.depth >= 2 && isList($from.node(range.depth - 1), schema)) {
|
||||
// const tr = state.tr
|
||||
// const node = $from.before(range.depth - 1)
|
||||
// console.log({node})
|
||||
// // TODO: how do I pass the node above to `setNodeType`?
|
||||
// // tr.setNodeType(range.start, listType);
|
||||
// if (dispatch) dispatch(tr)
|
||||
// return false
|
||||
// } else {
|
||||
// return wrap(state, dispatch)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
6
src/helpers/updateMark.js
Normal file
6
src/helpers/updateMark.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function (type, attrs) {
|
||||
return (state, dispatch) => {
|
||||
const { from, to } = state.selection
|
||||
return dispatch(state.tr.addMark(from, to, type.create(attrs)))
|
||||
}
|
||||
}
|
||||
3
src/index.js
Normal file
3
src/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Editor from './components/editor.vue'
|
||||
|
||||
export { Editor }
|
||||
39
src/marks/Bold.js
Normal file
39
src/marks/Bold.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Mark } from 'vue-mirror/utils'
|
||||
import { toggleMark } from 'vue-mirror/helpers'
|
||||
|
||||
export default class BoldMark extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'bold'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'strong',
|
||||
},
|
||||
{
|
||||
tag: 'b',
|
||||
getAttrs: node => node.style.fontWeight != 'normal' && null,
|
||||
},
|
||||
{
|
||||
style: 'font-weight',
|
||||
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['strong', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-b': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return toggleMark(type)
|
||||
}
|
||||
|
||||
}
|
||||
29
src/marks/Code.js
Normal file
29
src/marks/Code.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Mark } from 'vue-mirror/utils'
|
||||
import { toggleMark } from 'vue-mirror/helpers'
|
||||
|
||||
export default class CodeMark extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'code'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: 'code' },
|
||||
],
|
||||
toDOM: () => ['code', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-`': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return toggleMark(type)
|
||||
}
|
||||
|
||||
}
|
||||
31
src/marks/Italic.js
Normal file
31
src/marks/Italic.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Mark } from 'vue-mirror/utils'
|
||||
import { toggleMark } from 'vue-mirror/helpers'
|
||||
|
||||
export default class ItalicMark extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'italic'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
{ tag: 'i' },
|
||||
{ tag: 'em' },
|
||||
{ style: 'font-style=italic' },
|
||||
],
|
||||
toDOM: () => ['em', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Mod-i': toggleMark(type),
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return toggleMark(type)
|
||||
}
|
||||
|
||||
}
|
||||
55
src/marks/Link.js
Normal file
55
src/marks/Link.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Mark } from 'vue-mirror/utils'
|
||||
import { updateMark, removeMark } from 'vue-mirror/helpers'
|
||||
|
||||
export default class LinkMark extends Mark {
|
||||
|
||||
get name() {
|
||||
return 'link'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return {
|
||||
props: ['node'],
|
||||
methods: {
|
||||
onClick() {
|
||||
console.log('click on link')
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<a :href="node.attrs.href" rel="noopener noreferrer nofollow" ref="content" @click="onClick"></a>
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
href: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => ({
|
||||
href: dom.getAttribute('href'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => ['a', {
|
||||
...node.attrs,
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
}, 0],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type, attrs }) {
|
||||
if (attrs.href) {
|
||||
return updateMark(type, attrs)
|
||||
}
|
||||
|
||||
return removeMark(type)
|
||||
}
|
||||
|
||||
}
|
||||
11
src/marks/index.js
Normal file
11
src/marks/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Code from './Code'
|
||||
import Italic from './Italic'
|
||||
import Link from './Link'
|
||||
import Bold from './Bold'
|
||||
|
||||
export default [
|
||||
new Code(),
|
||||
new Italic(),
|
||||
new Link(),
|
||||
new Bold(),
|
||||
]
|
||||
39
src/nodes/Blockquote.js
Normal file
39
src/nodes/Blockquote.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { wrappingInputRule, setBlockType, wrapIn } from 'vue-mirror/helpers'
|
||||
|
||||
export default class BlockquoteNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'blockquote'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block+',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'blockquote' },
|
||||
],
|
||||
toDOM: () => ['blockquote', 0],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return setBlockType(type)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Ctrl->': wrapIn(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*>\s$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
37
src/nodes/BulletList.js
Normal file
37
src/nodes/BulletList.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { wrappingInputRule, wrapInList, toggleList } from 'vue-mirror/helpers'
|
||||
|
||||
export default class BulletNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'bullet_list'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'list_item+',
|
||||
group: 'block',
|
||||
parseDOM: [
|
||||
{ tag: 'ul' },
|
||||
],
|
||||
toDOM: () => ['ul', 0],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type, schema }) {
|
||||
return toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-8': wrapInList(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*([-+*])\s$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
41
src/nodes/CodeBlock.js
Normal file
41
src/nodes/CodeBlock.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'vue-mirror/helpers'
|
||||
|
||||
export default class CodeBlockNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'code_block'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||
],
|
||||
toDOM: () => ['pre', ['code', 0]],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type, schema }) {
|
||||
return toggleBlockType(type, schema.nodes.paragraph)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-\\': setBlockType(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
textblockTypeInputRule(/^```$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
15
src/nodes/Doc.js
Normal file
15
src/nodes/Doc.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
|
||||
export default class DocNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'doc'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block+',
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
33
src/nodes/HardBreak.js
Normal file
33
src/nodes/HardBreak.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { chainCommands, exitCode } from 'vue-mirror/helpers'
|
||||
|
||||
export default class HardBreakNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'hard_break'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
selectable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'br' },
|
||||
],
|
||||
toDOM: () => ['br'],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
const command = chainCommands(exitCode, (state, dispatch) => {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
||||
return true
|
||||
})
|
||||
return {
|
||||
'Mod-Enter': command,
|
||||
'Shift-Enter': command,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
59
src/nodes/Heading.js
Normal file
59
src/nodes/Heading.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { setBlockType, textblockTypeInputRule, toggleBlockType } from 'vue-mirror/helpers'
|
||||
|
||||
export default class HeadingNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'heading'
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
maxLevel: 6,
|
||||
}
|
||||
}
|
||||
|
||||
get levels() {
|
||||
return Array.from(new Array(this.options.maxLevel), (value, index) => index + 1)
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
level: {
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: this.levels.map(level => ({ tag: `h${level}`, attrs: { level } })),
|
||||
toDOM: node => [`h${node.attrs.level}`, 0],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type, schema, attrs }) {
|
||||
return toggleBlockType(type, schema.nodes.paragraph, attrs)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return this.levels.reduce((items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
[`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
|
||||
},
|
||||
}), {})
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
textblockTypeInputRule(
|
||||
new RegExp(`^(#{1,${this.options.maxLevel}})\\s$`),
|
||||
type,
|
||||
match => ({ level: match[1].length }),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
31
src/nodes/ListItem.js
Normal file
31
src/nodes/ListItem.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { splitListItem, liftListItem, sinkListItem } from 'vue-mirror/helpers'
|
||||
|
||||
export default class OrderedListNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'list_item'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'paragraph block*',
|
||||
group: 'block',
|
||||
defining: true,
|
||||
draggable: false,
|
||||
parseDOM: [
|
||||
{ tag: 'li' },
|
||||
],
|
||||
toDOM: () => ['li', 0],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitListItem(type),
|
||||
Tab: sinkListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
52
src/nodes/OrderedList.js
Normal file
52
src/nodes/OrderedList.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { wrappingInputRule, wrapInList, toggleList } from 'vue-mirror/helpers'
|
||||
|
||||
export default class OrderedListNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'ordered_list'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
order: {
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
content: 'list_item+',
|
||||
group: 'block',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'ol',
|
||||
getAttrs: dom => ({
|
||||
order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
|
||||
}
|
||||
}
|
||||
|
||||
command({ type, schema }) {
|
||||
return toggleList(type, schema.nodes.list_item)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-9': wrapInList(type),
|
||||
}
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(
|
||||
/^(\d+)\.\s$/,
|
||||
type,
|
||||
match => ({ order: +match[1] }),
|
||||
(match, node) => node.childCount + node.attrs.order === +match[1],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
32
src/nodes/Paragraph.js
Normal file
32
src/nodes/Paragraph.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { setBlockType } from 'vue-mirror/helpers'
|
||||
|
||||
export default class ParagraphNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'paragraph'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
draggable: false,
|
||||
parseDOM: [{
|
||||
tag: 'p',
|
||||
}],
|
||||
toDOM: () => ['p', 0],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return setBlockType(type)
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
'Shift-Ctrl-0': setBlockType(type),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
15
src/nodes/Text.js
Normal file
15
src/nodes/Text.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
|
||||
export default class TextNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'text'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'inline',
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
69
src/nodes/TodoItem.js
Normal file
69
src/nodes/TodoItem.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { splitListItem, liftListItem } from 'vue-mirror/helpers'
|
||||
|
||||
export default class TodoItemNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'todo_item'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return {
|
||||
props: ['node', 'updateAttrs', 'editable'],
|
||||
methods: {
|
||||
onChange() {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
this.updateAttrs({
|
||||
done: !this.node.attrs.done,
|
||||
})
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<li data-type="todo_item" :data-done="node.attrs.done.toString()">
|
||||
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
|
||||
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
|
||||
</li>
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
done: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
draggable: false,
|
||||
content: 'paragraph',
|
||||
toDOM(node) {
|
||||
const { done } = node.attrs
|
||||
|
||||
return ['li', {
|
||||
'data-type': 'todo_item',
|
||||
'data-done': done.toString(),
|
||||
},
|
||||
['span', { class: 'todo-checkbox', contenteditable: 'false' }],
|
||||
['div', { class: 'todo-content' }, 0],
|
||||
]
|
||||
},
|
||||
parseDOM: [{
|
||||
priority: 51,
|
||||
tag: '[data-type="todo_item"]',
|
||||
getAttrs: dom => ({
|
||||
done: dom.getAttribute('data-done') === 'true',
|
||||
}),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
keys({ type }) {
|
||||
return {
|
||||
Enter: splitListItem(type),
|
||||
'Shift-Tab': liftListItem(type),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
32
src/nodes/TodoList.js
Normal file
32
src/nodes/TodoList.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Node } from 'vue-mirror/utils'
|
||||
import { wrapInList, wrappingInputRule } from 'vue-mirror/helpers'
|
||||
|
||||
export default class BulletNode extends Node {
|
||||
|
||||
get name() {
|
||||
return 'todo_list'
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: 'todo_item+',
|
||||
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0],
|
||||
parseDOM: [{
|
||||
priority: 51,
|
||||
tag: '[data-type="todo_list"]',
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
command({ type }) {
|
||||
return wrapInList(type)
|
||||
}
|
||||
|
||||
inputRules({ type }) {
|
||||
return [
|
||||
wrappingInputRule(/^\s*(\[ \])\s$/, type),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
29
src/nodes/index.js
Normal file
29
src/nodes/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import Blockquote from './Blockquote'
|
||||
import BulletList from './BulletList'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import Doc from './Doc'
|
||||
import HardBreak from './HardBreak'
|
||||
import Heading from './Heading'
|
||||
import ListItem from './ListItem'
|
||||
import OrderedList from './OrderedList'
|
||||
import Paragraph from './Paragraph'
|
||||
import Text from './Text'
|
||||
import TodoList from './TodoList'
|
||||
import TodoItem from './TodoItem'
|
||||
|
||||
export default [
|
||||
// essentials
|
||||
new Doc(),
|
||||
new Paragraph(),
|
||||
new Text(),
|
||||
|
||||
new Blockquote(),
|
||||
new CodeBlock(),
|
||||
new Heading({ maxLevel: 3 }),
|
||||
new HardBreak(),
|
||||
new OrderedList(),
|
||||
new BulletList(),
|
||||
new ListItem(),
|
||||
new TodoList(),
|
||||
new TodoItem(),
|
||||
]
|
||||
74
src/utils/ComponentView.js
Normal file
74
src/utils/ComponentView.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default class ComponentView {
|
||||
constructor(component, {
|
||||
node,
|
||||
view,
|
||||
getPos,
|
||||
decorations,
|
||||
editable,
|
||||
}) {
|
||||
this.component = component
|
||||
this.node = node
|
||||
this.view = view
|
||||
this.getPos = getPos
|
||||
this.decorations = decorations
|
||||
this.editable = editable
|
||||
|
||||
this.dom = this.createDOM()
|
||||
this.contentDOM = this._vm.$refs.content
|
||||
}
|
||||
|
||||
createDOM() {
|
||||
const Component = Vue.extend(this.component)
|
||||
this._vm = new Component({
|
||||
propsData: {
|
||||
node: this.node,
|
||||
view: this.view,
|
||||
getPos: this.getPos,
|
||||
decorations: this.decorations,
|
||||
editable: this.editable,
|
||||
updateAttrs: attrs => this.updateAttrs(attrs),
|
||||
updateContent: content => this.updateContent(content),
|
||||
},
|
||||
}).$mount()
|
||||
return this._vm.$el
|
||||
}
|
||||
|
||||
updateAttrs(attrs) {
|
||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||
...this.node.attrs,
|
||||
...attrs,
|
||||
})
|
||||
this.view.dispatch(transaction)
|
||||
}
|
||||
|
||||
updateContent(content) {
|
||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), this.node.type, { content })
|
||||
this.view.dispatch(transaction)
|
||||
}
|
||||
|
||||
ignoreMutation() {
|
||||
return true
|
||||
}
|
||||
|
||||
update(node, decorations) {
|
||||
if (node.type !== this.node.type) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (node === this.node && this.decorations === decorations) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.node = node
|
||||
this.decorations = decorations
|
||||
this._vm._props.node = node
|
||||
this._vm._props.decorations = decorations
|
||||
return true
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._vm.$destroy()
|
||||
}
|
||||
}
|
||||
84
src/utils/PluginManager.js
Normal file
84
src/utils/PluginManager.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
|
||||
export default class PluginManager {
|
||||
|
||||
constructor(plugins = []) {
|
||||
this.plugins = plugins
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.type === 'node')
|
||||
.reduce((nodes, { name, schema }) => ({
|
||||
...nodes,
|
||||
[name]: schema,
|
||||
}), {})
|
||||
}
|
||||
|
||||
get marks() {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.type === 'mark')
|
||||
.reduce((marks, { name, schema }) => ({
|
||||
...marks,
|
||||
[name]: schema,
|
||||
}), {})
|
||||
}
|
||||
|
||||
get pluginplugins() {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.plugins)
|
||||
.reduce((allPlugins, { plugins }) => ([
|
||||
...allPlugins,
|
||||
...plugins,
|
||||
]), [])
|
||||
}
|
||||
|
||||
get views() {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.view)
|
||||
.reduce((views, { name, view }) => ({
|
||||
...views,
|
||||
[name]: view,
|
||||
}), {})
|
||||
}
|
||||
|
||||
keymaps({ schema }) {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.keys)
|
||||
.map(plugin => plugin.keys({
|
||||
type: schema[`${plugin.type}s`][plugin.name],
|
||||
schema,
|
||||
}))
|
||||
.map(keys => keymap(keys))
|
||||
}
|
||||
|
||||
inputRules({ schema }) {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.inputRules)
|
||||
.map(plugin => plugin.inputRules({
|
||||
type: schema[`${plugin.type}s`][plugin.name],
|
||||
schema,
|
||||
}))
|
||||
.reduce((allInputRules, inputRules) => ([
|
||||
...allInputRules,
|
||||
...inputRules,
|
||||
]), [])
|
||||
}
|
||||
|
||||
commands({ schema, view }) {
|
||||
return this.plugins
|
||||
.filter(plugin => plugin.command)
|
||||
.reduce((commands, { name, type, command }) => ({
|
||||
...commands,
|
||||
[name]: attrs => {
|
||||
view.focus()
|
||||
command({
|
||||
type: schema[`${type}s`][name],
|
||||
attrs,
|
||||
schema,
|
||||
})(view.state, view.dispatch, view)
|
||||
},
|
||||
}), {})
|
||||
}
|
||||
|
||||
}
|
||||
45
src/utils/buildMenuActions.js
Normal file
45
src/utils/buildMenuActions.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { markIsActive, nodeIsActive, getMarkAttrs } from '.'
|
||||
|
||||
export default function ({ schema, state, commands }) {
|
||||
|
||||
const nodes = Object.entries(schema.nodes)
|
||||
.map(([name]) => {
|
||||
const active = (attrs = {}) => nodeIsActive(state, schema.nodes[name], attrs)
|
||||
const command = commands[name] ? commands[name] : () => {}
|
||||
return { name, active, command }
|
||||
})
|
||||
.reduce((actions, { name, active, command }) => ({
|
||||
...actions,
|
||||
[name]: {
|
||||
active,
|
||||
command,
|
||||
},
|
||||
}), {})
|
||||
|
||||
const marks = Object.entries(schema.marks)
|
||||
.map(([name]) => {
|
||||
const active = () => markIsActive(state, schema.marks[name])
|
||||
const attrs = getMarkAttrs(state, schema.marks[name])
|
||||
const command = commands[name] ? commands[name] : () => {}
|
||||
return {
|
||||
name,
|
||||
active,
|
||||
attrs,
|
||||
command,
|
||||
}
|
||||
})
|
||||
.reduce((actions, { name, active, attrs, command }) => ({
|
||||
...actions,
|
||||
[name]: {
|
||||
active,
|
||||
attrs,
|
||||
command,
|
||||
},
|
||||
}), {})
|
||||
|
||||
return {
|
||||
nodes,
|
||||
marks,
|
||||
}
|
||||
|
||||
}
|
||||
18
src/utils/builtInKeymap.js
Normal file
18
src/utils/builtInKeymap.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { lift, selectParentNode } from 'prosemirror-commands'
|
||||
import { undo, redo } from 'prosemirror-history'
|
||||
import { undoInputRule } from 'prosemirror-inputrules'
|
||||
import { isMac } from 'vue-mirror/utils'
|
||||
|
||||
const keymap = {
|
||||
'Mod-z': undo,
|
||||
'Shift-Mod-z': undo,
|
||||
'Mod-BracketLeft': lift,
|
||||
Backspace: undoInputRule,
|
||||
Escape: selectParentNode,
|
||||
}
|
||||
|
||||
if (!isMac) {
|
||||
keymap['Mod-y'] = redo
|
||||
}
|
||||
|
||||
export default keymap
|
||||
16
src/utils/getMarkAttrs.js
Normal file
16
src/utils/getMarkAttrs.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export default function (state, type) {
|
||||
const { from, to } = state.selection
|
||||
let marks = []
|
||||
|
||||
state.doc.nodesBetween(from, to, node => {
|
||||
marks = [...marks, ...node.marks]
|
||||
})
|
||||
|
||||
const mark = marks.find(mark => mark.type.name === type.name)
|
||||
|
||||
if (mark) {
|
||||
return mark.attrs
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
12
src/utils/index.js
Normal file
12
src/utils/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export { default as buildMenuActions } from './buildMenuActions'
|
||||
export { default as builtInKeymap } from './builtInKeymap'
|
||||
export { default as ComponentView } from './ComponentView'
|
||||
export { default as initNodeViews } from './initNodeViews'
|
||||
export { default as isMac } from './isMac'
|
||||
export { default as getMarkAttrs } from './getMarkAttrs'
|
||||
export { default as markIsActive } from './markIsActive'
|
||||
export { default as nodeIsActive } from './nodeIsActive'
|
||||
export { default as menuBubble } from './menuBubble'
|
||||
export { default as Node } from './node'
|
||||
export { default as Mark } from './mark'
|
||||
export { default as PluginManager } from './PluginManager'
|
||||
18
src/utils/initNodeViews.js
Normal file
18
src/utils/initNodeViews.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ComponentView } from '.'
|
||||
|
||||
export default function initNodeViews({ nodes, editable }) {
|
||||
const nodeViews = {}
|
||||
Object.keys(nodes).forEach(nodeName => {
|
||||
nodeViews[nodeName] = (node, view, getPos, decorations) => {
|
||||
const component = nodes[nodeName]
|
||||
return new ComponentView(component, {
|
||||
node,
|
||||
view,
|
||||
getPos,
|
||||
decorations,
|
||||
editable,
|
||||
})
|
||||
}
|
||||
})
|
||||
return nodeViews
|
||||
}
|
||||
1
src/utils/isMac.js
Normal file
1
src/utils/isMac.js
Normal file
@@ -0,0 +1 @@
|
||||
export default typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
|
||||
46
src/utils/mark.js
Normal file
46
src/utils/mark.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export default class Mark {
|
||||
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
...this.defaultOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return null
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {}
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'mark'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return null
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return null
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return []
|
||||
}
|
||||
|
||||
command() {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
keys() {
|
||||
return {}
|
||||
}
|
||||
|
||||
inputRules() {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
14
src/utils/markIsActive.js
Normal file
14
src/utils/markIsActive.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function (state, type) {
|
||||
const {
|
||||
from,
|
||||
$from,
|
||||
to,
|
||||
empty,
|
||||
} = state.selection
|
||||
|
||||
if (empty) {
|
||||
return !!type.isInSet(state.storedMarks || $from.marks())
|
||||
}
|
||||
|
||||
return !!state.doc.rangeHasMark(from, to, type)
|
||||
}
|
||||
73
src/utils/menuBubble.js
Normal file
73
src/utils/menuBubble.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Plugin } from 'prosemirror-state'
|
||||
|
||||
class Toolbar {
|
||||
|
||||
constructor({ node, editorView }) {
|
||||
this.editorView = editorView
|
||||
this.node = node
|
||||
this.element = this.node.elm
|
||||
this.element.style.visibility = 'hidden'
|
||||
this.element.style.opacity = 0
|
||||
|
||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||
}
|
||||
|
||||
update(view, lastState) {
|
||||
const { state } = view
|
||||
|
||||
// Don't do anything if the document/selection didn't change
|
||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Hide the tooltip if the selection is empty
|
||||
if (state.selection.empty) {
|
||||
this.hide()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, reposition it and update its content
|
||||
this.show()
|
||||
const { from, to } = state.selection
|
||||
|
||||
// These are in screen coordinates
|
||||
const start = view.coordsAtPos(from)
|
||||
const end = view.coordsAtPos(to)
|
||||
|
||||
// The box in which the tooltip is positioned, to use as base
|
||||
const box = this.element.offsetParent.getBoundingClientRect()
|
||||
|
||||
// Find a center-ish x position from the selection endpoints (when
|
||||
// crossing lines, end may be more to the left)
|
||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||
this.element.style.left = `${left - box.left}px`
|
||||
this.element.style.bottom = `${box.bottom - start.top}px`
|
||||
}
|
||||
|
||||
show() {
|
||||
this.element.style.visibility = 'visible'
|
||||
this.element.style.opacity = 1
|
||||
}
|
||||
|
||||
hide(event) {
|
||||
if (event && event.relatedTarget) {
|
||||
return
|
||||
}
|
||||
this.element.style.visibility = 'hidden'
|
||||
this.element.style.opacity = 0
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function (node) {
|
||||
return new Plugin({
|
||||
view(editorView) {
|
||||
return new Toolbar({ editorView, node })
|
||||
},
|
||||
})
|
||||
}
|
||||
46
src/utils/node.js
Normal file
46
src/utils/node.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export default class Node {
|
||||
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
...this.defaultOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return null
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {}
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 'node'
|
||||
}
|
||||
|
||||
get view() {
|
||||
return null
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return null
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return []
|
||||
}
|
||||
|
||||
command() {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
keys() {
|
||||
return {}
|
||||
}
|
||||
|
||||
inputRules() {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
12
src/utils/nodeIsActive.js
Normal file
12
src/utils/nodeIsActive.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { findParentNode } from 'prosemirror-utils'
|
||||
|
||||
export default function (state, type, attrs) {
|
||||
const predicate = node => node.type === type
|
||||
const parent = findParentNode(predicate)(state.selection)
|
||||
|
||||
if (attrs === {} || !parent) {
|
||||
return !!parent
|
||||
}
|
||||
|
||||
return parent.node.hasMarkup(type, attrs)
|
||||
}
|
||||
Reference in New Issue
Block a user