├── .babelrc
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── rollup.config.js
└── src
├── Editor.vue
├── helpers.js
├── index.js
├── plugins
├── index.js
├── inputs.js
└── keys.js
├── schema
├── index.js
├── marks.js
└── nodes.js
└── utils.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", { "modules": false }],
4 | "stage-2",
5 | "es2015-rollup"
6 | ],
7 | "plugins": [
8 | ],
9 | "comments": false
10 | }
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | env: {
5 | browser: true
6 | },
7 | extends: 'standard',
8 | plugins: [
9 | 'html'
10 | ],
11 | rules: {},
12 | globals: {}
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # logs
5 | npm-debug.log
6 |
7 |
8 | # build
9 | dist
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Studbits
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Prose
2 |
3 | *This plugin was made while creating the editor for [studbits.com](https://studbits.com/). We hope you find it useful!*
4 |
5 | A minimal [Prosemirror](https://prosemirror.net/) Wrapper for Vue.js
6 |
7 | ### Installation
8 |
9 | ```
10 | npm install vue-prosemirror-editor
11 | ```
12 |
13 | The `vue-prose` will install prosemirror's dependencies.
14 |
15 | You must load the [`style/gapcursor.css`](https://github.com/ProseMirror/prosemirror-gapcursor) styles or define your own styling for the cursor.
16 |
17 | ### Basic Usage
18 |
19 | The most basic usage with an empty editor:
20 |
21 | ```
22 | // template
23 |
24 |
25 | // script
26 | import Editor from "vue-prosemirror-editor"
27 |
28 | export default {
29 | components: {
30 | Editor
31 | }
32 | }
33 | ```
34 |
35 | #### Editor
36 |
37 | The editor is a component that accepts these props:
38 |
39 | * `doc`, an Object, the prosemirror document to be edited. (*Or you can set the `fromContent` prop to true and the document will be parsed from the element with the `#content` id.*)
40 |
41 | * `schema`, an Object, the schema to be passed to the editor state.
42 |
43 | * `editorProps`, an Object, any props to be passed to editor view.
44 |
45 | * `plugins`, an Array, any editor plugins.
46 |
47 | * `keyCommands`, a Function, that passes the editor schema so you can register any additional editor key commands.
48 |
49 | * `inputRules`, an Array, any custom editor input rules.
50 |
51 | * `nodeViews`, an Object, any custom editor node views.
52 |
53 | * `interceptTransaction`, a function, can be passed to prevent an editor transaction.
54 |
55 | * 'editable', a boolean that represents whether editing is allowed.
56 |
57 | * `autofocus`, a boolean. Defaults to true.
58 |
59 | * `willCreate`, an event that fires when the editor instance is about to be created. Takes no arguments.
60 |
61 | * `didCreate`, an event that fires once the editor instance has been created. Will be called with the editor instance and may be used to configure it further.
62 |
63 | * `wasUpdated`, an event that will fire whenever the underlying document changes. It is called with the editor state and schema.
64 |
65 |
66 | ### Advanced Usage
67 |
68 | ### Editor Views as Vue Components
69 |
70 | This package includes a `ComponentView` utility that you can use to register
71 | editor views as vue components.
72 |
73 | It can be used as follows:
74 |
75 | ```
76 | import { ComponentView } from 'vue-prosemirror-editor'
77 | import Menu from '~/components/Menu'
78 |
79 | export default class MenuView extends ComponentView {
80 | constructor (node, view, getPos) {
81 | // call `ComponentView` constructor with desired component and
82 | // any props that should be passed to it
83 | super(Menu, { node, view, getPos })
84 | }
85 |
86 | // make sure to update any props so that you can handle the logic
87 | // inside the vue component by watching for changes
88 | update (node, decs) {
89 | this.node = node
90 | return true
91 | }
92 | }
93 |
94 | ```
95 |
96 | #### Getting Help
97 |
98 | If you'd like to report a bug or request a feature, please [open an issue](https://github.com/studbits/vue-prose/issues).
99 |
100 | ### License
101 |
102 | [MIT](./LICENSE)
103 |
104 | Copyright (c) 2017 Studbits
105 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-prosemirror-editor",
3 | "version": "0.3.3",
4 | "description": "vue prosemirror editor wrapper",
5 | "main": "dist/index.js",
6 | "author": "Alid Castano",
7 | "license": "MIT",
8 | "scripts": {
9 | "lint": "eslint --fix \"**/*.js\"",
10 | "build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
11 | "watch": "npm run build -- -w",
12 | "prepare": "npm run build",
13 | "release": "git push --follow-tags && npm publish"
14 | },
15 | "dependencies": {
16 | "prosemirror-commands": "^1.0.2",
17 | "prosemirror-dropcursor": "^1.0.0",
18 | "prosemirror-gapcursor": "^1.0.0",
19 | "prosemirror-history": "^1.0.0",
20 | "prosemirror-inputrules": "^1.0.1",
21 | "prosemirror-model": "^1.0.1",
22 | "prosemirror-schema-basic": "^1.0.0",
23 | "prosemirror-schema-list": "^1.0.0",
24 | "prosemirror-state": "^1.0.1",
25 | "prosemirror-view": "^1.0.4"
26 | },
27 | "devDependencies": {
28 | "babel-core": "^6.26.0",
29 | "babel-eslint": "^8.0.0",
30 | "babel-preset-env": "^1.6.1",
31 | "babel-preset-es2015": "^6.24.1",
32 | "babel-preset-es2015-rollup": "^3.0.0",
33 | "babel-preset-stage-2": "^6.24.1",
34 | "cross-env": "^5.0.5",
35 | "eslint": "^4.7.2",
36 | "eslint-config-i-am-meticulous": "^7.0.1",
37 | "eslint-config-prettier": "^2.6.0",
38 | "eslint-config-prettier-standard": "^1.0.1",
39 | "eslint-config-standard": "^10.2.1",
40 | "eslint-plugin-babel": "^4.1.2",
41 | "eslint-plugin-html": "^4.0.2",
42 | "eslint-plugin-jest": "^21.1.0",
43 | "eslint-plugin-node": "^5.1.1",
44 | "eslint-plugin-prettier": "^2.3.1",
45 | "eslint-plugin-promise": "^3.5.0",
46 | "eslint-plugin-standard": "^3.0.1",
47 | "rollup": "^0.50.0",
48 | "rollup-plugin-babel": "^3.0.2",
49 | "rollup-plugin-buble": "^0.18.0",
50 | "rollup-plugin-commonjs": "^8.2.1",
51 | "rollup-plugin-copy": "^0.2.3",
52 | "rollup-plugin-filesize": "^1.4.2",
53 | "rollup-plugin-json": "^2.3.0",
54 | "rollup-plugin-node-resolve": "^3.0.0",
55 | "rollup-plugin-uglify-es": "0.0.1",
56 | "rollup-plugin-vue": "^3.0.0",
57 | "vue-template-compiler": "^2.5.13"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve'
2 | import json from 'rollup-plugin-json'
3 | import babel from 'rollup-plugin-babel'
4 | import vue from 'rollup-plugin-vue'
5 | import commonjs from 'rollup-plugin-commonjs'
6 | import uglify from 'rollup-plugin-uglify-es'
7 | import filesize from 'rollup-plugin-filesize'
8 | import pkg from './package.json'
9 |
10 | const version = process.env.VERSION || pkg.version
11 |
12 | let globals = {}
13 | let external = []
14 | ;('model transform state view keymap inputrules history commands schema-basic ' +
15 | 'schema-list dropcursor menu example-setup gapcursor').split(' ').forEach(name => {
16 | globals['prosemirror-' + name] = 'PM.' + name.replace(/-/g, '_')
17 | external.push('prosemirror-' + name)
18 | })
19 |
20 | export default {
21 | input: `src/index.js`,
22 | output: {
23 | file: `dist/index.js`,
24 | format: 'cjs',
25 | globals,
26 | exports: 'named'
27 | },
28 | external,
29 | name: `vueProse`,
30 | plugins: [
31 | json(),
32 | resolve({
33 | preferBuiltins: false
34 | }),
35 | vue({
36 | compileTemplate: true
37 | }),
38 | babel({
39 | exclude: 'node_modules/**'
40 | }),
41 | commonjs({
42 | include: 'node_modules/**'
43 | }),
44 | uglify(),
45 | filesize()
46 | ],
47 | banner: `
48 | /**
49 | * Vue Prosemirror v${version}
50 | * (c) ${new Date().getFullYear()} Studbits
51 | * @license MIT
52 | */
53 | `
54 | }
55 |
--------------------------------------------------------------------------------
/src/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
130 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Wraps each node view in prosemirror render function.
3 | */
4 | export function initNodeViews (nodeViews) {
5 | const nvs = {}
6 | Object.keys(nodeViews).forEach(key => {
7 | nvs[key] = function (node, view, getPos, decorations) {
8 | const NV = nodeViews[key]
9 | return new NV(node, view, getPos, decorations)
10 | }
11 | })
12 | return nvs
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Editor from './Editor.vue'
2 | import { ComponentView, findNode } from './utils'
3 |
4 | export default Editor
5 |
6 | export {
7 | Editor,
8 | ComponentView,
9 | findNode
10 | }
11 |
--------------------------------------------------------------------------------
/src/plugins/index.js:
--------------------------------------------------------------------------------
1 | import { history } from 'prosemirror-history'
2 | import { dropCursor } from 'prosemirror-dropcursor'
3 | import { gapCursor } from 'prosemirror-gapcursor'
4 |
5 | import keyCommands from './keys'
6 | import inputRules from './inputs'
7 |
8 | export default ({ schema, keys, inputs }) => [
9 | inputRules({ schema, inputs }),
10 | keyCommands({ schema, keys }),
11 | dropCursor(),
12 | gapCursor(),
13 | history()
14 | ]
15 |
--------------------------------------------------------------------------------
/src/plugins/inputs.js:
--------------------------------------------------------------------------------
1 | import {
2 | inputRules,
3 | wrappingInputRule,
4 | textblockTypeInputRule,
5 | smartQuotes,
6 | emDash,
7 | ellipsis
8 | } from 'prosemirror-inputrules'
9 |
10 | const ruleTypes = [
11 | {
12 | name: 'blockquote',
13 | rule: (nodeType) => wrappingInputRule(
14 | /^\s*>\s$/,
15 | nodeType
16 | )
17 | },
18 | {
19 | name: 'ordered_list',
20 | rule: (nodeType) => wrappingInputRule(
21 | /^(\d+)\.\s$/,
22 | nodeType,
23 | (match) => ({ order: +match[1] }),
24 | (match, node) => node.childCount + node.attrs.order === +match[1]
25 | )
26 | },
27 | {
28 | name: 'bullet_list',
29 | rule: (nodeType) => wrappingInputRule(
30 | /^\s*([-+*])\s$/,
31 | nodeType
32 | )
33 | },
34 | {
35 | name: 'code_block',
36 | rule: (nodeType) => textblockTypeInputRule(
37 | /^```$/,
38 | nodeType
39 | )
40 | },
41 | {
42 | name: 'heading',
43 | rule: (nodeType, maxLevel = 6) => textblockTypeInputRule(
44 | new RegExp('^(#{1,' + maxLevel + '})\\s$'),
45 | nodeType,
46 | match => ({ level: match[1].length })
47 | )
48 | }
49 | ]
50 |
51 | export default function ({ schema: { nodes }, inputs }) {
52 | const rules = [
53 | ...smartQuotes,
54 | ellipsis,
55 | emDash
56 | ]
57 |
58 | ruleTypes.forEach(type => { // merge built-in inputs
59 | if (nodes[type.name] && !inputs.find(t => t[type.name])) {
60 | rules.push(type.rule(nodes[type.name]))
61 | }
62 | })
63 |
64 | inputs.forEach(initRule => {
65 | rules.push(initRule(nodes))
66 | })
67 |
68 | return inputRules({ rules })
69 | }
70 |
--------------------------------------------------------------------------------
/src/plugins/keys.js:
--------------------------------------------------------------------------------
1 | import { keymap } from 'prosemirror-keymap'
2 | import { undoInputRule } from 'prosemirror-inputrules'
3 | import { undo, redo } from 'prosemirror-history'
4 | import {
5 | wrapInList,
6 | splitListItem,
7 | liftListItem,
8 | sinkListItem
9 | } from 'prosemirror-schema-list'
10 | import {
11 | baseKeymap,
12 | toggleMark,
13 | wrapIn,
14 | setBlockType,
15 | chainCommands,
16 | exitCode,
17 | joinUp,
18 | joinDown,
19 | lift,
20 | selectParentNode
21 | } from 'prosemirror-commands'
22 |
23 | export const insertNode = (nodeType, keep = true) => (state, dispatch) => {
24 | dispatch(state.tr.replaceSelectionWith(nodeType.create()).scrollIntoView())
25 | return keep
26 | }
27 |
28 | export default function ({ schema: { marks, nodes }, keys: userKeys }) {
29 | const keyCommands = {
30 | 'Mod-z': () => undo,
31 | 'Shift-Mod-z': () => redo,
32 | 'Backspace': () => undoInputRule,
33 | 'Mod-y': () => redo,
34 | 'Alt-ArrowUp': () => joinUp,
35 | 'Alt-ArrowDown': () => joinDown,
36 | 'Mod-BracketLeft': () => lift,
37 | 'Escape': () => selectParentNode,
38 | 'Mod-b': (strong) => toggleMark(strong),
39 | 'Mod-i': (em) => toggleMark(em),
40 | 'Mod-u': (underline) => toggleMark(underline),
41 | 'Mod-`': (code) => toggleMark(code),
42 | 'Mod-q': (blockquote) => wrapIn(blockquote),
43 | 'Shift-Ctrl-8': (bulletList) => wrapInList(bulletList),
44 | 'Shift-Ctrl-9': (orderedList) => wrapInList(orderedList),
45 | 'Enter': (hardBreak) => chainCommands(exitCode, insertNode(hardBreak)),
46 | 'Shift-Enter': (listItem) => splitListItem(listItem),
47 | 'Mod-[': (listItem) => liftListItem(listItem),
48 | 'Mod-]': (listItem) => sinkListItem(listItem),
49 | 'Shift-Ctrl-0': (paragraph) => setBlockType(paragraph),
50 | 'Shift-Ctrl-\\': (codeBlock) => setBlockType(codeBlock),
51 | 'Shift-Ctrl-1': (heading) => setBlockType(heading, { level: 1 }),
52 | 'Shift-Ctrl-2': (heading) => setBlockType(heading, { level: 2 }),
53 | 'Shift-Ctrl-3': (heading) => setBlockType(heading, { level: 3 }),
54 | 'Shift-Ctrl-4': (heading) => setBlockType(heading, { level: 4 }),
55 | 'Mod-_': (horizontalRule) => insertNode(horizontalRule)
56 | }
57 |
58 | const keys = userKeys
59 | Object.keys(keyCommands).forEach(k => { // merge built in commands
60 | if (!keys[k] && k === 'Mod-b' && marks.strong) keys[k] = keyCommands[k](marks.strong)
61 | else if (!keys[k] && k === 'Mod-i' && marks.em) keys[k] = keyCommands[k](marks.em)
62 | else if (!keys[k] && k === 'Mod-' && marks.code) keys[k] = keyCommands[k](marks.code)
63 | else if (!keys[k] && k === 'Shift-Ctrl-8' && nodes.bullet_list) keys[k] = keyCommands[k](nodes.bullet_list)
64 | else if (!keys[k] && k === 'Shift-Ctrl-9' && nodes.ordered_list) keys[k] = keyCommands[k](nodes.ordered_list)
65 | else if (!keys[k] && k === 'Ctrl-q' && nodes.blockquote) keys[k] = keyCommands[k](nodes.blockquote)
66 | else if (!keys[k] && k === 'Enter' && nodes.hard_break) keys[k] = keyCommands[k](nodes.hard_break)
67 | else if (!keys[k] && k === 'Shift-Enter' && nodes.list_item) keys[k] = keyCommands[k](nodes.list_item)
68 | else if (!keys[k] && k === 'Mod-[' && nodes.list_item) keys[k] = keyCommands[k](nodes.list_item)
69 | else if (!keys[k] && k === 'Mod-]' && nodes.list_item) keys[k] = keyCommands[k](nodes.list_item)
70 | else if (!keys[k] && k === 'Shift-Ctrl-0' && nodes.paragraph) keys[k] = keyCommands[k](nodes.paragraph)
71 | else if (!keys[k] && k === 'Shift-Ctrl-\\' && nodes.code_block) keys[k] = keyCommands[k](nodes.code_block)
72 | else if (!keys[k] && k === 'Shift-Ctrl-0' && nodes.heading) keys[k] = keyCommands[k](nodes.heading)
73 | else if (!keys[k] && k === 'Shift-Ctrl-1' && nodes.heading) keys[k] = keyCommands[k](nodes.heading)
74 | else if (!keys[k] && k === 'Shift-Ctrl-2' && nodes.heading) keys[k] = keyCommands[k](nodes.heading)
75 | else if (!keys[k] && k === 'Shift-Ctrl-3' && nodes.heading) keys[k] = keyCommands[k](nodes.heading)
76 | else if (!keys[k] && k === 'Shift-Ctrl-4' && nodes.heading) keys[k] = keyCommands[k](nodes.heading)
77 | else if (!keys[k] && k === 'Mod-_' && nodes.horizontal_rule) keys[k] = keyCommands[k](nodes.horizontal_rule)
78 | else if (!keys[k]) keys[k] = keyCommands[k]()
79 | })
80 |
81 | Object.keys(baseKeymap).forEach(key => { // merge base commands
82 | if (keys[key]) {
83 | keys[key] = chainCommands(keys[key], baseKeymap[key])
84 | } else {
85 | keys[key] = baseKeymap[key]
86 | }
87 | })
88 |
89 | return keymap(keys)
90 | }
91 |
--------------------------------------------------------------------------------
/src/schema/index.js:
--------------------------------------------------------------------------------
1 | import nodes from './nodes'
2 | import marks from './marks'
3 |
4 | export default {
5 | nodes,
6 | marks
7 | }
8 |
--------------------------------------------------------------------------------
/src/schema/marks.js:
--------------------------------------------------------------------------------
1 | import { marks } from 'prosemirror-schema-basic'
2 |
3 | const subscript = {
4 | excludes: 'superscript',
5 | parseDOM: [
6 | { tag: 'sub' },
7 | { style: 'vertical-align=sub' }
8 | ],
9 | toDOM: () => ['sub']
10 | }
11 |
12 | const superscript = {
13 | excludes: 'subscript',
14 | parseDOM: [
15 | { tag: 'sup' },
16 | { style: 'vertical-align=super' }
17 | ],
18 | toDOM: () => ['sup']
19 | }
20 |
21 | const strikethrough = {
22 | parseDOM: [
23 | { tag: 'strike' },
24 | { style: 'text-decoration:line-through' },
25 | { style: 'text-decoration-line:line-through' }
26 | ],
27 | toDOM: () => ['span', {
28 | style: 'text-decoration-line:line-through'
29 | }]
30 | }
31 |
32 | const underline = {
33 | parseDOM: [
34 | { tag: 'u' },
35 | { style: 'text-decoration:underline' }
36 | ],
37 | toDOM: () => ['span', {
38 | style: 'text-decoration:underline'
39 | }]
40 | }
41 |
42 | export default {
43 | ...marks,
44 | subscript,
45 | superscript,
46 | strikethrough,
47 | underline
48 | }
49 |
--------------------------------------------------------------------------------
/src/schema/nodes.js:
--------------------------------------------------------------------------------
1 | import { nodes } from 'prosemirror-schema-basic'
2 | import { orderedList, bulletList, listItem } from 'prosemirror-schema-list'
3 |
4 | const listNodes = {
5 | ordered_list: {
6 | ...orderedList,
7 | content: 'list_item+',
8 | group: 'block'
9 | },
10 | bullet_list: {
11 | ...bulletList,
12 | content: 'list_item+',
13 | group: 'block'
14 | },
15 | list_item: {
16 | ...listItem,
17 | content: 'paragraph block*',
18 | group: 'block'
19 | }
20 | }
21 |
22 | export default {
23 | ...nodes,
24 | ...listNodes
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export class ComponentView {
4 | constructor (comp, props = {}) {
5 | Object.keys(props).forEach(key => {
6 | this[key] = props[key]
7 | })
8 |
9 | this.dom = this.toDOM(comp)
10 | }
11 |
12 | toDOM (comp) {
13 | const targetNode = document.createElement('div')
14 | const CompDom = Vue.extend(comp)
15 | this._vm = new CompDom({ propsData: this }).$mount()
16 | targetNode.appendChild(this._vm.$el)
17 | return targetNode
18 | }
19 |
20 | destroy () {
21 | this._vm.$destroy()
22 | }
23 | }
24 |
25 | export const findNode = function (topNode, predicate) {
26 | let found
27 | topNode.descendants((node, pos, parent) => {
28 | if (predicate(node)) found = { node, pos, parent }
29 | if (found) return false
30 | })
31 | return found
32 | }
33 |
--------------------------------------------------------------------------------