mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-12-15 02:59:01 +08:00
Merge branch 'master' into issue-232
This commit is contained in:
commit
5d2fac08f1
@ -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
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
15
README.md
15
README.md
@ -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
|
||||||
|
@ -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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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`),
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
126
examples/Components/Routes/Collaboration/index.vue
Normal file
126
examples/Components/Routes/Collaboration/index.vue
Normal 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>
|
@ -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"
|
||||||
|
@ -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`"
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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)"
|
||||||
|
@ -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;`"
|
||||||
|
@ -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(),
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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";
|
||||||
|
@ -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'),
|
||||||
|
27
package.json
27
package.json
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
78
packages/tiptap-extensions/src/extensions/Collaboration.js
Normal file
78
packages/tiptap-extensions/src/extensions/Collaboration.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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'
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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?
|
||||||
|
45
packages/tiptap/src/Utils/Emitter.js
Normal file
45
packages/tiptap/src/Utils/Emitter.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
3
packages/tiptap/src/Utils/camelCase.js
Normal file
3
packages/tiptap/src/Utils/camelCase.js
Normal 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, '')
|
||||||
|
}
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user