implemented codemirror binding with cursor support
This commit is contained in:
parent
c0ba56a21f
commit
7f4ae9fe14
@ -13,7 +13,7 @@ Yjs is a CRDT implementatation that exposes its internal structure as actual dat
|
||||
|---|:-:|---|
|
||||
| [ProseMirror](https://prosemirror.net/) | ✔ | [link](https://yjs.website/tutorial-prosemirror.html) |
|
||||
| [Quill](https://quilljs.com/) | | [link](https://yjs.website/tutorial-quill.html) |
|
||||
| [CodeMirror](https://codemirror.net/) | | [link]() |
|
||||
| [CodeMirror](https://codemirror.net/) | ✔ | [link](https://yjs.website/tutorial-codemirror.html) |
|
||||
| [Ace](https://ace.c9.io/) | | [link]() |
|
||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | | [link]() |
|
||||
|
||||
@ -177,11 +177,11 @@ sharedDocument.on('status', event => {
|
||||
|
||||
#### Scaling
|
||||
|
||||
In this model, there is a central server that handles the content. You need to make sure that all connections to the same document are handled by the same websocket server.
|
||||
These are mere suggestions how you could scale your server environment.
|
||||
|
||||
I recommend to implement a custom websocket proxy that routes server requests to the correct websocket server.
|
||||
**Option 1:** Websocket servers communicate with each other via a PubSub server. A room is represented by a PubSub channel. The downside of this approach is that the same shared document may be handled by many servers. But the upside is that this approach is fault tolerant, does not have a single point of failure, and is perfectly fit for route balancing.
|
||||
|
||||
One way to find "the correct websocket server" is to implement a consistent hashing algorithm that maps each document to a unique server. When the hashing function changes, the websocket connections must be re-routed.
|
||||
**Option 2:** Sharding with *consistent hashing*. Each document is handled by a unique server. This patterns requires an entity, like etcd, that performs regular health checks and manages servers. Based on the list of available servers (which is managed by etcd) a proxy calculates which server is responsible for each requested document. The disadvantage of this approach is that it is that load distribution may not be fair. Still, this approach may be the preferred solution if you want to store the shared document in a database - e.g. for indexing.
|
||||
|
||||
### Ydb Provider
|
||||
|
||||
|
180
bindings/codemirror.js
Normal file
180
bindings/codemirror.js
Normal file
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @module bindings/textarea
|
||||
*/
|
||||
|
||||
import { createMutex } from '../lib/mutex.js'
|
||||
import * as math from '../lib/math.js'
|
||||
import * as ypos from '../utils/relativePosition.js'
|
||||
|
||||
const typeObserver = (binding, event) => {
|
||||
binding._mux(() => {
|
||||
const cm = binding.target
|
||||
cm.operation(() => {
|
||||
const delta = event.delta
|
||||
let index = 0
|
||||
for (let i = 0; i < event.delta.length; i++) {
|
||||
const d = delta[i]
|
||||
if (d.retain) {
|
||||
index += d.retain
|
||||
} else if (d.insert) {
|
||||
const pos = cm.posFromIndex(index)
|
||||
cm.replaceRange(d.insert, pos, pos, 'prosemirror-binding')
|
||||
index += d.insert.length
|
||||
} else if (d.delete) {
|
||||
const start = cm.posFromIndex(index)
|
||||
const end = cm.posFromIndex(index + d.delete)
|
||||
cm.replaceRange('', start, end, 'prosemirror-binding')
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const targetObserver = (binding, change) => {
|
||||
binding._mux(() => {
|
||||
const start = binding.target.indexFromPos(change.from)
|
||||
const delLen = change.removed.map(s => s.length).reduce(math.add) + change.removed.length - 1
|
||||
if (delLen > 0) {
|
||||
binding.type.delete(start, delLen)
|
||||
}
|
||||
if (change.text.length > 0) {
|
||||
binding.type.insert(start, change.text.join('\n'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createRemoteCaret = (username, color) => {
|
||||
const caret = document.createElement('span')
|
||||
caret.classList.add('remote-caret')
|
||||
caret.setAttribute('style', `border-color: ${color}`)
|
||||
const userDiv = document.createElement('div')
|
||||
userDiv.setAttribute('style', `background-color: ${color}`)
|
||||
userDiv.insertBefore(document.createTextNode(username), null)
|
||||
caret.insertBefore(userDiv, null)
|
||||
return caret
|
||||
}
|
||||
|
||||
const updateRemoteSelection = (y, cm, type, cursors, clientId) => {
|
||||
// destroy current text mark
|
||||
const m = cursors.get(clientId)
|
||||
if (m !== undefined) {
|
||||
m.caret.clear()
|
||||
if (m.sel !== null) {
|
||||
m.sel.clear()
|
||||
}
|
||||
cursors.delete(clientId)
|
||||
}
|
||||
// redraw caret and selection for clientId
|
||||
const aw = y.awareness.get(clientId)
|
||||
if (aw === undefined) {
|
||||
return
|
||||
}
|
||||
const user = aw.user || {}
|
||||
if (user.color == null) {
|
||||
user.color = '#ffa500'
|
||||
}
|
||||
if (user.name == null) {
|
||||
user.name = `User: ${clientId}`
|
||||
}
|
||||
const cursor = aw.cursor
|
||||
if (cursor == null || cursor.anchor == null || cursor.head == null) {
|
||||
return
|
||||
}
|
||||
const anchor = ypos.fromRelativePosition(y, cursor.anchor || null)
|
||||
const head = ypos.fromRelativePosition(y, cursor.head || null)
|
||||
if (anchor !== null && head !== null && anchor.type === type && head.type === type) {
|
||||
const headpos = cm.posFromIndex(head.offset)
|
||||
const anchorpos = cm.posFromIndex(anchor.offset)
|
||||
let from, to
|
||||
if (head.offset < anchor.offset) {
|
||||
from = headpos
|
||||
to = anchorpos
|
||||
} else {
|
||||
from = anchorpos
|
||||
to = headpos
|
||||
}
|
||||
const caretEl = createRemoteCaret(user.name, user.color)
|
||||
const caret = cm.setBookmark(headpos, { widget: caretEl, insertLeft: true })
|
||||
let sel = null
|
||||
if (head.offset !== anchor.offset) {
|
||||
sel = cm.markText(from, to, { css: `background-color: ${user.color}70`, inclusiveRight: true, inclusiveLeft: false })
|
||||
}
|
||||
cursors.set(clientId, { caret, sel })
|
||||
}
|
||||
}
|
||||
|
||||
const prosemirrorCursorActivity = (y, cm, type) => {
|
||||
if (!cm.hasFocus()) {
|
||||
return
|
||||
}
|
||||
const aw = y.getLocalAwarenessInfo()
|
||||
const anchor = ypos.getRelativePosition(type, cm.indexFromPos(cm.getCursor('anchor')))
|
||||
const head = ypos.getRelativePosition(type, cm.indexFromPos(cm.getCursor('head')))
|
||||
if (aw.cursor == null || !ypos.equal(aw.cursor.anchor, anchor) || !ypos.equal(aw.cursor.head, head)) {
|
||||
y.setAwarenessField('cursor', {
|
||||
anchor, head
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A binding that binds a YText to a CodeMirror editor.
|
||||
*
|
||||
* @example
|
||||
* const ytext = ydocument.define('codemirror', Y.Text)
|
||||
* const editor = new CodeMirror(document.querySelector('#container'), {
|
||||
* mode: 'javascript',
|
||||
* lineNumbers: true
|
||||
* })
|
||||
* const binding = new CodeMirrorBinding(editor)
|
||||
*
|
||||
*/
|
||||
export class CodeMirrorBinding {
|
||||
/**
|
||||
* @param {YText} textType
|
||||
* @param {CodeMirror} codeMirror
|
||||
* @param {Object} [options={cursors: true}]
|
||||
*/
|
||||
constructor (textType, codeMirror, { cursors = true } = {}) {
|
||||
const y = textType._y
|
||||
this.type = textType
|
||||
this.target = codeMirror
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this._mux = createMutex()
|
||||
// set initial value
|
||||
codeMirror.setValue(textType.toString())
|
||||
// observe type and target
|
||||
this._typeObserver = event => typeObserver(this, event)
|
||||
this._targetObserver = (_, change) => targetObserver(this, change)
|
||||
this._cursors = new Map()
|
||||
this._awarenessListener = event => {
|
||||
const f = clientId => updateRemoteSelection(y, codeMirror, textType, this._cursors, clientId)
|
||||
event.added.forEach(f)
|
||||
event.removed.forEach(f)
|
||||
event.updated.forEach(f)
|
||||
}
|
||||
this._cursorListener = () => prosemirrorCursorActivity(y, codeMirror, textType)
|
||||
this._blurListeer = () =>
|
||||
y.setAwarenessField('cursor', null)
|
||||
textType.observe(this._typeObserver)
|
||||
codeMirror.on('change', this._targetObserver)
|
||||
if (cursors) {
|
||||
y.on('awareness', this._awarenessListener)
|
||||
codeMirror.on('cursorActivity', this._cursorListener)
|
||||
codeMirror.on('blur', this._blurListeer)
|
||||
codeMirror.on('focus', this._cursorListener)
|
||||
}
|
||||
}
|
||||
destroy () {
|
||||
this.type.unobserve(this._typeObserver)
|
||||
this.target.off('change', this._targetObserver)
|
||||
this.type.off('awareness', this._awarenessListener)
|
||||
this.target.off('cursorActivity', this._cursorListener)
|
||||
this.target.off('focus', this._cursorListener)
|
||||
this.target.off('blur', this._blurListeer)
|
||||
this.type = null
|
||||
this.target = null
|
||||
}
|
||||
}
|
@ -96,7 +96,7 @@ export const cursorPlugin = new Plugin({
|
||||
if (aw.cursor != null) {
|
||||
let user = aw.user || {}
|
||||
if (user.color == null) {
|
||||
user.color = '#ffa50070'
|
||||
user.color = '#ffa500'
|
||||
}
|
||||
if (user.name == null) {
|
||||
user.name = `User: ${userID}`
|
||||
@ -119,7 +119,7 @@ export const cursorPlugin = new Plugin({
|
||||
}, { key: userID + '' }))
|
||||
const from = math.min(anchor, head)
|
||||
const to = math.max(anchor, head)
|
||||
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}` }))
|
||||
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}70` }))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
70
examples/codemirror.html
Normal file
70
examples/codemirror.html
Normal file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Yjs CodeMirror Example</title>
|
||||
<link rel=stylesheet href="https://codemirror.net/lib/codemirror.css">
|
||||
<style>
|
||||
#container {
|
||||
border: grey;
|
||||
border-style: solid;
|
||||
border-width: thin;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>This example shows how to bind a YText type to <a href="https://codemirror.net/">CodeMirror</a> editor.</p>
|
||||
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||
<div class="code-html">
|
||||
<style>
|
||||
.remote-caret {
|
||||
position: absolute;
|
||||
border-left: black;
|
||||
border-left-style: solid;
|
||||
border-left-width: 2px;
|
||||
height: 1em;
|
||||
}
|
||||
.remote-caret > div {
|
||||
position: relative;
|
||||
top: -1.05em;
|
||||
font-size: 13px;
|
||||
background-color: rgb(250, 129, 0);
|
||||
font-family: serif;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
user-select: none;
|
||||
color: white;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
</style>
|
||||
<div id="container"></div>
|
||||
</div>
|
||||
<!-- The actual source file for the following code is found in ./codemirror.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/codemirror.js">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { CodeMirrorBinding } from 'yjs/bindings/codemirror.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror/mode/javascript/javascript.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('codemirror')
|
||||
const ytext = ydocument.define('codemirror', Y.Text)
|
||||
|
||||
const editor = new CodeMirror(document.querySelector('#container'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
|
||||
const binding = new CodeMirrorBinding(ytext, editor)
|
||||
|
||||
window.codemirrorExample = {
|
||||
binding, editor, ytext, ydocument
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
23
examples/codemirror.js
Normal file
23
examples/codemirror.js
Normal file
@ -0,0 +1,23 @@
|
||||
import * as Y from '../index.js'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { CodeMirrorBinding } from '../bindings/codemirror.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror/mode/javascript/javascript.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('codemirror')
|
||||
const ytext = ydocument.define('codemirror', Y.Text)
|
||||
|
||||
const editor = new CodeMirror(document.querySelector('#container'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
|
||||
const binding = new CodeMirrorBinding(ytext, editor)
|
||||
|
||||
window.codemirrorExample = {
|
||||
binding, editor, ytext, ydocument
|
||||
}
|
@ -22,8 +22,9 @@
|
||||
import * as Y from 'yjs/index.js'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { DomBinding } from 'yjs/bindings/dom.js'
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('dom')
|
||||
const type = ydocument.define('xml', Y.XmlFragment)
|
||||
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as Y from '../index.js'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { DomBinding } from '../bindings/dom.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
|
@ -1,6 +1,9 @@
|
||||
{
|
||||
"codemirror": {
|
||||
"title": "CodeMirror Binding"
|
||||
},
|
||||
"prosemirror": {
|
||||
"title": "Prosemirror Binding"
|
||||
"title": "ProseMirror Binding"
|
||||
},
|
||||
"textarea": {
|
||||
"title": "Textarea Binding"
|
||||
|
@ -58,8 +58,10 @@
|
||||
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
|
||||
<script class="code-js" src="./build/prosemirror.js">
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from '../provider/websocket.js'
|
||||
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { prosemirrorPlugin, cursorPlugin } from 'yjs/bindings/prosemirror'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
@ -67,7 +69,7 @@ import { DOMParser } from 'prosemirror-model'
|
||||
import { schema } from 'prosemirror-schema-basic'
|
||||
import { exampleSetup } from 'prosemirror-example-setup'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('prosemirror')
|
||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||
|
||||
|
@ -19,9 +19,11 @@ import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { QuillBinding } from 'yjs/bindings/quill.js'
|
||||
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
import Quill from 'quill'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('quill')
|
||||
const ytext = ydocument.define('quill', Y.Text)
|
||||
|
||||
|
@ -16,7 +16,9 @@ import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||
import { TextareaBinding } from 'yjs/bindings/textarea.js'
|
||||
|
||||
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||
import * as conf from './exampleConfig.js'
|
||||
|
||||
const provider = new WebsocketProvider(conf.serverAddress)
|
||||
const ydocument = provider.get('textarea')
|
||||
const type = ydocument.define('textarea', Y.Text)
|
||||
const textarea = document.querySelector('textarea')
|
||||
|
@ -162,7 +162,6 @@ export const peekVarUint = decoder => {
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read string of variable length
|
||||
* * varUint is used to store the length of the string
|
||||
@ -204,4 +203,3 @@ export const peekVarString = decoder => {
|
||||
decoder.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -336,7 +336,8 @@
|
||||
"async-limiter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
|
||||
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
|
||||
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
|
||||
"optional": true
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
@ -1826,6 +1827,11 @@
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
|
||||
"dev": true
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.42.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.42.0.tgz",
|
||||
"integrity": "sha512-pbApC8zDzItP3HRphD6kQVwS976qB5Qi0hU3MZMixLk+AyugOW1RF+8XJEjeyl5yWsHNe88tDUxzeRh5AOxPRw=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
|
||||
@ -7395,6 +7401,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz",
|
||||
"integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
|
@ -100,5 +100,8 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ws": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.42.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
import * as idb from '../lib/idb.js'
|
||||
|
||||
const bc = new BroadcastChannel('ydb-client')
|
||||
|
||||
idb.openDB()
|
@ -1,7 +1,7 @@
|
||||
|
||||
import * as encoding from '../lib/encoding.js'
|
||||
import * as decoding from '../lib/decoding.js'
|
||||
import { Y } from '../utils/Y.js';
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
|
||||
export const messagePermissionDenied = 0
|
||||
|
||||
@ -21,10 +21,10 @@ export const writePermissionDenied = (encoder, reason) => {
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {PermissionDeniedHandler} permissionDeniedHandler
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Y} y
|
||||
* @param {PermissionDeniedHandler} permissionDeniedHandler
|
||||
*/
|
||||
export const readAuthMessage = (decoder, y, permissionDeniedHandler) => {
|
||||
switch (decoding.readVarUint(decoder)) {
|
||||
|
@ -9,7 +9,6 @@ import { getStruct } from '../utils/structReferences.js'
|
||||
import { deleteItemRange } from '../utils/structManipulation.js'
|
||||
import { integrateRemoteStruct } from '../utils/integrateRemoteStructs.js'
|
||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||
import { Item } from '../structs/Item.js'
|
||||
import * as stringify from '../utils/structStringify.js'
|
||||
|
||||
/**
|
||||
|
@ -144,6 +144,16 @@ class WebsocketsSharedDocument extends Y.Y {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket Provider for Yjs. Creates a single websocket connection to each document.
|
||||
* The document name is attached to the provided url. I.e. the following example
|
||||
* creates a websocket connection to http://localhost:1234/my-document-name
|
||||
*
|
||||
* @example
|
||||
* import { WebsocketProvider } from 'yjs/provider/websocket/client.js'
|
||||
* const provider = new WebsocketProvider('http://localhost:1234')
|
||||
* const ydocument = provider.get('my-document-name')
|
||||
*/
|
||||
export class WebsocketProvider {
|
||||
constructor (url) {
|
||||
// ensure that url is always ends with /
|
||||
|
@ -29,6 +29,21 @@ export default [{
|
||||
}),
|
||||
commonjs()
|
||||
]
|
||||
}, {
|
||||
input: './examples/codemirror.js',
|
||||
output: {
|
||||
name: 'codemirror',
|
||||
file: 'examples/build/codemirror.js',
|
||||
format: 'iife',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
sourcemap: true,
|
||||
module: true
|
||||
}),
|
||||
commonjs()
|
||||
].concat(minificationPlugins)
|
||||
}, {
|
||||
input: './examples/prosemirror.js',
|
||||
output: {
|
||||
|
@ -2,4 +2,4 @@
|
||||
export const writeStructToTransaction = (transaction, struct) => {
|
||||
transaction.encodedStructsLen++
|
||||
struct._toBinary(transaction.encodedStructs)
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ export const stringifyItemID = item => {
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper utility to convert an item to a readable format.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user