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: [ plugins: [
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-proposal-class-properties',
], ],
} }

View File

@ -1,15 +1,25 @@
import { Extension } from 'tiptap' 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 { export default class CollabExtension extends Extension {
get name() { get name() {
return 'collab' return 'collab'
} }
init() {
this.editor.on('update', ({ state }) => {
this.getSendableSteps(state)
})
}
get defaultOptions() { get defaultOptions() {
return { return {
version: 0, version: 0,
clientID: Math.floor(Math.random() * 0xFFFFFFFF), 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> <script>
import io from 'socket.io-client' import io from 'socket.io-client'
import { debounce } from 'lodash-es'
import { Editor, EditorContent } from 'tiptap' import { Editor, EditorContent } from 'tiptap'
import { Step } from 'prosemirror-transform' import { Step } from 'prosemirror-transform'
import { receiveTransaction, sendableSteps, getVersion } from 'prosemirror-collab' import { receiveTransaction, getVersion } from 'prosemirror-collab'
import Collab from './Collab' import Collab from './Collab'
export default { export default {
@ -29,7 +28,7 @@ export default {
}, },
methods: { methods: {
initEditor({ doc, version }) { onInit({ doc, version }) {
this.loading = false this.loading = false
if (this.editor) { if (this.editor) {
@ -39,23 +38,20 @@ export default {
this.editor = new Editor({ this.editor = new Editor({
content: doc, content: doc,
extensions: [ 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) { onUpdate({ steps, version }) {
const sendable = sendableSteps(state)
if (sendable) {
this.socket.emit('update', sendable)
}
}, 250),
receiveData({ steps, version }) {
const { state, view, schema } = this.editor const { state, view, schema } = this.editor
if (getVersion(state) > version) { if (getVersion(state) > version) {
@ -72,8 +68,8 @@ export default {
mounted() { mounted() {
this.socket = io('wss://tiptap-sockets.glitch.me') this.socket = io('wss://tiptap-sockets.glitch.me')
.on('init', data => this.initEditor(data)) .on('init', data => this.onInit(data))
.on('update', data => this.receiveData(data)) .on('update', data => this.onUpdate(data))
}, },
beforeDestroy() { beforeDestroy() {

View File

@ -26,6 +26,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.4", "@babel/core": "^7.4.4",
"@babel/node": "^7.2.2", "@babel/node": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.4.4", "@babel/plugin-transform-runtime": "^7.4.4",
"@babel/polyfill": "^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 { 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)
} }
@ -69,7 +82,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,
}) })
@ -105,7 +122,7 @@ export default class Editor {
return new ExtensionManager([ return new ExtensionManager([
...this.builtInExtensions, ...this.builtInExtensions,
...this.options.extensions, ...this.options.extensions,
]) ], this)
} }
createPlugins() { createPlugins() {
@ -217,20 +234,20 @@ export default class Editor {
createView() { createView() {
const view = new EditorView(this.element, { const view = new EditorView(this.element, {
state: this.state, state: this.state,
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 +300,6 @@ export default class Editor {
} }
dispatchTransaction(transaction) { dispatchTransaction(transaction) {
const oldState = this.state
this.state = this.state.apply(transaction) this.state = this.state.apply(transaction)
this.view.updateState(this.state) this.view.updateState(this.state)
this.setActiveNodesAndMarks() this.setActiveNodesAndMarks()
@ -293,15 +308,14 @@ export default class Editor {
return return
} }
this.emitUpdate(transaction, oldState) this.emitUpdate(transaction)
} }
emitUpdate(transaction, oldState) { 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,
oldState,
transaction, 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() { 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
} }

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'

View File

@ -64,6 +64,18 @@
"@babel/traverse" "^7.4.4" "@babel/traverse" "^7.4.4"
"@babel/types" "^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": "@babel/helper-define-map@^7.4.4":
version "7.4.4" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" 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/helper-remap-async-to-generator" "^7.1.0"
"@babel/plugin-syntax-async-generators" "^7.2.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": "@babel/plugin-proposal-json-strings@^7.2.0":
version "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" 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" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== 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: chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4:
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.5.tgz#0ae8434d962281a5f56c72869e79cb6d9d86ad4d" 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" safer-buffer "^2.1.0"
ecstatic@^3.0.0: ecstatic@^3.0.0:
version "4.1.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-4.1.2.tgz#3afbe29849b32bc2a1f8a90f67e01dc048c7ad40" resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48"
integrity sha512-lnrAOpU2f7Ra8dm1pW0D1ucyUxQIEk8RjFrvROg1YqCV0ueVu9hzgiSEbSyROqXDDiHREdqC4w3AwOTb23P4UQ== integrity sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==
dependencies: dependencies:
charset "^1.0.1"
he "^1.1.1" he "^1.1.1"
mime "^2.4.1" mime "^1.6.0"
minimist "^1.1.0" minimist "^1.1.0"
on-finished "^2.3.0" url-join "^2.0.5"
url-join "^4.0.0"
ee-first@1.1.1: ee-first@1.1.1:
version "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" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
mime@^1.4.1: mime@^1.4.1, mime@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.3.1, mime@^2.4.1: mime@^2.3.1:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.2.tgz#ce5229a5e99ffc313abac806b482c10e7ba6ac78" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.2.tgz#ce5229a5e99ffc313abac806b482c10e7ba6ac78"
integrity sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg== 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" resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
on-finished@^2.3.0, on-finished@~2.3.0: on-finished@~2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
@ -12008,10 +12021,10 @@ urix@^0.1.0:
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-join@^4.0.0: url-join@^2.0.5:
version "4.0.0" version "2.0.5"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a" resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
integrity sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo= integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=
url-parse-lax@^1.0.0: url-parse-lax@^1.0.0:
version "1.0.0" version "1.0.0"