implement quill binding for y-text

This commit is contained in:
Kevin Jahns 2018-02-26 02:18:39 +01:00
parent da748a78f4
commit 248d08be30
17 changed files with 713 additions and 1090 deletions

View File

@ -1,32 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<!-- quill does not include dist files! We are using the hosted version instead -->
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
<style>
#quill-container {
border: 1px solid gray;
box-shadow: 0px 0px 10px gray;
}
</style>
<!-- Main Quill library -->
<script src="https://cdn.quilljs.com/1.3.5/quill.js"></script>
<!-- Theme included stylesheets -->
<link href="https://cdn.quilljs.com/1.3.5/quill.snow.css" rel="stylesheet">
<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="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
<!-- quill does not include dist files! We are using the hosted version instead (see above)
<script src="../bower_components/quill/dist/quill.js"></script>
-->
<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,40 +1,33 @@
/* global Y, Quill */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
let y = new Y('htmleditor10', {
connector: {
name: 'websockets-client',
room: 'richtext-example-quill-1.0-test',
url: 'http://localhost:1234'
},
sourceDir: '/bower_components',
share: {
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
url: 'http://127.0.0.1:1234'
}
}).then(function (y) {
window.yQuill = y
// create quill element
window.quill = new Quill('#quill', {
modules: {
formula: true,
syntax: true,
toolbar: [
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }]
]
},
theme: 'snow'
})
// bind quill to richtext type
y.share.richtext.bind(window.quill)
})
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

973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -53,6 +53,7 @@
"chance": "^1.0.9",
"concurrently": "^3.4.0",
"cutest": "^0.1.9",
"quill": "^1.3.5",
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-inject": "^2.0.0",

View File

@ -20,7 +20,7 @@ export default {
}),
commonjs(),
babel(),
uglify({
/*uglify({
mangle: {
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
},
@ -34,7 +34,7 @@ export default {
}
}
}
})
})*/
],
banner: `
/**

View File

@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
input: 'test/index.js',
input: 'test/y-text.tests.js',
name: 'y-tests',
sourcemap: true,
output: {

View File

@ -90,7 +90,7 @@ export default class BinaryDecoder {
for (let i = 0; i < len; i++) {
bytes[i] = this.uint8arr[this.pos++]
}
let encodedString = String.fromCodePoint(...bytes)
let encodedString = bytes.map(b => String.fromCodePoint(b)).join('')
return decodeURIComponent(escape(encodedString))
}
/**

View File

@ -0,0 +1,37 @@
import Binding from './Binding.js'
function typeObserver (event) {
const quill = this.target
quill.update('yjs')
this._mutualExclude(function () {
quill.updateContents(event.delta, 'yjs')
quill.update('yjs') // ignore applied changes
})
}
function quillObserver (delta) {
this._mutualExclude(() => {
this.type.applyDelta(delta.ops)
})
}
export default class QuillBinding extends Binding {
constructor (textType, quillInstance) {
// Binding handles textType as this.type and quillInstance as this.target
super(textType, quillInstance)
// set initial value
quillInstance.setContents(textType.toDelta(), 'yjs')
// Observers are handled by this class
this._typeObserver = typeObserver.bind(this)
this._quillObserver = quillObserver.bind(this)
textType.observe(this._typeObserver)
quillInstance.on('text-change', this._quillObserver)
}
destroy () {
// Remove everything that is handled by this class
this.type.unobserve(this._typeObserver)
this.target.unobserve(this._quillObserver)
super.destroy()
}
}

View File

@ -39,6 +39,11 @@ export function splitHelper (y, a, b, diff) {
o = o._right
}
y.os.put(b)
if (y._transaction.newTypes.has(a)) {
y._transaction.newTypes.add(b)
} else if (y._transaction.deletedStructs.has(a)) {
y._transaction.deletedStructs.add(b)
}
}
export default class Item {

31
src/Struct/ItemEmbed.js Normal file
View File

@ -0,0 +1,31 @@
import { default as Item } from './Item.js'
import { logID } from '../MessageHandler/messageToString.js'
export default class ItemEmbed extends Item {
constructor () {
super()
this.embed = null
}
_copy (undeleteChildren, copyPosition) {
let struct = super._copy(undeleteChildren, copyPosition)
struct.embed = this.embed
return struct
}
get _length () {
return 1
}
_fromBinary (y, decoder) {
const missing = super._fromBinary(y, decoder)
this.embed = JSON.parse(decoder.readVarString())
return missing
}
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(JSON.stringify(this.embed))
}
_logString () {
const left = this._left !== null ? this._left._lastId : null
const origin = this._origin !== null ? this._origin._lastId : null
return `ItemEmbed(id:${logID(this._id)},embed:${JSON.stringify(this.embed)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
}
}

View File

@ -1,7 +1,7 @@
import { default as Item } from './Item.js'
import { logID } from '../MessageHandler/messageToString.js'
export default class ItemString extends Item {
export default class ItemFormat extends Item {
constructor () {
super()
this.key = null
@ -20,26 +20,19 @@ export default class ItemString extends Item {
return false
}
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder)
const missing = super._fromBinary(y, decoder)
this.key = decoder.readVarString()
this.value = decoder.readVarString()
this.value = JSON.parse(decoder.readVarString())
return missing
}
_toBinary (encoder) {
super._toBinary(encoder)
encoder.writeVarString(this.key)
encoder.writeVarString(this.value)
encoder.writeVarString(JSON.stringify(this.value))
}
_logString () {
const left = this._left !== null ? this._left._lastId : null
const origin = this._origin !== null ? this._origin._lastId : null
return `ItemFormat(id:${logID(this._id)},key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
}
_splitAt (y, diff) {
if (diff === 0) {
return this
} else {
return this._right
}
}
}

View File

@ -4,12 +4,13 @@ import ItemString from '../Struct/ItemString.js'
import { logID } from '../MessageHandler/messageToString.js'
import YEvent from '../Util/YEvent.js'
class YArrayEvent extends YEvent {
export class YArrayEvent extends YEvent {
constructor (yarray, remote, transaction) {
super(yarray)
this.remote = remote
this._transaction = transaction
this._addedElements = null
this._removedElements = null
}
get addedElements () {
if (this._addedElements === null) {
@ -26,15 +27,18 @@ class YArrayEvent extends YEvent {
return this._addedElements
}
get removedElements () {
const target = this.target
const transaction = this._transaction
const removedElements = new Set()
transaction.deletedStructs.forEach(function (struct) {
if (struct._parent === target && !transaction.newTypes.has(struct)) {
removedElements.add(struct)
}
})
return removedElements
if (this._removedElements === null) {
const target = this.target
const transaction = this._transaction
const removedElements = new Set()
transaction.deletedStructs.forEach(function (struct) {
if (struct._parent === target && !transaction.newTypes.has(struct)) {
removedElements.add(struct)
}
})
this._removedElements = removedElements
}
return this._removedElements
}
}

View File

@ -1,7 +1,8 @@
import ItemString from '../Struct/ItemString.js'
import ItemEmbed from '../Struct/ItemEmbed.js'
import ItemFormat from '../Struct/ItemFormat.js'
import YArray from './YArray.js'
import { logID } from '../MessageHandler/messageToString.js'
import { YArrayEvent, default as YArray } from './YArray.js'
function integrateItem (item, parent, y, left, right) {
item._origin = left
@ -10,7 +11,7 @@ function integrateItem (item, parent, y, left, right) {
item._right_origin = right
item._parent = parent
if (y !== null) {
item._integrate(this._y)
item._integrate(y)
} else if (left === null) {
parent._start = item
} else {
@ -18,49 +19,365 @@ function integrateItem (item, parent, y, left, right) {
}
}
function findPosition (parent, pos, attributes) {
let currentAttributes = new Map()
let left = null
let right = parent._start
let count = 0
while (right !== null) {
function findNextPosition (currentAttributes, parent, left, right, count) {
while (right !== null && count > 0) {
switch (right.constructor) {
// case ItemBlockFormat: do not break..
case ItemEmbed:
case ItemString:
const rightLen = right._deleted ? 0 : (right._length - 1)
if (count <= pos && pos <= count + rightLen) {
const splitDiff = pos - count
right = right._splitAt(parent._y, splitDiff)
if (count <= rightLen) {
right = right._splitAt(parent._y, count)
left = right._left
count += splitDiff
break
return [left, right, currentAttributes]
}
if (!right._deleted) {
count += right._length
if (right._deleted === false) {
count -= right._length
}
break
case ItemFormat:
if (right._deleted === false) {
const key = right.key
const value = right.value
if (value === null) {
currentAttributes.delete(key)
} else if (attributes.hasOwnProperty(key)) {
// only set if relevant
currentAttributes.set(key, value)
}
updateCurrentAttributes(currentAttributes, right)
}
break
}
left = right
right = right._right
}
if (pos > count) {
throw new Error('Position exceeds array range!')
}
return [left, right, currentAttributes]
}
function findPosition (parent, pos) {
let currentAttributes = new Map()
let left = null
let right = parent._start
return findNextPosition(currentAttributes, parent, left, right, pos)
}
// negate applied formats
function insertNegatedAttributes (y, parent, left, right, negatedAttributes) {
// check if we really need to remove attributes
while (
right !== null && (
right._deleted === true || (
right.constructor === ItemFormat &&
(negatedAttributes.get(right.key) === right.value)
)
)
) {
if (right._deleted === false) {
negatedAttributes.delete(right.key)
}
left = right
right = right._right
}
for (let [key, val] of negatedAttributes) {
let format = new ItemFormat()
format.key = key
format.value = val
integrateItem(format, parent, y, left, right)
left = format
}
return [left, right]
}
function updateCurrentAttributes (currentAttributes, item) {
const value = item.value
const key = item.key
if (value === null) {
currentAttributes.delete(key)
} else {
currentAttributes.set(key, value)
}
}
function minimizeAttributeChanges (left, right, currentAttributes, attributes) {
// go right while attributes[right.key] === right.value (or right is deleted)
while (true) {
if (right === null) {
break
} else if (right._deleted === true) {
// continue
} else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
// found a format, update currentAttributes and continue
updateCurrentAttributes(currentAttributes, right)
} else {
break
}
left = right
right = right._right
}
return [left, right]
}
function insertText (y, text, parent, left, right, currentAttributes, attributes) {
for (let [key] of currentAttributes) {
if (attributes.hasOwnProperty(key) === false) {
attributes[key] = null
}
}
[left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes)
let negatedAttributes = new Map()
// insert format-start items
for (let key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key)
if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null)
let format = new ItemFormat()
format.key = key
format.value = val
integrateItem(format, parent, y, left, right)
left = format
}
}
// insert content
let item
if (text.constructor === String) {
item = new ItemString()
item._content = text
} else {
item = new ItemEmbed()
item.embed = text
}
integrateItem(item, parent, y, left, right)
left = item
return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
}
function formatText (y, length, parent, left, right, currentAttributes, attributes) {
[left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes)
let negatedAttributes = new Map()
// insert format-start items
for (let key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key)
if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null)
let format = new ItemFormat()
format.key = key
format.value = val
integrateItem(format, parent, y, left, right)
left = format
}
}
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
while (length > 0 && right !== null) {
if (right._deleted === false) {
switch (right.constructor) {
case ItemFormat:
if (attributes.hasOwnProperty(right.key)) {
if (attributes[right.key] === right.value) {
negatedAttributes.delete(right.key)
} else {
negatedAttributes.set(right.key, right.value)
}
right._delete(y)
}
updateCurrentAttributes(currentAttributes, right)
break
case ItemEmbed:
case ItemString:
right._splitAt(y, length)
length -= right._length
break
}
}
left = right
right = right._right
}
return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
}
function deleteText (y, length, parent, left, right, currentAttributes) {
while (length > 0 && right !== null) {
if (right._deleted === false) {
switch (right.constructor) {
case ItemFormat:
updateCurrentAttributes(currentAttributes, right)
break
case ItemEmbed:
case ItemString:
right._splitAt(y, length)
length -= right._length
right._delete(y)
break
}
}
left = right
right = right._right
}
return [left, right]
}
class YTextEvent extends YArrayEvent {
constructor (ytext, remote, transaction) {
super(ytext, remote, transaction)
this._delta = null
}
get delta () {
if (this._delta === null) {
const y = this.target._y
y.transact(() => {
let item = this.target._start
const delta = []
const added = this.addedElements
const removed = this.removedElements
this._delta = delta
let action = null
let attributes = {} // counts added or removed new attributes for retain
const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map()
let insert = ''
let retain = 0
let deleteLen = 0
const addOp = function addOp () {
if (action !== null) {
let op
switch (action) {
case 'delete':
op = { delete: deleteLen }
deleteLen = 0
break
case 'insert':
op = { insert }
if (currentAttributes.size > 0) {
op.attributes = {}
for (let [key, value] of currentAttributes) {
if (value !== null) {
op.attributes[key] = value
}
}
}
insert = ''
break
case 'retain':
op = { retain }
if (Object.keys(attributes).length > 0) {
op.attributes = {}
for (let key in attributes) {
op.attributes[key] = attributes[key]
}
}
retain = 0
break
}
delta.push(op)
action = null
}
}
while (item !== null) {
switch (item.constructor) {
case ItemEmbed:
if (added.has(item)) {
addOp()
action = 'insert'
insert = item.embed
addOp()
} else if (removed.has(item)) {
if (action !== 'delete') {
addOp()
action = 'delete'
}
deleteLen += 1
} else if (item._deleted === false) {
if (action !== 'retain') {
addOp()
action = 'retain'
}
retain += 1
}
break
case ItemString:
if (added.has(item)) {
if (action !== 'insert') {
addOp()
action = 'insert'
}
insert += item._content
} else if (removed.has(item)) {
if (action !== 'delete') {
addOp()
action = 'delete'
}
deleteLen += item._length
} else if (item._deleted === false) {
if (action !== 'retain') {
addOp()
action = 'retain'
}
retain += item._length
}
break
case ItemFormat:
if (added.has(item)) {
const curVal = currentAttributes.get(item.key) || null
if (curVal !== item.value) {
if (action === 'retain') {
addOp()
}
if (item.value === (oldAttributes.get(item.key) || null)) {
delete attributes[item.key]
} else {
attributes[item.key] = item.value
}
} else {
item._delete(y)
}
} else if (removed.has(item)) {
oldAttributes.set(item.key, item.value)
const curVal = currentAttributes.get(item.key) || null
if (curVal !== item.value) {
if (action === 'retain') {
addOp()
}
attributes[item.key] = curVal
}
} else if (item._deleted === false) {
oldAttributes.set(item.key, item.value)
if (attributes.hasOwnProperty(item.key)) {
if (attributes[item.key] !== item.value) {
if (action === 'retain') {
addOp()
}
if (item.value === null) {
attributes[item.key] = item.value
} else {
delete attributes[item.key]
}
} else {
item._delete(y)
}
}
}
if (item._deleted === false) {
if (action === 'insert') {
addOp()
}
updateCurrentAttributes(currentAttributes, item)
}
break
}
item = item._right
}
addOp()
while (this._delta.length > 0) {
let lastOp = this._delta[this._delta.length - 1]
if (lastOp.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) {
// retain delta's if they don't assign attributes
this._delta.pop()
} else {
break
}
}
})
}
return this._delta
}
}
export default class YText extends YArray {
constructor (string) {
super()
@ -71,6 +388,9 @@ export default class YText extends YArray {
this._start = start
}
}
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
}
toString () {
let str = ''
let n = this._start
@ -82,10 +402,27 @@ export default class YText extends YArray {
}
return str
}
applyDelta (delta) {
this._transact(y => {
let left = null
let right = this._start
const currentAttributes = new Map()
for (let i = 0; i < delta.length; i++) {
let op = delta[i]
if (op.hasOwnProperty('insert')) {
;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {})
} else if (op.hasOwnProperty('retain')) {
;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {})
} else if (op.hasOwnProperty('delete')) {
;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes)
}
}
})
}
/**
* As defined by Quilljs - https://quilljs.com/docs/delta/
*/
toRichtextDelta () {
toDelta () {
let ops = []
let currentAttributes = new Map()
let str = ''
@ -94,10 +431,16 @@ export default class YText extends YArray {
if (str.length > 0) {
// pack str with attributes to ops
let attributes = {}
let addAttributes = false
for (let [key, value] of currentAttributes) {
addAttributes = true
attributes[key] = value
}
ops.push({ insert: str, attributes })
let op = { insert: str }
if (addAttributes) {
op.attributes = attributes
}
ops.push(op)
str = ''
}
}
@ -109,13 +452,7 @@ export default class YText extends YArray {
break
case ItemFormat:
packStr()
const value = n.value
const key = n.key
if (value === null) {
currentAttributes.delete(key)
} else {
currentAttributes.set(key, value)
}
updateCurrentAttributes(currentAttributes, n)
break
}
}
@ -129,72 +466,35 @@ export default class YText extends YArray {
return
}
this._transact(y => {
let [left, right, currentAttributes] = findPosition(this, pos, attributes)
let negatedAttributes = new Map()
// insert format-start items
for (let key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key)
if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null)
let format = new ItemFormat()
format.key = key
format.value = val
integrateItem(format, this, y, left, right)
left = format
}
}
// insert text content
let item = new ItemString()
item._content = text
integrateItem(item, this, y, left, right)
left = item
// negate applied formats
for (let [key, value] of negatedAttributes) {
let format = new ItemFormat()
format.key = key
format.value = value
integrateItem(format, this, y, left, right)
left = format
}
let [left, right, currentAttributes] = findPosition(this, pos)
insertText(y, text, this, left, right, currentAttributes, attributes)
})
}
insertEmbed (pos, embed, attributes = {}) {
if (embed.constructor !== Object) {
throw new Error('Embed must be an Object')
}
this._transact(y => {
let [left, right, currentAttributes] = findPosition(this, pos)
insertText(y, embed, this, left, right, currentAttributes, attributes)
})
}
delete (pos, length) {
if (length === 0) {
return
}
this._transact(y => {
let [left, right, currentAttributes] = findPosition(this, pos)
deleteText(y, length, this, left, right, currentAttributes)
})
}
format (pos, length, attributes) {
this._transact(y => {
let [left, _right, currentAttributes] = findPosition(this, pos, attributes)
if (_right === null) {
let [left, right, currentAttributes] = findPosition(this, pos)
if (right === null) {
return
}
let negatedAttributes = new Map()
// insert format-start items
for (let key in attributes) {
const val = attributes[key]
const currentVal = currentAttributes.get(key)
if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null)
let format = new ItemFormat()
format.key = key
format.value = val
integrateItem(format, this, y, left, _right)
left = format
}
}
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
while (length > 0 && left !== null) {
if (left._deleted === false) {
if (left.constructor === ItemFormat) {
if (attributes[left.key] != null) {
left.delete(y)
}
} else if (length < left._length) {
}
}
left = left._right
}
formatText(y, length, this, left, right, currentAttributes, attributes)
})
}
_logString () {

View File

@ -6,6 +6,8 @@ import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Type/y-xml/y-x
import Delete from '../Struct/Delete.js'
import ItemJSON from '../Struct/ItemJSON.js'
import ItemString from '../Struct/ItemString.js'
import ItemFormat from '../Struct/ItemFormat.js'
import ItemEmbed from '../Struct/ItemEmbed.js'
const structs = new Map()
const references = new Map()
@ -23,8 +25,11 @@ export function getReference (typeConstructor) {
return references.get(typeConstructor)
}
// TODO: reorder (Item* should have low numbers)
addStruct(0, ItemJSON)
addStruct(1, ItemString)
addStruct(10, ItemFormat)
addStruct(11, ItemEmbed)
addStruct(2, Delete)
addStruct(3, YArray)

View File

@ -23,6 +23,7 @@ import debug from 'debug'
import Transaction from './Transaction.js'
import TextareaBinding from './Binding/TextareaBinding.js'
import QuillBinding from './Binding/QuillBinding.js'
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
@ -202,6 +203,7 @@ Y.XmlText = YXmlText
Y.XmlHook = YXmlHook
Y.TextareaBinding = TextareaBinding
Y.QuillBinding = QuillBinding
Y.utils = {
BinaryDecoder,

102
test/y-text.tests.js Normal file
View File

@ -0,0 +1,102 @@
import { initArrays, compareUsers, flushAll } from '../tests-lib/helper.js'
import { test, proxyConsole } from 'cutest'
proxyConsole()
test('basic insert delete', async function text0 (t) {
let { users, text0 } = await initArrays(t, { users: 2 })
let delta
text0.observe(function (event) {
delta = event.delta
})
text0.delete(0, 0)
t.assert(true, 'Does not throw when deleting zero elements with position 0')
text0.insert(0, 'abc')
t.assert(text0.toString() === 'abc', 'Basic insert works')
t.compare(delta, [{ insert: 'abc' }])
text0.delete(0, 1)
t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)')
t.compare(delta, [{ delete: 1 }])
text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
await compareUsers(t, users)
})
test('basic format', async function text1 (t) {
let { users, text0 } = await initArrays(t, { users: 2 })
let delta
text0.observe(function (event) {
delta = event.delta
})
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toString() === 'abc', 'Basic insert with attributes works')
t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }])
text0.delete(0, 1)
t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)')
t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }])
t.compare(delta, [{ delete: 1 }])
text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }])
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
text0.insert(0, 'z', {bold: true})
t.assert(text0.toString() === 'zb')
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
t.assert(text0._start._right._right._right._content === 'b', 'Does not insert duplicate attribute marker')
text0.insert(0, 'y')
t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'y' }])
text0.format(0, 2, { bold: null })
t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }])
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
await compareUsers(t, users)
})
test('quill issue 1', async function quill1 (t) {
let { users, quill0 } = await initArrays(t, { users: 2 })
quill0.insertText(0, 'x')
await flushAll(t, users)
quill0.insertText(1, '\n', 'list', 'ordered')
await flushAll(t, users)
quill0.insertText(1, '\n', 'list', 'ordered')
await compareUsers(t, users)
})
test('quill issue 2', async function quill2 (t) {
let { users, quill0, text0 } = await initArrays(t, { users: 2 })
let delta
text0.observe(function (event) {
delta = event.delta
})
quill0.insertText(0, 'abc', 'bold', true)
await flushAll(t, users)
quill0.insertText(1, 'x')
quill0.update()
t.compare(delta, [{ retain: 1 }, { insert: 'x', attributes: { bold: true } }])
await compareUsers(t, users)
})
test('quill issue 3', async function quill3 (t) {
let { users, quill0, text0 } = await initArrays(t, { users: 2 })
quill0.insertText(0, 'a')
quill0.insertText(1, '\n\n', 'list', 'ordered')
quill0.insertText(2, 'b')
t.compare(text0.toDelta(), [
{ insert: 'a' },
{ insert: '\n', attributes: { list: 'ordered' } },
{ insert: 'b' },
{ insert: '\n', attributes: { list: 'ordered' } }
])
await compareUsers(t, users)
})

View File

@ -6,6 +6,7 @@ import Chance from 'chance'
import ItemJSON from '../src/Struct/ItemJSON.js'
import ItemString from '../src/Struct/ItemString.js'
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
import Quill from 'quill'
export const Y = _Y
@ -92,9 +93,14 @@ export async function compareUsers (t, users) {
await wait()
await flushAll(t, users)
var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
var userMapValues = users.map(u => u.get('map', Y.Map).toJSON())
var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString())
var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
var userXmlValues = users.map(u => u.define('xml', Y.Xml).toString())
var userTextValues = users.map(u => u.define('text', Y.Text).toDelta())
var userQuillValues = users.map(u => {
u.quill.update('yjs') // get latest changes
return u.quill.getContents().ops
})
var data = users.map(u => {
defragmentItemContent(u)
@ -124,6 +130,8 @@ export async function compareUsers (t, users) {
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
t.compare(userTextValues[i], userTextValues[i + 1], 'text types')
t.compare(userQuillValues[i], userQuillValues[i + 1], 'quill delta content')
t.compare(data[i].os, data[i + 1].os, 'os')
t.compare(data[i].ds, data[i + 1].ds, 'ds')
t.compare(data[i].ss, data[i + 1].ss, 'ss')
@ -153,6 +161,13 @@ export async function initArrays (t, opts) {
result['array' + i] = y.define('array', Y.Array)
result['map' + i] = y.define('map', Y.Map)
result['xml' + i] = y.define('xml', Y.XmlElement)
const textType = y.define('text', Y.Text)
result['text' + i] = textType
const quill = new Quill(document.createElement('div'))
const quillBinding = new Y.QuillBinding(textType, quill)
result['quill' + i] = quill
result['quillBinding' + i] = quillBinding
y.quill = quill // put quill on the y object (so we can use it later)
y.get('xml').setDomFilter(function (nodeName, attrs) {
if (nodeName === 'HIDDEN') {
return null