add emitter, move some collab logic to extension

This commit is contained in:
Philipp Kühn 2019-05-04 00:05:39 +02:00
parent 2475bf6123
commit cd46b163d0
11 changed files with 173 additions and 53 deletions

View File

@ -4,5 +4,6 @@ module.exports = {
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-class-properties',
],
}

View File

@ -1,15 +1,25 @@
import { Extension } from 'tiptap'
import { collab } from 'prosemirror-collab'
import { collab, sendableSteps } from 'prosemirror-collab'
import { debounce } from 'lodash-es'
export default class CollabExtension extends Extension {
get name() {
return 'collab'
}
init() {
this.editor.on('update', ({ state }) => {
this.getSendableSteps(state)
})
}
get defaultOptions() {
return {
version: 0,
clientID: Math.floor(Math.random() * 0xFFFFFFFF),
debounce: 250,
onSend: () => {},
}
}
@ -21,4 +31,13 @@ export default class CollabExtension extends Extension {
}),
]
}
getSendableSteps = debounce(state => {
const sendable = sendableSteps(state)
if (sendable) {
this.options.onSend(sendable)
}
}, this.options.debounce)
}

View File

@ -9,10 +9,9 @@
<script>
import io from 'socket.io-client'
import { debounce } from 'lodash-es'
import { Editor, EditorContent } from 'tiptap'
import { Step } from 'prosemirror-transform'
import { receiveTransaction, sendableSteps, getVersion } from 'prosemirror-collab'
import { receiveTransaction, getVersion } from 'prosemirror-collab'
import Collab from './Collab'
export default {
@ -29,7 +28,7 @@ export default {
},
methods: {
initEditor({ doc, version }) {
onInit({ doc, version }) {
this.loading = false
if (this.editor) {
@ -39,23 +38,20 @@ export default {
this.editor = new Editor({
content: doc,
extensions: [
new Collab({ version }),
new Collab({
version,
debounce: 250,
onSend: sendable => {
this.socket.emit('update', sendable)
},
}),
],
onUpdate: ({ state }) => {
this.getSendableSteps(state)
},
})
// console.log(this.editor.extensions.options.collab.version)
},
getSendableSteps: debounce(function (state) {
const sendable = sendableSteps(state)
if (sendable) {
this.socket.emit('update', sendable)
}
}, 250),
receiveData({ steps, version }) {
onUpdate({ steps, version }) {
const { state, view, schema } = this.editor
if (getVersion(state) > version) {
@ -72,8 +68,8 @@ export default {
mounted() {
this.socket = io('wss://tiptap-sockets.glitch.me')
.on('init', data => this.initEditor(data))
.on('update', data => this.receiveData(data))
.on('init', data => this.onInit(data))
.on('update', data => this.onUpdate(data))
},
beforeDestroy() {

View File

@ -26,6 +26,7 @@
"devDependencies": {
"@babel/core": "^7.4.4",
"@babel/node": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/polyfill": "^7.4.4",

View File

@ -12,12 +12,16 @@ import { keymap } from 'prosemirror-keymap'
import { baseKeymap } from 'prosemirror-commands'
import { inputRules, undoInputRule } from 'prosemirror-inputrules'
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'
export default class Editor {
export default class Editor extends Emitter {
constructor(options = {}) {
super()
this.defaultOptions = {
editorProps: {},
editable: true,
@ -41,6 +45,15 @@ export default class Editor {
onDrop: () => {},
}
this.events = [
'init',
'update',
'focus',
'blur',
'paste',
'drop',
]
this.init(options)
}
@ -69,7 +82,11 @@ export default class Editor {
}, 10)
}
this.options.onInit({
this.events.forEach(name => {
this.on(name, this.options[camelCase(`on ${name}`)])
})
this.emit('init', {
view: this.view,
state: this.state,
})
@ -105,7 +122,7 @@ export default class Editor {
return new ExtensionManager([
...this.builtInExtensions,
...this.options.extensions,
])
], this)
}
createPlugins() {
@ -217,20 +234,20 @@ export default class Editor {
createView() {
const view = new EditorView(this.element, {
state: this.state,
handlePaste: this.options.onPaste,
handleDrop: this.options.onDrop,
handlePaste: (...args) => { this.emit('paste', ...args) },
handleDrop: (...args) => { this.emit('drop', ...args) },
dispatchTransaction: this.dispatchTransaction.bind(this),
})
view.dom.style.whiteSpace = 'pre-wrap'
view.dom.addEventListener('focus', event => this.options.onFocus({
view.dom.addEventListener('focus', event => this.emit('focus', {
event,
state: this.state,
view: this.view,
}))
view.dom.addEventListener('blur', event => this.options.onBlur({
view.dom.addEventListener('blur', event => this.emit('blur', {
event,
state: this.state,
view: this.view,
@ -283,8 +300,6 @@ export default class Editor {
}
dispatchTransaction(transaction) {
const oldState = this.state
this.state = this.state.apply(transaction)
this.view.updateState(this.state)
this.setActiveNodesAndMarks()
@ -293,15 +308,14 @@ export default class Editor {
return
}
this.emitUpdate(transaction, oldState)
this.emitUpdate(transaction)
}
emitUpdate(transaction, oldState) {
this.options.onUpdate({
emitUpdate(transaction) {
this.emit('update', {
getHTML: this.getHTML.bind(this),
getJSON: this.getJSON.bind(this),
state: this.state,
oldState,
transaction,
})
}

View File

@ -0,0 +1,59 @@
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) {
for (const callback of callbacks) {
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 (!this._callbacks || (arguments.length === 0)) {
this._callbacks = {}
return this
}
// specific event
const callbacks = this._callbacks[event]
if (!callbacks) {
return this
}
// remove all handlers
if (arguments.length === 1) {
delete this._callbacks[event]
return this
}
// remove specific handler
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i]
if (callback === fn) {
callbacks.splice(i, 1)
break
}
}
return this
}
}

View File

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

View File

@ -2,7 +2,11 @@ import { keymap } from 'prosemirror-keymap'
export default class ExtensionManager {
constructor(extensions = []) {
constructor(extensions = [], editor) {
extensions.forEach(extension => {
extension.bindEditor(editor)
extension.init()
})
this.extensions = extensions
}

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 Emitter } from './Emitter'
export { default as Extension } from './Extension'
export { default as ExtensionManager } from './ExtensionManager'
export { default as Mark } from './Mark'

View File

@ -64,6 +64,18 @@
"@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.4"
"@babel/helper-create-class-features-plugin@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz#fc3d690af6554cc9efc607364a82d48f58736dba"
integrity sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA==
dependencies:
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-member-expression-to-functions" "^7.0.0"
"@babel/helper-optimise-call-expression" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-replace-supers" "^7.4.4"
"@babel/helper-split-export-declaration" "^7.4.4"
"@babel/helper-define-map@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a"
@ -238,6 +250,14 @@
"@babel/helper-remap-async-to-generator" "^7.1.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
"@babel/plugin-proposal-class-properties@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce"
integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.4.4"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-json-strings@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
@ -3032,11 +3052,6 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
charset@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd"
integrity sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==
chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d"
@ -4399,16 +4414,14 @@ ecc-jsbn@~0.1.1:
safer-buffer "^2.1.0"
ecstatic@^3.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-4.1.2.tgz#3afbe29849b32bc2a1f8a90f67e01dc048c7ad40"
integrity sha512-lnrAOpU2f7Ra8dm1pW0D1ucyUxQIEk8RjFrvROg1YqCV0ueVu9hzgiSEbSyROqXDDiHREdqC4w3AwOTb23P4UQ==
version "3.3.2"
resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48"
integrity sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==
dependencies:
charset "^1.0.1"
he "^1.1.1"
mime "^2.4.1"
mime "^1.6.0"
minimist "^1.1.0"
on-finished "^2.3.0"
url-join "^4.0.0"
url-join "^2.0.5"
ee-first@1.1.1:
version "1.1.1"
@ -7893,12 +7906,12 @@ mime@1.4.1:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
mime@^1.4.1:
mime@^1.4.1, mime@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.3.1, mime@^2.4.1:
mime@^2.3.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.2.tgz#ce5229a5e99ffc313abac806b482c10e7ba6ac78"
integrity sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==
@ -8489,7 +8502,7 @@ octokit-pagination-methods@^1.1.0:
resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
on-finished@^2.3.0, on-finished@~2.3.0:
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
@ -12008,10 +12021,10 @@ urix@^0.1.0:
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-join@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
integrity sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=
url-join@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=
url-parse-lax@^1.0.0:
version "1.0.0"