From 32b8fac37fdaf2a9a39f57061c795d0515569c2e Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 6 Nov 2018 13:41:05 +0100 Subject: [PATCH] added prosemirror binding --- .gitignore | 4 +- bindings/BindMapping.js | 67 ++++++ {src/Bindings => bindings}/Binding.js | 0 .../DomBinding/DomBinding.js | 0 .../DomBinding/domObserver.js | 0 .../DomBinding/domToType.js | 6 +- .../DomBinding/filter.js | 2 +- .../DomBinding/selection.js | 2 +- .../DomBinding/typeObserver.js | 4 +- {src/Bindings => bindings}/DomBinding/util.js | 6 +- .../ProsemirrorBinding/ProsemirrorBinding.js | 190 ++++++++++++++++++ .../QuillBinding/QuillBinding.js | 0 .../TextareaBinding/TextareaBinding.js | 4 +- examples/bower.json | 19 -- examples/html-editor/index.js | 2 +- examples/infiniteyjs/index.html | 55 ----- examples/infiniteyjs/index.js | 38 ---- examples/notes/index.js | 2 +- examples/package.json | 23 --- examples/prosemirror/index.html | 29 +++ examples/prosemirror/index.js | 112 +++++++++++ examples/prosemirror/rollup.browser.js | 20 ++ examples/rollup.config.js | 29 --- package-lock.json | 176 ++++++++++++++++ package.json | 4 + .../CodeMirrorBinding/CodeMirrorBinding.js | 56 ------ src/Types/YText/YText.js | 33 +++ src/Types/YXml/YXmlElement.js | 12 +- src/Types/YXml/YXmlFragment.js | 12 +- src/Types/YXml/YXmlHook.js | 4 +- src/Types/YXml/YXmlText.js | 4 +- src/index.js | 6 - 32 files changed, 667 insertions(+), 254 deletions(-) create mode 100644 bindings/BindMapping.js rename {src/Bindings => bindings}/Binding.js (100%) rename {src/Bindings => bindings}/DomBinding/DomBinding.js (100%) rename {src/Bindings => bindings}/DomBinding/domObserver.js (100%) rename {src/Bindings => bindings}/DomBinding/domToType.js (92%) rename {src/Bindings => bindings}/DomBinding/filter.js (97%) rename {src/Bindings => bindings}/DomBinding/selection.js (93%) rename {src/Bindings => bindings}/DomBinding/typeObserver.js (96%) rename {src/Bindings => bindings}/DomBinding/util.js (94%) create mode 100644 bindings/ProsemirrorBinding/ProsemirrorBinding.js rename {src/Bindings => bindings}/QuillBinding/QuillBinding.js (100%) rename {src/Bindings => bindings}/TextareaBinding/TextareaBinding.js (95%) delete mode 100644 examples/bower.json delete mode 100644 examples/infiniteyjs/index.html delete mode 100644 examples/infiniteyjs/index.js delete mode 100644 examples/package.json create mode 100644 examples/prosemirror/index.html create mode 100644 examples/prosemirror/index.js create mode 100644 examples/prosemirror/rollup.browser.js delete mode 100644 examples/rollup.config.js delete mode 100644 src/Bindings/CodeMirrorBinding/CodeMirrorBinding.js diff --git a/.gitignore b/.gitignore index ef6b38b4..d54e3689 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ node_modules bower_components docs /y.* -/examples/yjs-dist.js* +/examples/*/index.dist.* .vscode .yjsPersisted -build \ No newline at end of file +build diff --git a/bindings/BindMapping.js b/bindings/BindMapping.js new file mode 100644 index 00000000..22565f6c --- /dev/null +++ b/bindings/BindMapping.js @@ -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 + */ + this.yt = new Map() + /** + * @type Map + */ + 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) + } +} diff --git a/src/Bindings/Binding.js b/bindings/Binding.js similarity index 100% rename from src/Bindings/Binding.js rename to bindings/Binding.js diff --git a/src/Bindings/DomBinding/DomBinding.js b/bindings/DomBinding/DomBinding.js similarity index 100% rename from src/Bindings/DomBinding/DomBinding.js rename to bindings/DomBinding/DomBinding.js diff --git a/src/Bindings/DomBinding/domObserver.js b/bindings/DomBinding/domObserver.js similarity index 100% rename from src/Bindings/DomBinding/domObserver.js rename to bindings/DomBinding/domObserver.js diff --git a/src/Bindings/DomBinding/domToType.js b/bindings/DomBinding/domToType.js similarity index 92% rename from src/Bindings/DomBinding/domToType.js rename to bindings/DomBinding/domToType.js index ee685d67..c0bb9361 100644 --- a/src/Bindings/DomBinding/domToType.js +++ b/bindings/DomBinding/domToType.js @@ -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' diff --git a/src/Bindings/DomBinding/filter.js b/bindings/DomBinding/filter.js similarity index 97% rename from src/Bindings/DomBinding/filter.js rename to bindings/DomBinding/filter.js index 273b997d..38136584 100644 --- a/src/Bindings/DomBinding/filter.js +++ b/bindings/DomBinding/filter.js @@ -1,4 +1,4 @@ -import isParentOf from '../../Util/isParentOf.js' +import isParentOf from '../../src/Util/isParentOf.js' /** * @callback DomFilter diff --git a/src/Bindings/DomBinding/selection.js b/bindings/DomBinding/selection.js similarity index 93% rename from src/Bindings/DomBinding/selection.js rename to bindings/DomBinding/selection.js index d04a4e48..6e1cdec6 100644 --- a/src/Bindings/DomBinding/selection.js +++ b/bindings/DomBinding/selection.js @@ -1,6 +1,6 @@ /* globals getSelection */ -import { getRelativePosition } from '../../Util/relativePosition.js' +import { getRelativePosition } from '../../src/Util/relativePosition.js' let relativeSelection = null diff --git a/src/Bindings/DomBinding/typeObserver.js b/bindings/DomBinding/typeObserver.js similarity index 96% rename from src/Bindings/DomBinding/typeObserver.js rename to bindings/DomBinding/typeObserver.js index 17510bab..5e64f8c6 100644 --- a/src/Bindings/DomBinding/typeObserver.js +++ b/bindings/DomBinding/typeObserver.js @@ -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) { diff --git a/src/Bindings/DomBinding/util.js b/bindings/DomBinding/util.js similarity index 94% rename from src/Bindings/DomBinding/util.js rename to bindings/DomBinding/util.js index 664d0cb5..591e8f95 100644 --- a/src/Bindings/DomBinding/util.js +++ b/bindings/DomBinding/util.js @@ -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 */ diff --git a/bindings/ProsemirrorBinding/ProsemirrorBinding.js b/bindings/ProsemirrorBinding/ProsemirrorBinding.js new file mode 100644 index 00000000..12c85c0e --- /dev/null +++ b/bindings/ProsemirrorBinding/ProsemirrorBinding.js @@ -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} 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} + */ +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()) +} diff --git a/src/Bindings/QuillBinding/QuillBinding.js b/bindings/QuillBinding/QuillBinding.js similarity index 100% rename from src/Bindings/QuillBinding/QuillBinding.js rename to bindings/QuillBinding/QuillBinding.js diff --git a/src/Bindings/TextareaBinding/TextareaBinding.js b/bindings/TextareaBinding/TextareaBinding.js similarity index 95% rename from src/Bindings/TextareaBinding/TextareaBinding.js rename to bindings/TextareaBinding/TextareaBinding.js index 70e1942b..bdb55abe 100644 --- a/src/Bindings/TextareaBinding/TextareaBinding.js +++ b/bindings/TextareaBinding/TextareaBinding.js @@ -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(() => { diff --git a/examples/bower.json b/examples/bower.json deleted file mode 100644 index e1f399be..00000000 --- a/examples/bower.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "yjs-examples", - "version": "0.0.0", - "homepage": "y-js.org", - "authors": [ - "Kevin Jahns " - ], - "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" - } -} diff --git a/examples/html-editor/index.js b/examples/html-editor/index.js index 09456df9..6e49544d 100644 --- a/examples/html-editor/index.js +++ b/examples/html-editor/index.js @@ -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' diff --git a/examples/infiniteyjs/index.html b/examples/infiniteyjs/index.html deleted file mode 100644 index 7495c9db..00000000 --- a/examples/infiniteyjs/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - -
-
-

Server 1 (disconnected)

- -
-
-

Server 2 (disconnected)

- -
-
-

Server 3 (disconnected)

- -
-
- - - - - diff --git a/examples/infiniteyjs/index.js b/examples/infiniteyjs/index.js deleted file mode 100644 index f320fa4e..00000000 --- a/examples/infiniteyjs/index.js +++ /dev/null @@ -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') diff --git a/examples/notes/index.js b/examples/notes/index.js index 0872f4d9..74f31c6e 100644 --- a/examples/notes/index.js +++ b/examples/notes/index.js @@ -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 diff --git a/examples/package.json b/examples/package.json deleted file mode 100644 index 20fdc5dd..00000000 --- a/examples/package.json +++ /dev/null @@ -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" - ] - } -} diff --git a/examples/prosemirror/index.html b/examples/prosemirror/index.html new file mode 100644 index 00000000..b9772123 --- /dev/null +++ b/examples/prosemirror/index.html @@ -0,0 +1,29 @@ + + + + + + + + + ​
+ +
Insert image:
+ + diff --git a/examples/prosemirror/index.js b/examples/prosemirror/index.js new file mode 100644 index 00000000..a24246ab --- /dev/null +++ b/examples/prosemirror/index.js @@ -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 diff --git a/examples/prosemirror/rollup.browser.js b/examples/prosemirror/rollup.browser.js new file mode 100644 index 00000000..3d115b17 --- /dev/null +++ b/examples/prosemirror/rollup.browser.js @@ -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() + ] +} diff --git a/examples/rollup.config.js b/examples/rollup.config.js deleted file mode 100644 index 2a42c947..00000000 --- a/examples/rollup.config.js +++ /dev/null @@ -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} - */ -` -} diff --git a/package-lock.json b/package-lock.json index 7115d0bf..23da8cb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ba93b093..e26429e5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Bindings/CodeMirrorBinding/CodeMirrorBinding.js b/src/Bindings/CodeMirrorBinding/CodeMirrorBinding.js deleted file mode 100644 index e02edafe..00000000 --- a/src/Bindings/CodeMirrorBinding/CodeMirrorBinding.js +++ /dev/null @@ -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() - } -} diff --git a/src/Types/YText/YText.js b/src/Types/YText/YText.js index 57cb29d3..0ce1e030 100644 --- a/src/Types/YText/YText.js +++ b/src/Types/YText/YText.js @@ -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 += `` + } + return str + }) + } + /** * Apply a {@link Delta} on this shared YText type. * diff --git a/src/Types/YXml/YXmlElement.js b/src/Types/YXml/YXmlElement.js index fa1e72d2..92f64e67 100644 --- a/src/Types/YXml/YXmlElement.js +++ b/src/Types/YXml/YXmlElement.js @@ -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()}` + return `<${nodeName}${attrsString}>${super.toDomString()}` } /** @@ -170,7 +174,7 @@ export default class YXmlElement extends YXmlFragment { * nodejs) * @param {Object} [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} diff --git a/src/Types/YXml/YXmlFragment.js b/src/Types/YXml/YXmlFragment.js index 61a0864e..cd581677 100644 --- a/src/Types/YXml/YXmlFragment.js +++ b/src/Types/YXml/YXmlFragment.js @@ -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('') } /** diff --git a/src/Types/YXml/YXmlHook.js b/src/Types/YXml/YXmlHook.js index 4bbb3d51..6bb7ea39 100644 --- a/src/Types/YXml/YXmlHook.js +++ b/src/Types/YXml/YXmlHook.js @@ -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 */ diff --git a/src/Types/YXml/YXmlText.js b/src/Types/YXml/YXmlText.js index 34baade4..2d38affd 100644 --- a/src/Types/YXml/YXmlText.js +++ b/src/Types/YXml/YXmlText.js @@ -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 */ diff --git a/src/index.js b/src/index.js index f4d65e51..87acf628 100644 --- a/src/index.js +++ b/src/index.js @@ -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'