Compare commits
30 Commits
v13.0.0-71
...
v13.0.0-76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4063e28b5e | ||
|
|
b6f7cd7869 | ||
|
|
1a79e429ed | ||
|
|
04066a5678 | ||
|
|
e09ef15349 | ||
|
|
3d70eee959 | ||
|
|
582095e5a3 | ||
|
|
c9ea3a412e | ||
|
|
a2c51c36e9 | ||
|
|
ab3dba5b06 | ||
|
|
3ddff186c2 | ||
|
|
9bd199a6e7 | ||
|
|
01d0825ae6 | ||
|
|
e2f98525d2 | ||
|
|
70a0a03130 | ||
|
|
656d85c62e | ||
|
|
e168dd48fb | ||
|
|
12d43199d5 | ||
|
|
539fa8b21d | ||
|
|
f572f94586 | ||
|
|
c12d00b227 | ||
|
|
e4a5f2caec | ||
|
|
9f9f465238 | ||
|
|
8450ff86d7 | ||
|
|
70139262c5 | ||
|
|
9c0da271eb | ||
|
|
ade3e1949d | ||
|
|
eec63a008f | ||
|
|
52abcdd043 | ||
|
|
f94653424a |
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"source": "./src",
|
"source": ".",
|
||||||
"destination": "./docs",
|
"destination": "./docs",
|
||||||
|
"excludes": ["build", "node_modules", "tests-lib", "test"],
|
||||||
"plugins": [{
|
"plugins": [{
|
||||||
"name": "esdoc-standard-plugin",
|
"name": "esdoc-standard-plugin",
|
||||||
"option": {
|
"option": {
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,7 +2,7 @@ node_modules
|
|||||||
bower_components
|
bower_components
|
||||||
docs
|
docs
|
||||||
/y.*
|
/y.*
|
||||||
/examples/*/index.dist.*
|
/examples_all/*/index.dist.*
|
||||||
.vscode
|
.vscode
|
||||||
.yjsPersisted
|
.yjsPersisted
|
||||||
build
|
build
|
||||||
|
|||||||
50
.jsdoc.json
Normal file
50
.jsdoc.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"sourceType": "module",
|
||||||
|
"tags": {
|
||||||
|
"allowUnknownTags": true,
|
||||||
|
"dictionaries": ["jsdoc"]
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"include": ["./types", "./utils/UndoManager.js", "./utils/Y.js", "./provider", "./bindings"],
|
||||||
|
"includePattern": ".js$"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"plugins/markdown"
|
||||||
|
],
|
||||||
|
"templates": {
|
||||||
|
"referenceTitle": "Yjs",
|
||||||
|
"disableSort": false,
|
||||||
|
"useCollapsibles": true,
|
||||||
|
"collapse": true,
|
||||||
|
"resources": {
|
||||||
|
"y-js.org": "yjs.website"
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"url": "https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png",
|
||||||
|
"width": "162px",
|
||||||
|
"height": "162px",
|
||||||
|
"link": "/"
|
||||||
|
},
|
||||||
|
"tabNames": {
|
||||||
|
"api": "API",
|
||||||
|
"tutorials": "Examples"
|
||||||
|
},
|
||||||
|
"footerText": "Shared Editing",
|
||||||
|
"css": [
|
||||||
|
"./style.css"
|
||||||
|
],
|
||||||
|
"default": {
|
||||||
|
"staticFiles": {
|
||||||
|
"include": ["examples/"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opts": {
|
||||||
|
"destination": "./docs/",
|
||||||
|
"encoding": "utf8",
|
||||||
|
"private": false,
|
||||||
|
"recurse": true,
|
||||||
|
"template": "./node_modules/tui-jsdoc-template",
|
||||||
|
"tutorials": "./examples"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
# 
|
# 
|
||||||
|
|
||||||
Yjs is a framework for offline-first p2p shared editing on structured data like
|
Yjs is a framework for offline-first p2p shared editing on structured data like
|
||||||
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
||||||
@@ -66,7 +66,7 @@ missing modules.
|
|||||||
|
|
||||||
### CDN
|
### CDN
|
||||||
```
|
```
|
||||||
<script src="https://cdn.jsdelivr.net/npm/yjs@12/src/y.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
||||||
|
|||||||
21
README.v13.md
Normal file
21
README.v13.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 
|
||||||
|
> A CRDT library with a powerful abstraction of shared data
|
||||||
|
|
||||||
|
Yjs v13 is a work in progress.
|
||||||
|
|
||||||
|
### Typescript Declarations
|
||||||
|
|
||||||
|
Until [this](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, the only way to get type declarations is by adding Yjs to the list of checked files:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./node_modules/yjs/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
import { createMutex } from '../../lib/mutex.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class for bindings.
|
|
||||||
*
|
|
||||||
* A binding handles data binding from a Yjs type to a data object. For example,
|
|
||||||
* you can bind a Quill editor instance to a YText instance with the `QuillBinding` class.
|
|
||||||
*
|
|
||||||
* It is expected that a concrete implementation accepts two parameters
|
|
||||||
* (type and binding target).
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const quill = new Quill(document.createElement('div'))
|
|
||||||
* const type = y.define('quill', Y.Text)
|
|
||||||
* const binding = new Y.QuillBinding(quill, type)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export default class Binding {
|
|
||||||
/**
|
|
||||||
* @param {YType} type Yjs type.
|
|
||||||
* @param {any} target Binding Target.
|
|
||||||
*/
|
|
||||||
constructor (type, target) {
|
|
||||||
/**
|
|
||||||
* The Yjs type that is bound to `target`
|
|
||||||
* @type {YType}
|
|
||||||
*/
|
|
||||||
this.type = type
|
|
||||||
/**
|
|
||||||
* The target that `type` is bound to.
|
|
||||||
* @type {*}
|
|
||||||
*/
|
|
||||||
this.target = target
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
this._mutualExclude = createMutex()
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Remove all data observers (both from the type and the target).
|
|
||||||
*/
|
|
||||||
destroy () {
|
|
||||||
this.type = null
|
|
||||||
this.target = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import BindMapping from '../BindMapping.js'
|
|
||||||
import * as PModel from 'prosemirror-model'
|
|
||||||
import * as Y from '../../src/index.js'
|
|
||||||
import { createMutex } from '../../lib/mutex.js'
|
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
|
||||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('prosemirror-view').EditorView} EditorView
|
|
||||||
* @typedef {import('prosemirror-state').EditorState} EditorState
|
|
||||||
* @typedef {BindMapping<Y.Text | Y.XmlElement, PModel.Node>} ProsemirrorMapping
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const prosemirrorPluginKey = new PluginKey('yjs')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
|
||||||
*
|
|
||||||
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
|
||||||
* @param {Y.XmlFragment} yXmlFragment
|
|
||||||
*/
|
|
||||||
export const prosemirrorPlugin = yXmlFragment => {
|
|
||||||
const pluginState = {
|
|
||||||
type: yXmlFragment,
|
|
||||||
y: yXmlFragment._y,
|
|
||||||
binding: null
|
|
||||||
}
|
|
||||||
const plugin = new Plugin({
|
|
||||||
key: prosemirrorPluginKey,
|
|
||||||
state: {
|
|
||||||
init: (initargs, state) => {
|
|
||||||
return pluginState
|
|
||||||
},
|
|
||||||
apply: (tr, pluginState) => {
|
|
||||||
return pluginState
|
|
||||||
}
|
|
||||||
},
|
|
||||||
view: view => {
|
|
||||||
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
|
||||||
pluginState.binding = binding
|
|
||||||
return {
|
|
||||||
update: () => {
|
|
||||||
binding._prosemirrorChanged()
|
|
||||||
},
|
|
||||||
destroy: () => {
|
|
||||||
binding.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
|
||||||
|
|
||||||
export const cursorPlugin = new Plugin({
|
|
||||||
key: cursorPluginKey,
|
|
||||||
props: {
|
|
||||||
decorations: state => {
|
|
||||||
const y = prosemirrorPluginKey.getState(state).y
|
|
||||||
const awareness = y.getAwarenessInfo()
|
|
||||||
const decorations = []
|
|
||||||
awareness.forEach((state, userID) => {
|
|
||||||
if (state.cursor != null) {
|
|
||||||
const username = `User: ${userID}`
|
|
||||||
decorations.push(Decoration.widget(state.cursor.from, () => {
|
|
||||||
const cursor = document.createElement('span')
|
|
||||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
|
||||||
const user = document.createElement('div')
|
|
||||||
user.insertBefore(document.createTextNode(username), null)
|
|
||||||
cursor.insertBefore(user, null)
|
|
||||||
return cursor
|
|
||||||
}, { key: username }))
|
|
||||||
decorations.push(Decoration.inline(state.cursor.from, state.cursor.to, { style: 'background-color: #ffa50070' }))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return DecorationSet.create(state.doc, decorations)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
view: view => {
|
|
||||||
const y = prosemirrorPluginKey.getState(view.state).y
|
|
||||||
const awarenessListener = () => {
|
|
||||||
view.updateState(view.state)
|
|
||||||
}
|
|
||||||
y.on('awareness', awarenessListener)
|
|
||||||
return {
|
|
||||||
update: () => {
|
|
||||||
const y = prosemirrorPluginKey.getState(view.state).y
|
|
||||||
const from = view.state.selection.from
|
|
||||||
const to = view.state.selection.to
|
|
||||||
const current = y.getLocalAwarenessInfo()
|
|
||||||
if (current.cursor == null || current.cursor.to !== to || current.cursor.from !== from) {
|
|
||||||
y.setAwarenessField('cursor', {
|
|
||||||
from, to
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
destroy: () => {
|
|
||||||
const y = prosemirrorPluginKey.getState(view.state).y
|
|
||||||
y.setAwarenessField('cursor', null)
|
|
||||||
y.off('awareness', awarenessListener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default class ProsemirrorBinding {
|
|
||||||
/**
|
|
||||||
* @param {Y.XmlFragment} yXmlFragment The bind source
|
|
||||||
* @param {EditorView} prosemirrorView The target binding
|
|
||||||
*/
|
|
||||||
constructor (yXmlFragment, prosemirrorView) {
|
|
||||||
this.type = yXmlFragment
|
|
||||||
this.prosemirrorView = prosemirrorView
|
|
||||||
this.mux = createMutex()
|
|
||||||
/**
|
|
||||||
* @type {ProsemirrorMapping}
|
|
||||||
*/
|
|
||||||
this.mapping = new BindMapping()
|
|
||||||
this._observeFunction = this._typeChanged.bind(this)
|
|
||||||
yXmlFragment.observeDeep(this._observeFunction)
|
|
||||||
}
|
|
||||||
_typeChanged (events) {
|
|
||||||
if (events.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.mux(() => {
|
|
||||||
events.forEach(event => {
|
|
||||||
// recompute node for each parent
|
|
||||||
// except main node, compute main node in the end
|
|
||||||
let target = event.target
|
|
||||||
if (target !== this.type) {
|
|
||||||
do {
|
|
||||||
if (target.constructor === Y.XmlElement) {
|
|
||||||
createNodeFromYElement(target, this.prosemirrorView.state.schema, this.mapping)
|
|
||||||
}
|
|
||||||
target = target._parent
|
|
||||||
} while (target._parent !== this.type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping))
|
|
||||||
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
|
|
||||||
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_prosemirrorChanged () {
|
|
||||||
this.mux(() => {
|
|
||||||
updateYFragment(this.type, this.prosemirrorView.state, this.mapping)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this.type.unobserveDeep(this._observeFunction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Y.XmlElement} el
|
|
||||||
* @param {PModel.Schema} schema
|
|
||||||
* @param {ProsemirrorMapping} mapping
|
|
||||||
* @return {PModel.Node}
|
|
||||||
*/
|
|
||||||
export const createNodeIfNotExists = (el, schema, mapping) => {
|
|
||||||
const node = mapping.getY(el)
|
|
||||||
if (node === undefined) {
|
|
||||||
return createNodeFromYElement(el, schema, mapping)
|
|
||||||
}
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Y.XmlElement} el
|
|
||||||
* @param {PModel.Schema} schema
|
|
||||||
* @param {ProsemirrorMapping} mapping
|
|
||||||
* @return {PModel.Node}
|
|
||||||
*/
|
|
||||||
export const createNodeFromYElement = (el, schema, mapping) => {
|
|
||||||
const children = []
|
|
||||||
el.toArray().forEach(type => {
|
|
||||||
if (type.constructor === Y.XmlElement) {
|
|
||||||
children.push(createNodeIfNotExists(type, schema, mapping))
|
|
||||||
} else {
|
|
||||||
children.concat(createTextNodesFromYText(type, schema, mapping)).forEach(textchild => children.push(textchild))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), el.toArray().map(t => createNodeIfNotExists(t, schema, mapping)))
|
|
||||||
mapping.bind(el, node)
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Y.Text} text
|
|
||||||
* @param {PModel.Schema} schema
|
|
||||||
* @param {ProsemirrorMapping} mapping
|
|
||||||
* @return {Array<PModel.Node>}
|
|
||||||
*/
|
|
||||||
export const createTextNodesFromYText = (text, schema, mapping) => {
|
|
||||||
const nodes = []
|
|
||||||
const deltas = text.toDelta()
|
|
||||||
for (let i = 0; i < deltas.length; i++) {
|
|
||||||
const delta = deltas[i]
|
|
||||||
const marks = []
|
|
||||||
for (let markName in delta.attributes) {
|
|
||||||
marks.push(schema.mark(markName, delta.attributes[markName]))
|
|
||||||
}
|
|
||||||
nodes.push(schema.text(delta.insert, marks))
|
|
||||||
}
|
|
||||||
if (nodes.length > 0) {
|
|
||||||
mapping.bind(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {PModel.Node} node
|
|
||||||
* @param {ProsemirrorMapping} mapping
|
|
||||||
* @return {Y.XmlElement | Y.Text}
|
|
||||||
*/
|
|
||||||
export const createTypeFromNode = (node, mapping) => {
|
|
||||||
let type
|
|
||||||
if (node.isText) {
|
|
||||||
type = new Y.Text()
|
|
||||||
const attrs = {}
|
|
||||||
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
|
||||||
type.insert(0, node.text, attrs)
|
|
||||||
} else {
|
|
||||||
type = new Y.XmlElement(node.type.name)
|
|
||||||
for (let key in node.attrs) {
|
|
||||||
type.setAttribute(key, node.attrs[key])
|
|
||||||
}
|
|
||||||
type.insert(0, node.content.content.map(node => createTypeFromNode(node, mapping)))
|
|
||||||
}
|
|
||||||
mapping.bind(type, node)
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Y.XmlFragment} yDomFragment
|
|
||||||
* @param {EditorState} state
|
|
||||||
* @param {BindMapping} mapping
|
|
||||||
*/
|
|
||||||
const updateYFragment = (yDomFragment, state, mapping) => {
|
|
||||||
const pChildCnt = state.doc.content.childCount
|
|
||||||
const yChildren = yDomFragment.toArray()
|
|
||||||
const yChildCnt = yChildren.length
|
|
||||||
const minCnt = pChildCnt < yChildCnt ? pChildCnt : yChildCnt
|
|
||||||
let left = 0
|
|
||||||
let right = 0
|
|
||||||
// find number of matching elements from left
|
|
||||||
for (;left < minCnt; left++) {
|
|
||||||
if (state.doc.content.child(left) !== mapping.getY(yChildren[left])) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// find number of matching elements from right
|
|
||||||
for (;right < minCnt; right++) {
|
|
||||||
if (state.doc.content.child(pChildCnt - right - 1) !== mapping.getY(yChildren[yChildCnt - right - 1])) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (left + right > pChildCnt) {
|
|
||||||
// nothing changed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
yDomFragment._y.transact(() => {
|
|
||||||
// now update y to match editor state
|
|
||||||
yDomFragment.delete(left, yChildCnt - left - right)
|
|
||||||
yDomFragment.insert(left, state.doc.content.content.slice(left, pChildCnt - right).map(node => createTypeFromNode(node, mapping)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
1
bindings/dom.js
Normal file
1
bindings/dom.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dom/DomBinding.js'
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/dom
|
||||||
|
*/
|
||||||
|
|
||||||
/* global MutationObserver, getSelection */
|
/* global MutationObserver, getSelection */
|
||||||
|
|
||||||
import { fromRelativePosition } from '../../Util/relativePosition.js'
|
import { fromRelativePosition } from '../../utils/relativePosition.js'
|
||||||
import Binding from '../Binding.js'
|
import { createMutex } from '../../lib/mutex.js'
|
||||||
import { createAssociation, removeAssociation } from './util.js'
|
import { createAssociation, removeAssociation } from './util.js'
|
||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
|
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
|
||||||
import { defaultFilter, applyFilterOnType } from './filter.js'
|
import { defaultFilter, applyFilterOnType } from './filter.js'
|
||||||
import typeObserver from './typeObserver.js'
|
import { typeObserver } from './typeObserver.js'
|
||||||
import domObserver from './domObserver.js'
|
import { domObserver } from './domObserver.js'
|
||||||
|
import { YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
* @callback DomFilter
|
||||||
|
* @param {string} nodeName
|
||||||
|
* @param {Map<string, string>} attrs
|
||||||
|
* @return {Map | null}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,8 +30,9 @@ import domObserver from './domObserver.js'
|
|||||||
* const type = y.define('xml', Y.XmlFragment)
|
* const type = y.define('xml', Y.XmlFragment)
|
||||||
* const binding = new Y.QuillBinding(type, div)
|
* const binding = new Y.QuillBinding(type, div)
|
||||||
*
|
*
|
||||||
|
* @class
|
||||||
*/
|
*/
|
||||||
export default class DomBinding extends Binding {
|
export class DomBinding {
|
||||||
/**
|
/**
|
||||||
* @param {YXmlFragment} type The bind source. This is the ultimate source of
|
* @param {YXmlFragment} type The bind source. This is the ultimate source of
|
||||||
* truth.
|
* truth.
|
||||||
@@ -31,10 +40,26 @@ export default class DomBinding extends Binding {
|
|||||||
* @param {Object} [opts] Optional configurations
|
* @param {Object} [opts] Optional configurations
|
||||||
|
|
||||||
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
|
* @param {DomFilter} [opts.filter=defaultFilter] The filter function to use.
|
||||||
|
* @param {Document} [opts.document=document] The filter function to use.
|
||||||
|
* @param {Object} [opts.hooks] The filter function to use.
|
||||||
|
* @param {Element} [opts.scrollingElement=null] The filter function to use.
|
||||||
*/
|
*/
|
||||||
constructor (type, target, opts = {}) {
|
constructor (type, target, opts = {}) {
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
// Binding handles textType as this.type and domTextarea as this.target
|
||||||
super(type, target)
|
/**
|
||||||
|
* The Yjs type that is bound to `target`
|
||||||
|
* @type {YXmlFragment}
|
||||||
|
*/
|
||||||
|
this.type = type
|
||||||
|
/**
|
||||||
|
* The target that `type` is bound to.
|
||||||
|
* @type {Element}
|
||||||
|
*/
|
||||||
|
this.target = target
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._mutualExclude = createMutex()
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
opts.document = opts.document || document
|
opts.document = opts.document || document
|
||||||
opts.hooks = opts.hooks || {}
|
opts.hooks = opts.hooks || {}
|
||||||
@@ -81,16 +106,16 @@ export default class DomBinding extends Binding {
|
|||||||
this.y = y
|
this.y = y
|
||||||
// Force flush dom changes before Type changes are applied (they might
|
// Force flush dom changes before Type changes are applied (they might
|
||||||
// modify the dom)
|
// modify the dom)
|
||||||
this._beforeTransactionHandler = (y, transaction, remote) => {
|
this._beforeTransactionHandler = y => {
|
||||||
this._domObserver(this._mutationObserver.takeRecords())
|
this._domObserver(this._mutationObserver.takeRecords())
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
beforeTransactionSelectionFixer(this, remote)
|
beforeTransactionSelectionFixer(this)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
y.on('beforeTransaction', this._beforeTransactionHandler)
|
y.on('beforeTransaction', this._beforeTransactionHandler)
|
||||||
this._afterTransactionHandler = (y, transaction, remote) => {
|
this._afterTransactionHandler = (y, transaction) => {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
afterTransactionSelectionFixer(this, remote)
|
afterTransactionSelectionFixer(this)
|
||||||
})
|
})
|
||||||
// remove associations
|
// remove associations
|
||||||
// TODO: this could be done more efficiently
|
// TODO: this could be done more efficiently
|
||||||
@@ -121,8 +146,15 @@ export default class DomBinding extends Binding {
|
|||||||
createAssociation(this, target, type)
|
createAssociation(this, target, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flushDomChanges () {
|
||||||
|
this._domObserver(this._mutationObserver.takeRecords())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE: currently does not apply filter to existing elements!
|
* NOTE:
|
||||||
|
* * does not apply filter to existing elements!
|
||||||
|
* * only guarantees that changes are filtered locally. Remote sites may see different content.
|
||||||
|
*
|
||||||
* @param {DomFilter} filter The filter function to use from now on.
|
* @param {DomFilter} filter The filter function to use from now on.
|
||||||
*/
|
*/
|
||||||
setFilter (filter) {
|
setFilter (filter) {
|
||||||
@@ -199,13 +231,18 @@ export default class DomBinding extends Binding {
|
|||||||
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
|
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
|
||||||
y.off('afterTransaction', this._afterTransactionHandler)
|
y.off('afterTransaction', this._afterTransactionHandler)
|
||||||
document.removeEventListener('selectionchange', this._selectionchange)
|
document.removeEventListener('selectionchange', this._selectionchange)
|
||||||
super.destroy()
|
this.type = null
|
||||||
|
this.target = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A filter defines which elements and attributes to share.
|
* A filter defines which elements and attributes to share.
|
||||||
* Return null if the node should be filtered. Otherwise return the Map of
|
* Return null if the node should be filtered. Otherwise return the Map of
|
||||||
* accepted attributes.
|
* accepted attributes.
|
||||||
*
|
*
|
||||||
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
|
* @callback FilterFunction
|
||||||
|
* @param {string} nodeName
|
||||||
|
* @param {Map} attrs
|
||||||
|
* @return {Map|null}
|
||||||
*/
|
*/
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/dom
|
||||||
|
*/
|
||||||
|
|
||||||
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||||
import {
|
import {
|
||||||
iterateUntilUndeleted,
|
iterateUntilUndeleted,
|
||||||
removeAssociation,
|
removeAssociation,
|
||||||
insertNodeHelper } from './util.js'
|
insertNodeHelper } from './util.js'
|
||||||
import diff from '../../../lib/simpleDiff.js'
|
import { simpleDiff } from '../../lib/diff.js'
|
||||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
import { YXmlFragment } from '../../types/YXmlElement.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Check if any of the nodes was deleted
|
* 1. Check if any of the nodes was deleted
|
||||||
@@ -17,9 +20,11 @@ import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
|||||||
* recreate a new yxml element that is bound to that node.
|
* recreate a new yxml element that is bound to that node.
|
||||||
* You can detect that a node was moved because expectedId
|
* You can detect that a node was moved because expectedId
|
||||||
* !== actualId in the list
|
* !== actualId in the list
|
||||||
|
*
|
||||||
|
* @function
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function applyChangesFromDom (binding, dom, yxml, _document) {
|
const applyChangesFromDom = (binding, dom, yxml, _document) => {
|
||||||
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
|
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -32,7 +37,7 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 1. Check if any of the nodes was deleted
|
// 1. Check if any of the nodes was deleted
|
||||||
yxml.forEach(function (childType) {
|
yxml.forEach(childType => {
|
||||||
if (knownChildren.has(childType) === false) {
|
if (knownChildren.has(childType) === false) {
|
||||||
childType._delete(y)
|
childType._delete(y)
|
||||||
removeAssociation(binding, binding.typeToDom.get(childType), childType)
|
removeAssociation(binding, binding.typeToDom.get(childType), childType)
|
||||||
@@ -82,8 +87,9 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
*/
|
*/
|
||||||
export default function domObserver (mutations, _document) {
|
export function domObserver (mutations, _document) {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
this.type._y.transact(() => {
|
this.type._y.transact(() => {
|
||||||
let diffChildren = new Set()
|
let diffChildren = new Set()
|
||||||
@@ -107,7 +113,7 @@ export default function domObserver (mutations, _document) {
|
|||||||
}
|
}
|
||||||
switch (mutation.type) {
|
switch (mutation.type) {
|
||||||
case 'characterData':
|
case 'characterData':
|
||||||
var change = diff(yxml.toString(), dom.nodeValue)
|
var change = simpleDiff(yxml.toString(), dom.nodeValue)
|
||||||
yxml.delete(change.pos, change.remove)
|
yxml.delete(change.pos, change.remove)
|
||||||
yxml.insert(change.pos, change.insert)
|
yxml.insert(change.pos, change.insert)
|
||||||
break
|
break
|
||||||
@@ -1,18 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/dom
|
||||||
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
import { YXmlText } from '../../types/YXmlText.js'
|
||||||
import YXmlHook from '../../src/Types/YXml/YXmlHook.js'
|
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
|
import { YXmlElement } from '../../types/YXmlElement.js'
|
||||||
import { createAssociation, domsToTypes } from './util.js'
|
import { createAssociation, domsToTypes } from './util.js'
|
||||||
import { filterDomAttributes, defaultFilter } from './filter.js'
|
import { filterDomAttributes, defaultFilter } from './filter.js'
|
||||||
|
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./filter.js').DomFilter} DomFilter
|
* @callback DomFilter
|
||||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
* @param {string} nodeName
|
||||||
|
* @param {Map<string, string>} attrs
|
||||||
|
* @return {Map | null}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Element|Text} element The DOM Element
|
* @param {Element|Text} element The DOM Element
|
||||||
* @param {?Document} _document Optional. Provide the global document object
|
* @param {?Document} _document Optional. Provide the global document object
|
||||||
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
* @param {Object<string, any>} [hooks = {}] Optional. Set of Yjs Hooks
|
||||||
@@ -20,7 +28,7 @@ import { filterDomAttributes, defaultFilter } from './filter.js'
|
|||||||
* @param {?DomBinding} binding Warning: This property is for internal use only!
|
* @param {?DomBinding} binding Warning: This property is for internal use only!
|
||||||
* @return {YXmlElement | YXmlText | false}
|
* @return {YXmlElement | YXmlText | false}
|
||||||
*/
|
*/
|
||||||
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
|
export const domToType = (element, _document = document, hooks = {}, filter = defaultFilter, binding) => {
|
||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
import isParentOf from '../../src/Util/isParentOf.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback DomFilter
|
* @module bindings/dom
|
||||||
* @param {string} nodeName
|
|
||||||
* @param {Map<string, string>} attrs
|
|
||||||
* @return {Map | null}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Y } from '../../utils/Y.js' // eslint-disable-line
|
||||||
|
import { YXmlElement, YXmlFragment } from '../../types/YXmlElement.js' // eslint-disable-line
|
||||||
|
import { isParentOf } from '../../utils/isParentOf.js'
|
||||||
|
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default filter method (does nothing).
|
* Default filter method (does nothing).
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {String} nodeName The nodeName of the element
|
* @param {String} nodeName The nodeName of the element
|
||||||
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
|
* @param {Map} attrs Map of key-value pairs that are attributes of the node.
|
||||||
* @return {Map | null} The allowed attributes or null, if the element should be
|
* @return {Map | null} The allowed attributes or null, if the element should be
|
||||||
* filtered.
|
* filtered.
|
||||||
*/
|
*/
|
||||||
export function defaultFilter (nodeName, attrs) {
|
export const defaultFilter = (nodeName, attrs) => {
|
||||||
// TODO: implement basic filter that filters out dangerous properties!
|
// TODO: implement basic filter that filters out dangerous properties!
|
||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* @private
|
||||||
|
* @function
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {Function} filter
|
||||||
*/
|
*/
|
||||||
export function filterDomAttributes (dom, filter) {
|
export const filterDomAttributes = (dom, filter) => {
|
||||||
const attrs = new Map()
|
const attrs = new Map()
|
||||||
for (let i = dom.attributes.length - 1; i >= 0; i--) {
|
for (let i = dom.attributes.length - 1; i >= 0; i--) {
|
||||||
const attr = dom.attributes[i]
|
const attr = dom.attributes[i]
|
||||||
@@ -35,14 +39,14 @@ export function filterDomAttributes (dom, filter) {
|
|||||||
/**
|
/**
|
||||||
* Applies a filter on a type.
|
* Applies a filter on a type.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {Y} y The Yjs instance.
|
* @param {Y} y The Yjs instance.
|
||||||
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
* @param {DomBinding} binding The DOM binding instance that has the dom filter.
|
||||||
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
* @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
export function applyFilterOnType (y, binding, type) {
|
export const applyFilterOnType = (y, binding, type) => {
|
||||||
if (isParentOf(binding.type, type)) {
|
if (isParentOf(binding.type, type) && type instanceof YXmlElement) {
|
||||||
const nodeName = type.nodeName
|
const nodeName = type.nodeName
|
||||||
let attributes = new Map()
|
let attributes = new Map()
|
||||||
if (type.getAttributes !== undefined) {
|
if (type.getAttributes !== undefined) {
|
||||||
@@ -53,7 +57,7 @@ export function applyFilterOnType (y, binding, type) {
|
|||||||
}
|
}
|
||||||
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
|
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
|
||||||
if (filteredAttributes === null) {
|
if (filteredAttributes === null) {
|
||||||
type._delete(y)
|
type._delete(y, true)
|
||||||
} else {
|
} else {
|
||||||
// iterate original attributes
|
// iterate original attributes
|
||||||
attributes.forEach((value, key) => {
|
attributes.forEach((value, key) => {
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/dom
|
||||||
|
*/
|
||||||
|
|
||||||
/* globals getSelection */
|
/* globals getSelection */
|
||||||
|
|
||||||
import { getRelativePosition } from '../../src/Util/relativePosition.js'
|
import { getRelativePosition } from '../../utils/relativePosition.js'
|
||||||
|
|
||||||
let relativeSelection = null
|
let relativeSelection = null
|
||||||
|
|
||||||
function _getCurrentRelativeSelection (domBinding) {
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const _getCurrentRelativeSelection = domBinding => {
|
||||||
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
|
||||||
const baseNodeType = domBinding.domToType.get(baseNode)
|
const baseNodeType = domBinding.domToType.get(baseNode)
|
||||||
const extentNodeType = domBinding.domToType.get(extentNode)
|
const extentNodeType = domBinding.domToType.get(extentNode)
|
||||||
@@ -17,18 +24,25 @@ function _getCurrentRelativeSelection (domBinding) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : domBinding => null
|
||||||
|
|
||||||
export function beforeTransactionSelectionFixer (domBinding) {
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const beforeTransactionSelectionFixer = domBinding => {
|
||||||
relativeSelection = getCurrentRelativeSelection(domBinding)
|
relativeSelection = getCurrentRelativeSelection(domBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the browser range after every transaction.
|
* Reset the browser range after every transaction.
|
||||||
* This prevents any collapsing issues with the local selection.
|
* This prevents any collapsing issues with the local selection.
|
||||||
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export function afterTransactionSelectionFixer (domBinding) {
|
export const afterTransactionSelectionFixer = domBinding => {
|
||||||
if (relativeSelection !== null) {
|
if (relativeSelection !== null) {
|
||||||
domBinding.restoreSelection(relativeSelection)
|
domBinding.restoreSelection(relativeSelection)
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/dom
|
||||||
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
/* global getSelection */
|
/* global getSelection */
|
||||||
|
|
||||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
import { YXmlText } from '../../types/YXmlText.js'
|
||||||
import YXmlHook from '../../src/Types/YXml/YXmlHook.js'
|
import { YXmlHook } from '../../types/YXmlHook.js'
|
||||||
import { removeDomChildrenUntilElementFound } from './util.js'
|
import { removeDomChildrenUntilElementFound } from './util.js'
|
||||||
|
|
||||||
function findScrollReference (scrollingElement) {
|
const findScrollReference = scrollingElement => {
|
||||||
if (scrollingElement !== null) {
|
if (scrollingElement !== null) {
|
||||||
let anchor = getSelection().anchorNode
|
let anchor = getSelection().anchorNode
|
||||||
if (anchor == null) {
|
if (anchor == null) {
|
||||||
@@ -34,7 +38,7 @@ function findScrollReference (scrollingElement) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixScroll (scrollingElement, ref) {
|
const fixScroll = (scrollingElement, ref) => {
|
||||||
if (ref !== null) {
|
if (ref !== null) {
|
||||||
const { elem, top } = ref
|
const { elem, top } = ref
|
||||||
const currentTop = elem.getBoundingClientRect().top
|
const currentTop = elem.getBoundingClientRect().top
|
||||||
@@ -48,7 +52,7 @@ function fixScroll (scrollingElement, ref) {
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export default function typeObserver (events) {
|
export const typeObserver = function (events) {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
const scrollRef = findScrollReference(this.scrollingElement)
|
const scrollRef = findScrollReference(this.scrollingElement)
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
|
|
||||||
import domToType from './domToType.js'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../src/Types/YXml/YXmlText.js').default} YXmlText
|
* @module bindings/dom
|
||||||
* @typedef {import('../../src/Types/YXml/YXmlElement.js').default} YXmlElement
|
|
||||||
* @typedef {import('../../src/Types/YXml/YXmlHook.js').default} YXmlHook
|
|
||||||
* @typedef {import('./DomBinding.js').default} DomBinding
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { domToType } from './domToType.js'
|
||||||
|
import { DomBinding } from './DomBinding.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterates items until an undeleted item is found.
|
* Iterates items until an undeleted item is found.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export function iterateUntilUndeleted (item) {
|
export const iterateUntilUndeleted = item => {
|
||||||
while (item !== null && item._deleted) {
|
while (item !== null && item._deleted) {
|
||||||
item = item._right
|
item = item._right
|
||||||
}
|
}
|
||||||
@@ -24,12 +21,14 @@ export function iterateUntilUndeleted (item) {
|
|||||||
* Removes an association (the information that a DOM element belongs to a
|
* Removes an association (the information that a DOM element belongs to a
|
||||||
* type).
|
* type).
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {DomBinding} domBinding The binding object
|
* @param {DomBinding} domBinding The binding object
|
||||||
* @param {Element} dom The dom that is to be associated with type
|
* @param {Element} dom The dom that is to be associated with type
|
||||||
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function removeAssociation (domBinding, dom, type) {
|
export const removeAssociation = (domBinding, dom, type) => {
|
||||||
domBinding.domToType.delete(dom)
|
domBinding.domToType.delete(dom)
|
||||||
domBinding.typeToDom.delete(type)
|
domBinding.typeToDom.delete(type)
|
||||||
}
|
}
|
||||||
@@ -38,12 +37,14 @@ export function removeAssociation (domBinding, dom, type) {
|
|||||||
* Creates an association (the information that a DOM element belongs to a
|
* Creates an association (the information that a DOM element belongs to a
|
||||||
* type).
|
* type).
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {DomBinding} domBinding The binding object
|
* @param {DomBinding} domBinding The binding object
|
||||||
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
|
* @param {DocumentFragment|Element|Text} dom The dom that is to be associated with type
|
||||||
* @param {YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
|
* @param {YXmlFragment|YXmlElement|YXmlHook|YXmlText} type The type that is to be associated with dom
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function createAssociation (domBinding, dom, type) {
|
export const createAssociation = (domBinding, dom, type) => {
|
||||||
if (domBinding !== undefined) {
|
if (domBinding !== undefined) {
|
||||||
domBinding.domToType.set(dom, type)
|
domBinding.domToType.set(dom, type)
|
||||||
domBinding.typeToDom.set(type, dom)
|
domBinding.typeToDom.set(type, dom)
|
||||||
@@ -54,11 +55,13 @@ export function createAssociation (domBinding, dom, type) {
|
|||||||
* If oldDom is associated with a type, associate newDom with the type and
|
* If oldDom is associated with a type, associate newDom with the type and
|
||||||
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
|
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {DomBinding} domBinding The binding object
|
* @param {DomBinding} domBinding The binding object
|
||||||
* @param {Element} oldDom The existing dom
|
* @param {Element} oldDom The existing dom
|
||||||
* @param {Element} newDom The new dom object
|
* @param {Element} newDom The new dom object
|
||||||
*/
|
*/
|
||||||
export function switchAssociation (domBinding, oldDom, newDom) {
|
export const switchAssociation = (domBinding, oldDom, newDom) => {
|
||||||
if (domBinding !== undefined) {
|
if (domBinding !== undefined) {
|
||||||
const type = domBinding.domToType.get(oldDom)
|
const type = domBinding.domToType.get(oldDom)
|
||||||
if (type !== undefined) {
|
if (type !== undefined) {
|
||||||
@@ -73,6 +76,8 @@ export function switchAssociation (domBinding, oldDom, newDom) {
|
|||||||
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
||||||
* specified position.
|
* specified position.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {YXmlElement} type The type in which to insert DOM elements.
|
* @param {YXmlElement} type The type in which to insert DOM elements.
|
||||||
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
||||||
* inserted after this node. Set null to insert at
|
* inserted after this node. Set null to insert at
|
||||||
@@ -81,15 +86,13 @@ export function switchAssociation (domBinding, oldDom, newDom) {
|
|||||||
* @param {?Document} _document Optional. Provide the global document object.
|
* @param {?Document} _document Optional. Provide the global document object.
|
||||||
* @param {DomBinding} binding The dom binding
|
* @param {DomBinding} binding The dom binding
|
||||||
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
export function insertDomElementsAfter (type, prev, doms, _document, binding) {
|
export const insertDomElementsAfter = (type, prev, doms, _document, binding) => {
|
||||||
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
|
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
|
||||||
return type.insertAfter(prev, types)
|
return type.insertAfter(prev, types)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function domsToTypes (doms, _document, hooks, filter, binding) {
|
export const domsToTypes = (doms, _document, hooks, filter, binding) => {
|
||||||
const types = []
|
const types = []
|
||||||
for (let dom of doms) {
|
for (let dom of doms) {
|
||||||
const t = domToType(dom, _document, hooks, filter, binding)
|
const t = domToType(dom, _document, hooks, filter, binding)
|
||||||
@@ -102,8 +105,9 @@ export function domsToTypes (doms, _document, hooks, filter, binding) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
|
* @function
|
||||||
*/
|
*/
|
||||||
export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) {
|
export const insertNodeHelper = (yxml, prevExpectedNode, child, _document, binding) => {
|
||||||
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
|
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
|
||||||
if (insertedNodes.length > 0) {
|
if (insertedNodes.length > 0) {
|
||||||
return insertedNodes[0]
|
return insertedNodes[0]
|
||||||
@@ -115,14 +119,14 @@ export function insertNodeHelper (yxml, prevExpectedNode, child, _document, bind
|
|||||||
/**
|
/**
|
||||||
* Remove children until `elem` is found.
|
* Remove children until `elem` is found.
|
||||||
*
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
* @param {Element} parent The parent of `elem` and `currentChild`.
|
* @param {Element} parent The parent of `elem` and `currentChild`.
|
||||||
* @param {Element} currentChild Start removing elements with `currentChild`. If
|
* @param {Node} currentChild Start removing elements with `currentChild`. If
|
||||||
* `currentChild` is `elem` it won't be removed.
|
* `currentChild` is `elem` it won't be removed.
|
||||||
* @param {Element|null} elem The elemnt to look for.
|
* @param {Element|null} elem The elemnt to look for.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
export function removeDomChildrenUntilElementFound (parent, currentChild, elem) {
|
export const removeDomChildrenUntilElementFound = (parent, currentChild, elem) => {
|
||||||
while (currentChild !== elem) {
|
while (currentChild !== elem) {
|
||||||
const del = currentChild
|
const del = currentChild
|
||||||
currentChild = currentChild.nextSibling
|
currentChild = currentChild.nextSibling
|
||||||
633
bindings/prosemirror.js
Normal file
633
bindings/prosemirror.js
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/prosemirror
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { YText } from '../types/YText.js' // eslint-disable-line
|
||||||
|
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
|
||||||
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
import * as PModel from 'prosemirror-model'
|
||||||
|
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
|
||||||
|
import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state' // eslint-disable-line
|
||||||
|
import * as math from '../lib/math.js'
|
||||||
|
import * as object from '../lib/object.js'
|
||||||
|
import * as YPos from '../utils/relativePosition.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique prosemirror plugin key for prosemirrorPlugin.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const prosemirrorPluginKey = new PluginKey('yjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
||||||
|
*
|
||||||
|
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
||||||
|
* @param {YXmlFragment} yXmlFragment
|
||||||
|
* @return {Plugin} Returns a prosemirror plugin that binds to this type
|
||||||
|
*/
|
||||||
|
export const prosemirrorPlugin = yXmlFragment => {
|
||||||
|
const pluginState = {
|
||||||
|
type: yXmlFragment,
|
||||||
|
y: yXmlFragment._y,
|
||||||
|
binding: null
|
||||||
|
}
|
||||||
|
let changedInitialContent = false
|
||||||
|
const plugin = new Plugin({
|
||||||
|
key: prosemirrorPluginKey,
|
||||||
|
state: {
|
||||||
|
init: (initargs, state) => {
|
||||||
|
return pluginState
|
||||||
|
},
|
||||||
|
apply: (tr, pluginState) => {
|
||||||
|
// update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin
|
||||||
|
if (pluginState.binding !== null && (changedInitialContent || tr.doc.content.size > 4)) {
|
||||||
|
changedInitialContent = true
|
||||||
|
pluginState.binding._prosemirrorChanged(tr.doc)
|
||||||
|
}
|
||||||
|
return pluginState
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view: view => {
|
||||||
|
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
||||||
|
pluginState.binding = binding
|
||||||
|
return {
|
||||||
|
update: () => {
|
||||||
|
if (changedInitialContent || view.state.doc.content.size > 4) {
|
||||||
|
changedInitialContent = true
|
||||||
|
binding._prosemirrorChanged(view.state.doc)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroy: () => {
|
||||||
|
binding.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique prosemirror plugin key for cursorPlugin.type
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A prosemirror plugin that listens to awareness information on Yjs.
|
||||||
|
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const cursorPlugin = new Plugin({
|
||||||
|
key: cursorPluginKey,
|
||||||
|
props: {
|
||||||
|
decorations: state => {
|
||||||
|
const ystate = prosemirrorPluginKey.getState(state)
|
||||||
|
const y = ystate.y
|
||||||
|
const awareness = y.getAwarenessInfo()
|
||||||
|
const decorations = []
|
||||||
|
awareness.forEach((aw, userID) => {
|
||||||
|
if (aw.cursor != null) {
|
||||||
|
let user = aw.user || {}
|
||||||
|
if (user.color == null) {
|
||||||
|
user.color = '#ffa50070'
|
||||||
|
}
|
||||||
|
if (user.name == null) {
|
||||||
|
user.name = `User: ${userID}`
|
||||||
|
}
|
||||||
|
let anchor = relativePositionToAbsolutePosition(ystate.type, aw.cursor.anchor || null, ystate.binding.mapping)
|
||||||
|
let head = relativePositionToAbsolutePosition(ystate.type, aw.cursor.head || null, ystate.binding.mapping)
|
||||||
|
if (anchor !== null && head !== null) {
|
||||||
|
let maxsize = math.max(state.doc.content.size - 1, 0)
|
||||||
|
anchor = math.min(anchor, maxsize)
|
||||||
|
head = math.min(head, maxsize)
|
||||||
|
decorations.push(Decoration.widget(head, () => {
|
||||||
|
const cursor = document.createElement('span')
|
||||||
|
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||||
|
cursor.setAttribute('style', `border-color: ${user.color}`)
|
||||||
|
const userDiv = document.createElement('div')
|
||||||
|
userDiv.setAttribute('style', `background-color: ${user.color}`)
|
||||||
|
userDiv.insertBefore(document.createTextNode(user.name), null)
|
||||||
|
cursor.insertBefore(userDiv, null)
|
||||||
|
return cursor
|
||||||
|
}, { 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}` }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return DecorationSet.create(state.doc, decorations)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view: view => {
|
||||||
|
const ystate = prosemirrorPluginKey.getState(view.state)
|
||||||
|
const y = ystate.y
|
||||||
|
const awarenessListener = () => {
|
||||||
|
view.updateState(view.state)
|
||||||
|
}
|
||||||
|
const updateCursorInfo = () => {
|
||||||
|
const current = y.getLocalAwarenessInfo()
|
||||||
|
if (view.hasFocus()) {
|
||||||
|
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
|
||||||
|
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
|
||||||
|
if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
|
||||||
|
y.setAwarenessField('cursor', {
|
||||||
|
anchor, head
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (current.cursor !== null) {
|
||||||
|
y.setAwarenessField('cursor', null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y.on('awareness', awarenessListener)
|
||||||
|
view.dom.addEventListener('focusin', updateCursorInfo)
|
||||||
|
view.dom.addEventListener('focusout', updateCursorInfo)
|
||||||
|
return {
|
||||||
|
update: updateCursorInfo,
|
||||||
|
destroy: () => {
|
||||||
|
const y = prosemirrorPluginKey.getState(view.state).y
|
||||||
|
y.setAwarenessField('cursor', null)
|
||||||
|
y.off('awareness', awarenessListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a Prosemirror based absolute position to a Yjs based relative position.
|
||||||
|
*
|
||||||
|
* @param {number} pos
|
||||||
|
* @param {YXmlFragment} type
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
* @return {any} relative position
|
||||||
|
*/
|
||||||
|
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
|
||||||
|
if (pos === 0) {
|
||||||
|
return YPos.getRelativePosition(type, 0)
|
||||||
|
}
|
||||||
|
let n = type._first
|
||||||
|
if (n !== null) {
|
||||||
|
while (type !== n) {
|
||||||
|
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize
|
||||||
|
if (n.constructor === YText) {
|
||||||
|
if (n.length >= pos) {
|
||||||
|
return YPos.getRelativePosition(n, pos)
|
||||||
|
} else {
|
||||||
|
pos -= n.length
|
||||||
|
}
|
||||||
|
if (n._next !== null) {
|
||||||
|
n = n._next
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
n = n._parent
|
||||||
|
pos--
|
||||||
|
} while (n._next === null && n !== type)
|
||||||
|
if (n !== type) {
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (n._first !== null && pos < pNodeSize) {
|
||||||
|
n = n._first
|
||||||
|
pos--
|
||||||
|
} else {
|
||||||
|
if (pos === 1 && n.length === 0 && pNodeSize > 1) {
|
||||||
|
// edge case, should end in this paragraph
|
||||||
|
return ['endof', n._id.user, n._id.clock, null, null]
|
||||||
|
}
|
||||||
|
pos -= pNodeSize
|
||||||
|
if (n._next !== null) {
|
||||||
|
n = n._next
|
||||||
|
} else {
|
||||||
|
if (pos === 0) {
|
||||||
|
n = n._parent
|
||||||
|
return ['endof', n._id.user, n._id.clock || null, n._id.name || null, n._id.type || null]
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
n = n._parent
|
||||||
|
pos--
|
||||||
|
} while (n._next === null && n !== type)
|
||||||
|
if (n !== type) {
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos === 0 && n.constructor !== YText && n !== type) { // TODO: set to <= 0
|
||||||
|
return [n._id.user, n._id.clock]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YPos.getRelativePosition(type, type.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {YXmlFragment} yDoc Top level type that is bound to pView
|
||||||
|
* @param {any} relPos Encoded Yjs based relative position
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
*/
|
||||||
|
export const relativePositionToAbsolutePosition = (yDoc, relPos, mapping) => {
|
||||||
|
const decodedPos = YPos.fromRelativePosition(yDoc._y, relPos)
|
||||||
|
if (decodedPos === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let type = decodedPos.type
|
||||||
|
let pos = 0
|
||||||
|
if (type.constructor === YText) {
|
||||||
|
pos = decodedPos.offset
|
||||||
|
} else if (!type._deleted) {
|
||||||
|
let n = type._first
|
||||||
|
let i = 0
|
||||||
|
while (i < type.length && i < decodedPos.offset && n !== null) {
|
||||||
|
i++
|
||||||
|
pos += mapping.get(n).nodeSize
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
pos += 1 // increase because we go out of n
|
||||||
|
}
|
||||||
|
while (type !== yDoc) {
|
||||||
|
const parent = type._parent
|
||||||
|
if (!parent._deleted) {
|
||||||
|
pos += 1 // the start tag
|
||||||
|
let n = parent._first
|
||||||
|
// now iterate until we found type
|
||||||
|
while (n !== null) {
|
||||||
|
if (n === type) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += mapping.get(n).nodeSize
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type = parent
|
||||||
|
}
|
||||||
|
return pos - 1 // we don't count the most outer tag, because it is a fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binding for prosemirror.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
export class ProsemirrorBinding {
|
||||||
|
/**
|
||||||
|
* @param {YXmlFragment} yXmlFragment The bind source
|
||||||
|
* @param {EditorView} prosemirrorView The target binding
|
||||||
|
*/
|
||||||
|
constructor (yXmlFragment, prosemirrorView) {
|
||||||
|
this.type = yXmlFragment
|
||||||
|
this.prosemirrorView = prosemirrorView
|
||||||
|
this.mux = createMutex()
|
||||||
|
/**
|
||||||
|
* @type {ProsemirrorMapping}
|
||||||
|
*/
|
||||||
|
this.mapping = new Map()
|
||||||
|
this._observeFunction = this._typeChanged.bind(this)
|
||||||
|
this.y = yXmlFragment._y
|
||||||
|
/**
|
||||||
|
* current selection as relative positions in the Yjs model
|
||||||
|
*/
|
||||||
|
this._relSelection = null
|
||||||
|
this.y.on('beforeTransaction', e => {
|
||||||
|
this._relSelection = {
|
||||||
|
anchor: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.anchor, yXmlFragment, this.mapping),
|
||||||
|
head: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.head, yXmlFragment, this.mapping)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
yXmlFragment.observeDeep(this._observeFunction)
|
||||||
|
}
|
||||||
|
_typeChanged (events, transaction) {
|
||||||
|
if (events.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
|
||||||
|
this.mux(() => {
|
||||||
|
const delStruct = (_, struct) => this.mapping.delete(struct)
|
||||||
|
transaction.deletedStructs.forEach(struct => this.mapping.delete(struct))
|
||||||
|
transaction.changedTypes.forEach(delStruct)
|
||||||
|
transaction.changedParentTypes.forEach(delStruct)
|
||||||
|
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
|
||||||
|
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
|
||||||
|
const relSel = this._relSelection
|
||||||
|
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
|
||||||
|
const anchor = relativePositionToAbsolutePosition(this.type, relSel.anchor, this.mapping)
|
||||||
|
const head = relativePositionToAbsolutePosition(this.type, relSel.head, this.mapping)
|
||||||
|
if (anchor !== null && head !== null) {
|
||||||
|
tr.setSelection(TextSelection.create(tr.doc, anchor, head))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_prosemirrorChanged (doc) {
|
||||||
|
this.mux(() => {
|
||||||
|
updateYFragment(this.type, doc.content, this.mapping)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
destroy () {
|
||||||
|
this.type.unobserveDeep(this._observeFunction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @privateMapping
|
||||||
|
* @param {YXmlElement} el
|
||||||
|
* @param {PModel.Schema} schema
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
* @return {PModel.Node}
|
||||||
|
*/
|
||||||
|
export const createNodeIfNotExists = (el, schema, mapping) => {
|
||||||
|
const node = mapping.get(el)
|
||||||
|
if (node === undefined) {
|
||||||
|
return createNodeFromYElement(el, schema, mapping)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {YXmlElement} el
|
||||||
|
* @param {PModel.Schema} schema
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
|
||||||
|
*/
|
||||||
|
export const createNodeFromYElement = (el, schema, mapping) => {
|
||||||
|
const children = []
|
||||||
|
el.toArray().forEach(type => {
|
||||||
|
if (type.constructor === YXmlElement) {
|
||||||
|
const n = createNodeIfNotExists(type, schema, mapping)
|
||||||
|
if (n !== null) {
|
||||||
|
children.push(n)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const ns = createTextNodesFromYText(type, schema, mapping)
|
||||||
|
if (ns !== null) {
|
||||||
|
ns.forEach(textchild => {
|
||||||
|
if (textchild !== null) {
|
||||||
|
children.push(textchild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let node
|
||||||
|
try {
|
||||||
|
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), children)
|
||||||
|
} catch (e) {
|
||||||
|
// an error occured while creating the node. This is probably a result because of a concurrent action.
|
||||||
|
// delete the node and do not push to children
|
||||||
|
el._y.transact(() => {
|
||||||
|
el._delete(el._y, true)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
mapping.set(el, node)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {YText} text
|
||||||
|
* @param {PModel.Schema} schema
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
* @return {Array<PModel.Node>}
|
||||||
|
*/
|
||||||
|
export const createTextNodesFromYText = (text, schema, mapping) => {
|
||||||
|
const nodes = []
|
||||||
|
const deltas = text.toDelta()
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < deltas.length; i++) {
|
||||||
|
const delta = deltas[i]
|
||||||
|
const marks = []
|
||||||
|
for (let markName in delta.attributes) {
|
||||||
|
marks.push(schema.mark(markName, delta.attributes[markName]))
|
||||||
|
}
|
||||||
|
nodes.push(schema.text(delta.insert, marks))
|
||||||
|
}
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
text._y.transact(() => {
|
||||||
|
text._delete(text._y, true)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {PModel.Node} node
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
* @return {YXmlElement | YText}
|
||||||
|
*/
|
||||||
|
export const createTypeFromNode = (node, mapping) => {
|
||||||
|
let type
|
||||||
|
if (node.isText) {
|
||||||
|
type = new YText()
|
||||||
|
const attrs = {}
|
||||||
|
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
||||||
|
type.insert(0, node.text, attrs)
|
||||||
|
} else {
|
||||||
|
type = new YXmlElement(node.type.name)
|
||||||
|
for (let key in node.attrs) {
|
||||||
|
const val = node.attrs[key]
|
||||||
|
if (val !== null) {
|
||||||
|
type.setAttribute(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ins = []
|
||||||
|
for (let i = 0; i < node.childCount; i++) {
|
||||||
|
ins.push(createTypeFromNode(node.child(i), mapping))
|
||||||
|
}
|
||||||
|
type.insert(0, ins)
|
||||||
|
}
|
||||||
|
mapping.set(type, node)
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
const equalAttrs = (pattrs, yattrs) => {
|
||||||
|
const keys = Object.keys(pattrs).filter(key => pattrs[key] === null)
|
||||||
|
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
|
||||||
|
for (let i = 0; i < keys.length && eq; i++) {
|
||||||
|
const key = keys[i]
|
||||||
|
eq = pattrs[key] === yattrs[key]
|
||||||
|
}
|
||||||
|
return eq
|
||||||
|
}
|
||||||
|
|
||||||
|
const equalYTextPText = (ytext, ptext) => {
|
||||||
|
const d = ytext.toDelta()[0]
|
||||||
|
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => equalAttrs(d.attributes[mark.type.name], mark.attrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
const equalYTypePNode = (ytype, pnode) =>
|
||||||
|
ytype.constructor === YText
|
||||||
|
? equalYTextPText(ytype, pnode)
|
||||||
|
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i))))
|
||||||
|
|
||||||
|
const computeChildEqualityFactor = (ytype, pnode, mapping) => {
|
||||||
|
const yChildren = ytype.toArray()
|
||||||
|
const pChildCnt = pnode.childCount
|
||||||
|
const yChildCnt = yChildren.length
|
||||||
|
const minCnt = math.min(yChildCnt, pChildCnt)
|
||||||
|
let left = 0
|
||||||
|
let right = 0
|
||||||
|
let foundMappedChild = false
|
||||||
|
for (; left < minCnt; left++) {
|
||||||
|
const leftY = yChildren[left]
|
||||||
|
const leftP = pnode.child(left)
|
||||||
|
if (mapping.get(leftY) === leftP) {
|
||||||
|
foundMappedChild = true// definite (good) match!
|
||||||
|
} else if (!equalYTypePNode(leftY, leftP)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (; left + right < minCnt; right++) {
|
||||||
|
const rightY = yChildren[yChildCnt - right - 1]
|
||||||
|
const rightP = pnode.child(pChildCnt - right - 1)
|
||||||
|
if (mapping.get(rightY) !== rightP) {
|
||||||
|
foundMappedChild = true
|
||||||
|
} else if (!equalYTypePNode(rightP, rightP)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
equalityFactor: left + right,
|
||||||
|
foundMappedChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {YXmlFragment} yDomFragment
|
||||||
|
* @param {PModel.Node} pContent
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
*/
|
||||||
|
const updateYFragment = (yDomFragment, pContent, mapping) => {
|
||||||
|
if (yDomFragment instanceof YXmlElement && yDomFragment.nodeName.toLowerCase() !== pContent.type.name) {
|
||||||
|
throw new Error('node name mismatch!')
|
||||||
|
}
|
||||||
|
mapping.set(yDomFragment, pContent)
|
||||||
|
// update attributes
|
||||||
|
if (yDomFragment instanceof YXmlElement) {
|
||||||
|
const yDomAttrs = yDomFragment.getAttributes()
|
||||||
|
const pAttrs = pContent.attrs
|
||||||
|
for (let key in pAttrs) {
|
||||||
|
if (pAttrs[key] !== null) {
|
||||||
|
if (yDomAttrs[key] !== pAttrs[key]) {
|
||||||
|
yDomFragment.setAttribute(key, pAttrs[key])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
yDomFragment.removeAttribute(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove all keys that are no longer in pAttrs
|
||||||
|
for (let key in yDomAttrs) {
|
||||||
|
if (pAttrs[key] === undefined) {
|
||||||
|
yDomFragment.removeAttribute(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update children
|
||||||
|
const pChildCnt = pContent.childCount
|
||||||
|
const yChildren = yDomFragment.toArray()
|
||||||
|
const yChildCnt = yChildren.length
|
||||||
|
const minCnt = math.min(pChildCnt, yChildCnt)
|
||||||
|
let left = 0
|
||||||
|
let right = 0
|
||||||
|
// find number of matching elements from left
|
||||||
|
for (;left < minCnt; left++) {
|
||||||
|
const leftY = yChildren[left]
|
||||||
|
const leftP = pContent.child(left)
|
||||||
|
if (mapping.get(leftY) !== leftP) {
|
||||||
|
if (equalYTypePNode(leftY, leftP)) {
|
||||||
|
// update mapping
|
||||||
|
mapping.set(leftY, leftP)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// find number of matching elements from right
|
||||||
|
for (;right + left < minCnt; right++) {
|
||||||
|
const rightY = yChildren[yChildCnt - right - 1]
|
||||||
|
const rightP = pContent.child(pChildCnt - right - 1)
|
||||||
|
if (mapping.get(rightY) !== rightP) {
|
||||||
|
if (equalYTypePNode(rightY, rightP)) {
|
||||||
|
// update mapping
|
||||||
|
mapping.set(rightY, rightP)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yDomFragment._y.transact(() => {
|
||||||
|
// try to compare and update
|
||||||
|
while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
|
||||||
|
const leftY = yChildren[left]
|
||||||
|
const leftP = pContent.child(left)
|
||||||
|
const rightY = yChildren[yChildCnt - right - 1]
|
||||||
|
const rightP = pContent.child(pChildCnt - right - 1)
|
||||||
|
if (leftY.constructor === YText && leftP.isText) {
|
||||||
|
if (!equalYTextPText(leftY, leftP)) {
|
||||||
|
yDomFragment.delete(left, 1)
|
||||||
|
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||||
|
}
|
||||||
|
left += 1
|
||||||
|
} else {
|
||||||
|
let updateLeft = matchNodeName(leftY, leftP)
|
||||||
|
let updateRight = matchNodeName(rightY, rightP)
|
||||||
|
if (updateLeft && updateRight) {
|
||||||
|
// decide which which element to update
|
||||||
|
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping)
|
||||||
|
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping)
|
||||||
|
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
|
||||||
|
updateRight = false
|
||||||
|
} else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
|
||||||
|
updateLeft = false
|
||||||
|
} else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
|
||||||
|
updateLeft = false
|
||||||
|
} else {
|
||||||
|
updateRight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateLeft) {
|
||||||
|
updateYFragment(leftY, leftP, mapping)
|
||||||
|
left += 1
|
||||||
|
} else if (updateRight) {
|
||||||
|
updateYFragment(rightY, rightP, mapping)
|
||||||
|
right += 1
|
||||||
|
} else {
|
||||||
|
yDomFragment.delete(left, 1)
|
||||||
|
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||||
|
left += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const yDelLen = yChildCnt - left - right
|
||||||
|
if (yDelLen > 0) {
|
||||||
|
yDomFragment.delete(left, yDelLen)
|
||||||
|
}
|
||||||
|
if (left + right < pChildCnt) {
|
||||||
|
const ins = []
|
||||||
|
for (let i = left; i < pChildCnt - right; i++) {
|
||||||
|
ins.push(createTypeFromNode(pContent.child(i), mapping))
|
||||||
|
}
|
||||||
|
yDomFragment.insert(left, ins)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {YXmlElement} yElement
|
||||||
|
* @param {any} pNode Prosemirror Node
|
||||||
|
*/
|
||||||
|
const matchNodeName = (yElement, pNode) => yElement.nodeName === pNode.type.name.toUpperCase()
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import Binding from '../Binding.js'
|
/**
|
||||||
|
* @module bindings/quill
|
||||||
|
*/
|
||||||
|
|
||||||
function typeObserver (event) {
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
|
||||||
|
const typeObserver = function (event) {
|
||||||
const quill = this.target
|
const quill = this.target
|
||||||
// Force flush Quill changes.
|
// Force flush Quill changes.
|
||||||
quill.update('yjs')
|
quill.update('yjs')
|
||||||
this._mutualExclude(function () {
|
this._mutualExclude(() => {
|
||||||
// Apply computed delta.
|
// Apply computed delta.
|
||||||
quill.updateContents(event.delta, 'yjs')
|
quill.updateContents(event.delta, 'yjs')
|
||||||
// Force flush Quill changes. Ignore applied changes.
|
// Force flush Quill changes. Ignore applied changes.
|
||||||
@@ -12,7 +16,7 @@ function typeObserver (event) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function quillObserver (delta) {
|
const quillObserver = function (delta) {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
this.type.applyDelta(delta.ops)
|
this.type.applyDelta(delta.ops)
|
||||||
})
|
})
|
||||||
@@ -28,14 +32,27 @@ function quillObserver (delta) {
|
|||||||
* // Now modifications on the DOM will be reflected in the Type, and the other
|
* // Now modifications on the DOM will be reflected in the Type, and the other
|
||||||
* // way around!
|
* // way around!
|
||||||
*/
|
*/
|
||||||
export default class QuillBinding extends Binding {
|
export class QuillBinding {
|
||||||
/**
|
/**
|
||||||
* @param {YText} textType
|
* @param {YText} textType
|
||||||
* @param {Quill} quill
|
* @param {Quill} quill
|
||||||
*/
|
*/
|
||||||
constructor (textType, quill) {
|
constructor (textType, quill) {
|
||||||
// Binding handles textType as this.type and quill as this.target.
|
// Binding handles textType as this.type and quill as this.target.
|
||||||
super(textType, quill)
|
/**
|
||||||
|
* The Yjs type that is bound to `target`
|
||||||
|
* @type {YText}
|
||||||
|
*/
|
||||||
|
this.type = textType
|
||||||
|
/**
|
||||||
|
* The target that `type` is bound to.
|
||||||
|
* @type {Quill}
|
||||||
|
*/
|
||||||
|
this.target = quill
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._mutualExclude = createMutex()
|
||||||
// Set initial value.
|
// Set initial value.
|
||||||
quill.setContents(textType.toDelta(), 'yjs')
|
quill.setContents(textType.toDelta(), 'yjs')
|
||||||
// Observers are handled by this class.
|
// Observers are handled by this class.
|
||||||
@@ -48,6 +65,7 @@ export default class QuillBinding extends Binding {
|
|||||||
// Remove everything that is handled by this class.
|
// Remove everything that is handled by this class.
|
||||||
this.type.unobserve(this._typeObserver)
|
this.type.unobserve(this._typeObserver)
|
||||||
this.target.off('text-change', this._quillObserver)
|
this.target.off('text-change', this._quillObserver)
|
||||||
super.destroy()
|
this.type = null
|
||||||
|
this.target = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* @module bindings/textarea
|
||||||
|
*/
|
||||||
|
|
||||||
import Binding from '../Binding.js'
|
import { simpleDiff } from '../lib/diff.js'
|
||||||
import simpleDiff from '../../lib/simpleDiff.js'
|
import { getRelativePosition, fromRelativePosition } from '../utils/relativePosition.js'
|
||||||
import { getRelativePosition, fromRelativePosition } from '../../src/Util/relativePosition.js'
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
|
||||||
function typeObserver () {
|
function typeObserver () {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
@@ -35,10 +38,22 @@ function domObserver () {
|
|||||||
* const binding = new Y.QuillBinding(type, textarea)
|
* const binding = new Y.QuillBinding(type, textarea)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default class TextareaBinding extends Binding {
|
export class TextareaBinding {
|
||||||
constructor (textType, domTextarea) {
|
constructor (textType, domTextarea) {
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
/**
|
||||||
super(textType, domTextarea)
|
* The Yjs type that is bound to `target`
|
||||||
|
* @type {Type}
|
||||||
|
*/
|
||||||
|
this.type = textType
|
||||||
|
/**
|
||||||
|
* The target that `type` is bound to.
|
||||||
|
* @type {*}
|
||||||
|
*/
|
||||||
|
this.target = domTextarea
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._mutualExclude = createMutex()
|
||||||
// set initial value
|
// set initial value
|
||||||
domTextarea.value = textType.toString()
|
domTextarea.value = textType.toString()
|
||||||
// Observers are handled by this class
|
// Observers are handled by this class
|
||||||
@@ -51,6 +66,7 @@ export default class TextareaBinding extends Binding {
|
|||||||
// Remove everything that is handled by this class
|
// Remove everything that is handled by this class
|
||||||
this.type.unobserve(this._typeObserver)
|
this.type.unobserve(this._typeObserver)
|
||||||
this.target.unobserve(this._domObserver)
|
this.target.unobserve(this._domObserver)
|
||||||
super.destroy()
|
this.type = null
|
||||||
|
this.target = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
examples/.gitignore
vendored
Normal file
1
examples/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
build
|
||||||
36
examples/dom.html
Normal file
36
examples/dom.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Yjs Prosemirror Example</title>
|
||||||
|
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
||||||
|
<style>
|
||||||
|
#content {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This example shows how to bind a YXmlFragment type to an arbitrary DOM element. We set the DOM element to contenteditable so it basically behaves like a very powerful rich-text editor.</p>
|
||||||
|
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||||
|
<hr>
|
||||||
|
<div class="code-html">
|
||||||
|
|
||||||
|
<div id="content" contenteditable=""></div>
|
||||||
|
</div>
|
||||||
|
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
|
||||||
|
<script class="code-js" src="./build/dom.js">
|
||||||
|
import * as Y from 'yjs/index.js'
|
||||||
|
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||||
|
import { DomBinding } from 'yjs/bindings/dom.js'
|
||||||
|
|
||||||
|
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||||
|
const ydocument = provider.get('dom')
|
||||||
|
const type = ydocument.define('xml', Y.XmlFragment)
|
||||||
|
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||||
|
|
||||||
|
window.example = {
|
||||||
|
provider, ydocument, type, binding
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
examples/dom.js
Normal file
14
examples/dom.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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)
|
||||||
|
const ydocument = provider.get('dom')
|
||||||
|
const type = ydocument.define('xml', Y.XmlFragment)
|
||||||
|
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||||
|
|
||||||
|
window.example = {
|
||||||
|
provider, ydocument, type, binding
|
||||||
|
}
|
||||||
9
examples/exampleConfig.js
Normal file
9
examples/exampleConfig.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
const isDeployed = location.hostname === 'yjs.website'
|
||||||
|
|
||||||
|
if (!isDeployed) {
|
||||||
|
console.log('%cYjs: Start your local websocket server by running %c`npm run websocket-server`', 'color:blue', 'color: grey; font-weight: bold')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serverAddress = isDeployed ? 'wss://api.yjs.website' : 'ws://localhost:1234'
|
||||||
14
examples/examples.json
Normal file
14
examples/examples.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"prosemirror": {
|
||||||
|
"title": "Prosemirror Binding"
|
||||||
|
},
|
||||||
|
"textarea": {
|
||||||
|
"title": "Textarea Binding"
|
||||||
|
},
|
||||||
|
"quill": {
|
||||||
|
"title": "Quill Binding"
|
||||||
|
},
|
||||||
|
"dom": {
|
||||||
|
"title": "Dom Binding"
|
||||||
|
}
|
||||||
|
}
|
||||||
84
examples/prosemirror.html
Normal file
84
examples/prosemirror.html
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Yjs Prosemirror Example</title>
|
||||||
|
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
||||||
|
<style>
|
||||||
|
placeholder {
|
||||||
|
display: inline;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
placeholder:after {
|
||||||
|
content: "☁";
|
||||||
|
font-size: 200%;
|
||||||
|
line-height: 0.1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.ProseMirror img { max-width: 100px }
|
||||||
|
/* this is a rough fix for the first cursor position when the first paragraph is empty */
|
||||||
|
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
|
||||||
|
margin-top: 16px
|
||||||
|
}
|
||||||
|
.ProseMirror-yjs-cursor {
|
||||||
|
position: absolute;
|
||||||
|
border-left: black;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-width: 2px;
|
||||||
|
border-color: orange;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
.ProseMirror-yjs-cursor > 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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This example shows how to bind a YXmlFragment type to a <a href="http://prosemirror.net">Prosemirror</a> editor.</p>
|
||||||
|
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||||
|
<div class="code-html">
|
||||||
|
|
||||||
|
<div id="editor" style="margin-bottom: 23px"></div>
|
||||||
|
<div style="display: none" id="content"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 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 { EditorState } from 'prosemirror-state'
|
||||||
|
import { EditorView } from 'prosemirror-view'
|
||||||
|
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 ydocument = provider.get('prosemirror')
|
||||||
|
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||||
|
|
||||||
|
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
||||||
|
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
window.example = { provider, ydocument, type, prosemirrorView }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
examples/prosemirror.js
Normal file
24
examples/prosemirror.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as Y from '../index.js'
|
||||||
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
|
import { prosemirrorPlugin, cursorPlugin } from '../bindings/prosemirror.js'
|
||||||
|
|
||||||
|
import * as conf from './exampleConfig.js'
|
||||||
|
|
||||||
|
import { EditorState } from 'prosemirror-state'
|
||||||
|
import { EditorView } from 'prosemirror-view'
|
||||||
|
import { DOMParser } from 'prosemirror-model'
|
||||||
|
import { schema } from 'prosemirror-schema-basic'
|
||||||
|
import { exampleSetup } from 'prosemirror-example-setup'
|
||||||
|
|
||||||
|
const provider = new WebsocketProvider(conf.serverAddress)
|
||||||
|
const ydocument = provider.get('prosemirror')
|
||||||
|
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||||
|
|
||||||
|
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
||||||
|
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
window.example = { provider, ydocument, type, prosemirrorView }
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
|
|
||||||
import {Plugin} from 'prosemirror-state'
|
|
||||||
import {Decoration, DecorationSet} from 'prosemirror-view'
|
|
||||||
import {schema} from 'prosemirror-schema-basic'
|
|
||||||
|
|
||||||
const findPlaceholder = (state, id) => {
|
|
||||||
let decos = PlaceholderPlugin.getState(state)
|
|
||||||
let found = decos.find(null, null, spec => spec.id === id)
|
|
||||||
return found.length ? found[0].from : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const startImageUpload = (view, file) => {
|
|
||||||
// A fresh object to act as the ID for this upload
|
|
||||||
let id = {}
|
|
||||||
|
|
||||||
// Replace the selection with a placeholder
|
|
||||||
let tr = view.state.tr
|
|
||||||
if (!tr.selection.empty) tr.deleteSelection()
|
|
||||||
tr.setMeta(PlaceholderPlugin, {add: {id, pos: tr.selection.from}})
|
|
||||||
view.dispatch(tr)
|
|
||||||
|
|
||||||
uploadFile(file).then(url => {
|
|
||||||
let pos = findPlaceholder(view.state, id)
|
|
||||||
// If the content around the placeholder has been deleted, drop
|
|
||||||
// the image
|
|
||||||
if (pos == null) return
|
|
||||||
// Otherwise, insert it at the placeholder's position, and remove
|
|
||||||
// the placeholder
|
|
||||||
view.dispatch(view.state.tr
|
|
||||||
.replaceWith(pos, pos, schema.nodes.image.create({src: url}))
|
|
||||||
.setMeta(PlaceholderPlugin, {remove: {id}}))
|
|
||||||
}, () => {
|
|
||||||
// On failure, just clean up the placeholder
|
|
||||||
view.dispatch(tr.setMeta(PlaceholderPlugin, {remove: {id}}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just a dummy that loads the file and creates a data URL.
|
|
||||||
// You could swap it out with a function that does an actual upload
|
|
||||||
// and returns a regular URL for the uploaded file.
|
|
||||||
function uploadFile (file) {
|
|
||||||
let reader = new FileReader()
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
reader.onload = () => resolve(reader.result)
|
|
||||||
reader.onerror = () => reject(reader.error)
|
|
||||||
// Some extra delay to make the asynchronicity visible
|
|
||||||
setTimeout(() => reader.readAsDataURL(file), 1500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PlaceholderPlugin = new Plugin({
|
|
||||||
state: {
|
|
||||||
init () { return DecorationSet.empty },
|
|
||||||
apply (tr, set) {
|
|
||||||
// Adjust decoration positions to changes made by the transaction
|
|
||||||
set = set.map(tr.mapping, tr.doc)
|
|
||||||
// See if the transaction adds or removes any placeholders
|
|
||||||
let action = tr.getMeta(this)
|
|
||||||
if (action && action.add) {
|
|
||||||
let widget = document.createElement('placeholder')
|
|
||||||
let deco = Decoration.widget(action.add.pos, widget, {id: action.add.id})
|
|
||||||
set = set.add(tr.doc, [deco])
|
|
||||||
} else if (action && action.remove) {
|
|
||||||
set = set.remove(set.find(null, null, spec => spec.id === action.remove.id))
|
|
||||||
}
|
|
||||||
return set
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
decorations (state) { return this.getState(state) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
|
|
||||||
# Prosemirror Example
|
|
||||||
|
|
||||||
### Run basic websockets server
|
|
||||||
|
|
||||||
```sh
|
|
||||||
node /provider/websocket/server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Bundle Prosemirror Example
|
|
||||||
|
|
||||||
This example requires external modules and needs to be bundled before shipping it to the browser.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd /examples/prosemirror/
|
|
||||||
# bundle prosemirror example
|
|
||||||
npx rollup -wc
|
|
||||||
# serve example
|
|
||||||
npx serve .
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script type="module" src="./index.dist.js"></script>
|
|
||||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
|
||||||
<style>
|
|
||||||
placeholder {
|
|
||||||
display: inline;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
placeholder:after {
|
|
||||||
content: "☁";
|
|
||||||
font-size: 200%;
|
|
||||||
line-height: 0.1;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.ProseMirror img { max-width: 100px }
|
|
||||||
.ProseMirror-yjs-cursor {
|
|
||||||
position: absolute;
|
|
||||||
border-left: black;
|
|
||||||
border-left-style: solid;
|
|
||||||
border-left-width: 2px;
|
|
||||||
border-color: orange;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
.ProseMirror-yjs-cursor > 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>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="editor" style="margin-bottom: 23px"></div>
|
|
||||||
<div style="display: none" id="content">
|
|
||||||
<h3>Hello User</h3>
|
|
||||||
<p>type something ...</p>
|
|
||||||
</div>
|
|
||||||
<div>Insert image: <input type=file id=image-upload></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
import * as Y from '../../src/index.js'
|
|
||||||
import { prosemirrorPlugin, cursorPlugin } from '../../bindings/ProsemirrorBinding/ProsemirrorBinding.js'
|
|
||||||
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
|
|
||||||
|
|
||||||
import {EditorState} from 'prosemirror-state'
|
|
||||||
import {EditorView} from 'prosemirror-view'
|
|
||||||
import {Schema, DOMParser, Mark, Fragment, Node, Slice} from 'prosemirror-model'
|
|
||||||
import {schema} from 'prosemirror-schema-basic'
|
|
||||||
import {exampleSetup} from 'prosemirror-example-setup'
|
|
||||||
import { PlaceholderPlugin, startImageUpload } from './PlaceholderPlugin.js'
|
|
||||||
|
|
||||||
const provider = new WebsocketProvider('ws://localhost:1234/')
|
|
||||||
const ydocument = provider.get('prosemirror')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {any}
|
|
||||||
*/
|
|
||||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
|
||||||
|
|
||||||
const view = new EditorView(document.querySelector('#editor'), {
|
|
||||||
state: EditorState.create({
|
|
||||||
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
|
||||||
plugins: exampleSetup({schema}).concat([PlaceholderPlugin, prosemirrorPlugin(type), cursorPlugin])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
window.provider = provider
|
|
||||||
window.ydocument = ydocument
|
|
||||||
window.type = type
|
|
||||||
window.view = view
|
|
||||||
window.EditorState = EditorState
|
|
||||||
window.EditorView = EditorView
|
|
||||||
window.Mark = Mark
|
|
||||||
window.Fragment = Fragment
|
|
||||||
window.Node = Node
|
|
||||||
window.Schema = Schema
|
|
||||||
window.Slice = Slice
|
|
||||||
|
|
||||||
document.querySelector('#image-upload').addEventListener('change', e => {
|
|
||||||
if (view.state.selection.$from.parent.inlineContent && e.target.files.length) {
|
|
||||||
startImageUpload(view, e.target.files[0])
|
|
||||||
}
|
|
||||||
view.focus()
|
|
||||||
})
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: './index.js',
|
|
||||||
output: {
|
|
||||||
name: 'index',
|
|
||||||
file: 'index.dist.js',
|
|
||||||
format: 'umd',
|
|
||||||
sourcemap: true
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
49
examples/quill.html
Normal file
49
examples/quill.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Yjs Quill Example</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This example shows how to bind a YText type to <a href="https://quilljs.com">Quill</a> editor.</p>
|
||||||
|
<p>The content of this editor is shared with every client who visits this domain.</p>
|
||||||
|
<div class="code-html">
|
||||||
|
<div id="quill-container">
|
||||||
|
<div id="quill">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
|
||||||
|
<script class="code-js" src="./build/quill.js">
|
||||||
|
import * as Y from 'yjs'
|
||||||
|
import { WebsocketProvider } from 'yjs/provider/websocket.js'
|
||||||
|
import { QuillBinding } from 'yjs/bindings/quill.js'
|
||||||
|
|
||||||
|
import Quill from 'quill'
|
||||||
|
|
||||||
|
const provider = new WebsocketProvider('wss://api.yjs.website')
|
||||||
|
const ydocument = provider.get('quill')
|
||||||
|
const ytext = ydocument.define('quill', Y.Text)
|
||||||
|
|
||||||
|
const quill = new Quill('#quill-container', {
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
[{ header: [1, 2, false] }],
|
||||||
|
['bold', 'italic', 'underline'],
|
||||||
|
['image', 'code-block'],
|
||||||
|
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||||
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
|
['link', 'image'],
|
||||||
|
['link', 'code-block'],
|
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
placeholder: 'Compose an epic...',
|
||||||
|
theme: 'snow' // or 'bubble'
|
||||||
|
})
|
||||||
|
|
||||||
|
window.quillBinding = new QuillBinding(ytext, quill)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
examples/quill.js
Normal file
30
examples/quill.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import * as Y from '../index.js'
|
||||||
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
|
import { QuillBinding } from '../bindings/quill.js'
|
||||||
|
|
||||||
|
import * as conf from './exampleConfig.js'
|
||||||
|
|
||||||
|
import Quill from 'quill'
|
||||||
|
|
||||||
|
const provider = new WebsocketProvider(conf.serverAddress)
|
||||||
|
const ydocument = provider.get('quill')
|
||||||
|
const ytext = ydocument.define('quill', Y.Text)
|
||||||
|
|
||||||
|
const quill = new Quill('#quill-container', {
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
[{ header: [1, 2, false] }],
|
||||||
|
['bold', 'italic', 'underline'],
|
||||||
|
['image', 'code-block'],
|
||||||
|
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||||
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
|
['link', 'image'],
|
||||||
|
['link', 'code-block'],
|
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
placeholder: 'Compose an epic...',
|
||||||
|
theme: 'snow' // or 'bubble'
|
||||||
|
})
|
||||||
|
|
||||||
|
window.quillBinding = new QuillBinding(ytext, quill)
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Main Quill library -->
|
|
||||||
<script src="../../node_modules/quill/dist/quill.min.js"></script>
|
|
||||||
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
|
|
||||||
<!-- Yjs Library and connector -->
|
|
||||||
<script src="../../y.js"></script>
|
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="quill-container">
|
|
||||||
<div id="quill">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
/* global Y, Quill */
|
|
||||||
|
|
||||||
let y = new Y('quill-cursors-0', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let quill = new Quill('#quill-container', {
|
|
||||||
modules: {
|
|
||||||
toolbar: [
|
|
||||||
[{ header: [1, 2, false] }],
|
|
||||||
['bold', 'italic', 'underline'],
|
|
||||||
['image', 'code-block'],
|
|
||||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
|
||||||
[{ script: 'sub' }, { script: 'super' }],
|
|
||||||
['link', 'image'],
|
|
||||||
['link', 'code-block'],
|
|
||||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
placeholder: 'Compose an epic...',
|
|
||||||
theme: 'snow' // or 'bubble'
|
|
||||||
})
|
|
||||||
|
|
||||||
let yText = y.define('quill', Y.Text)
|
|
||||||
|
|
||||||
let quillBinding = new Y.QuillBinding(yText, quill)
|
|
||||||
window.quillBinding = quillBinding
|
|
||||||
window.yText = yText
|
|
||||||
window.y = y
|
|
||||||
window.quill = quill
|
|
||||||
29
examples/style.css
Normal file
29
examples/style.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
footer img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav .title h1 a {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #b93c1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resizer {
|
||||||
|
background-color: #b93c1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main section article.readme h1:first-child img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main section article.readme h1:first-child {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main section article.readme h1:first-child::before {
|
||||||
|
content: "Yjs";
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
30
examples/textarea.html
Normal file
30
examples/textarea.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Yjs Textarea Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This example shows how to bind a YText type to a DOM Textarea.</p>
|
||||||
|
<p>The content of this textarea is shared with every client who visits this domain.</p>
|
||||||
|
<div class="code-html">
|
||||||
|
|
||||||
|
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
|
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
|
||||||
|
<script class="code-js" src="./build/textarea.js">
|
||||||
|
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')
|
||||||
|
const ydocument = provider.get('textarea')
|
||||||
|
const type = ydocument.define('textarea', Y.Text)
|
||||||
|
const textarea = document.querySelector('textarea')
|
||||||
|
const binding = new TextareaBinding(type, textarea)
|
||||||
|
|
||||||
|
window.textareaExample = {
|
||||||
|
provider, ydocument, type, textarea, binding
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
examples/textarea.js
Normal file
15
examples/textarea.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as Y from '../index.js'
|
||||||
|
import { WebsocketProvider } from '../provider/websocket.js'
|
||||||
|
import { TextareaBinding } from '../bindings/textarea.js'
|
||||||
|
|
||||||
|
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')
|
||||||
|
const binding = new TextareaBinding(type, textarea)
|
||||||
|
|
||||||
|
window.textareaExample = {
|
||||||
|
provider, ydocument, type, textarea, binding
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
|
||||||
<script type="module" src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/* eslint-env browser */
|
|
||||||
import * as Y from '../../src/index.js'
|
|
||||||
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
|
|
||||||
|
|
||||||
const provider = new WebsocketProvider('ws://localhost:1234/')
|
|
||||||
const ydocument = provider.get('textarea')
|
|
||||||
const type = ydocument.define('textarea', Y.Text)
|
|
||||||
const textarea = document.querySelector('textarea')
|
|
||||||
const binding = new Y.TextareaBinding(type, textarea)
|
|
||||||
|
|
||||||
window.textareaExample = {
|
|
||||||
provider, ydocument, type, textarea, binding
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
</head>
|
|
||||||
<!-- jquery is not required for YXml. It is just here for convenience, and to test batch operations. -->
|
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
|
||||||
<script src="../../y.js"></script>
|
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1> Shared DOM Example </h1>
|
|
||||||
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
|
|
||||||
<div class="command">
|
|
||||||
<button type="button">Execute</button>
|
|
||||||
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
|
|
||||||
</div>
|
|
||||||
<div class="command">
|
|
||||||
<button type="button">Execute</button>
|
|
||||||
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
|
|
||||||
</div>
|
|
||||||
<div class="command">
|
|
||||||
<button type="button">Execute</button>
|
|
||||||
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/* global $ */
|
|
||||||
var commands = document.querySelectorAll('.command')
|
|
||||||
Array.prototype.forEach.call(commands, function (command) {
|
|
||||||
var execute = function () {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
eval(command.querySelector('input').value)
|
|
||||||
}
|
|
||||||
command.querySelector('button').onclick = execute
|
|
||||||
$(command.querySelector('input')).keyup(function (e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
execute()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
let y = new Y('xml-example', {
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.yXml = y
|
|
||||||
// bind xml type to a dom, and put it in body
|
|
||||||
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
|
|
||||||
document.body.appendChild(window.sharedDom)
|
|
||||||
@@ -14,7 +14,7 @@ let chatprotocol = y.define('chatprotocol', Y.Array)
|
|||||||
let chatcontainer = document.querySelector('#chat')
|
let chatcontainer = document.querySelector('#chat')
|
||||||
|
|
||||||
// This functions inserts a message at the specified position in the DOM
|
// This functions inserts a message at the specified position in the DOM
|
||||||
function appendMessage (message, position) {
|
const appendMessage = (message, position) => {
|
||||||
var p = document.createElement('p')
|
var p = document.createElement('p')
|
||||||
var uname = document.createElement('span')
|
var uname = document.createElement('span')
|
||||||
uname.appendChild(document.createTextNode(message.username + ': '))
|
uname.appendChild(document.createTextNode(message.username + ': '))
|
||||||
@@ -25,7 +25,7 @@ function appendMessage (message, position) {
|
|||||||
|
|
||||||
// This function makes sure that only 7 messages exist in the chat history.
|
// This function makes sure that only 7 messages exist in the chat history.
|
||||||
// The rest is deleted
|
// The rest is deleted
|
||||||
function cleanupChat () {
|
const cleanupChat = () => {
|
||||||
if (chatprotocol.length > 7) {
|
if (chatprotocol.length > 7) {
|
||||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
chatprotocol.delete(0, chatprotocol.length - 7)
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ cleanupChat()
|
|||||||
chatprotocol.toArray().forEach(appendMessage)
|
chatprotocol.toArray().forEach(appendMessage)
|
||||||
|
|
||||||
// whenever content changes, make sure to reflect the changes in the DOM
|
// whenever content changes, make sure to reflect the changes in the DOM
|
||||||
chatprotocol.observe(function (event) {
|
chatprotocol.observe(event => {
|
||||||
// concurrent insertions may result in a history > 7, so cleanup here
|
// concurrent insertions may result in a history > 7, so cleanup here
|
||||||
cleanupChat()
|
cleanupChat()
|
||||||
chatcontainer.innerHTML = ''
|
chatcontainer.innerHTML = ''
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
</style>
|
</style>
|
||||||
<script src="../../y.js"></script>
|
<script src="../../y.js"></script>
|
||||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||||
<script src='../../../y-indexeddb/y-indexeddb.js'></script>
|
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -36,13 +36,13 @@ let quill = new Quill('#quill-container', {
|
|||||||
|
|
||||||
let cursors = quill.getModule('cursors')
|
let cursors = quill.getModule('cursors')
|
||||||
|
|
||||||
function drawCursors () {
|
const drawCursors = () => {
|
||||||
cursors.clearCursors()
|
cursors.clearCursors()
|
||||||
users.map((user, userId) => {
|
users.map((user, userId) => {
|
||||||
if (user !== myUserInfo) {
|
if (user !== myUserInfo) {
|
||||||
let relativeRange = user.get('range')
|
let relativeRange = user.get('range')
|
||||||
let lastUpdated = new Date(user.get('last updated'))
|
let lastUpdated = new Date(user.get('last updated')).getTime()
|
||||||
if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) {
|
if (lastUpdated != null && new Date().getTime() - lastUpdated < 20000 && relativeRange != null) {
|
||||||
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
|
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
|
||||||
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
|
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
|
||||||
let range = { index: start, length: end - start }
|
let range = { index: start, length: end - start }
|
||||||
55
index.js
Normal file
55
index.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
import './structs/Item.js'
|
||||||
|
import { Delete } from './structs/Delete.js'
|
||||||
|
import { ItemJSON } from './structs/ItemJSON.js'
|
||||||
|
import { ItemString } from './structs/ItemString.js'
|
||||||
|
import { ItemFormat } from './structs/ItemFormat.js'
|
||||||
|
import { ItemEmbed } from './structs/ItemEmbed.js'
|
||||||
|
import { GC } from './structs/GC.js'
|
||||||
|
|
||||||
|
import { YArray } from './types/YArray.js'
|
||||||
|
import { YMap } from './types/YMap.js'
|
||||||
|
import { YText } from './types/YText.js'
|
||||||
|
import { YXmlText } from './types/YXmlText.js'
|
||||||
|
import { YXmlHook } from './types/YXmlHook.js'
|
||||||
|
import { YXmlElement, YXmlFragment } from './types/YXmlElement.js'
|
||||||
|
|
||||||
|
import { registerStruct } from './utils/structReferences.js'
|
||||||
|
|
||||||
|
import * as decoding from './lib/decoding.js'
|
||||||
|
import * as encoding from './lib/encoding.js'
|
||||||
|
import * as awarenessProtocol from './protocols/awareness.js'
|
||||||
|
import * as syncProtocol from './protocols/sync.js'
|
||||||
|
import * as authProtocol from './protocols/auth.js'
|
||||||
|
|
||||||
|
export { decoding, encoding, awarenessProtocol, syncProtocol, authProtocol }
|
||||||
|
|
||||||
|
export { Y } from './utils/Y.js'
|
||||||
|
export { UndoManager } from './utils/UndoManager.js'
|
||||||
|
export { Transaction } from './utils/Transaction.js'
|
||||||
|
|
||||||
|
export { YArray as Array } from './types/YArray.js'
|
||||||
|
export { YMap as Map } from './types/YMap.js'
|
||||||
|
export { YText as Text } from './types/YText.js'
|
||||||
|
export { YXmlText as XmlText } from './types/YXmlText.js'
|
||||||
|
export { YXmlHook as XmlHook } from './types/YXmlHook.js'
|
||||||
|
export { YXmlElement as XmlElement, YXmlFragment as XmlFragment } from './types/YXmlElement.js'
|
||||||
|
|
||||||
|
export { getRelativePosition, fromRelativePosition } from './utils/relativePosition.js'
|
||||||
|
export { registerStruct } from './utils/structReferences.js'
|
||||||
|
export * from './lib/mutex.js'
|
||||||
|
|
||||||
|
registerStruct(0, GC)
|
||||||
|
registerStruct(1, ItemJSON)
|
||||||
|
registerStruct(2, ItemString)
|
||||||
|
registerStruct(3, ItemFormat)
|
||||||
|
registerStruct(4, Delete)
|
||||||
|
|
||||||
|
registerStruct(5, YArray)
|
||||||
|
registerStruct(6, YMap)
|
||||||
|
registerStruct(7, YText)
|
||||||
|
registerStruct(8, YXmlFragment)
|
||||||
|
registerStruct(9, YXmlElement)
|
||||||
|
registerStruct(10, YXmlText)
|
||||||
|
registerStruct(11, YXmlHook)
|
||||||
|
registerStruct(12, ItemEmbed)
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Handles named events.
|
* Handles named events.
|
||||||
*/
|
*/
|
||||||
export default class NamedEventHandler {
|
export class NamedEventHandler {
|
||||||
constructor () {
|
constructor () {
|
||||||
this._eventListener = new Map()
|
this._eventListener = new Map()
|
||||||
this._stateListener = new Map()
|
this._stateListener = new Map()
|
||||||
@@ -57,7 +57,7 @@ export default class NamedEventHandler {
|
|||||||
let state = this._stateListener.get(name)
|
let state = this._stateListener.get(name)
|
||||||
if (state === undefined) {
|
if (state === undefined) {
|
||||||
state = {}
|
state = {}
|
||||||
state.promise = new Promise(function (resolve) {
|
state.promise = new Promise(resolve => {
|
||||||
state.resolve = resolve
|
state.resolve = resolve
|
||||||
})
|
})
|
||||||
this._stateListener.set(name, state)
|
this._stateListener.set(name, state)
|
||||||
|
|||||||
19
lib/Tree.js
19
lib/Tree.js
@@ -1,5 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @module tree
|
||||||
|
*/
|
||||||
|
|
||||||
function rotate (tree, parent, newParent, n) {
|
const rotate = (tree, parent, newParent, n) => {
|
||||||
if (parent === null) {
|
if (parent === null) {
|
||||||
tree.root = newParent
|
tree.root = newParent
|
||||||
newParent._parent = null
|
newParent._parent = null
|
||||||
@@ -111,10 +114,16 @@ class N {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isBlack = node =>
|
||||||
|
node !== null ? node.isBlack() : true
|
||||||
|
|
||||||
|
const isRed = (node) =>
|
||||||
|
node !== null ? node.isRed() : false
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This is a Red Black Tree implementation
|
* This is a Red Black Tree implementation
|
||||||
*/
|
*/
|
||||||
export default class Tree {
|
export class Tree {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.root = null
|
this.root = null
|
||||||
this.length = 0
|
this.length = 0
|
||||||
@@ -310,12 +319,6 @@ export default class Tree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_fixDelete (n) {
|
_fixDelete (n) {
|
||||||
function isBlack (node) {
|
|
||||||
return node !== null ? node.isBlack() : true
|
|
||||||
}
|
|
||||||
function isRed (node) {
|
|
||||||
return node !== null ? node.isRed() : false
|
|
||||||
}
|
|
||||||
if (n.parent === null) {
|
if (n.parent === null) {
|
||||||
// this can only be called after the first iteration of fixDelete.
|
// this can only be called after the first iteration of fixDelete.
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module binary
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as string from './string.js'
|
||||||
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
export const BITS32 = 0xFFFFFFFF
|
export const BITS32 = 0xFFFFFFFF
|
||||||
export const BITS21 = (1 << 21) - 1
|
export const BITS21 = (1 << 21) - 1
|
||||||
@@ -5,3 +13,28 @@ export const BITS16 = (1 << 16) - 1
|
|||||||
|
|
||||||
export const BIT26 = 1 << 26
|
export const BIT26 = 1 << 26
|
||||||
export const BIT32 = 1 << 32
|
export const BIT32 = 1 << 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const toBase64 = bytes => {
|
||||||
|
let s = ''
|
||||||
|
for (let i = 0; i < bytes.byteLength; i++) {
|
||||||
|
s += string.fromCharCode(bytes[i])
|
||||||
|
}
|
||||||
|
return btoa(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} s
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const fromBase64 = s => {
|
||||||
|
const a = atob(s)
|
||||||
|
const bytes = globals.createUint8ArrayFromLen(a.length)
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
bytes[i] = a.charCodeAt(i)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|||||||
72
lib/broadcastchannel.js
Normal file
72
lib/broadcastchannel.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-env browser */
|
||||||
|
|
||||||
|
import * as binary from './binary.js'
|
||||||
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Channel
|
||||||
|
* @property {Set<Function>} Channel.subs
|
||||||
|
* @property {BC} Channel.bc
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Map<string, Channel>}
|
||||||
|
*/
|
||||||
|
const channels = new Map()
|
||||||
|
|
||||||
|
class LocalStoragePolyfill {
|
||||||
|
constructor (room) {
|
||||||
|
this.room = room
|
||||||
|
this.onmessage = null
|
||||||
|
addEventListener('storage', e => e.key === room && this.onmessage !== null && this.onmessage({ data: binary.fromBase64(e.newValue) }))
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {ArrayBuffer} data
|
||||||
|
*/
|
||||||
|
postMessage (buf) {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem(this.room, binary.toBase64(globals.createUint8ArrayFromArrayBuffer(buf)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use BroadcastChannel or Polyfill
|
||||||
|
const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} room
|
||||||
|
* @return {Channel}
|
||||||
|
*/
|
||||||
|
const getChannel = room => {
|
||||||
|
let c = channels.get(room)
|
||||||
|
if (c === undefined) {
|
||||||
|
const subs = new Set()
|
||||||
|
const bc = new BC(room)
|
||||||
|
bc.onmessage = e => subs.forEach(sub => sub(e.data))
|
||||||
|
c = {
|
||||||
|
bc, subs
|
||||||
|
}
|
||||||
|
channels.set(room, c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {string} room
|
||||||
|
* @param {Function} f
|
||||||
|
*/
|
||||||
|
export const subscribe = (room, f) => getChannel(room).subs.add(f)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish data to all subscribers (including subscribers on this tab)
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
* @param {string} room
|
||||||
|
* @param {ArrayBuffer} data
|
||||||
|
*/
|
||||||
|
export const publish = (room, data) => {
|
||||||
|
const c = getChannel(room)
|
||||||
|
c.bc.postMessage(data)
|
||||||
|
c.subs.forEach(sub => sub(data))
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @module decoding
|
||||||
|
*/
|
||||||
|
|
||||||
/* global Buffer */
|
/* global Buffer */
|
||||||
|
|
||||||
@@ -17,17 +20,26 @@ export class Decoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @function
|
||||||
* @param {ArrayBuffer} buffer
|
* @param {ArrayBuffer} buffer
|
||||||
* @return {Decoder}
|
* @return {Decoder}
|
||||||
*/
|
*/
|
||||||
export const createDecoder = buffer => new Decoder(buffer)
|
export const createDecoder = buffer => new Decoder(buffer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {Decoder} decoder
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
export const hasContent = decoder => decoder.pos !== decoder.arr.length
|
export const hasContent = decoder => decoder.pos !== decoder.arr.length
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone a decoder instance.
|
* Clone a decoder instance.
|
||||||
* Optionally set a new position parameter.
|
* Optionally set a new position parameter.
|
||||||
|
*
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder The decoder instance
|
* @param {Decoder} decoder The decoder instance
|
||||||
|
* @param {number} [newPos] Defaults to current position
|
||||||
* @return {Decoder} A clone of `decoder`
|
* @return {Decoder} A clone of `decoder`
|
||||||
*/
|
*/
|
||||||
export const clone = (decoder, newPos = decoder.pos) => {
|
export const clone = (decoder, newPos = decoder.pos) => {
|
||||||
@@ -38,6 +50,7 @@ export const clone = (decoder, newPos = decoder.pos) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read `len` bytes as an ArrayBuffer.
|
* Read `len` bytes as an ArrayBuffer.
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder The decoder instance
|
* @param {Decoder} decoder The decoder instance
|
||||||
* @param {number} len The length of bytes to read
|
* @param {number} len The length of bytes to read
|
||||||
* @return {ArrayBuffer}
|
* @return {ArrayBuffer}
|
||||||
@@ -52,6 +65,7 @@ export const readArrayBuffer = (decoder, len) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read variable length payload as ArrayBuffer
|
* Read variable length payload as ArrayBuffer
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {ArrayBuffer}
|
* @return {ArrayBuffer}
|
||||||
*/
|
*/
|
||||||
@@ -59,6 +73,7 @@ export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decod
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the rest of the content as an ArrayBuffer
|
* Read the rest of the content as an ArrayBuffer
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {ArrayBuffer}
|
* @return {ArrayBuffer}
|
||||||
*/
|
*/
|
||||||
@@ -66,6 +81,7 @@ export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length -
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Skip one byte, jump to the next position.
|
* Skip one byte, jump to the next position.
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder The decoder instance
|
* @param {Decoder} decoder The decoder instance
|
||||||
* @return {number} The next position
|
* @return {number} The next position
|
||||||
*/
|
*/
|
||||||
@@ -73,6 +89,7 @@ export const skip8 = decoder => decoder.pos++
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read one byte as unsigned integer.
|
* Read one byte as unsigned integer.
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder The decoder instance
|
* @param {Decoder} decoder The decoder instance
|
||||||
* @return {number} Unsigned 8-bit integer
|
* @return {number} Unsigned 8-bit integer
|
||||||
*/
|
*/
|
||||||
@@ -81,6 +98,7 @@ export const readUint8 = decoder => decoder.arr[decoder.pos++]
|
|||||||
/**
|
/**
|
||||||
* Read 4 bytes as unsigned integer.
|
* Read 4 bytes as unsigned integer.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {number} An unsigned integer.
|
* @return {number} An unsigned integer.
|
||||||
*/
|
*/
|
||||||
@@ -98,6 +116,7 @@ export const readUint32 = decoder => {
|
|||||||
* Look ahead without incrementing position.
|
* Look ahead without incrementing position.
|
||||||
* to the next byte and read it as unsigned integer.
|
* to the next byte and read it as unsigned integer.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {number} An unsigned integer.
|
* @return {number} An unsigned integer.
|
||||||
*/
|
*/
|
||||||
@@ -109,6 +128,7 @@ export const peekUint8 = decoder => decoder.arr[decoder.pos]
|
|||||||
* * numbers < 2^7 is stored in one bytlength
|
* * numbers < 2^7 is stored in one bytlength
|
||||||
* * numbers < 2^14 is stored in two bylength
|
* * numbers < 2^14 is stored in two bylength
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {number} An unsigned integer.length
|
* @return {number} An unsigned integer.length
|
||||||
*/
|
*/
|
||||||
@@ -128,6 +148,21 @@ export const readVarUint = decoder => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look ahead and read varUint without incrementing position
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
* @param {Decoder} decoder
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
export const peekVarUint = decoder => {
|
||||||
|
let pos = decoder.pos
|
||||||
|
let s = readVarUint(decoder)
|
||||||
|
decoder.pos = pos
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read string of variable length
|
* Read string of variable length
|
||||||
* * varUint is used to store the length of the string
|
* * varUint is used to store the length of the string
|
||||||
@@ -137,6 +172,7 @@ export const readVarUint = decoder => {
|
|||||||
* But most environments have a maximum number of arguments per functions.
|
* But most environments have a maximum number of arguments per functions.
|
||||||
* For effiency reasons we apply a maximum of 10000 characters at once.
|
* For effiency reasons we apply a maximum of 10000 characters at once.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {String} The read String.
|
* @return {String} The read String.
|
||||||
*/
|
*/
|
||||||
@@ -157,6 +193,8 @@ export const readVarString = decoder => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Look ahead and read varString without incrementing position
|
* Look ahead and read varString without incrementing position
|
||||||
|
*
|
||||||
|
* @function
|
||||||
* @param {Decoder} decoder
|
* @param {Decoder} decoder
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
@@ -166,3 +204,4 @@ export const peekVarString = decoder => {
|
|||||||
decoder.pos = pos
|
decoder.pos = pos
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @module diff
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A SimpleDiff describes a change on a String.
|
* A SimpleDiff describes a change on a String.
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
* @param {String} b The updated version of the string
|
* @param {String} b The updated version of the string
|
||||||
* @return {SimpleDiff} The diff description.
|
* @return {SimpleDiff} The diff description.
|
||||||
*/
|
*/
|
||||||
export default function simpleDiff (a, b) {
|
export const simpleDiff = (a, b) => {
|
||||||
let left = 0 // number of same characters counting from left
|
let left = 0 // number of same characters counting from left
|
||||||
let right = 0 // number of same characters counting from right
|
let right = 0 // number of same characters counting from right
|
||||||
while (left < a.length && left < b.length && a[left] === b[left]) {
|
while (left < a.length && left < b.length && a[left] === b[left]) {
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @module encoding
|
||||||
|
*/
|
||||||
import * as globals from './globals.js'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
const bits7 = 0b1111111
|
const bits7 = 0b1111111
|
||||||
@@ -15,10 +17,18 @@ export class Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @return {Encoder}
|
||||||
|
*/
|
||||||
export const createEncoder = () => new Encoder()
|
export const createEncoder = () => new Encoder()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current length of the encoded data.
|
* The current length of the encoded data.
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
* @param {Encoder} encoder
|
||||||
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
export const length = encoder => {
|
export const length = encoder => {
|
||||||
let len = encoder.cpos
|
let len = encoder.cpos
|
||||||
@@ -30,6 +40,8 @@ export const length = encoder => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
|
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
|
||||||
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @return {ArrayBuffer} The created ArrayBuffer.
|
* @return {ArrayBuffer} The created ArrayBuffer.
|
||||||
*/
|
*/
|
||||||
@@ -48,6 +60,7 @@ export const toBuffer = encoder => {
|
|||||||
/**
|
/**
|
||||||
* Write one byte to the encoder.
|
* Write one byte to the encoder.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} num The byte that is to be encoded.
|
* @param {number} num The byte that is to be encoded.
|
||||||
*/
|
*/
|
||||||
@@ -64,6 +77,7 @@ export const write = (encoder, num) => {
|
|||||||
* Write one byte at a specific position.
|
* Write one byte at a specific position.
|
||||||
* Position must already be written (i.e. encoder.length > pos)
|
* Position must already be written (i.e. encoder.length > pos)
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} pos Position to which to write data
|
* @param {number} pos Position to which to write data
|
||||||
* @param {number} num Unsigned 8-bit integer
|
* @param {number} num Unsigned 8-bit integer
|
||||||
@@ -89,6 +103,7 @@ export const set = (encoder, pos, num) => {
|
|||||||
/**
|
/**
|
||||||
* Write one byte as an unsigned integer.
|
* Write one byte as an unsigned integer.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
*/
|
*/
|
||||||
@@ -97,6 +112,7 @@ export const writeUint8 = (encoder, num) => write(encoder, num & bits8)
|
|||||||
/**
|
/**
|
||||||
* Write one byte as an unsigned Integer at a specific location.
|
* Write one byte as an unsigned Integer at a specific location.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} pos The location where the data will be written.
|
* @param {number} pos The location where the data will be written.
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
@@ -106,6 +122,7 @@ export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)
|
|||||||
/**
|
/**
|
||||||
* Write two bytes as an unsigned integer.
|
* Write two bytes as an unsigned integer.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
*/
|
*/
|
||||||
@@ -116,6 +133,7 @@ export const writeUint16 = (encoder, num) => {
|
|||||||
/**
|
/**
|
||||||
* Write two bytes as an unsigned integer at a specific location.
|
* Write two bytes as an unsigned integer at a specific location.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} pos The location where the data will be written.
|
* @param {number} pos The location where the data will be written.
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
@@ -128,6 +146,7 @@ export const setUint16 = (encoder, pos, num) => {
|
|||||||
/**
|
/**
|
||||||
* Write two bytes as an unsigned integer
|
* Write two bytes as an unsigned integer
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
*/
|
*/
|
||||||
@@ -141,6 +160,7 @@ export const writeUint32 = (encoder, num) => {
|
|||||||
/**
|
/**
|
||||||
* Write two bytes as an unsigned integer at a specific location.
|
* Write two bytes as an unsigned integer at a specific location.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} pos The location where the data will be written.
|
* @param {number} pos The location where the data will be written.
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
@@ -157,6 +177,7 @@ export const setUint32 = (encoder, pos, num) => {
|
|||||||
*
|
*
|
||||||
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
|
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {number} num The number that is to be encoded.
|
* @param {number} num The number that is to be encoded.
|
||||||
*/
|
*/
|
||||||
@@ -171,6 +192,7 @@ export const writeVarUint = (encoder, num) => {
|
|||||||
/**
|
/**
|
||||||
* Write a variable length string.
|
* Write a variable length string.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {String} str The string that is to be encoded.
|
* @param {String} str The string that is to be encoded.
|
||||||
*/
|
*/
|
||||||
@@ -188,6 +210,7 @@ export const writeVarString = (encoder, str) => {
|
|||||||
*
|
*
|
||||||
* TODO: can be improved!
|
* TODO: can be improved!
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder The enUint8Arr
|
* @param {Encoder} encoder The enUint8Arr
|
||||||
* @param {Encoder} append The BinaryEncoder to be written.
|
* @param {Encoder} append The BinaryEncoder to be written.
|
||||||
*/
|
*/
|
||||||
@@ -196,6 +219,7 @@ export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder,
|
|||||||
/**
|
/**
|
||||||
* Append an arrayBuffer to the encoder.
|
* Append an arrayBuffer to the encoder.
|
||||||
*
|
*
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {ArrayBuffer} arrayBuffer
|
* @param {ArrayBuffer} arrayBuffer
|
||||||
*/
|
*/
|
||||||
@@ -209,6 +233,7 @@ export const writeArrayBuffer = (encoder, arrayBuffer) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @function
|
||||||
* @param {Encoder} encoder
|
* @param {Encoder} encoder
|
||||||
* @param {ArrayBuffer} arrayBuffer
|
* @param {ArrayBuffer} arrayBuffer
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @module globals
|
||||||
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
export const Uint8Array_ = Uint8Array
|
export const Uint8Array_ = Uint8Array
|
||||||
@@ -54,8 +58,6 @@ export const until = (timeout, check) => createPromise((resolve, reject) => {
|
|||||||
|
|
||||||
export const error = description => new Error(description)
|
export const error = description => new Error(description)
|
||||||
|
|
||||||
export const max = (a, b) => a > b ? a : b
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} t Time to wait
|
* @param {number} t Time to wait
|
||||||
* @return {Promise} Promise that is resolved after t ms
|
* @return {Promise} Promise that is resolved after t ms
|
||||||
|
|||||||
11
lib/idb.js
11
lib/idb.js
@@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @module lib/idb
|
||||||
|
*/
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
|
|
||||||
import * as globals from './globals.js'
|
import * as globals from './globals.js'
|
||||||
@@ -12,6 +16,8 @@ export const rtop = request => globals.createPromise((resolve, reject) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Function} initDB Called when the database is first created
|
||||||
* @return {Promise<IDBDatabase>}
|
* @return {Promise<IDBDatabase>}
|
||||||
*/
|
*/
|
||||||
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
|
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
|
||||||
@@ -115,7 +121,7 @@ export const getAllKeysValues = (store, range) =>
|
|||||||
* Iterate on keys and values
|
* Iterate on keys and values
|
||||||
* @param {IDBObjectStore} store
|
* @param {IDBObjectStore} store
|
||||||
* @param {IDBKeyRange?} keyrange
|
* @param {IDBKeyRange?} keyrange
|
||||||
* @param {function(any, any)} f Return true in order to continue the cursor
|
* @param {Function} f Return true in order to continue the cursor
|
||||||
*/
|
*/
|
||||||
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
|
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
|
||||||
const request = store.openCursor(keyrange)
|
const request = store.openCursor(keyrange)
|
||||||
@@ -135,9 +141,10 @@ export const iterate = (store, keyrange, f) => globals.createPromise((resolve, r
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterate on the keys (no values)
|
* Iterate on the keys (no values)
|
||||||
|
*
|
||||||
* @param {IDBObjectStore} store
|
* @param {IDBObjectStore} store
|
||||||
* @param {IDBKeyRange} keyrange
|
* @param {IDBKeyRange} keyrange
|
||||||
* @param {function(IDBCursor)} f Call `idbcursor.continue()` to iterate further
|
* @param {function} f Call `idbcursor.continue()` to iterate further
|
||||||
*/
|
*/
|
||||||
export const iterateKeys = (store, keyrange, f) => {
|
export const iterateKeys = (store, keyrange, f) => {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as test from './test.js'
|
import * as test from './testing.js'
|
||||||
import * as idb from './idb.js'
|
import * as idb from './idb.js'
|
||||||
import * as logging from './logging.js'
|
import * as logging from './logging.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @module logging
|
||||||
|
*/
|
||||||
|
|
||||||
import * as globals from './globals.js'
|
import * as globals from './globals.js'
|
||||||
|
|
||||||
|
|||||||
28
lib/math.js
28
lib/math.js
@@ -1,2 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @module math
|
||||||
|
*/
|
||||||
export const floor = Math.floor
|
export const floor = Math.floor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @return {number} The sum of a and b
|
||||||
|
*/
|
||||||
|
export const add = (a, b) => a + b
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @return {number} The smaller element of a and b
|
||||||
|
*/
|
||||||
|
export const min = (a, b) => a < b ? a : b
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @return {number} The bigger element of a and b
|
||||||
|
*/
|
||||||
|
export const max = (a, b) => a > b ? a : b
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const mutex = createMutex()
|
* const mutex = createMutex()
|
||||||
* mutex(function () {
|
* mutex(() => {
|
||||||
* // This function is immediately executed
|
* // This function is immediately executed
|
||||||
* mutex(function () {
|
* mutex(() => {
|
||||||
* // This function is never executed, as it is called with the same
|
* // This function is not executed, as the mutex is already active.
|
||||||
* // mutex function
|
|
||||||
* })
|
* })
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @module number
|
||||||
|
*/
|
||||||
|
|
||||||
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
||||||
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
||||||
|
|||||||
14
lib/object.js
Normal file
14
lib/object.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
export const create = Object.create(null)
|
||||||
|
|
||||||
|
export const keys = Object.keys
|
||||||
|
|
||||||
|
export const equalFlat = (a, b) => {
|
||||||
|
const keys = Object.keys(a)
|
||||||
|
let eq = keys.length === Object.keys(b).length
|
||||||
|
for (let i = 0; i < keys.length && eq; i++) {
|
||||||
|
const key = keys[i]
|
||||||
|
eq = a[key] === b[key]
|
||||||
|
}
|
||||||
|
return eq
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* @module prng
|
||||||
|
*/
|
||||||
const N = 624
|
const N = 624
|
||||||
const M = 397
|
const M = 397
|
||||||
|
|
||||||
function twist (u, v) {
|
const twist = (u, v) => ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
||||||
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextState (state) {
|
const nextState = (state) => {
|
||||||
let p = 0
|
let p = 0
|
||||||
let j
|
let j
|
||||||
for (j = N - M + 1; --j; p++) {
|
for (j = N - M + 1; --j; p++) {
|
||||||
@@ -29,7 +30,7 @@ function nextState (state) {
|
|||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export default class Mt19937 {
|
export class Mt19937 {
|
||||||
/**
|
/**
|
||||||
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
||||||
*/
|
*/
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @module prng
|
||||||
|
*/
|
||||||
|
|
||||||
import Mt19937 from './Mt19937.js'
|
import { Mt19937 } from './Mt19937.js'
|
||||||
import Xoroshiro128plus from './Xoroshiro128plus.js'
|
import { Xoroshiro128plus } from './Xoroshiro128plus.js'
|
||||||
import Xorshift32 from './Xorshift32.js'
|
import { Xorshift32 } from './Xorshift32.js'
|
||||||
import * as time from '../../time.js'
|
import * as time from '../../time.js'
|
||||||
|
|
||||||
const DIAMETER = 300
|
const DIAMETER = 300
|
||||||
const NUMBERS = 10000
|
const NUMBERS = 10000
|
||||||
|
|
||||||
function runPRNG (name, Gen) {
|
const runPRNG = (name, Gen) => {
|
||||||
console.log('== ' + name + ' ==')
|
console.log('== ' + name + ' ==')
|
||||||
const gen = new Gen(1234)
|
const gen = new Gen(1234)
|
||||||
let head = 0
|
let head = 0
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @module prng
|
||||||
|
*/
|
||||||
|
|
||||||
import Xorshift32 from './Xorshift32.js'
|
import { Xorshift32 } from './Xorshift32.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
||||||
@@ -14,7 +17,7 @@ import Xorshift32 from './Xorshift32.js'
|
|||||||
*
|
*
|
||||||
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
|
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
|
||||||
*/
|
*/
|
||||||
export default class Xoroshiro128plus {
|
export class Xoroshiro128plus {
|
||||||
constructor (seed) {
|
constructor (seed) {
|
||||||
this.seed = seed
|
this.seed = seed
|
||||||
// This is a variant of Xoroshiro128plus to fill the initial state
|
// This is a variant of Xoroshiro128plus to fill the initial state
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* @module prng
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
|
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
|
||||||
*/
|
*/
|
||||||
export default class Xorshift32 {
|
export class Xorshift32 {
|
||||||
/**
|
/**
|
||||||
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
||||||
*/
|
*/
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* @module prng
|
||||||
|
*/
|
||||||
|
|
||||||
import * as binary from '../binary.js'
|
import * as binary from '../binary.js'
|
||||||
import { fromCharCode, fromCodePoint } from '../string.js'
|
import { fromCharCode, fromCodePoint } from '../string.js'
|
||||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
||||||
import * as math from '../math.js'
|
import * as math from '../math.js'
|
||||||
|
|
||||||
import DefaultPRNG from './PRNG/Xoroshiro128plus.js'
|
import { Xoroshiro128plus as DefaultPRNG } from './PRNG/Xoroshiro128plus.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Description of the function
|
* Description of the function
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @module prng
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*TODO: enable tests
|
*TODO: enable tests
|
||||||
import * as rt from '../rich-text/formatters.mjs'
|
import * as rt from '../rich-text/formatters.js''
|
||||||
import { test } from '../test/test.mjs'
|
import { test } from '../test/test.js''
|
||||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs'
|
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.js''
|
||||||
import Xorshift32 from './PRNG/Xorshift32.mjs'
|
import Xorshift32 from './PRNG/Xorshift32.js''
|
||||||
import MT19937 from './PRNG/Mt19937.mjs'
|
import MT19937 from './PRNG/Mt19937.js''
|
||||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs'
|
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.js''
|
||||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs'
|
import { MAX_SAFE_INTEGER } from '../number/constants.js''
|
||||||
import { BIT32 } from '../binary/constants.mjs'
|
import { BIT32 } from '../binary/constants.js''
|
||||||
|
|
||||||
function init (Gen) {
|
function init (Gen) {
|
||||||
return {
|
return {
|
||||||
3
lib/random.js
Normal file
3
lib/random.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/**
|
||||||
|
* @module random
|
||||||
|
*/
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @module string
|
||||||
|
*/
|
||||||
|
|
||||||
export const fromCharCode = String.fromCharCode
|
export const fromCharCode = String.fromCharCode
|
||||||
export const fromCodePoint = String.fromCodePoint
|
export const fromCodePoint = String.fromCodePoint
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @module testing
|
||||||
|
*/
|
||||||
|
|
||||||
import * as logging from './logging.js'
|
import * as logging from './logging.js'
|
||||||
import simpleDiff from './simpleDiff.js'
|
import { simpleDiff } from './diff.js'
|
||||||
|
|
||||||
export const run = async (name, f) => {
|
export const run = async (name, f) => {
|
||||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
||||||
391
package-lock.json
generated
391
package-lock.json
generated
@@ -1,21 +1,94 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-71",
|
"version": "13.0.0-76",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/code-frame": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/highlight": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@babel/highlight": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"chalk": "^2.0.0",
|
||||||
|
"esutils": "^2.0.2",
|
||||||
|
"js-tokens": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^1.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^3.2.1",
|
||||||
|
"escape-string-regexp": "^1.0.5",
|
||||||
|
"supports-color": "^5.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/estree": {
|
"@types/estree": {
|
||||||
"version": "0.0.38",
|
"version": "0.0.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz",
|
||||||
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
|
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/events": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "6.0.110",
|
"version": "6.0.110",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
|
||||||
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
|
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/ws": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/events": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"abab": {
|
"abab": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||||
@@ -110,17 +183,6 @@
|
|||||||
"integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
|
"integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"align-text": {
|
|
||||||
"version": "0.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
|
|
||||||
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"kind-of": "^3.0.2",
|
|
||||||
"longest": "^1.0.1",
|
|
||||||
"repeat-string": "^1.5.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ansi-escapes": {
|
"ansi-escapes": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
|
||||||
@@ -1577,6 +1639,12 @@
|
|||||||
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
|
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"bluebird": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"boolbase": {
|
"boolbase": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
@@ -1631,12 +1699,6 @@
|
|||||||
"integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
|
"integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"camelcase": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
|
|
||||||
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"camelcase-keys": {
|
"camelcase-keys": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||||
@@ -1662,22 +1724,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"center-align": {
|
"catharsis": {
|
||||||
"version": "0.1.3",
|
"version": "0.8.9",
|
||||||
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz",
|
||||||
"integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
|
"integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"align-text": "^0.1.3",
|
"underscore-contrib": "~0.3.0"
|
||||||
"lazy-cache": "^1.0.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lazy-cache": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
|
|
||||||
"integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
@@ -1761,17 +1814,6 @@
|
|||||||
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
|
"integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cliui": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
|
|
||||||
"integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"center-align": "^0.1.1",
|
|
||||||
"right-align": "^0.1.1",
|
|
||||||
"wordwrap": "0.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"clone": {
|
"clone": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
@@ -3079,9 +3121,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extend": {
|
"extend": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"extend-shallow": {
|
"extend-shallow": {
|
||||||
@@ -3984,6 +4026,12 @@
|
|||||||
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"graceful-readlink": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"har-schema": {
|
"har-schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||||
@@ -4585,6 +4633,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"jest-worker": {
|
||||||
|
"version": "23.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-23.2.0.tgz",
|
||||||
|
"integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"merge-stream": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||||
@@ -4609,12 +4666,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"js2xmlparser": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz",
|
||||||
|
"integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"xmlcreate": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jsbn": {
|
"jsbn": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"jsdoc": {
|
||||||
|
"version": "3.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz",
|
||||||
|
"integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"babylon": "7.0.0-beta.19",
|
||||||
|
"bluebird": "~3.5.0",
|
||||||
|
"catharsis": "~0.8.9",
|
||||||
|
"escape-string-regexp": "~1.0.5",
|
||||||
|
"js2xmlparser": "~3.0.0",
|
||||||
|
"klaw": "~2.0.0",
|
||||||
|
"marked": "~0.3.6",
|
||||||
|
"mkdirp": "~0.5.1",
|
||||||
|
"requizzle": "~0.2.1",
|
||||||
|
"strip-json-comments": "~2.0.1",
|
||||||
|
"taffydb": "2.6.2",
|
||||||
|
"underscore": "~1.8.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"babylon": {
|
||||||
|
"version": "7.0.0-beta.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz",
|
||||||
|
"integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"klaw": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"taffydb": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
|
||||||
|
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"jsdom": {
|
"jsdom": {
|
||||||
"version": "7.2.2",
|
"version": "7.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz",
|
||||||
@@ -5009,12 +5118,6 @@
|
|||||||
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
|
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"longest": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"loose-envify": {
|
"loose-envify": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
|
||||||
@@ -5120,6 +5223,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"merge-stream": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"readable-stream": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"micromatch": {
|
"micromatch": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
|
||||||
@@ -5879,14 +5991,6 @@
|
|||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"fast-diff": "1.1.2"
|
"fast-diff": "1.1.2"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"extend": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"randomatic": {
|
"randomatic": {
|
||||||
@@ -6198,6 +6302,23 @@
|
|||||||
"resolve-from": "^1.0.0"
|
"resolve-from": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"requizzle": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"underscore": "~1.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"underscore": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz",
|
||||||
@@ -6233,15 +6354,6 @@
|
|||||||
"signal-exit": "^3.0.2"
|
"signal-exit": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"right-align": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
|
|
||||||
"integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"align-text": "^0.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||||
@@ -6261,6 +6373,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rollup-cli": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-cli/-/rollup-cli-1.0.9.tgz",
|
||||||
|
"integrity": "sha1-N/ShwgYxHikuMpfql3eduKIduZQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"rollup-plugin-babel": {
|
"rollup-plugin-babel": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz",
|
||||||
@@ -6380,12 +6498,46 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup-plugin-uglify": {
|
"rollup-plugin-uglify": {
|
||||||
"version": "1.0.2",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.0.tgz",
|
||||||
"integrity": "sha1-1KpvXfE1Iurhuhd4DHxMcJYDg1k=",
|
"integrity": "sha512-XtzZd159QuOaXNvcxyBcbUCSoBsv5YYWK+7ZwUyujSmISst8avRfjWlp7cGu8T2O52OJnpEBvl+D4WLV1k1iQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"uglify-js": "^2.6.1"
|
"@babel/code-frame": "^7.0.0",
|
||||||
|
"jest-worker": "^23.2.0",
|
||||||
|
"serialize-javascript": "^1.5.0",
|
||||||
|
"uglify-js": "^3.4.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rollup-plugin-uglify-es": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify-es/-/rollup-plugin-uglify-es-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-5FZE8raFpZq9uTY0ByB6A6e1qbc=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"uglify-es": "3.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
|
||||||
|
"integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-readlink": ">= 1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uglify-es": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.0.3.tgz",
|
||||||
|
"integrity": "sha1-Y8yEqpRos0lzpIh3h8ZMAaiodXY=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"commander": "~2.9.0",
|
||||||
|
"source-map": "~0.5.1",
|
||||||
|
"uglify-to-browserify": "~1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup-pluginutils": {
|
"rollup-pluginutils": {
|
||||||
@@ -6483,6 +6635,12 @@
|
|||||||
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
|
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"serialize-javascript": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"serve-index": {
|
"serve-index": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
|
||||||
@@ -6974,6 +7132,15 @@
|
|||||||
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"tui-jsdoc-template": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tui-jsdoc-template/-/tui-jsdoc-template-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-oqw0IYaot86VJ2owKBozJnilgta0Z55x8r9PeHj7vb+jDoSvJGRUQUcgs56SZh9HE20fx54Pe75p84X85/ygLA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"cheerio": "^0.22.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tunnel-agent": {
|
"tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
@@ -7012,14 +7179,27 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "2.8.29",
|
"version": "3.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
|
||||||
"integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
|
"integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"source-map": "~0.5.1",
|
"commander": "~2.17.1",
|
||||||
"uglify-to-browserify": "~1.0.0",
|
"source-map": "~0.6.1"
|
||||||
"yargs": "~3.10.0"
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": {
|
||||||
|
"version": "2.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
|
||||||
|
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uglify-to-browserify": {
|
"uglify-to-browserify": {
|
||||||
@@ -7029,6 +7209,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"underscore": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
|
||||||
|
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"underscore-contrib": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz",
|
||||||
|
"integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"underscore": "1.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"underscore": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"uniq": {
|
"uniq": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
|
||||||
@@ -7173,18 +7376,6 @@
|
|||||||
"isexe": "^2.0.0"
|
"isexe": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"window-size": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
|
|
||||||
"integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"wordwrap": {
|
|
||||||
"version": "0.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
|
|
||||||
"integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
@@ -7215,6 +7406,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"xmlcreate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||||
@@ -7226,18 +7423,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
||||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
|
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
|
||||||
"yargs": {
|
|
||||||
"version": "3.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
|
|
||||||
"integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"camelcase": "^1.0.2",
|
|
||||||
"cliui": "^2.1.0",
|
|
||||||
"decamelize": "^1.0.0",
|
|
||||||
"window-size": "0.1.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
69
package.json
69
package.json
@@ -1,35 +1,55 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-71",
|
"version": "13.0.0-76",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "A ",
|
||||||
"main": "./build/node/index.js",
|
"main": "./build/yjs.js",
|
||||||
"module": "./src/index.js",
|
"module": "./index.js'",
|
||||||
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run lint",
|
||||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
"build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
|
||||||
"lint": "standard src/**/*.js test/**/*.js tests-lib/**/*.js",
|
"watch": "rollup -wc",
|
||||||
"docs": "esdoc",
|
"debug": "concurrently 'npm run watch' 'cutest-serve build/y.test.js -o'",
|
||||||
|
"lint": "standard **/*.js",
|
||||||
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||||
"serve-docs": "npm run docs && serve ./docs/",
|
"serve-docs": "npm run docs && serve ./docs/",
|
||||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
"postversion": "npm run build",
|
||||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
"websocket-server": "node ./provider/websocket/server.js",
|
||||||
"postversion": "npm run dist"
|
"now-start": "npm run websocket-server"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src/*",
|
|
||||||
".esdoc.json",
|
|
||||||
"docs/*",
|
|
||||||
"build/*",
|
"build/*",
|
||||||
|
"bindings/*",
|
||||||
|
"docs/*",
|
||||||
|
"examples/*",
|
||||||
"lib/*",
|
"lib/*",
|
||||||
|
"persistences/*",
|
||||||
|
"protocols/*",
|
||||||
"provider/*",
|
"provider/*",
|
||||||
"bindings/*"
|
"bindings/*",
|
||||||
|
"structs/*",
|
||||||
|
"tests/*",
|
||||||
|
"types/*",
|
||||||
|
"utils/*",
|
||||||
|
"index.js",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
|
"dictionaries": {
|
||||||
|
"doc": "docs",
|
||||||
|
"example": "examples",
|
||||||
|
"test": "tests",
|
||||||
|
"lib": "./"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"y-websockets": "provider/websocket/server.js"
|
"y-websockets": "provider/websocket/server.js"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"/y.js",
|
"/build",
|
||||||
"/y.js.map"
|
"/node_modules",
|
||||||
|
"/rollup.test.js",
|
||||||
|
"/rollup.test.js"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -37,13 +57,7 @@
|
|||||||
"url": "https://github.com/y-js/yjs.git"
|
"url": "https://github.com/y-js/yjs.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Yjs",
|
"crdt"
|
||||||
"OT",
|
|
||||||
"Collaboration",
|
|
||||||
"Synchronization",
|
|
||||||
"ShareJS",
|
|
||||||
"Coweb",
|
|
||||||
"Concurrency"
|
|
||||||
],
|
],
|
||||||
"author": "Kevin Jahns",
|
"author": "Kevin Jahns",
|
||||||
"email": "kevin.jahns@rwth-aachen.de",
|
"email": "kevin.jahns@rwth-aachen.de",
|
||||||
@@ -53,6 +67,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "http://y-js.org",
|
"homepage": "http://y-js.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/ws": "^6.0.1",
|
||||||
"babel-cli": "^6.26.0",
|
"babel-cli": "^6.26.0",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babel-plugin-transform-regenerator": "^6.26.0",
|
"babel-plugin-transform-regenerator": "^6.26.0",
|
||||||
@@ -62,6 +77,7 @@
|
|||||||
"cutest": "^0.1.9",
|
"cutest": "^0.1.9",
|
||||||
"esdoc": "^1.1.0",
|
"esdoc": "^1.1.0",
|
||||||
"esdoc-standard-plugin": "^1.0.0",
|
"esdoc-standard-plugin": "^1.0.0",
|
||||||
|
"jsdoc": "^3.5.5",
|
||||||
"prosemirror-example-setup": "^1.0.1",
|
"prosemirror-example-setup": "^1.0.1",
|
||||||
"prosemirror-schema-basic": "^1.0.0",
|
"prosemirror-schema-basic": "^1.0.0",
|
||||||
"prosemirror-state": "^1.2.2",
|
"prosemirror-state": "^1.2.2",
|
||||||
@@ -69,15 +85,18 @@
|
|||||||
"quill": "^1.3.6",
|
"quill": "^1.3.6",
|
||||||
"quill-cursors": "^1.0.3",
|
"quill-cursors": "^1.0.3",
|
||||||
"rollup": "^0.58.2",
|
"rollup": "^0.58.2",
|
||||||
|
"rollup-cli": "^1.0.9",
|
||||||
"rollup-plugin-babel": "^2.7.1",
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
"rollup-plugin-commonjs": "^8.4.1",
|
"rollup-plugin-commonjs": "^8.4.1",
|
||||||
"rollup-plugin-inject": "^2.2.0",
|
"rollup-plugin-inject": "^2.2.0",
|
||||||
"rollup-plugin-multi-entry": "^2.0.2",
|
"rollup-plugin-multi-entry": "^2.0.2",
|
||||||
"rollup-plugin-node-resolve": "^3.4.0",
|
"rollup-plugin-node-resolve": "^3.4.0",
|
||||||
"rollup-plugin-uglify": "^1.0.2",
|
"rollup-plugin-uglify": "^6.0.0",
|
||||||
|
"rollup-plugin-uglify-es": "0.0.1",
|
||||||
"rollup-regenerator-runtime": "^6.23.1",
|
"rollup-regenerator-runtime": "^6.23.1",
|
||||||
"rollup-watch": "^3.2.2",
|
"rollup-watch": "^3.2.2",
|
||||||
"standard": "^11.0.1"
|
"standard": "^11.0.1",
|
||||||
|
"tui-jsdoc-template": "^1.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^6.1.0"
|
"ws": "^6.1.0"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
/*
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import * as encoding from '../../lib/encoding.js'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../../lib/decoding.js'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { createMutualExclude } from '../../lib/mutualExclude.js'
|
import { createMutex } from '../lib/mutex.js'
|
||||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.js'
|
||||||
|
|
||||||
function createFilePath (persistence, roomName) {
|
function createFilePath (persistence, roomName) {
|
||||||
@@ -10,10 +11,10 @@ function createFilePath (persistence, roomName) {
|
|||||||
return path.join(persistence.dir, roomName)
|
return path.join(persistence.dir, roomName)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FilePersistence {
|
export class FilePersistence {
|
||||||
constructor (dir) {
|
constructor (dir) {
|
||||||
this.dir = dir
|
this.dir = dir
|
||||||
this._mutex = createMutualExclude()
|
this._mutex = createMutex()
|
||||||
}
|
}
|
||||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
@@ -70,3 +71,4 @@ export default class FilePersistence {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
553
persistences/IndexedDBPersistence.js
Normal file
553
persistences/IndexedDBPersistence.js
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
/*
|
||||||
|
import { Y } from '../utils/Y.js'
|
||||||
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||||
|
|
||||||
|
function rtop (request) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
request.onerror = function (event) {
|
||||||
|
reject(new Error(event.target.error))
|
||||||
|
}
|
||||||
|
request.onblocked = function () {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
request.onsuccess = function (event) {
|
||||||
|
resolve(event.target.result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDB (room) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
let request = indexedDB.open(room)
|
||||||
|
request.onupgradeneeded = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
if (db.objectStoreNames.contains('updates')) {
|
||||||
|
db.deleteObjectStore('updates')
|
||||||
|
}
|
||||||
|
db.createObjectStore('updates', {autoIncrement: true})
|
||||||
|
}
|
||||||
|
request.onerror = function (event) {
|
||||||
|
reject(new Error(event.target.error))
|
||||||
|
}
|
||||||
|
request.onblocked = function () {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
request.onsuccess = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
db.onversionchange = function () { db.close() }
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function persist (room) {
|
||||||
|
let t = room.db.transaction(['updates'], 'readwrite')
|
||||||
|
let updatesStore = t.objectStore('updates')
|
||||||
|
return rtop(updatesStore.getAll())
|
||||||
|
.then(updates => {
|
||||||
|
// apply all previous updates before deleting them
|
||||||
|
room.mutex(() => {
|
||||||
|
updates.forEach(update => {
|
||||||
|
decodePersisted(y, new BinaryDecoder(update))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encodeStructsDS(y, encoder)
|
||||||
|
// delete all pending updates
|
||||||
|
rtop(updatesStore.clear()).then(() => {
|
||||||
|
// write current model
|
||||||
|
updatesStore.put(encoder.createBuffer())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUpdate (room, updateBuffer) {
|
||||||
|
const db = room.db
|
||||||
|
if (db !== null) {
|
||||||
|
const t = db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||||
|
rtop(updatesStore.count()).then(cnt => {
|
||||||
|
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||||
|
persist(room)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return updatePut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerRoomInPersistence (documentsDB, roomName) {
|
||||||
|
return documentsDB.then(
|
||||||
|
db => Promise.all([
|
||||||
|
db,
|
||||||
|
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||||
|
])
|
||||||
|
).then(
|
||||||
|
([db, doc]) => {
|
||||||
|
if (doc === undefined) {
|
||||||
|
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFERRED_TRIM_SIZE = 400
|
||||||
|
|
||||||
|
export class IndexedDBPersistence {
|
||||||
|
constructor () {
|
||||||
|
this._rooms = new Map()
|
||||||
|
this._documentsDB = new Promise(function (resolve, reject) {
|
||||||
|
let request = indexedDB.open('_yjs_documents')
|
||||||
|
request.onupgradeneeded = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
if (db.objectStoreNames.contains('documents')) {
|
||||||
|
db.deleteObjectStore('documents')
|
||||||
|
}
|
||||||
|
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||||
|
}
|
||||||
|
request.onerror = function (event) {
|
||||||
|
reject(new Error(event.target.error))
|
||||||
|
}
|
||||||
|
request.onblocked = function () {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
request.onsuccess = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
db.onversionchange = function () { db.close() }
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
addEventListener('unload', () => {
|
||||||
|
// close everything when page unloads
|
||||||
|
this._rooms.forEach(room => {
|
||||||
|
if (room.db !== null) {
|
||||||
|
room.db.close()
|
||||||
|
} else {
|
||||||
|
room.dbPromise.then(db => db.close())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this._documentsDB.then(db => db.close())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getAllDocuments () {
|
||||||
|
return this._documentsDB.then(
|
||||||
|
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||||
|
this._documentsDB.then(
|
||||||
|
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_createYInstance (roomName) {
|
||||||
|
const room = this._rooms.get(roomName)
|
||||||
|
if (room !== undefined) {
|
||||||
|
return room.y
|
||||||
|
}
|
||||||
|
const y = new Y()
|
||||||
|
return openDB(roomName).then(
|
||||||
|
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||||
|
).then(
|
||||||
|
updates =>
|
||||||
|
y.transact(() => {
|
||||||
|
updates.forEach(update => {
|
||||||
|
decodePersisted(y, new BinaryDecoder(update))
|
||||||
|
})
|
||||||
|
}, true)
|
||||||
|
).then(() => Promise.resolve(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistStructsDS (roomName, structsDS) {
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||||
|
encoder.writeArrayBuffer(structsDS)
|
||||||
|
return openDB(roomName).then(db => {
|
||||||
|
const t = db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistStructs (roomName, structs) {
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encoder.writeVarUint(PERSIST_UPDATE)
|
||||||
|
encoder.writeArrayBuffer(structs)
|
||||||
|
return openDB(roomName).then(db => {
|
||||||
|
const t = db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connectY (roomName, y) {
|
||||||
|
if (this._rooms.has(roomName)) {
|
||||||
|
throw new Error('A Y instance is already bound to this room!')
|
||||||
|
}
|
||||||
|
let room = {
|
||||||
|
db: null,
|
||||||
|
dbPromise: null,
|
||||||
|
channel: null,
|
||||||
|
mutex: createMutex(),
|
||||||
|
y
|
||||||
|
}
|
||||||
|
if (typeof BroadcastChannel !== 'undefined') {
|
||||||
|
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||||
|
room.channel.addEventListener('message', e => {
|
||||||
|
room.mutex(function () {
|
||||||
|
decodePersisted(y, new BinaryDecoder(e.data))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
y.on('destroyed', () => {
|
||||||
|
this.disconnectY(roomName, y)
|
||||||
|
})
|
||||||
|
y.on('afterTransaction', (y, transaction) => {
|
||||||
|
room.mutex(() => {
|
||||||
|
if (transaction.encodedStructsLen > 0) {
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
const update = new BinaryEncoder()
|
||||||
|
encodeUpdate(y, transaction.encodedStructs, update)
|
||||||
|
const updateBuffer = update.createBuffer()
|
||||||
|
if (room.channel !== null) {
|
||||||
|
room.channel.postMessage(updateBuffer)
|
||||||
|
}
|
||||||
|
if (transaction.encodedStructsLen > 0
|
||||||
|
import { Y } from '../utils/Y.js'
|
||||||
|
import { createMutex } from '../lib/mutex.js'
|
||||||
|
import { decodePersisted, encodeStructsDS, encodeUpdate, PERSIST_STRUCTS_DS, PERSIST_UPDATE } from './decodePersisted.js'
|
||||||
|
|
||||||
|
function rtop (request) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
request.onerror = function (event) {
|
||||||
|
reject(new Error(event.target.error))
|
||||||
|
}
|
||||||
|
request.onblocked = function () {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
request.onsuccess = function (event) {
|
||||||
|
resolve(event.target.result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDB (room) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
let request = indexedDB.open(room)
|
||||||
|
request.onupgradeneeded = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
if (db.objectStoreNames.contains('updates')) {
|
||||||
|
db.deleteObjectStore('updates')
|
||||||
|
}
|
||||||
|
db.createObjectStore('updates', {autoIncrement: true})
|
||||||
|
}
|
||||||
|
request.onerror = function (event) {
|
||||||
|
reject(new Error(event.target.error))
|
||||||
|
}
|
||||||
|
request.onblocked = function () {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
request.onsuccess = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
db.onversionchange = function () { db.close() }
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function persist (room) {
|
||||||
|
let t = room.db.transaction(['updates'], 'readwrite')
|
||||||
|
let updatesStore = t.objectStore('updates')
|
||||||
|
return rtop(updatesStore.getAll())
|
||||||
|
.then(updates => {
|
||||||
|
// apply all previous updates before deleting them
|
||||||
|
room.mutex(() => {
|
||||||
|
updates.forEach(update => {
|
||||||
|
decodePersisted(y, new BinaryDecoder(update))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encodeStructsDS(y, encoder)
|
||||||
|
// delete all pending updates
|
||||||
|
rtop(updatesStore.clear()).then(() => {
|
||||||
|
// write current model
|
||||||
|
updatesStore.put(encoder.createBuffer())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUpdate (room, updateBuffer) {
|
||||||
|
const db = room.db
|
||||||
|
if (db !== null) {
|
||||||
|
const t = db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||||
|
rtop(updatesStore.count()).then(cnt => {
|
||||||
|
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||||
|
persist(room)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return updatePut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerRoomInPersistence (documentsDB, roomName) {
|
||||||
|
return documentsDB.then(
|
||||||
|
db => Promise.all([
|
||||||
|
db,
|
||||||
|
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||||
|
])
|
||||||
|
).then(
|
||||||
|
([db, doc]) => {
|
||||||
|
if (doc === undefined) {
|
||||||
|
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFERRED_TRIM_SIZE = 400
|
||||||
|
|
||||||
|
export class IndexedDBPersistence {
|
||||||
|
constructor () {
|
||||||
|
this._rooms = new Map()
|
||||||
|
this._documentsDB = new Promise(function (resolve, reject) {
|
||||||
|
let request = indexedDB.open('_yjs_documents')
|
||||||
|
request.onupgradeneeded = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
if (db.objectStoreNames.contains('documents')) {
|
||||||
|
db.deleteObjectStore('documents')
|
||||||
|
}
|
||||||
|
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||||
|
}
|
||||||
|
request.onerror = function (event) {
|
||||||
|
reject(new Error(event.target.error))
|
||||||
|
}
|
||||||
|
request.onblocked = function () {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
request.onsuccess = function (event) {
|
||||||
|
const db = event.target.result
|
||||||
|
db.onversionchange = function () { db.close() }
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
addEventListener('unload', () => {
|
||||||
|
// close everything when page unloads
|
||||||
|
this._rooms.forEach(room => {
|
||||||
|
if (room.db !== null) {
|
||||||
|
room.db.close()
|
||||||
|
} else {
|
||||||
|
room.dbPromise.then(db => db.close())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this._documentsDB.then(db => db.close())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getAllDocuments () {
|
||||||
|
return this._documentsDB.then(
|
||||||
|
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||||
|
this._documentsDB.then(
|
||||||
|
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_createYInstance (roomName) {
|
||||||
|
const room = this._rooms.get(roomName)
|
||||||
|
if (room !== undefined) {
|
||||||
|
return room.y
|
||||||
|
}
|
||||||
|
const y = new Y()
|
||||||
|
return openDB(roomName).then(
|
||||||
|
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||||
|
).then(
|
||||||
|
updates =>
|
||||||
|
y.transact(() => {
|
||||||
|
updates.forEach(update => {
|
||||||
|
decodePersisted(y, new BinaryDecoder(update))
|
||||||
|
})
|
||||||
|
}, true)
|
||||||
|
).then(() => Promise.resolve(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistStructsDS (roomName, structsDS) {
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||||
|
encoder.writeArrayBuffer(structsDS)
|
||||||
|
return openDB(roomName).then(db => {
|
||||||
|
const t = db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistStructs (roomName, structs) {
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encoder.writeVarUint(PERSIST_UPDATE)
|
||||||
|
encoder.writeArrayBuffer(structs)
|
||||||
|
return openDB(roomName).then(db => {
|
||||||
|
const t = db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connectY (roomName, y) {
|
||||||
|
if (this._rooms.has(roomName)) {
|
||||||
|
throw new Error('A Y instance is already bound to this room!')
|
||||||
|
}
|
||||||
|
let room = {
|
||||||
|
db: null,
|
||||||
|
dbPromise: null,
|
||||||
|
channel: null,
|
||||||
|
mutex: createMutex(),
|
||||||
|
y
|
||||||
|
}
|
||||||
|
if (typeof BroadcastChannel !== 'undefined') {
|
||||||
|
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||||
|
room.channel.addEventListener('message', e => {
|
||||||
|
room.mutex(function () {
|
||||||
|
decodePersisted(y, new BinaryDecoder(e.data))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
y.on('destroyed', () => {
|
||||||
|
this.disconnectY(roomName, y)
|
||||||
|
})
|
||||||
|
y.on('afterTransaction', (y, transaction) => {
|
||||||
|
room.mutex(() => {
|
||||||
|
if (transaction.encodedStructsLen > 0) {
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
const update = new BinaryEncoder()
|
||||||
|
encodeUpdate(y, transaction.encodedStructs, update)
|
||||||
|
const updateBuffer = update.createBuffer()
|
||||||
|
if (room.channel !== null) {
|
||||||
|
room.channel.postMessage(updateBuffer)
|
||||||
|
}
|
||||||
|
if (transaction.encodedStructsLen > 0) {
|
||||||
|
if (room.db !== null) {
|
||||||
|
saveUpdate(room, updateBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// register document in documentsDB
|
||||||
|
this._documentsDB.then(
|
||||||
|
db =>
|
||||||
|
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||||
|
.then(
|
||||||
|
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// open room db and read existing data
|
||||||
|
return room.dbPromise = openDB(roomName)
|
||||||
|
.then(db => {
|
||||||
|
room.db = db
|
||||||
|
const t = room.db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
// write current state as update
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encodeStructsDS(y, encoder)
|
||||||
|
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||||
|
// read persisted state
|
||||||
|
return rtop(updatesStore.getAll()).then(updates => {
|
||||||
|
room.mutex(() => {
|
||||||
|
y.transact(() => {
|
||||||
|
updates.forEach(update => {
|
||||||
|
decodePersisted(y, new BinaryDecoder(update))
|
||||||
|
})
|
||||||
|
}, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disconnectY (roomName) {
|
||||||
|
const {
|
||||||
|
db, channel
|
||||||
|
} = this._rooms.get(roomName)
|
||||||
|
db.close()
|
||||||
|
if (channel !== null) {
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
this._rooms.delete(roomName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all persisted data that belongs to a room.
|
||||||
|
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||||
|
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||||
|
* will be removed from the Yjs instances.
|
||||||
|
*
|
||||||
|
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||||
|
this.disconnectY(roomName)
|
||||||
|
return rtop(indexedDB.deleteDatabase(roomName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (room.db !== null) {
|
||||||
|
saveUpdate(room, updateBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// register document in documentsDB
|
||||||
|
this._documentsDB.then(
|
||||||
|
db =>
|
||||||
|
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||||
|
.then(
|
||||||
|
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// open room db and read existing data
|
||||||
|
return room.dbPromise = openDB(roomName)
|
||||||
|
.then(db => {
|
||||||
|
room.db = db
|
||||||
|
const t = room.db.transaction(['updates'], 'readwrite')
|
||||||
|
const updatesStore = t.objectStore('updates')
|
||||||
|
// write current state as update
|
||||||
|
const encoder = new BinaryEncoder()
|
||||||
|
encodeStructsDS(y, encoder)
|
||||||
|
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||||
|
// read persisted state
|
||||||
|
return rtop(updatesStore.getAll()).then(updates => {
|
||||||
|
room.mutex(() => {
|
||||||
|
y.transact(() => {
|
||||||
|
updates.forEach(update => {
|
||||||
|
decodePersisted(y, new BinaryDecoder(update))
|
||||||
|
})
|
||||||
|
}, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disconnectY (roomName) {
|
||||||
|
const {
|
||||||
|
db, channel
|
||||||
|
} = this._rooms.get(roomName)
|
||||||
|
db.close()
|
||||||
|
if (channel !== null) {
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
this._rooms.delete(roomName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all persisted data that belongs to a room.
|
||||||
|
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||||
|
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||||
|
* will be removed from the Yjs instances.
|
||||||
|
*
|
||||||
|
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||||
|
this.disconnectY(roomName)
|
||||||
|
return rtop(indexedDB.deleteDatabase(roomName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*
|
||||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.js'
|
||||||
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
import { writeStructs } from '../MessageHandler/syncStep1.js'
|
||||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.js'
|
||||||
@@ -6,10 +7,10 @@ export const PERSIST_UPDATE = 0
|
|||||||
/**
|
/**
|
||||||
* Write an update to an encoder.
|
* Write an update to an encoder.
|
||||||
*
|
*
|
||||||
* @param {Yjs} y A Yjs instance
|
* @param {Y} y A Yjs instance
|
||||||
* @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs
|
* @param {Encoder} updateEncoder I.e. transaction.encodedStructs
|
||||||
*/
|
*
|
||||||
export function encodeUpdate (y, updateEncoder, encoder) {
|
export const encodeUpdate = (y, updateEncoder, encoder) => {
|
||||||
encoder.writeVarUint(PERSIST_UPDATE)
|
encoder.writeVarUint(PERSIST_UPDATE)
|
||||||
encoder.writeBinaryEncoder(updateEncoder)
|
encoder.writeBinaryEncoder(updateEncoder)
|
||||||
}
|
}
|
||||||
@@ -19,10 +20,10 @@ export const PERSIST_STRUCTS_DS = 1
|
|||||||
/**
|
/**
|
||||||
* Write the current Yjs data model to an encoder.
|
* Write the current Yjs data model to an encoder.
|
||||||
*
|
*
|
||||||
* @param {Yjs} y A Yjs instance
|
* @param {Y} y A Yjs instance
|
||||||
* @param {BinaryEncoder} encoder An encoder to write to
|
* @param {Encoder} encoder An encoder to write to
|
||||||
*/
|
*
|
||||||
export function encodeStructsDS (y, encoder) {
|
export const encodeStructsDS = (y, encoder) => {
|
||||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||||
writeStructs(y, encoder, new Map())
|
writeStructs(y, encoder, new Map())
|
||||||
writeDeleteSet(y, encoder)
|
writeDeleteSet(y, encoder)
|
||||||
@@ -30,10 +31,10 @@ export function encodeStructsDS (y, encoder) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Feed the Yjs instance with the persisted state
|
* Feed the Yjs instance with the persisted state
|
||||||
* @param {Yjs} y A Yjs instance.
|
* @param {Y} y A Yjs instance.
|
||||||
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
|
* @param {Decoder} decoder A Decoder instance that holds the file content.
|
||||||
*/
|
*
|
||||||
export function decodePersisted (y, decoder) {
|
export const decodePersisted = (y, decoder) => {
|
||||||
y.transact(() => {
|
y.transact(() => {
|
||||||
while (decoder.hasContent()) {
|
while (decoder.hasContent()) {
|
||||||
const contentType = decoder.readVarUint()
|
const contentType = decoder.readVarUint()
|
||||||
@@ -49,3 +50,4 @@ export function decodePersisted (y, decoder) {
|
|||||||
}
|
}
|
||||||
}, true)
|
}, true)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
5
persistences/indexeddb.js
Normal file
5
persistences/indexeddb.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as idb from '../lib/idb.js'
|
||||||
|
|
||||||
|
const bc = new BroadcastChannel('ydb-client')
|
||||||
|
|
||||||
|
idb.openDB()
|
||||||
33
protocols/auth.js
Normal file
33
protocols/auth.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import * as encoding from '../lib/encoding.js'
|
||||||
|
import * as decoding from '../lib/decoding.js'
|
||||||
|
import { Y } from '../utils/Y.js';
|
||||||
|
|
||||||
|
export const messagePermissionDenied = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {string} reason
|
||||||
|
*/
|
||||||
|
export const writePermissionDenied = (encoder, reason) => {
|
||||||
|
encoding.writeVarUint(encoder, messagePermissionDenied)
|
||||||
|
encoding.writeVarString(encoder, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback PermissionDeniedHandler
|
||||||
|
* @param {any} y
|
||||||
|
* @param {string} reason
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {Y} y
|
||||||
|
* @param {PermissionDeniedHandler} permissionDeniedHandler
|
||||||
|
*/
|
||||||
|
export const readAuthMessage = (decoder, y, permissionDeniedHandler) => {
|
||||||
|
switch (decoding.readVarUint(decoder)) {
|
||||||
|
case messagePermissionDenied: permissionDeniedHandler(y, decoding.readVarString(decoder))
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user