added prosemirror binding

This commit is contained in:
Kevin Jahns 2018-11-06 13:41:05 +01:00
parent e8060de914
commit 32b8fac37f
32 changed files with 667 additions and 254 deletions

4
.gitignore vendored
View File

@ -2,7 +2,7 @@ node_modules
bower_components
docs
/y.*
/examples/yjs-dist.js*
/examples/*/index.dist.*
.vscode
.yjsPersisted
build
build

67
bindings/BindMapping.js Normal file
View File

@ -0,0 +1,67 @@
/**
* Type that maps from Yjs type to Target type.
* Used to implement double bindings.
*
* @template Y
* @template T
*/
export default class BindMapping {
/**
*/
constructor () {
/**
* @type Map<Y, T>
*/
this.yt = new Map()
/**
* @type Map<T, Y>
*/
this.ty = new Map()
}
/**
* Map y to t. Removes all existing bindings from y and t
* @param {Y} y
* @param {T} t
*/
bind (y, t) {
const existingT = this.yt.get(y)
if (existingT !== undefined) {
this.ty.delete(existingT)
}
const existingY = this.ty.get(t)
if (existingY !== undefined) {
this.yt.delete(existingY)
}
this.yt.set(y, t)
this.ty.set(t, y)
}
/**
* @param {Y} y
* @return {boolean}
*/
hasY (y) {
return this.yt.has(y)
}
/**
* @param {T} t
* @return {boolean}
*/
hasT (t) {
return this.ty.has(t)
}
/**
* @param {Y} y
* @return {T}
*/
getY (y) {
return this.yt.get(y)
}
/**
* @param {T} t
* @return {Y}
*/
getT (t) {
return this.ty.get(t)
}
}

View File

@ -1,7 +1,7 @@
/* eslint-env browser */
import YXmlText from '../../Types/YXml/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import YXmlElement from '../../Types/YXml/YXmlElement.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlHook from '../../src/Types/YXml/YXmlHook.js'
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js'

View File

@ -1,4 +1,4 @@
import isParentOf from '../../Util/isParentOf.js'
import isParentOf from '../../src/Util/isParentOf.js'
/**
* @callback DomFilter

View File

@ -1,6 +1,6 @@
/* globals getSelection */
import { getRelativePosition } from '../../Util/relativePosition.js'
import { getRelativePosition } from '../../src/Util/relativePosition.js'
let relativeSelection = null

View File

@ -1,8 +1,8 @@
/* eslint-env browser */
/* global getSelection */
import YXmlText from '../../Types/YXml/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js'
import YXmlHook from '../../src/Types/YXml/YXmlHook.js'
import { removeDomChildrenUntilElementFound } from './util.js'
function findScrollReference (scrollingElement) {

View File

@ -2,9 +2,9 @@
import domToType from './domToType.js'
/**
* @typedef {import('../../Types/YXml/YXmlText.js').default} YXmlText
* @typedef {import('../../Types/YXml/YXmlElement.js').default} YXmlElement
* @typedef {import('../../Types/YXml/YXmlHook.js').default} YXmlHook
* @typedef {import('../../src/Types/YXml/YXmlText.js').default} YXmlText
* @typedef {import('../../src/Types/YXml/YXmlElement.js').default} YXmlElement
* @typedef {import('../../src/Types/YXml/YXmlHook.js').default} YXmlHook
* @typedef {import('./DomBinding.js').default} DomBinding
*/

View File

@ -0,0 +1,190 @@
import BindMapping from '../BindMapping.js'
import * as PModel from 'prosemirror-model'
import * as Y from '../../src/index.js'
import { createMutex } from '../../lib/mutex.js'
/**
* @typedef {import('prosemirror-view').EditorView} EditorView
* @typedef {import('prosemirror-state').EditorState} EditorState
* @typedef {BindMapping<Y.Text | Y.XmlElement, PModel.Node>} ProsemirrorMapping
*/
export default class ProsemirrorBinding {
/**
* @param {Y.XmlFragment} yDomFragment The bind source
* @param {EditorView} prosemirror The target binding
*/
constructor (yDomFragment, prosemirror) {
this.type = yDomFragment
this.prosemirror = prosemirror
const mux = createMutex()
this.mux = mux
/**
* @type {ProsemirrorMapping}
*/
const mapping = new BindMapping()
this.mapping = mapping
const oldDispatch = prosemirror.props.dispatchTransaction || null
/**
* @type {any}
*/
const updatedProps = {
dispatchTransaction: function (tr) {
// TODO: remove
const time = performance.now()
const newState = prosemirror.state.apply(tr)
mux(() => {
updateYFragment(yDomFragment, newState, mapping)
})
if (oldDispatch !== null) {
oldDispatch.call(this, tr)
} else {
prosemirror.updateState(newState)
}
console.info('time for Yjs update: ', performance.now() - time)
}
}
prosemirror.setProps(updatedProps)
yDomFragment.observeDeep(events => {
if (events.length === 0) {
return
}
mux(() => {
events.forEach(event => {
// recompute node for each parent
// except main node, compute main node in the end
let target = event.target
if (target !== yDomFragment) {
do {
if (target.constructor === Y.XmlElement) {
createNodeFromYElement(target, prosemirror.state.schema, mapping)
}
target = target._parent
} while (target._parent !== yDomFragment)
}
})
const fragmentContent = yDomFragment.toArray().map(t => createNodeIfNotExists(t, prosemirror.state.schema, mapping))
const tr = prosemirror.state.tr.replace(0, prosemirror.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
const newState = prosemirror.updateState(prosemirror.state.apply(tr))
console.log('state updated', newState, tr)
})
})
}
}
/**
* @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)))
})
console.log(yDomFragment.toDomString())
}

View File

@ -1,7 +1,7 @@
import Binding from '../Binding.js'
import simpleDiff from '../../../lib/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
import simpleDiff from '../../lib/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../../src/Util/relativePosition.js'
function typeObserver () {
this._mutualExclude(() => {

View File

@ -1,19 +0,0 @@
{
"name": "yjs-examples",
"version": "0.0.0",
"homepage": "y-js.org",
"authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
],
"description": "Examples for Yjs",
"license": "MIT",
"ignore": [],
"dependencies": {
"quill": "^1.0.0-rc.2",
"ace": "~1.2.3",
"ace-builds": "~1.2.3",
"jquery": "~2.2.2",
"d3": "^3.5.16",
"codemirror": "^5.25.0"
}
}

View File

@ -1,7 +1,7 @@
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
import Y from '../../src/Y.js'
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
import UndoManager from '../../src/Util/UndoManager.js'
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
import YXmlText from '../../src/Types/YXml/YXmlText.js'

View File

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 7px;
}
.one {
grid-column: 1 ;
}
.two {
grid-column: 2;
}
.three {
grid-column: 3;
}
textarea {
width: calc(100% - 10px)
}
.editor-container {
background-color: #4caf50;
padding: 4px 5px 10px 5px;
border-radius: 11px;
}
.editor-container[disconnected] {
background-color: red;
}
.disconnected-info {
display: none;
}
.editor-container[disconnected] .disconnected-info {
display: inline;
}
</style>
<div class="wrapper">
<div id="container1" class="one editor-container">
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<div id="container2" class="two editor-container">
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<div id="container3" class="three editor-container">
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
</div>
<script src="../../y.js"></script>
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
<script src="./index.js"></script>
</body>
</html>

View File

@ -1,38 +0,0 @@
/* global Y */
function bindYjsInstance (y, suffix) {
y.define('textarea', Y.Text).bind(document.getElementById('textarea' + suffix))
y.connector.socket.on('connection', function () {
document.getElementById('container' + suffix).removeAttribute('disconnected')
})
y.connector.socket.on('disconnect', function () {
document.getElementById('container' + suffix).setAttribute('disconnected', true)
})
}
let y1 = new Y('infinite-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.y1 = y1
bindYjsInstance(y1, '1')
let y2 = new Y('infinite-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.y2 = y2
bindYjsInstance(y2, '2')
let y3 = new Y('infinite-example', {
connector: {
name: 'websockets-client',
url: 'http://127.0.0.1:1234'
}
})
window.y3 = y3
bindYjsInstance(y1, '3')

View File

@ -3,7 +3,7 @@
import { createYdbClient } from '../../YdbClient/index.js'
import Y from '../../src/Y.dist.js'
import * as ydb from '../../YdbClient/YdbClient.js'
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
import DomBinding from '../../bindings/DomBinding/DomBinding.js'
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0

View File

@ -1,23 +0,0 @@
{
"name": "examples",
"version": "0.0.0",
"description": "",
"scripts": {
"dist": "rollup -c",
"watch": "rollup -cw"
},
"author": "Kevin Jahns",
"license": "MIT",
"dependencies": {
"monaco-editor": "^0.8.3",
"rollup": "^0.52.3"
},
"devDependencies": {
"standard": "^10.0.2"
},
"standard": {
"ignore": [
"bower_components"
]
}
}

View File

@ -0,0 +1,29 @@
<!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 }
</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>

View File

@ -0,0 +1,112 @@
/* eslint-env browser */
import * as Y from '../../src/index.js'
import ProsemirrorBinding 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 {Plugin} from 'prosemirror-state'
import {Decoration, DecorationSet} from 'prosemirror-view'
let 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) }
}
})
function 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
}
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()
})
function 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((accept, fail) => {
reader.onload = () => accept(reader.result)
reader.onerror = () => fail(reader.error)
// Some extra delay to make the asynchronicity visible
setTimeout(() => reader.readAsDataURL(file), 1500)
})
}
const view = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat(placeholderPlugin)
})
})
const provider = new WebsocketProvider('ws://localhost:1234/')
const ydocument = provider.get('prosemirror')
/**
* @type {any}
*/
const type = ydocument.define('prosemirror', Y.XmlFragment)
const prosemirrorBinding = new ProsemirrorBinding(type, view)
window.view = view
window.EditorState = EditorState
window.EditorView = EditorView
window.Mark = Mark
window.Fragment = Fragment
window.Node = Node
window.Schema = Schema
window.Slice = Slice
window.prosemirrorBinding = prosemirrorBinding

View File

@ -0,0 +1,20 @@
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()
]
}

View File

@ -1,29 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
input: 'yjs-dist.js',
name: 'Y',
output: {
file: 'yjs-dist.js',
format: 'umd'
},
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs()
],
sourcemap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}

176
package-lock.json generated
View File

@ -2012,6 +2012,12 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
"crel": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz",
"integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==",
"dev": true
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
@ -5363,6 +5369,12 @@
}
}
},
"orderedmap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.0.0.tgz",
"integrity": "sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo=",
"dev": true
},
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
@ -5656,6 +5668,158 @@
"object-assign": "^4.1.1"
}
},
"prosemirror-commands": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz",
"integrity": "sha512-IR8yMSdw7XlKuF68tydAak1J9P/lLD5ohsrL7pzoLsJAJAQU7mVPDXtGbQrrm0mesddFjcc1zNo/cJQN3lRYnA==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-dropcursor": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz",
"integrity": "sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==",
"dev": true,
"requires": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"prosemirror-example-setup": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.0.1.tgz",
"integrity": "sha512-4NKWpdmm75Zzgq/dIrypRnkBNPx+ONKyoGF42a9g3VIVv0TWglf1CBNxt5kzCgli9xdfut/xE5B42F9DR6BLHw==",
"dev": true,
"requires": {
"prosemirror-commands": "^1.0.0",
"prosemirror-dropcursor": "^1.0.0",
"prosemirror-gapcursor": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-inputrules": "^1.0.0",
"prosemirror-keymap": "^1.0.0",
"prosemirror-menu": "^1.0.0",
"prosemirror-schema-list": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"prosemirror-gapcursor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz",
"integrity": "sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ==",
"dev": true,
"requires": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"prosemirror-history": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.0.3.tgz",
"integrity": "sha512-IfFGbhafSx+R3aq7nLJGkXeu2iaUiP8mkU3aRu2uQcIIjU8Fq7RJfuvhIOJ2RNUoSyqF/ANkdTjnZ74F5eHs1Q==",
"dev": true,
"requires": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"rope-sequence": "^1.2.0"
}
},
"prosemirror-inputrules": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz",
"integrity": "sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA==",
"dev": true,
"requires": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-keymap": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz",
"integrity": "sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg==",
"dev": true,
"requires": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^1.1.8"
}
},
"prosemirror-menu": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.0.5.tgz",
"integrity": "sha512-9Vrn7CC191v7FA4QrAkL8W1SrR73V3CRIYCDuk94R8oFVk4VxSFdoKVLHuvGzxZ8b5LCu3DMJfh86YW9uL4RkQ==",
"dev": true,
"requires": {
"crel": "^3.0.0",
"prosemirror-commands": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"prosemirror-model": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.6.3.tgz",
"integrity": "sha512-iqIml664X9MUVGLz2nzK4xfAofX8+o7gs2mi2/k+pVD0qZ7th1Jm5eG3AsqWoEUIZuWeaOWCKpBl/dPnhIIWew==",
"dev": true,
"requires": {
"orderedmap": "^1.0.0"
}
},
"prosemirror-schema-basic": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.0.0.tgz",
"integrity": "sha512-xTFjtuLZgcRS4MoDbUyI9NSk/k/ACLGKZQcDXH18ctM9BOmP4z5rGZcA014fCF2FnMFOU+lKwusL0JjVrEectQ==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0"
}
},
"prosemirror-schema-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.0.1.tgz",
"integrity": "sha512-AiLIX6qm6PEeDtMCKZLcSLi55WXo1ls7DnRK+4hSkoi0IIzNdxGsRlecCd3MzEu//DVz3nAEh+zEmslyW+uk8g==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.2.2.tgz",
"integrity": "sha512-j8aC/kf9BJSCQau485I/9pj39XQoce+TqH5xzekT7WWFARTsRYFLJtiXBcCKakv1VSeev+sC3bJP0pLfz7Ft8g==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"prosemirror-transform": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz",
"integrity": "sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ==",
"dev": true,
"requires": {
"prosemirror-model": "^1.0.0"
}
},
"prosemirror-view": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.6.5.tgz",
"integrity": "sha512-brg8fExNrmklbLs8VJ7uvmo/Lh93EHErH47alI55hkJ12EF73K+t2+IyrlkJF84tt5wFBJ20LeSxF8HlJHXiYg==",
"dev": true,
"requires": {
"prosemirror-model": "^1.1.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@ -6252,6 +6416,12 @@
"require-relative": "0.8.7"
}
},
"rope-sequence": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.2.2.tgz",
"integrity": "sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4=",
"dev": true
},
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@ -6950,6 +7120,12 @@
"integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
"dev": true
},
"w3c-keyname": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-1.1.8.tgz",
"integrity": "sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA==",
"dev": true
},
"webidl-conversions": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz",

View File

@ -57,6 +57,10 @@
"cutest": "^0.1.9",
"esdoc": "^1.1.0",
"esdoc-standard-plugin": "^1.0.0",
"prosemirror-example-setup": "^1.0.1",
"prosemirror-schema-basic": "^1.0.0",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.6.5",
"quill": "^1.3.6",
"quill-cursors": "^1.0.3",
"rollup": "^0.58.2",

View File

@ -1,56 +0,0 @@
import Binding from '../Binding.js'
import simpleDiff from '../../Util/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
function typeObserver () {
this._mutualExclude(() => {
const textarea = this.target
const textType = this.type
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
textarea.value = textType.toString()
const start = fromRelativePosition(textType._y, relativeStart)
const end = fromRelativePosition(textType._y, relativeEnd)
textarea.setSelectionRange(start, end)
})
}
function domObserver () {
this._mutualExclude(() => {
let diff = simpleDiff(this.type.toString(), this.target.value)
this.type.delete(diff.pos, diff.remove)
this.type.insert(diff.pos, diff.insert)
})
}
/**
* A binding that binds a YText to a dom textarea.
*
* This binding is automatically destroyed when its parent is deleted.
*
* @example
* const textare = document.createElement('textarea')
* const type = y.define('textarea', Y.Text)
* const binding = new Y.QuillBinding(type, textarea)
*
*/
export default class TextareaBinding extends Binding {
constructor (textType, domTextarea) {
// Binding handles textType as this.type and domTextarea as this.target
super(textType, domTextarea)
// set initial value
domTextarea.value = textType.toString()
// Observers are handled by this class
this._typeObserver = typeObserver.bind(this)
this._domObserver = domObserver.bind(this)
textType.observe(this._typeObserver)
domTextarea.addEventListener('input', this._domObserver)
}
destroy () {
// Remove everything that is handled by this class
this.type.unobserve(this._typeObserver)
this.target.unobserve(this._domObserver)
super.destroy()
}
}

View File

@ -499,6 +499,39 @@ export default class YText extends YArray {
return str
}
toDomString () {
return this.toDelta().map(delta => {
const nestedNodes = []
for (let nodeName in delta.attributes) {
const attrs = []
for (let key in delta.attributes[nodeName]) {
attrs.push({key, value: delta.attributes[nodeName][key]})
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += delta.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
})
}
/**
* Apply a {@link Delta} on this shared YText type.
*

View File

@ -1,6 +1,6 @@
import YMap from '../YMap/YMap.js'
import YXmlFragment from './YXmlFragment.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import { createAssociation } from '../../../bindings/DomBinding/util.js'
import * as encoding from '../../../lib/encoding.js'
import * as decoding from '../../../lib/decoding.js'
@ -82,6 +82,10 @@ export default class YXmlElement extends YXmlFragment {
super._integrate(y)
}
toString () {
return this.toDomString()
}
/**
* Returns the string representation of this YXmlElement.
* The attributes are ordered by attribute-name, so you can easily use this
@ -91,7 +95,7 @@ export default class YXmlElement extends YXmlFragment {
*
* @public
*/
toString () {
toDomString () {
const attrs = this.getAttributes()
const stringBuilder = []
const keys = []
@ -106,7 +110,7 @@ export default class YXmlElement extends YXmlFragment {
}
const nodeName = this.nodeName.toLocaleLowerCase()
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
return `<${nodeName}${attrsString}>${super.toDomString()}</${nodeName}>`
}
/**
@ -170,7 +174,7 @@ export default class YXmlElement extends YXmlFragment {
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {import('../../Bindings/DomBinding/DomBinding.js').default} [binding] You should not set this property. This is
* @param {import('../../../bindings/DomBinding/DomBinding.js').default} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}

View File

@ -1,4 +1,4 @@
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import { createAssociation } from '../../../bindings/DomBinding/util.js'
import YXmlTreeWalker from './YXmlTreeWalker.js'
import YArray from '../YArray/YArray.js'
@ -7,7 +7,7 @@ import { logItemHelper } from '../../message.js'
/**
* @typedef {import('./YXmlElement.js').default} YXmlElement
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../../bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../Y.js').default} Y
*/
@ -113,13 +113,17 @@ export default class YXmlFragment extends YArray {
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote, transaction))
}
toString () {
return this.toDomString()
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toString () {
return this.map(xml => xml.toString()).join('')
toDomString () {
return this.map(xml => xml.toDomString()).join('')
}
/**

View File

@ -1,10 +1,10 @@
import YMap from '../YMap/YMap.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import { createAssociation } from '../../../bindings/DomBinding/util.js'
import * as encoding from '../../../lib/encoding.js'
import * as decoding from '../../../lib/decoding.js'
/**
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../../bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../Y.js').default} Y
*/

View File

@ -1,8 +1,8 @@
import YText from '../YText/YText.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import { createAssociation } from '../../../bindings/DomBinding/util.js'
/**
* @typedef {import('../../Bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../../bindings/DomBinding/DomBinding.js').default} DomBinding
* @typedef {import('../../index.js').Y} Y
*/

View File

@ -30,12 +30,6 @@ export { default as XmlElement } from './Types/YXml/YXmlElement.js'
export { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
export { registerStruct as registerType } from './Util/structReferences.js'
export { default as TextareaBinding } from './Bindings/TextareaBinding/TextareaBinding.js'
export { default as QuillBinding } from './Bindings/QuillBinding/QuillBinding.js'
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
export { default as domToType } from './Bindings/DomBinding/domToType.js'
export { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js'
export * from './message.js'
export * from '../lib/encoding.js'
export * from '../lib/decoding.js'