Merge branch 'master' into feature/collab-plugin

# Conflicts:
#	packages/tiptap/src/Editor.js
#	yarn.lock
This commit is contained in:
Philipp Kühn 2019-05-03 10:56:52 +02:00
commit 83075f28e6
36 changed files with 3679 additions and 3422 deletions

View File

@ -14,5 +14,6 @@ install:
- yarn build:packages - yarn build:packages
script: script:
- yarn audit-ci
- yarn lint - yarn lint
- yarn test - yarn test

View File

@ -72,6 +72,7 @@ export default {
| `extensions` | `Array` | `[]` | A list of extensions used, by the editor. This can be `Nodes`, `Marks` or `Plugins`. | | `extensions` | `Array` | `[]` | A list of extensions used, by the editor. This can be `Nodes`, `Marks` or `Plugins`. |
| `useBuiltInExtensions` | `Boolean` | `true` | By default tiptap adds a `Doc`, `Paragraph` and `Text` node to the Prosemirror schema. | | `useBuiltInExtensions` | `Boolean` | `true` | By default tiptap adds a `Doc`, `Paragraph` and `Text` node to the Prosemirror schema. |
| `dropCursor` | `Object` | `{}` | Config for `prosemirror-dropcursor`. | | `dropCursor` | `Object` | `{}` | Config for `prosemirror-dropcursor`. |
| `parseOptions` | `Object` | `{}` | A list of [Prosemirror parseOptions](https://prosemirror.net/docs/ref/#model.ParseOptions). |
| `onInit` | `Function` | `undefined` | This will return an Object with the current `state` and `view` of Prosemirror on init. | | `onInit` | `Function` | `undefined` | This will return an Object with the current `state` and `view` of Prosemirror on init. |
| `onFocus` | `Function` | `undefined` | This will return an Object with the `event` and current `state` and `view` of Prosemirror on focus. | | `onFocus` | `Function` | `undefined` | This will return an Object with the `event` and current `state` and `view` of Prosemirror on focus. |
| `onBlur` | `Function` | `undefined` | This will return an Object with the `event` and current `state` and `view` of Prosemirror on blur. | | `onBlur` | `Function` | `undefined` | This will return an Object with the `event` and current `state` and `view` of Prosemirror on blur. |
@ -81,14 +82,15 @@ export default {
| **Method** | **Arguments**| **Description** | | **Method** | **Arguments**| **Description** |
| --- | :---: | --- | | --- | :---: | --- |
| `setContent` | `content, emitUpdate` | Replace the current content. You can pass an HTML string or a JSON document. `emitUpdate` defaults to `false`. | | `setContent` | `content, emitUpdate, parseOptions` | Replace the current content. You can pass an HTML string or a JSON document. `emitUpdate` defaults to `false`. `parseOptions` defaults to those provided in constructor. |
| `clearContent` | `emitUpdate` | Clears the current content. `emitUpdate` defaults to `false`. | | `clearContent` | `emitUpdate` | Clears the current content. `emitUpdate` defaults to `false`. |
| `setOptions` | `options` | Overwrites the current editor properties. | | `setOptions` | `options` | Overwrites the current editor properties. |
| `registerPlugin` | `plugin` | Register a Prosemirror plugin. | | `registerPlugin` | `plugin` | Register a Prosemirror plugin. |
| `getJSON` | | Get the current content as JSON. | | `getJSON` | | Get the current content as JSON. |
| `getHTML` | | Get the current content as HTML. | | `getHTML` | | Get the current content as HTML. |
| `focus` | — | Focus the editor | | `focus` | | Focus the editor. |
| `destroy` | | Destroys the editor. | | `blur` | | Blur the editor. |
| `destroy` | | Destroy the editor. |
## Components ## Components
@ -298,6 +300,7 @@ The most powerful feature of tiptap is that you can create your own extensions.
| `commands({ schema, attrs })` | `Object` | `null` | Define a command. | | `commands({ schema, attrs })` | `Object` | `null` | Define a command. |
| `inputRules({ schema })` | `Array` | `[]` | Define a list of input rules. | | `inputRules({ schema })` | `Array` | `[]` | Define a list of input rules. |
| `pasteRules({ schema })` | `Array` | `[]` | Define a list of paste rules. | | `pasteRules({ schema })` | `Array` | `[]` | Define a list of paste rules. |
| `get update()` | `Function` | `undefined` | Called when options of extension are changed via `editor.extensions.options` |
### Node|Mark Class ### Node|Mark Class
@ -473,6 +476,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
- [Philipp Kühn](https://github.com/philippkuehn) - [Philipp Kühn](https://github.com/philippkuehn)
- [Christoph Flathmann](https://github.com/Chrissi2812) - [Christoph Flathmann](https://github.com/Chrissi2812)
- [Erick Wilder](https://github.com/erickwilder)
- [All Contributors](../../contributors) - [All Contributors](../../contributors)
## Packages Using Tiptap ## Packages Using Tiptap

View File

@ -1,5 +1,7 @@
import path from 'path' import path from 'path'
import webpack from 'webpack' import webpack from 'webpack'
import Fiber from 'fibers'
import DartSass from 'dart-sass'
import { VueLoaderPlugin } from 'vue-loader' import { VueLoaderPlugin } from 'vue-loader'
import SvgStore from 'webpack-svgstore-plugin' import SvgStore from 'webpack-svgstore-plugin'
import CopyWebpackPlugin from 'copy-webpack-plugin' import CopyWebpackPlugin from 'copy-webpack-plugin'
@ -87,7 +89,13 @@ export default {
ifDev('vue-style-loader', MiniCssExtractPlugin.loader), ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
'css-loader', 'css-loader',
'postcss-loader', 'postcss-loader',
'sass-loader', {
loader: 'sass-loader',
options: {
implementation: DartSass,
fiber: Fiber,
},
},
]), ]),
}, },
{ {

View File

@ -1,7 +1,7 @@
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import zlib from 'zlib' import zlib from 'zlib'
import uglify from 'uglify-js' import Terser from 'terser'
import { rollup } from 'rollup' import { rollup } from 'rollup'
import config from './config' import config from './config'
@ -43,9 +43,8 @@ function buildEntry({ input, output }) {
return rollup(input) return rollup(input)
.then(bundle => bundle.generate(output)) .then(bundle => bundle.generate(output))
.then(response => { .then(response => {
// console.log({ bla })
if (isProd) { if (isProd) {
const minified = uglify.minify(response.output[0].code, { const minified = Terser.minify(response.output[0].code, {
output: { output: {
preamble: output.banner, preamble: output.banner,
ascii_only: true, ascii_only: true,
@ -53,8 +52,6 @@ function buildEntry({ input, output }) {
}).code }).code
return write(output.file, minified, true) return write(output.file, minified, true)
} }
// console.log({ isProd })
// console.dir(response, { depth: null })
return write(output.file, response.output[0].code) return write(output.file, response.output[0].code)
}) })
} }

View File

@ -41,7 +41,7 @@ function genConfig(opts) {
file: opts.file, file: opts.file,
format: opts.format, format: opts.format,
banner, banner,
name: 'tiptap', name: opts.outputName,
}, },
} }
@ -57,23 +57,28 @@ function genConfig(opts) {
export default [ export default [
{ {
package: 'tiptap', package: 'tiptap',
outputName: 'tiptap',
outputFileName: 'tiptap', outputFileName: 'tiptap',
}, },
{ {
package: 'tiptap-commands', package: 'tiptap-commands',
outputName: 'tiptapCommands',
outputFileName: 'commands', outputFileName: 'commands',
}, },
{ {
package: 'tiptap-utils', package: 'tiptap-utils',
outputName: 'tiptapUtils',
outputFileName: 'utils', outputFileName: 'utils',
}, },
{ {
package: 'tiptap-extensions', package: 'tiptap-extensions',
outputName: 'tiptapExtensions',
outputFileName: 'extensions', outputFileName: 'extensions',
}, },
].map(item => [ ].map(item => [
{ {
name: item.package, name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`), package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`), input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.js`), file: resolve(`packages/${item.package}/dist/${item.outputFileName}.js`),
@ -82,6 +87,7 @@ export default [
}, },
{ {
name: item.package, name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`), package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`), input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.min.js`), file: resolve(`packages/${item.package}/dist/${item.outputFileName}.min.js`),
@ -90,6 +96,7 @@ export default [
}, },
{ {
name: item.package, name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`), package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`), input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.common.js`), file: resolve(`packages/${item.package}/dist/${item.outputFileName}.common.js`),
@ -97,6 +104,7 @@ export default [
}, },
{ {
name: item.package, name: item.package,
outputName: item.outputName,
package: resolve(`packages/${item.package}/package.json`), package: resolve(`packages/${item.package}/package.json`),
input: resolve(`packages/${item.package}/src/index.js`), input: resolve(`packages/${item.package}/src/index.js`),
file: resolve(`packages/${item.package}/dist/${item.outputFileName}.esm.js`), file: resolve(`packages/${item.package}/dist/${item.outputFileName}.esm.js`),

View File

@ -6,6 +6,9 @@
</h1> </h1>
<div> <div>
<a class="navigation__link" href="https://tiptap.scrumpy.io/docs" target="_blank">
Documentation
</a>
<a class="navigation__link" href="https://github.com/scrumpy/tiptap/blob/master/CONTRIBUTING.md" target="_blank"> <a class="navigation__link" href="https://github.com/scrumpy/tiptap/blob/master/CONTRIBUTING.md" target="_blank">
Contribute Contribute
</a> </a>

View File

@ -47,7 +47,7 @@ export default class Iframe extends Node {
template: ` template: `
<div class="iframe"> <div class="iframe">
<iframe class="iframe__embed" :src="src"></iframe> <iframe class="iframe__embed" :src="src"></iframe>
<input class="iframe__input" type="text" v-model="src" v-if="editable" /> <input class="iframe__input" @paste.stop type="text" v-model="src" v-if="editable" />
</div> </div>
`, `,
} }

View File

@ -51,7 +51,7 @@ export default {
<p> <p>
This is basic example of implementing images. Try to drop new images here. Reordering also works. This is basic example of implementing images. Try to drop new images here. Reordering also works.
</p> </p>
<img src="https://ljdchost.com/8I2DeFn.gif" /> <img src="https://66.media.tumblr.com/dcd3d24b79d78a3ee0f9192246e727f1/tumblr_o00xgqMhPM1qak053o1_400.gif" />
`, `,
}), }),
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bubble class="menububble" :editor="editor"> <editor-menu-bubble class="menububble" :editor="editor" @hide="hideLinkMenu">
<div <div
slot-scope="{ commands, isActive, getMarkAttrs, menu }" slot-scope="{ commands, isActive, getMarkAttrs, menu }"
class="menububble" class="menububble"
@ -21,7 +21,7 @@
@click="showLinkMenu(getMarkAttrs('link'))" @click="showLinkMenu(getMarkAttrs('link'))"
:class="{ 'is-active': isActive.link() }" :class="{ 'is-active': isActive.link() }"
> >
<span>Add Link</span> <span>{{ isActive.link() ? 'Update Link' : 'Add Link'}}</span>
<icon name="link" /> <icon name="link" />
</button> </button>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bubble :editor="editor"> <editor-menu-bubble :editor="editor" :keep-in-bounds="keepInBounds">
<div <div
slot-scope="{ commands, isActive, menu }" slot-scope="{ commands, isActive, menu }"
class="menububble" class="menububble"
@ -69,6 +69,7 @@ export default {
}, },
data() { data() {
return { return {
keepInBounds: true,
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new Blockquote(), new Blockquote(),

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="editor"> <div class="editor">
<input type="text" v-model="placeholder">
<editor-content class="editor__content" :editor="editor" /> <editor-content class="editor__content" :editor="editor" />
</div> </div>
</template> </template>
@ -18,6 +19,7 @@ export default {
}, },
data() { data() {
return { return {
placeholder: 'Write something …',
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new BulletList(), new BulletList(),
@ -25,6 +27,7 @@ export default {
new Placeholder({ new Placeholder({
emptyClass: 'is-empty', emptyClass: 'is-empty',
emptyNodeText: 'Write something …', emptyNodeText: 'Write something …',
showOnlyWhenEditable: true,
}), }),
], ],
}), }),
@ -33,6 +36,11 @@ export default {
beforeDestroy() { beforeDestroy() {
this.editor.destroy() this.editor.destroy()
}, },
watch: {
placeholder(newValue) {
this.editor.extensions.options.placeholder.emptyNodeText = newValue
},
},
} }
</script> </script>

View File

@ -159,6 +159,7 @@ export default {
filteredUsers: [], filteredUsers: [],
navigatedUserIndex: 0, navigatedUserIndex: 0,
insertMention: () => {}, insertMention: () => {},
observer: null,
} }
}, },
@ -222,20 +223,35 @@ export default {
interactive: true, interactive: true,
theme: 'dark', theme: 'dark',
placement: 'top-start', placement: 'top-start',
performance: true,
inertia: true, inertia: true,
duration: [400, 200], duration: [400, 200],
showOnInit: true, showOnInit: true,
arrow: true, arrow: true,
arrowType: 'round', arrowType: 'round',
}) })
// we have to update tippy whenever the DOM is updated
if (MutationObserver) {
this.observer = new MutationObserver(() => {
this.popup.popperInstance.scheduleUpdate()
})
this.observer.observe(this.$refs.suggestions, {
childList: true,
subtree: true,
characterData: true,
})
}
}, },
destroyPopup() { destroyPopup() {
if (this.popup) { if (this.popup) {
this.popup.destroyAll() this.popup.destroy()
this.popup = null this.popup = null
} }
if (this.observer) {
this.observer.disconnect()
}
}, },
}, },
@ -244,7 +260,6 @@ export default {
<style lang="scss"> <style lang="scss">
@import "~variables"; @import "~variables";
@import '~modules/tippy.js/dist/tippy.css';
.mention { .mention {
background: rgba($color-black, 0.1); background: rgba($color-black, 0.1);

View File

@ -10,7 +10,8 @@
"build:examples": "node ./node_modules/@babel/node/bin/babel-node.js ./build/examples/build.js --env=production", "build:examples": "node ./node_modules/@babel/node/bin/babel-node.js ./build/examples/build.js --env=production",
"release": "yarn build:packages && yarn lint && yarn test && lerna publish", "release": "yarn build:packages && yarn lint && yarn test && lerna publish",
"lint": "eslint ./packages/**/src/**", "lint": "eslint ./packages/**/src/**",
"test": "jest" "test": "jest",
"audit-ci": "audit-ci --critical"
}, },
"postcss": { "postcss": {
"plugins": { "plugins": {
@ -23,65 +24,67 @@
"ie >= 9" "ie >= 9"
], ],
"devDependencies": { "devDependencies": {
"@babel/core": "^7.2.2", "@babel/core": "^7.4.4",
"@babel/node": "^7.2.2", "@babel/node": "^7.2.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.2.0", "@babel/plugin-transform-runtime": "^7.4.4",
"@babel/polyfill": "^7.2.5", "@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.3.1", "@babel/preset-env": "^7.4.4",
"@babel/preset-stage-2": "^7.0.0", "@babel/preset-stage-2": "^7.0.0",
"@babel/runtime": "^7.3.1", "@babel/runtime": "^7.4.4",
"autoprefixer": "^9.4.7", "audit-ci": "^1.6.0",
"autoprefixer": "^9.5.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-jest": "^24.0.0", "babel-jest": "^24.7.1",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"browser-sync": "^2.26.3", "browser-sync": "^2.26.5",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
"copy-webpack-plugin": "^4.6.0", "copy-webpack-plugin": "^5.0.3",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"eslint": "^5.12.1", "dart-sass": "^1.19.0",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.0.0", "eslint-config-airbnb-base": "^13.0.0",
"eslint-plugin-html": "^5.0.0", "eslint-plugin-html": "^5.0.3",
"eslint-plugin-import": "^2.15.0", "eslint-plugin-import": "^2.17.2",
"eslint-plugin-vue": "5.1.0", "eslint-plugin-vue": "5.2.2",
"fibers": "^4.0.0",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"fuse.js": "^3.3.0", "fuse.js": "^3.4.2",
"glob": "^7.1.3", "glob": "^7.1.3",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.19.1", "http-proxy-middleware": "^0.19.1",
"http-server": "^0.11.1", "http-server": "^0.11.1",
"imagemin-webpack-plugin": "^2.4.0", "imagemin-webpack-plugin": "^2.4.2",
"jest": "^24.0.0", "jest": "^24.7.1",
"lerna": "^3.10.7", "lerna": "^3.13.4",
"mini-css-extract-plugin": "^0.5.0", "mini-css-extract-plugin": "^0.6.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"node-sass": "^4.11.0",
"optimize-css-assets-webpack-plugin": "^5.0.1", "optimize-css-assets-webpack-plugin": "^5.0.1",
"ora": "^3.0.0", "ora": "^3.4.0",
"postcss": "^7.0.14", "postcss": "^7.0.14",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-scss": "^2.0.0", "postcss-scss": "^2.0.0",
"regenerator-runtime": "^0.13.1", "regenerator-runtime": "^0.13.2",
"rollup": "^1.1.2", "rollup": "^1.10.1",
"rollup-plugin-babel": "^4.3.2", "rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.0", "rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-flow-no-whitespace": "^1.0.0", "rollup-plugin-flow-no-whitespace": "^1.0.0",
"rollup-plugin-node-resolve": "^4.0.0", "rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-replace": "^2.1.0", "rollup-plugin-replace": "^2.2.0",
"rollup-plugin-vue": "^4.6.2", "rollup-plugin-vue": "^5.0.0",
"sass-loader": "^7.0.3", "sass-loader": "^7.0.3",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"tippy.js": "^3.4.1", "terser": "^3.17.0",
"uglify-js": "^3.4.9", "tippy.js": "^4.3.0",
"vue": "^2.5.22", "vue": "^2.6.10",
"vue-loader": "^15.6.2", "vue-loader": "^15.7.0",
"vue-router": "^3.0.2", "vue-router": "^3.0.6",
"vue-style-loader": "^4.1.0", "vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.22", "vue-template-compiler": "^2.6.10",
"webpack": "^4.29.0", "webpack": "^4.30.0",
"webpack-dev-middleware": "^3.5.1", "webpack-dev-middleware": "^3.6.2",
"webpack-hot-middleware": "^2.24.3", "webpack-hot-middleware": "^2.24.4",
"webpack-manifest-plugin": "^2.0.4", "webpack-manifest-plugin": "^2.0.4",
"webpack-svgstore-plugin": "^4.1.0", "webpack-svgstore-plugin": "^4.1.0",
"zlib": "^1.0.5" "zlib": "^1.0.5"

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap-commands", "name": "tiptap-commands",
"version": "1.5.0", "version": "1.8.0",
"description": "Commands for tiptap", "description": "Commands for tiptap",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@ -22,8 +22,8 @@
"dependencies": { "dependencies": {
"prosemirror-commands": "^1.0.7", "prosemirror-commands": "^1.0.7",
"prosemirror-inputrules": "^1.0.1", "prosemirror-inputrules": "^1.0.1",
"prosemirror-schema-list": "^1.0.1", "prosemirror-schema-list": "^1.0.3",
"prosemirror-state": "^1.2.2", "prosemirror-state": "^1.2.2",
"tiptap-utils": "^1.1.1" "tiptap-utils": "^1.4.0"
} }
} }

View File

@ -69,7 +69,7 @@ keepItem = $from.index(-1) > 0
/* Change ends here */ /* Change ends here */
if (!canSplit(tr.doc, $from.pos, 2, types)) return false if (!canSplit(tr.doc, $from.pos, 2, types)) return false
if (dispatch) dispatch(tr.split($from.pos, 2, [{ type: state.schema.nodes.todo_item, attrs: { done: false } }]).scrollIntoView()) if (dispatch) dispatch(tr.split($from.pos, 2, types).scrollIntoView())
return true return true
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap-extensions", "name": "tiptap-extensions",
"version": "1.11.0", "version": "1.17.0",
"description": "Extensions for tiptap", "description": "Extensions for tiptap",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@ -22,13 +22,13 @@
}, },
"dependencies": { "dependencies": {
"lowlight": "^1.11.0", "lowlight": "^1.11.0",
"prosemirror-history": "^1.0.3", "prosemirror-history": "^1.0.4",
"prosemirror-state": "^1.2.2", "prosemirror-state": "^1.2.2",
"prosemirror-tables": "^0.7.10", "prosemirror-tables": "^0.7.11",
"prosemirror-utils": "^0.7.5", "prosemirror-utils": "^0.7.6",
"prosemirror-view": "^1.6.8", "prosemirror-view": "^1.8.9",
"tiptap": "^1.11.0", "tiptap": "^1.17.0",
"tiptap-commands": "^1.5.0" "tiptap-commands": "^1.8.0"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^2.5.17", "vue": "^2.5.17",

View File

@ -7,6 +7,13 @@ export default class History extends Extension {
return 'history' return 'history'
} }
get defaultOptions() {
return {
depth: '',
newGroupDelay: '',
}
}
keys() { keys() {
const isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false const isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
const keymap = { const keymap = {
@ -23,7 +30,10 @@ export default class History extends Extension {
get plugins() { get plugins() {
return [ return [
history(), history({
depth: this.options.depth,
newGroupDelay: this.options.newGroupDelay,
}),
] ]
} }

View File

@ -11,6 +11,13 @@ export default class Placeholder extends Extension {
return { return {
emptyNodeClass: 'is-empty', emptyNodeClass: 'is-empty',
emptyNodeText: 'Write something...', emptyNodeText: 'Write something...',
showOnlyWhenEditable: true,
}
}
get update() {
return view => {
view.updateState(view.state)
} }
} }
@ -18,7 +25,15 @@ export default class Placeholder extends Extension {
return [ return [
new Plugin({ new Plugin({
props: { props: {
decorations: ({ doc }) => { decorations: ({ doc, plugins }) => {
const editablePlugin = plugins.find(plugin => plugin.key.startsWith('editable$'))
const editable = editablePlugin.props.editable()
const active = editable || !this.options.showOnlyWhenEditable
if (!active) {
return false
}
const decorations = [] const decorations = []
const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2 const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2

View File

@ -27,3 +27,4 @@ export { default as History } from './extensions/History'
export { default as Placeholder } from './extensions/Placeholder' export { default as Placeholder } from './extensions/Placeholder'
export { default as Suggestions } from './plugins/Suggestions' export { default as Suggestions } from './plugins/Suggestions'
export { default as Highlight } from './plugins/Highlight'

View File

@ -1,6 +1,6 @@
import { Mark, Plugin, TextSelection } from 'tiptap' import { Mark, Plugin } from 'tiptap'
import { updateMark, removeMark, pasteRule } from 'tiptap-commands' import { updateMark, removeMark, pasteRule } from 'tiptap-commands'
import { getMarkRange } from 'tiptap-utils' import { getMarkAttrs } from 'tiptap-utils'
export default class Link extends Mark { export default class Link extends Mark {
@ -55,19 +55,14 @@ export default class Link extends Mark {
return [ return [
new Plugin({ new Plugin({
props: { props: {
handleClick(view, pos) { handleClickOn(view, pos, node, nodePos, event) {
const { schema, doc, tr } = view.state const { schema } = view.state
const range = getMarkRange(doc.resolve(pos), schema.marks.link) const attrs = getMarkAttrs(view.state, schema.marks.link)
if (!range) { if (attrs.href && event.target instanceof HTMLAnchorElement) {
return event.stopPropagation()
window.open(attrs.href)
} }
const $start = doc.resolve(range.from)
const $end = doc.resolve(range.to)
const transaction = tr.setSelection(new TextSelection($start, $end))
view.dispatch(transaction)
}, },
}, },
}), }),

View File

@ -1,7 +1,7 @@
import { Node } from 'tiptap' import { Node } from 'tiptap'
import { wrappingInputRule, toggleList } from 'tiptap-commands' import { wrappingInputRule, toggleList } from 'tiptap-commands'
export default class Bullet extends Node { export default class BulletList extends Node {
get name() { get name() {
return 'bullet_list' return 'bullet_list'

View File

@ -1,65 +1,7 @@
import { Node, Plugin } from 'tiptap' import { Node } from 'tiptap'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
import { findBlockNodes } from 'prosemirror-utils'
import low from 'lowlight/lib/core' import low from 'lowlight/lib/core'
import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-commands'
function getDecorations(doc) { import HighlightPlugin from '../plugins/Highlight'
const decorations = []
const blocks = findBlockNodes(doc)
.filter(item => item.node.type.name === 'code_block')
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
)
function parseNodes(nodes, className = []) {
return nodes.map(node => {
const classes = [
...className,
...node.properties ? node.properties.className : [],
]
if (node.children) {
return parseNodes(node.children, classes)
}
return {
text: node.value,
classes,
}
})
}
blocks.forEach(block => {
let startPos = block.pos + 1
const nodes = low.highlightAuto(block.node.textContent).value
flatten(parseNodes(nodes))
.map(node => {
const from = startPos
const to = from + node.text.length
startPos = to
return {
...node,
from,
to,
}
})
.forEach(node => {
const decoration = Decoration.inline(node.from, node.to, {
class: node.classes.join(' '),
})
decorations.push(decoration)
})
})
return DecorationSet.create(doc, decorations)
}
export default class CodeBlockHighlight extends Node { export default class CodeBlockHighlight extends Node {
@ -74,16 +16,16 @@ export default class CodeBlockHighlight extends Node {
} }
} }
get name() {
return 'code_block'
}
get defaultOptions() { get defaultOptions() {
return { return {
languages: {}, languages: {},
} }
} }
get name() {
return 'code_block'
}
get schema() { get schema() {
return { return {
content: 'text*', content: 'text*',
@ -117,29 +59,7 @@ export default class CodeBlockHighlight extends Node {
get plugins() { get plugins() {
return [ return [
new Plugin({ HighlightPlugin({ name: this.name }),
state: {
init(_, { doc }) {
return getDecorations(doc)
},
apply(transaction, decorationSet, oldState) {
// TODO: find way to cache decorations
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
const previousNodeName = oldState.selection.$head.parent.type.name
if (transaction.docChanged && previousNodeName === 'code_block') {
return getDecorations(transaction.doc)
}
return decorationSet.map(transaction.mapping, transaction.doc)
},
},
props: {
decorations(state) {
return this.getState(state)
},
},
}),
] ]
} }

View File

@ -18,7 +18,7 @@ export default class TodoItem extends Node {
}, },
}, },
template: ` template: `
<li data-type="todo_item" :data-done="node.attrs.done.toString()"> <li :data-type="node.type.name" :data-done="node.attrs.done.toString()">
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span> <span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div> <div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
</li> </li>
@ -35,11 +35,13 @@ export default class TodoItem extends Node {
}, },
draggable: true, draggable: true,
content: 'paragraph', content: 'paragraph',
toDOM(node) { toDOM: node => {
const { done } = node.attrs const { done } = node.attrs
return ['li', { return [
'data-type': 'todo_item', 'li',
{
'data-type': this.name,
'data-done': done.toString(), 'data-done': done.toString(),
}, },
['span', { class: 'todo-checkbox', contenteditable: 'false' }], ['span', { class: 'todo-checkbox', contenteditable: 'false' }],
@ -48,7 +50,7 @@ export default class TodoItem extends Node {
}, },
parseDOM: [{ parseDOM: [{
priority: 51, priority: 51,
tag: '[data-type="todo_item"]', tag: `[data-type="${this.name}"]`,
getAttrs: dom => ({ getAttrs: dom => ({
done: dom.getAttribute('data-done') === 'true', done: dom.getAttribute('data-done') === 'true',
}), }),

View File

@ -11,10 +11,10 @@ export default class TodoList extends Node {
return { return {
group: 'block', group: 'block',
content: 'todo_item+', content: 'todo_item+',
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0], toDOM: () => ['ul', { 'data-type': this.name }, 0],
parseDOM: [{ parseDOM: [{
priority: 51, priority: 51,
tag: '[data-type="todo_list"]', tag: `[data-type="${this.name}"]`,
}], }],
} }
} }

View File

@ -0,0 +1,85 @@
import { Plugin, PluginKey } from 'tiptap'
import { Decoration, DecorationSet } from 'prosemirror-view'
import { findBlockNodes } from 'prosemirror-utils'
import low from 'lowlight/lib/core'
function getDecorations({ doc, name }) {
const decorations = []
const blocks = findBlockNodes(doc).filter(item => item.node.type.name === name)
const flatten = list => list.reduce(
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
)
function parseNodes(nodes, className = []) {
return nodes.map(node => {
const classes = [
...className,
...node.properties ? node.properties.className : [],
]
if (node.children) {
return parseNodes(node.children, classes)
}
return {
text: node.value,
classes,
}
})
}
blocks.forEach(block => {
let startPos = block.pos + 1
const nodes = low.highlightAuto(block.node.textContent).value
flatten(parseNodes(nodes))
.map(node => {
const from = startPos
const to = from + node.text.length
startPos = to
return {
...node,
from,
to,
}
})
.forEach(node => {
const decoration = Decoration.inline(node.from, node.to, {
class: node.classes.join(' '),
})
decorations.push(decoration)
})
})
return DecorationSet.create(doc, decorations)
}
export default function HighlightPlugin({ name }) {
return new Plugin({
name: new PluginKey('highlight'),
state: {
init: (_, { doc }) => getDecorations({ doc, name }),
apply: (transaction, decorationSet, oldState, state) => {
// TODO: find way to cache decorations
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
const nodeName = state.selection.$head.parent.type.name
const previousNodeName = oldState.selection.$head.parent.type.name
if (transaction.docChanged && [nodeName, previousNodeName].includes(name)) {
return getDecorations({ doc: transaction.doc, name })
}
return decorationSet.map(transaction.mapping, transaction.doc)
},
},
props: {
decorations(state) {
return this.getState(state)
},
},
})
}

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap-utils", "name": "tiptap-utils",
"version": "1.1.1", "version": "1.4.0",
"description": "Utility functions for tiptap", "description": "Utility functions for tiptap",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@ -20,9 +20,9 @@
"url": "https://github.com/scrumpy/tiptap/issues" "url": "https://github.com/scrumpy/tiptap/issues"
}, },
"dependencies": { "dependencies": {
"prosemirror-model": "^1.6.4", "prosemirror-model": "^1.7.0",
"prosemirror-state": "^1.2.2", "prosemirror-state": "^1.2.2",
"prosemirror-tables": "^0.7.9", "prosemirror-tables": "^0.7.11",
"prosemirror-utils": "^0.7.5" "prosemirror-utils": "^0.7.6"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap", "name": "tiptap",
"version": "1.11.0", "version": "1.17.0",
"description": "A rich-text editor for Vue.js", "description": "A rich-text editor for Vue.js",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@ -25,11 +25,11 @@
"prosemirror-gapcursor": "^1.0.3", "prosemirror-gapcursor": "^1.0.3",
"prosemirror-inputrules": "^1.0.1", "prosemirror-inputrules": "^1.0.1",
"prosemirror-keymap": "^1.0.1", "prosemirror-keymap": "^1.0.1",
"prosemirror-model": "^1.6.4", "prosemirror-model": "^1.7.0",
"prosemirror-state": "^1.2.1", "prosemirror-state": "^1.2.1",
"prosemirror-view": "^1.6.8", "prosemirror-view": "^1.8.9",
"tiptap-commands": "^1.5.0", "tiptap-commands": "^1.8.0",
"tiptap-utils": "^1.1.1" "tiptap-utils": "^1.4.0"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^2.5.17", "vue": "^2.5.17",

View File

@ -25,4 +25,8 @@ export default {
return createElement('div') return createElement('div')
}, },
beforeDestroy() {
this.editor.element = this.$el
},
} }

View File

@ -7,6 +7,10 @@ export default {
default: null, default: null,
type: Object, type: Object,
}, },
keepInBounds: {
default: true,
type: Boolean,
},
}, },
data() { data() {
@ -27,7 +31,14 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
editor.registerPlugin(MenuBubble({ editor.registerPlugin(MenuBubble({
element: this.$el, element: this.$el,
keepInBounds: this.keepInBounds,
onUpdate: menu => { onUpdate: menu => {
// the second check ensures event is fired only once
if (menu.isActive && this.menu.isActive === false) {
this.$emit('show', menu)
} else if (!menu.isActive && this.menu.isActive === true) {
this.$emit('hide', menu)
}
this.menu = menu this.menu = menu
}, },
})) }))

View File

@ -1,10 +1,15 @@
import { EditorState, Plugin } from 'prosemirror-state' import {
EditorState,
Plugin,
PluginKey,
TextSelection,
} from 'prosemirror-state'
import { EditorView } from 'prosemirror-view' import { EditorView } from 'prosemirror-view'
import { Schema, DOMParser, DOMSerializer } from 'prosemirror-model' import { Schema, DOMParser, DOMSerializer } from 'prosemirror-model'
import { dropCursor } from 'prosemirror-dropcursor' import { dropCursor } from 'prosemirror-dropcursor'
import { gapCursor } from 'prosemirror-gapcursor' import { gapCursor } from 'prosemirror-gapcursor'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { baseKeymap, selectParentNode } from 'prosemirror-commands' import { baseKeymap } from 'prosemirror-commands'
import { inputRules, undoInputRule } from 'prosemirror-inputrules' import { inputRules, undoInputRule } from 'prosemirror-inputrules'
import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils' import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils'
import { ExtensionManager, ComponentView } from './Utils' import { ExtensionManager, ComponentView } from './Utils'
@ -27,11 +32,13 @@ export default class Editor {
}, },
useBuiltInExtensions: true, useBuiltInExtensions: true,
dropCursor: {}, dropCursor: {},
parseOptions: {},
onInit: () => {}, onInit: () => {},
onUpdate: () => {}, onUpdate: () => {},
onFocus: () => {}, onFocus: () => {},
onBlur: () => {}, onBlur: () => {},
onPaste: () => {}, onPaste: () => {},
onDrop: () => {},
onTransaction: () => true, onTransaction: () => true,
} }
@ -67,6 +74,9 @@ export default class Editor {
view: this.view, view: this.view,
state: this.state, state: this.state,
}) })
// give extension manager access to our view
this.extensions.view = this.view
} }
setOptions(options) { setOptions(options) {
@ -157,14 +167,18 @@ export default class Editor {
...this.keymaps, ...this.keymaps,
keymap({ keymap({
Backspace: undoInputRule, Backspace: undoInputRule,
Escape: selectParentNode,
}), }),
keymap(baseKeymap), keymap(baseKeymap),
dropCursor(this.options.dropCursor), dropCursor(this.options.dropCursor),
gapCursor(), gapCursor(),
new Plugin({ new Plugin({
key: new PluginKey('editable'),
props: { props: {
editable: () => this.options.editable, editable: () => this.options.editable,
},
}),
new Plugin({
props: {
attributes: { attributes: {
tabindex: 0, tabindex: 0,
}, },
@ -177,7 +191,11 @@ export default class Editor {
}) })
} }
createDocument(content) { createDocument(content, parseOptions = this.options.parseOptions) {
if (content === null) {
return this.schema.nodeFromJSON(this.options.emptyDocument)
}
if (typeof content === 'object') { if (typeof content === 'object') {
try { try {
return this.schema.nodeFromJSON(content) return this.schema.nodeFromJSON(content)
@ -191,7 +209,7 @@ export default class Editor {
const element = document.createElement('div') const element = document.createElement('div')
element.innerHTML = content.trim() element.innerHTML = content.trim()
return DOMParser.fromSchema(this.schema).parse(element) return DOMParser.fromSchema(this.schema).parse(element, parseOptions)
} }
return false return false
@ -201,7 +219,7 @@ export default class Editor {
const view = new EditorView(this.element, { const view = new EditorView(this.element, {
state: this.state, state: this.state,
handlePaste: this.options.onPaste, handlePaste: this.options.onPaste,
handleDrop: this.options.onPaste, handleDrop: this.options.onDrop,
dispatchTransaction: this.dispatchTransaction.bind(this), dispatchTransaction: this.dispatchTransaction.bind(this),
}) })
@ -290,10 +308,28 @@ export default class Editor {
}) })
} }
focus() { focus(position = null) {
if (position !== null) {
let pos = position
if (position === 'start') {
pos = 0
} else if (position === 'end') {
pos = this.view.state.doc.nodeSize - 2
}
const selection = TextSelection.near(this.view.state.doc.resolve(pos))
const transaction = this.view.state.tr.setSelection(selection)
this.view.dispatch(transaction)
}
this.view.focus() this.view.focus()
} }
blur() {
this.view.dom.blur()
}
getHTML() { getHTML() {
const div = document.createElement('div') const div = document.createElement('div')
const fragment = DOMSerializer const fragment = DOMSerializer
@ -309,10 +345,10 @@ export default class Editor {
return this.state.doc.toJSON() return this.state.doc.toJSON()
} }
setContent(content = {}, emitUpdate = false) { setContent(content = {}, emitUpdate = false, parseOptions) {
this.state = EditorState.create({ this.state = EditorState.create({
schema: this.state.schema, schema: this.state.schema,
doc: this.createDocument(content), doc: this.createDocument(content, parseOptions),
plugins: this.state.plugins, plugins: this.state.plugins,
}) })

View File

@ -1,11 +1,63 @@
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
function textRange(node, from, to) {
const range = document.createRange()
range.setEnd(node, to == null ? node.nodeValue.length : to)
range.setStart(node, from || 0)
return range
}
function singleRect(object, bias) {
const rects = object.getClientRects()
return !rects.length ? object.getBoundingClientRect() : rects[bias < 0 ? 0 : rects.length - 1]
}
function coordsAtPos(view, pos, end = false) {
const { node, offset } = view.docView.domFromPos(pos)
let side
let rect
if (node.nodeType === 3) {
if (end && offset < node.nodeValue.length) {
rect = singleRect(textRange(node, offset - 1, offset), -1)
side = 'right'
} else if (offset < node.nodeValue.length) {
rect = singleRect(textRange(node, offset, offset + 1), -1)
side = 'left'
}
} else if (node.firstChild) {
if (offset < node.childNodes.length) {
const child = node.childNodes[offset]
rect = singleRect(child.nodeType === 3 ? textRange(child) : child, -1)
side = 'left'
}
if ((!rect || rect.top === rect.bottom) && offset) {
const child = node.childNodes[offset - 1]
rect = singleRect(child.nodeType === 3 ? textRange(child) : child, 1)
side = 'right'
}
} else {
rect = node.getBoundingClientRect()
side = 'left'
}
const x = rect[side]
return {
top: rect.top,
bottom: rect.bottom,
left: x,
right: x,
}
}
class Menu { class Menu {
constructor({ options, editorView }) { constructor({ options, editorView }) {
this.options = { this.options = {
...{ ...{
element: null, element: null,
keepInBounds: true,
onUpdate: () => false, onUpdate: () => false,
}, },
...options, ...options,
@ -36,19 +88,24 @@ class Menu {
const { from, to } = state.selection const { from, to } = state.selection
// These are in screen coordinates // These are in screen coordinates
const start = view.coordsAtPos(from) // We can't use EditorView.cordsAtPos here because it can't handle linebreaks correctly
const end = view.coordsAtPos(to) // See: https://github.com/ProseMirror/prosemirror-view/pull/47
const start = coordsAtPos(view, from)
const end = coordsAtPos(view, to, true)
// The box in which the tooltip is positioned, to use as base // The box in which the tooltip is positioned, to use as base
const box = this.options.element.offsetParent.getBoundingClientRect() const box = this.options.element.offsetParent.getBoundingClientRect()
const el = this.options.element.getBoundingClientRect()
// Find a center-ish x position from the selection endpoints (when // Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left) // crossing lines, end may be more to the left)
const left = Math.max((start.left + end.left) / 2, start.left + 3) const left = ((start.left + end.left) / 2) - box.left
// Keep the menuBubble in the bounding box of the offsetParent i
this.left = Math.round(this.options.keepInBounds
? Math.min(box.width - (el.width / 2), Math.max(left, el.width / 2)) : left)
this.bottom = Math.round(box.bottom - start.top)
this.isActive = true this.isActive = true
this.left = parseInt(left - box.left, 10)
this.bottom = parseInt(box.bottom - start.top, 10)
this.sendUpdate() this.sendUpdate()
} }

View File

@ -108,6 +108,10 @@ export default class ComponentView {
// disable (almost) all prosemirror event listener for node views // disable (almost) all prosemirror event listener for node views
stopEvent(event) { stopEvent(event) {
if (typeof this.extension.stopEvent === 'function') {
return this.extension.stopEvent(event)
}
const isPaste = event.type === 'paste' const isPaste = event.type === 'paste'
const draggable = !!this.extension.schema.draggable const draggable = !!this.extension.schema.draggable

View File

@ -15,6 +15,10 @@ export default class Extension {
return 'extension' return 'extension'
} }
get update() {
return () => {}
}
get defaultOptions() { get defaultOptions() {
return {} return {}
} }

View File

@ -15,6 +15,27 @@ export default class ExtensionManager {
}), {}) }), {})
} }
get options() {
const { view } = this
return this.extensions
.reduce((nodes, extension) => ({
...nodes,
[extension.name]: new Proxy(extension.options, {
set(obj, prop, value) {
const changed = (obj[prop] !== value)
Object.assign(obj, { [prop]: value })
if (changed) {
extension.update(view)
}
return true
},
}),
}), {})
}
get marks() { get marks() {
return this.extensions return this.extensions
.filter(extension => extension.type === 'mark') .filter(extension => extension.type === 'mark')

View File

@ -25,6 +25,14 @@ test('create editor', () => {
expect(editor).toBeDefined() expect(editor).toBeDefined()
}) })
test('check empty content (null)', () => {
const editor = new Editor({
content: null,
})
expect(editor.getHTML()).toEqual('<p></p>')
})
test('check invalid content (JSON)', () => { test('check invalid content (JSON)', () => {
const editor = new Editor({ const editor = new Editor({
content: { thisIsNotAValidDocument: true }, content: { thisIsNotAValidDocument: true },
@ -270,3 +278,52 @@ test('update callback', done => {
editor.setContent('<p>Bar</p>', true) editor.setContent('<p>Bar</p>', true)
}) })
test('parse options in set content', done => {
const editor = new Editor({
content: '<p>Foo</p>',
onUpdate: ({ getHTML, getJSON }) => {
expect(getHTML()).toEqual('<p> Foo </p>')
expect(getJSON()).toEqual({
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: ' Foo ',
},
],
},
],
})
done()
},
})
editor.setContent('<p> Foo </p>', true, { preserveWhitespace: true })
})
test('parse options in constructor', () => {
const editor = new Editor({
content: '<p> Foo </p>',
parseOptions: { preserveWhitespace: true },
})
expect(editor.getHTML()).toEqual('<p> Foo </p>')
expect(editor.getJSON()).toEqual({
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: ' Foo ',
},
],
},
],
})
})

6433
yarn.lock

File diff suppressed because it is too large Load Diff