├── .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 | 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 | --------------------------------------------------------------------------------