Merge branch 'master' into issue-232

This commit is contained in:
Christoph Flathmann 2019-05-08 13:34:12 +02:00 committed by GitHub
commit 5d2fac08f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1614 additions and 1490 deletions

View File

@ -56,5 +56,7 @@ module.exports = {
'class-methods-use-this': 'off', 'class-methods-use-this': 'off',
'global-require': 'off', 'global-require': 'off',
'func-names': ['error', 'never'],
} }
} }

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to Reproduce / Codesandbox Example**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
Fork this or create a new Codesandbox replicating your error
https://codesandbox.io/s/qxv5m9y6oq?fontsize=14
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- Mobile / Desktop: [eg. Desktop]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature request'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v11.6.0

View File

@ -117,8 +117,8 @@ The `<editor-menu-bar />` component is renderless and will receive some properti
```vue ```vue
<template> <template>
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div slot-scope="{ commands, isActive }"> <div>
<button :class="{ 'is-active': isActive.bold() }" @click="commands.bold"> <button :class="{ 'is-active': isActive.bold() }" @click="commands.bold">
Bold Bold
</button> </button>
@ -147,9 +147,8 @@ The `<editor-menu-bubble />` component is renderless and will receive some prope
```vue ```vue
<template> <template>
<editor-menu-bubble :editor="editor"> <editor-menu-bubble :editor="editor" v-slot="{ commands, isActive, menu }">
<div <div
slot-scope="{ commands, isActive, menu }"
:class="{ 'is-active': menu.isActive }" :class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
> >
@ -181,9 +180,8 @@ The `<editor-floating-menu />` component is renderless and will receive some pro
```vue ```vue
<template> <template>
<editor-floating-menu :editor="editor"> <editor-floating-menu :editor="editor" v-slot="{ commands, isActive, menu }">
<div <div
slot-scope="{ commands, isActive, menu }"
:class="{ 'is-active': menu.isActive }" :class="{ 'is-active': menu.isActive }"
:style="`top: ${menu.top}px`" :style="`top: ${menu.top}px`"
> >
@ -207,12 +205,10 @@ By default, the editor will only support paragraphs. Other nodes and marks are a
```vue ```vue
<template> <template>
<div> <div>
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div slot-scope="{ commands, isActive }">
<button :class="{ 'is-active': isActive.bold() }" @click="commands.bold"> <button :class="{ 'is-active': isActive.bold() }" @click="commands.bold">
Bold Bold
</button> </button>
</div>
</editor-menu-bar> </editor-menu-bar>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</div> </div>
@ -477,6 +473,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) - [Erick Wilder](https://github.com/erickwilder)
- [Marius Tolzmann](https://github.com/mariux)
- [All Contributors](../../contributors) - [All Contributors](../../contributors)
## Packages Using Tiptap ## Packages Using Tiptap

View File

@ -1,6 +1,5 @@
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 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'
@ -93,7 +92,6 @@ export default {
loader: 'sass-loader', loader: 'sass-loader',
options: { options: {
implementation: DartSass, 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

@ -1,7 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar" slot-scope="{ commands, isActive }"> <div class="menubar">
<button <button
class="menubar__button" class="menubar__button"

View File

@ -0,0 +1,126 @@
<template>
<div class="editor">
<h2>
Collaborative Editing
</h2>
<div class="message">
With the Collaboration Extension it's possible for several users to work on a document at the same time. To make this possible, client-side and server-side code is required. This example shows this using a <a href="https://glitch.com/edit/#!/tiptap-sockets" target="_blank">socket server on glitch.com</a>. To keep the demo code clean, only a few nodes and marks are activated. There is also set a 250ms debounce for all changes. Try it out below:
</div>
<template v-if="editor && !loading">
<div class="count">
{{ count }} {{ count === 1 ? 'user' : 'users' }} connected
</div>
<editor-content class="editor__content" :editor="editor" />
</template>
<em v-else>
Connecting to socket server
</em>
</div>
</template>
<script>
import io from 'socket.io-client'
import { Editor, EditorContent } from 'tiptap'
import {
HardBreak,
Heading,
Bold,
Code,
Italic,
History,
Collaboration,
} from 'tiptap-extensions'
export default {
components: {
EditorContent,
},
data() {
return {
loading: true,
editor: null,
socket: null,
count: 0,
}
},
methods: {
onInit({ doc, version }) {
this.loading = false
if (this.editor) {
this.editor.destroy()
}
this.editor = new Editor({
content: doc,
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new Bold(),
new Code(),
new Italic(),
new History(),
new Collaboration({
// the initial version we start with
// version is an integer which is incremented with every change
version,
// debounce changes so we can save some bandwidth
debounce: 250,
// onSendable is called whenever there are changed we have to send to our server
onSendable: data => {
this.socket.emit('update', data)
},
}),
],
})
},
setCount(count) {
this.count = count
},
},
mounted() {
// server implementation: https://glitch.com/edit/#!/tiptap-sockets
this.socket = io('wss://tiptap-sockets.glitch.me')
// get the current document and its version
.on('init', data => this.onInit(data))
// send all updates to the collaboration extension
.on('update', data => this.editor.extensions.options.collaboration.update(data))
// get count of connected users
.on('getCount', count => this.setCount(count))
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
@import "~variables";
.count {
display: flex;
align-items: center;
font-weight: bold;
color: rgba($color-black, 0.5);
color: #27b127;
margin-bottom: 1rem;
text-transform: uppercase;
font-size: 0.7rem;
line-height: 1;
&:before {
content: '';
display: inline-flex;
background-color: #27b127;
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
margin-right: 0.3rem;
}
}
</style>

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar" slot-scope="{ commands, isActive }"> <div class="menubar">
<button <button
class="menubar__button" class="menubar__button"

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-floating-menu :editor="editor"> <editor-floating-menu :editor="editor" v-slot="{ commands, isActive, menu }">
<div <div
slot-scope="{ commands, isActive, menu }"
class="editor__floating-menu" class="editor__floating-menu"
:class="{ 'is-active': menu.isActive }" :class="{ 'is-active': menu.isActive }"
:style="`top: ${menu.top}px`" :style="`top: ${menu.top}px`"

View File

@ -1,10 +1,9 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
<div <div
class="menubar is-hidden" class="menubar is-hidden"
:class="{ 'is-focused': focused }" :class="{ 'is-focused': focused }"
slot-scope="{ commands, isActive, focused }"
> >
<button <button

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar" slot-scope="{ commands, isActive }"> <div class="menubar">
<button <button
class="menubar__button" class="menubar__button"

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands }">
<div class="menubar" slot-scope="{ commands }"> <div class="menubar">
<button <button
class="menubar__button" class="menubar__button"
@click="showImagePrompt(commands.image)" @click="showImagePrompt(commands.image)"

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bubble class="menububble" :editor="editor" @hide="hideLinkMenu"> <editor-menu-bubble class="menububble" :editor="editor" @hide="hideLinkMenu" v-slot="{ commands, isActive, getMarkAttrs, menu }">
<div <div
slot-scope="{ commands, isActive, getMarkAttrs, menu }"
class="menububble" class="menububble"
:class="{ 'is-active': menu.isActive }" :class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bubble :editor="editor"> <editor-menu-bubble :editor="editor" :keep-in-bounds="keepInBounds" v-slot="{ commands, isActive, menu }">
<div <div
slot-scope="{ commands, isActive, menu }"
class="menububble" class="menububble"
:class="{ 'is-active': menu.isActive }" :class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
@ -69,6 +68,7 @@ export default {
}, },
data() { data() {
return { return {
keepInBounds: true,
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new Blockquote(), new Blockquote(),

View File

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

View File

@ -2,8 +2,8 @@
<div> <div>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands }">
<div class="menubar" slot-scope="{ commands }"> <div class="menubar">
<button class="menubar__button" @click="commands.mention({ id: 1, label: 'Philipp Kühn' })"> <button class="menubar__button" @click="commands.mention({ id: 1, label: 'Philipp Kühn' })">
<icon name="mention" /> <icon name="mention" />
<span>Insert Mention</span> <span>Insert Mention</span>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar" slot-scope="{ commands, isActive }"> <div class="menubar">
<div class="toolbar"> <div class="toolbar">
<button <button
class="menubar__button" class="menubar__button"

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="editor"> <div class="editor">
<editor-menu-bar :editor="editor"> <editor-menu-bar :editor="editor" v-slot="{ commands, isActive }">
<div class="menubar" slot-scope="{ commands, isActive }"> <div class="menubar">
<button <button
class="menubar__button" class="menubar__button"

View File

@ -45,6 +45,9 @@
<router-link class="subnavigation__link" to="/placeholder"> <router-link class="subnavigation__link" to="/placeholder">
Placeholder Placeholder
</router-link> </router-link>
<router-link class="subnavigation__link" to="/collaboration">
Collaboration
</router-link>
<router-link class="subnavigation__link" to="/export"> <router-link class="subnavigation__link" to="/export">
Export HTML or JSON Export HTML or JSON
</router-link> </router-link>

View File

@ -74,6 +74,15 @@ h3 {
background-color: rgba($color-black, 0.1); background-color: rgba($color-black, 0.1);
} }
.message {
background-color: rgba($color-black, 0.05);
color: rgba($color-black, 0.7);
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
font-style: italic;
}
@import "./editor"; @import "./editor";
@import "./menubar"; @import "./menubar";
@import "./menububble"; @import "./menububble";

View File

@ -117,6 +117,13 @@ const routes = [
githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Placeholder', githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Placeholder',
}, },
}, },
{
path: '/collaboration',
component: () => import('Components/Routes/Collaboration'),
meta: {
githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Collaboration',
},
},
{ {
path: '/export', path: '/export',
component: () => import('Components/Routes/Export'), component: () => import('Components/Routes/Export'),

View File

@ -24,23 +24,23 @@
"ie >= 9" "ie >= 9"
], ],
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.3", "@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.4.3", "@babel/plugin-transform-runtime": "^7.4.4",
"@babel/polyfill": "^7.4.3", "@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.4.3", "@babel/preset-env": "^7.4.4",
"@babel/preset-stage-2": "^7.0.0", "@babel/preset-stage-2": "^7.0.0",
"@babel/runtime": "^7.4.3", "@babel/runtime": "^7.4.4",
"audit-ci": "^1.6.0", "audit-ci": "^1.6.0",
"autoprefixer": "^9.5.1", "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.7.1", "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": "^5.0.2", "copy-webpack-plugin": "^5.0.3",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"dart-sass": "^1.19.0", "dart-sass": "^1.19.0",
"eslint": "^5.16.0", "eslint": "^5.16.0",
@ -48,7 +48,6 @@
"eslint-plugin-html": "^5.0.3", "eslint-plugin-html": "^5.0.3",
"eslint-plugin-import": "^2.17.2", "eslint-plugin-import": "^2.17.2",
"eslint-plugin-vue": "5.2.2", "eslint-plugin-vue": "5.2.2",
"fibers": "^3.1.1",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"fuse.js": "^3.4.2", "fuse.js": "^3.4.2",
"glob": "^7.1.3", "glob": "^7.1.3",
@ -57,7 +56,7 @@
"http-server": "^0.11.1", "http-server": "^0.11.1",
"imagemin-webpack-plugin": "^2.4.2", "imagemin-webpack-plugin": "^2.4.2",
"jest": "^24.7.1", "jest": "^24.7.1",
"lerna": "^3.13.3", "lerna": "^3.13.4",
"mini-css-extract-plugin": "^0.6.0", "mini-css-extract-plugin": "^0.6.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"optimize-css-assets-webpack-plugin": "^5.0.1", "optimize-css-assets-webpack-plugin": "^5.0.1",
@ -75,8 +74,8 @@
"rollup-plugin-vue": "^5.0.0", "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": "^4.2.1", "terser": "^3.17.0",
"uglify-js": "^3.5.5", "tippy.js": "^4.3.0",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-loader": "^15.7.0", "vue-loader": "^15.7.0",
"vue-router": "^3.0.6", "vue-router": "^3.0.6",
@ -84,10 +83,12 @@
"vue-template-compiler": "^2.6.10", "vue-template-compiler": "^2.6.10",
"webpack": "^4.30.0", "webpack": "^4.30.0",
"webpack-dev-middleware": "^3.6.2", "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"
}, },
"dependencies": {} "dependencies": {
"socket.io-client": "^2.2.0"
}
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap-commands", "name": "tiptap-commands",
"version": "1.7.1", "version": "1.9.1",
"description": "Commands for tiptap", "description": "Commands for tiptap",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@ -22,9 +22,9 @@
"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.2", "prosemirror-schema-list": "^1.0.3",
"prosemirror-state": "^1.2.2", "prosemirror-state": "^1.2.2",
"prosemirror-model": "^1.7.0", "prosemirror-model": "^1.7.0",
"tiptap-utils": "^1.3.0" "tiptap-utils": "^1.4.1"
} }
} }

View File

@ -34,8 +34,8 @@ index = $pos.index(d)
// this is a copy of splitListItem // this is a copy of splitListItem
// see https://github.com/ProseMirror/prosemirror-schema-list/blob/master/src/schema-list.js // see https://github.com/ProseMirror/prosemirror-schema-list/blob/master/src/schema-list.js
export default function splitListItem(itemType) { export default function splitToDefaultListItem(itemType) {
return function _splitListItem(state, dispatch) { return function (state, dispatch) {
const { $from, $to, node } = state.selection const { $from, $to, node } = state.selection
if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) return false
const grandParent = $from.node(-1) const grandParent = $from.node(-1)

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap-extensions", "name": "tiptap-extensions",
"version": "1.16.2", "version": "1.18.1",
"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,16 @@
}, },
"dependencies": { "dependencies": {
"lowlight": "^1.11.0", "lowlight": "^1.11.0",
"prosemirror-collab": "^1.1.1",
"prosemirror-history": "^1.0.4", "prosemirror-history": "^1.0.4",
"prosemirror-model": "^1.7.0",
"prosemirror-state": "^1.2.2", "prosemirror-state": "^1.2.2",
"prosemirror-tables": "^0.7.10", "prosemirror-tables": "^0.7.11",
"prosemirror-transform": "^1.1.3",
"prosemirror-utils": "^0.7.6", "prosemirror-utils": "^0.7.6",
"prosemirror-view": "^1.8.9", "prosemirror-view": "^1.9.1",
"tiptap": "^1.16.2", "tiptap": "^1.18.1",
"tiptap-commands": "^1.7.1" "tiptap-commands": "^1.9.1"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^2.5.17", "vue": "^2.5.17",

View File

@ -0,0 +1,78 @@
import { Extension } from 'tiptap'
import { Step } from 'prosemirror-transform'
import {
collab,
sendableSteps,
getVersion,
receiveTransaction,
} from 'prosemirror-collab'
export default class Collaboration extends Extension {
get name() {
return 'collaboration'
}
init() {
this.getSendableSteps = this.debounce(state => {
const sendable = sendableSteps(state)
if (sendable) {
this.options.onSendable({
version: sendable.version,
steps: sendable.steps.map(step => step.toJSON()),
clientID: sendable.clientID,
})
}
}, this.options.debounce)
this.editor.on('update', ({ state }) => {
this.getSendableSteps(state)
})
}
get defaultOptions() {
return {
version: 0,
clientID: Math.floor(Math.random() * 0xFFFFFFFF),
debounce: 250,
onSendable: () => {},
update: ({ steps, version }) => {
const { state, view, schema } = this.editor
if (getVersion(state) > version) {
return
}
view.dispatch(receiveTransaction(
state,
steps.map(item => Step.fromJSON(schema, item.step)),
steps.map(item => item.clientID),
))
},
}
}
get plugins() {
return [
collab({
version: this.options.version,
clientID: this.options.clientID,
}),
]
}
debounce(fn, delay) {
let timeout
return function (...args) {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
fn(...args)
timeout = null
}, delay)
}
}
}

View File

@ -23,6 +23,7 @@ export { default as Link } from './marks/Link'
export { default as Strike } from './marks/Strike' export { default as Strike } from './marks/Strike'
export { default as Underline } from './marks/Underline' export { default as Underline } from './marks/Underline'
export { default as Collaboration } from './extensions/Collaboration'
export { default as History } from './extensions/History' export { default as History } from './extensions/History'
export { default as Placeholder } from './extensions/Placeholder' export { default as Placeholder } from './extensions/Placeholder'

View File

@ -44,7 +44,7 @@ export default class Link extends Mark {
pasteRules({ type }) { pasteRules({ type }) {
return [ return [
pasteRule( pasteRule(
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g, /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-zA-Z]{2,}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g,
type, type,
url => ({ href: url }), url => ({ href: url }),
), ),
@ -59,7 +59,7 @@ export default class Link extends Mark {
const { schema } = view.state const { schema } = view.state
const attrs = getMarkAttrs(view.state, schema.marks.link) const attrs = getMarkAttrs(view.state, schema.marks.link)
if (attrs.href) { if (attrs.href && event.target instanceof HTMLAnchorElement) {
event.stopPropagation() event.stopPropagation()
window.open(attrs.href) window.open(attrs.href)
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap-utils", "name": "tiptap-utils",
"version": "1.3.0", "version": "1.4.1",
"description": "Utility functions for tiptap", "description": "Utility functions for tiptap",
"homepage": "https://tiptap.scrumpy.io", "homepage": "https://tiptap.scrumpy.io",
"license": "MIT", "license": "MIT",
@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"prosemirror-model": "^1.7.0", "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.6" "prosemirror-utils": "^0.7.6"
} }
} }

View File

@ -17,19 +17,18 @@ export default function ($pos = null, type = null) {
let startIndex = $pos.index() let startIndex = $pos.index()
let startPos = $pos.start() + start.offset let startPos = $pos.start() + start.offset
let endIndex = startIndex + 1
let endPos = startPos + start.node.nodeSize
while (startIndex > 0 && link.isInSet($pos.parent.child(startIndex - 1).marks)) { while (startIndex > 0 && link.isInSet($pos.parent.child(startIndex - 1).marks)) {
startIndex -= 1 startIndex -= 1
startPos -= $pos.parent.child(startIndex).nodeSize startPos -= $pos.parent.child(startIndex).nodeSize
} }
// const endIndex = $pos.indexAfter() while (endIndex < $pos.parent.childCount && link.isInSet($pos.parent.child(endIndex).marks)) {
const endPos = startPos + start.node.nodeSize endPos += $pos.parent.child(endIndex).nodeSize
endIndex += 1
// disable for now. see #156 }
// while (endIndex < $pos.parent.childCount && link.isInSet($pos.parent.child(endIndex).marks)) {
// endPos += $pos.parent.child(endIndex).nodeSize
// endIndex += 1
// }
return { from: startPos, to: endPos } return { from: startPos, to: endPos }

View File

@ -1,12 +1,16 @@
import { findParentNode } from 'prosemirror-utils' import {
findParentNode,
findSelectedNodeOfType,
} from 'prosemirror-utils'
export default function (state, type, attrs = {}) { export default function (state, type, attrs = {}) {
const predicate = node => node.type === type const predicate = node => node.type === type
const parent = findParentNode(predicate)(state.selection) const node = findSelectedNodeOfType(type)(state.selection)
|| findParentNode(predicate)(state.selection)
if (!Object.keys(attrs).length || !parent) { if (!Object.keys(attrs).length || !node) {
return !!parent return !!node
} }
return parent.node.hasMarkup(type, attrs) return node.node.hasMarkup(type, attrs)
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tiptap", "name": "tiptap",
"version": "1.16.2", "version": "1.18.1",
"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",
@ -26,10 +26,10 @@
"prosemirror-inputrules": "^1.0.1", "prosemirror-inputrules": "^1.0.1",
"prosemirror-keymap": "^1.0.1", "prosemirror-keymap": "^1.0.1",
"prosemirror-model": "^1.7.0", "prosemirror-model": "^1.7.0",
"prosemirror-state": "^1.2.1", "prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.8.9", "prosemirror-view": "^1.9.1",
"tiptap-commands": "^1.7.1", "tiptap-commands": "^1.9.1",
"tiptap-utils": "^1.3.0" "tiptap-utils": "^1.4.1"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^2.5.17", "vue": "^2.5.17",

View File

@ -7,6 +7,10 @@ export default {
default: null, default: null,
type: Object, type: Object,
}, },
keepInBounds: {
default: true,
type: Boolean,
},
}, },
data() { data() {
@ -27,6 +31,7 @@ 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 // the second check ensures event is fired only once
if (menu.isActive && this.menu.isActive === false) { if (menu.isActive && this.menu.isActive === false) {

View File

@ -12,12 +12,16 @@ import { keymap } from 'prosemirror-keymap'
import { baseKeymap } 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 {
camelCase, Emitter, ExtensionManager, ComponentView,
} from './Utils'
import { Doc, Paragraph, Text } from './Nodes' import { Doc, Paragraph, Text } from './Nodes'
export default class Editor { export default class Editor extends Emitter {
constructor(options = {}) { constructor(options = {}) {
super()
this.defaultOptions = { this.defaultOptions = {
editorProps: {}, editorProps: {},
editable: true, editable: true,
@ -41,6 +45,15 @@ export default class Editor {
onDrop: () => {}, onDrop: () => {},
} }
this.events = [
'init',
'update',
'focus',
'blur',
'paste',
'drop',
]
this.init(options) this.init(options)
} }
@ -58,7 +71,6 @@ export default class Editor {
this.keymaps = this.createKeymaps() this.keymaps = this.createKeymaps()
this.inputRules = this.createInputRules() this.inputRules = this.createInputRules()
this.pasteRules = this.createPasteRules() this.pasteRules = this.createPasteRules()
this.state = this.createState()
this.view = this.createView() this.view = this.createView()
this.commands = this.createCommands() this.commands = this.createCommands()
this.setActiveNodesAndMarks() this.setActiveNodesAndMarks()
@ -69,7 +81,11 @@ export default class Editor {
}, 10) }, 10)
} }
this.options.onInit({ this.events.forEach(name => {
this.on(name, this.options[camelCase(`on ${name}`)])
})
this.emit('init', {
view: this.view, view: this.view,
state: this.state, state: this.state,
}) })
@ -101,11 +117,15 @@ export default class Editor {
] ]
} }
get state() {
return this.view ? this.view.state : null
}
createExtensions() { createExtensions() {
return new ExtensionManager([ return new ExtensionManager([
...this.builtInExtensions, ...this.builtInExtensions,
...this.options.extensions, ...this.options.extensions,
]) ], this)
} }
createPlugins() { createPlugins() {
@ -216,21 +236,21 @@ export default class Editor {
createView() { createView() {
const view = new EditorView(this.element, { const view = new EditorView(this.element, {
state: this.state, state: this.createState(),
handlePaste: this.options.onPaste, handlePaste: (...args) => { this.emit('paste', ...args) },
handleDrop: this.options.onDrop, handleDrop: (...args) => { this.emit('drop', ...args) },
dispatchTransaction: this.dispatchTransaction.bind(this), dispatchTransaction: this.dispatchTransaction.bind(this),
}) })
view.dom.style.whiteSpace = 'pre-wrap' view.dom.style.whiteSpace = 'pre-wrap'
view.dom.addEventListener('focus', event => this.options.onFocus({ view.dom.addEventListener('focus', event => this.emit('focus', {
event, event,
state: this.state, state: this.state,
view: this.view, view: this.view,
})) }))
view.dom.addEventListener('blur', event => this.options.onBlur({ view.dom.addEventListener('blur', event => this.emit('blur', {
event, event,
state: this.state, state: this.state,
view: this.view, view: this.view,
@ -283,8 +303,8 @@ export default class Editor {
} }
dispatchTransaction(transaction) { dispatchTransaction(transaction) {
this.state = this.state.apply(transaction) const newState = this.state.apply(transaction)
this.view.updateState(this.state) this.view.updateState(newState)
this.setActiveNodesAndMarks() this.setActiveNodesAndMarks()
if (!transaction.docChanged) { if (!transaction.docChanged) {
@ -295,7 +315,7 @@ export default class Editor {
} }
emitUpdate(transaction) { emitUpdate(transaction) {
this.options.onUpdate({ this.emit('update', {
getHTML: this.getHTML.bind(this), getHTML: this.getHTML.bind(this),
getJSON: this.getJSON.bind(this), getJSON: this.getJSON.bind(this),
state: this.state, state: this.state,
@ -325,6 +345,13 @@ export default class Editor {
this.view.dom.blur() this.view.dom.blur()
} }
getSchemaJSON() {
return JSON.parse(JSON.stringify({
nodes: this.extensions.nodes,
marks: this.extensions.marks,
}))
}
getHTML() { getHTML() {
const div = document.createElement('div') const div = document.createElement('div')
const fragment = DOMSerializer const fragment = DOMSerializer
@ -341,13 +368,13 @@ export default class Editor {
} }
setContent(content = {}, emitUpdate = false, parseOptions) { setContent(content = {}, emitUpdate = false, parseOptions) {
this.state = EditorState.create({ const newState = EditorState.create({
schema: this.state.schema, schema: this.state.schema,
doc: this.createDocument(content, parseOptions), doc: this.createDocument(content, parseOptions),
plugins: this.state.plugins, plugins: this.state.plugins,
}) })
this.view.updateState(this.state) this.view.updateState(newState)
if (emitUpdate) { if (emitUpdate) {
this.emitUpdate() this.emitUpdate()
@ -402,10 +429,11 @@ export default class Editor {
return return
} }
this.state = this.state.reconfigure({ const newState = this.state.reconfigure({
plugins: this.state.plugins.concat([plugin]), plugins: this.state.plugins.concat([plugin]),
}) })
this.view.updateState(this.state) this.view.updateState(newState)
this.view.updatePluginViews()
} }
destroy() { destroy() {

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

@ -65,6 +65,10 @@ export default class ComponentView {
} }
updateComponentProps(props) { updateComponentProps(props) {
if (!this.vm._props) {
return
}
// Update props in component // Update props in component
// TODO: Avoid mutating a prop directly. // TODO: Avoid mutating a prop directly.
// Maybe there is a better way to do this? // Maybe there is a better way to do this?

View File

@ -0,0 +1,45 @@
export default class Emitter {
// Add an event listener for given event
on(event, fn) {
this._callbacks = this._callbacks || {}
// Create namespace for this event
if (!this._callbacks[event]) {
this._callbacks[event] = []
}
this._callbacks[event].push(fn)
return this
}
emit(event, ...args) {
this._callbacks = this._callbacks || {}
const callbacks = this._callbacks[event]
if (callbacks) {
callbacks.forEach(callback => callback.apply(this, args))
}
return this
}
// Remove event listener for given event.
// If fn is not provided, all event listeners for that event will be removed.
// If neither is provided, all event listeners will be removed.
off(event, fn) {
if (!arguments.length) {
this._callbacks = {}
} else {
// event listeners for the given event
const callbacks = this._callbacks ? this._callbacks[event] : null
if (callbacks) {
if (fn) {
this._callbacks[event] = callbacks.filter(cb => cb !== fn) // remove specific handler
} else {
delete this._callbacks[event] // remove all handlers
}
}
}
return this
}
}

View File

@ -7,6 +7,14 @@ export default class Extension {
} }
} }
init() {
return null
}
bindEditor(editor = null) {
this.editor = editor
}
get name() { get name() {
return null return null
} }

View File

@ -2,7 +2,11 @@ import { keymap } from 'prosemirror-keymap'
export default class ExtensionManager { export default class ExtensionManager {
constructor(extensions = []) { constructor(extensions = [], editor) {
extensions.forEach(extension => {
extension.bindEditor(editor)
extension.init()
})
this.extensions = extensions this.extensions = extensions
} }
@ -133,44 +137,28 @@ export default class ExtensionManager {
} : {}, } : {},
}) })
if (Array.isArray(value)) { const apply = (cb, attrs) => {
commands[name] = attrs => value
.forEach(callback => {
if (!editable) { if (!editable) {
return false return false
} }
view.focus() view.focus()
return callback(attrs)(view.state, view.dispatch, view) return cb(attrs)(view.state, view.dispatch, view)
})
} else if (typeof value === 'function') {
commands[name] = attrs => {
if (!editable) {
return false
} }
view.focus()
return value(attrs)(view.state, view.dispatch, view) const handle = (_name, _value) => {
if (Array.isArray(_value)) {
commands[_name] = attrs => _value.forEach(callback => apply(callback, attrs))
} else if (typeof _value === 'function') {
commands[_name] = attrs => apply(_value, attrs)
} }
} else if (typeof value === 'object') { }
if (typeof value === 'object') {
Object.entries(value).forEach(([commandName, commandValue]) => { Object.entries(value).forEach(([commandName, commandValue]) => {
if (Array.isArray(commandValue)) { handle(commandName, commandValue)
commands[commandName] = attrs => commandValue
.forEach(callback => {
if (!editable) {
return false
}
view.focus()
return callback(attrs)(view.state, view.dispatch, view)
}) })
} else { } else {
commands[commandName] = attrs => { handle(name, value)
if (!editable) {
return false
}
view.focus()
return commandValue(attrs)(view.state, view.dispatch, view)
}
}
})
} }
return { return {

View File

@ -0,0 +1,3 @@
export default function (str) {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase())).replace(/\s+/g, '')
}

View File

@ -1,4 +1,6 @@
export { default as camelCase } from './camelCase'
export { default as ComponentView } from './ComponentView' export { default as ComponentView } from './ComponentView'
export { default as Emitter } from './Emitter'
export { default as Extension } from './Extension' export { default as Extension } from './Extension'
export { default as ExtensionManager } from './ExtensionManager' export { default as ExtensionManager } from './ExtensionManager'
export { default as Mark } from './Mark' export { default as Mark } from './Mark'

2382
yarn.lock

File diff suppressed because it is too large Load Diff