implement quill binding for y-text
This commit is contained in:
parent
da748a78f4
commit
248d08be30
@ -1,32 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
<!-- Main Quill library -->
|
||||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
<script src="https://cdn.quilljs.com/1.3.5/quill.js"></script>
|
||||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
<!-- Theme included stylesheets -->
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
<link href="https://cdn.quilljs.com/1.3.5/quill.snow.css" rel="stylesheet">
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
<script src="../../y.js"></script>
|
||||||
<style>
|
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||||
#quill-container {
|
|
||||||
border: 1px solid gray;
|
|
||||||
box-shadow: 0px 0px 10px gray;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="quill-container">
|
<div id="quill-container">
|
||||||
<div id="quill">
|
<div id="quill">
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,40 +1,33 @@
|
|||||||
/* global Y, Quill */
|
/* global Y, Quill */
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
let y = new Y('htmleditor10', {
|
||||||
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'richtext-example-quill-1.0-test',
|
url: 'http://127.0.0.1:1234'
|
||||||
url: 'http://localhost:1234'
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
|
||||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
|
||||||
}
|
}
|
||||||
}).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
973
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -53,6 +53,7 @@
|
|||||||
"chance": "^1.0.9",
|
"chance": "^1.0.9",
|
||||||
"concurrently": "^3.4.0",
|
"concurrently": "^3.4.0",
|
||||||
"cutest": "^0.1.9",
|
"cutest": "^0.1.9",
|
||||||
|
"quill": "^1.3.5",
|
||||||
"rollup-plugin-babel": "^2.7.1",
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
"rollup-plugin-commonjs": "^8.0.2",
|
"rollup-plugin-commonjs": "^8.0.2",
|
||||||
"rollup-plugin-inject": "^2.0.0",
|
"rollup-plugin-inject": "^2.0.0",
|
||||||
|
@ -20,7 +20,7 @@ export default {
|
|||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
babel(),
|
babel(),
|
||||||
uglify({
|
/*uglify({
|
||||||
mangle: {
|
mangle: {
|
||||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
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: `
|
banner: `
|
||||||
/**
|
/**
|
||||||
|
@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
|||||||
import multiEntry from 'rollup-plugin-multi-entry'
|
import multiEntry from 'rollup-plugin-multi-entry'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'test/index.js',
|
input: 'test/y-text.tests.js',
|
||||||
name: 'y-tests',
|
name: 'y-tests',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
output: {
|
output: {
|
||||||
|
@ -90,7 +90,7 @@ export default class BinaryDecoder {
|
|||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
bytes[i] = this.uint8arr[this.pos++]
|
bytes[i] = this.uint8arr[this.pos++]
|
||||||
}
|
}
|
||||||
let encodedString = String.fromCodePoint(...bytes)
|
let encodedString = bytes.map(b => String.fromCodePoint(b)).join('')
|
||||||
return decodeURIComponent(escape(encodedString))
|
return decodeURIComponent(escape(encodedString))
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
37
src/Binding/QuillBinding.js
Normal file
37
src/Binding/QuillBinding.js
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,11 @@ export function splitHelper (y, a, b, diff) {
|
|||||||
o = o._right
|
o = o._right
|
||||||
}
|
}
|
||||||
y.os.put(b)
|
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 {
|
export default class Item {
|
||||||
|
31
src/Struct/ItemEmbed.js
Normal file
31
src/Struct/ItemEmbed.js
Normal 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})`
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { default as Item } from './Item.js'
|
import { default as Item } from './Item.js'
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
|
|
||||||
export default class ItemString extends Item {
|
export default class ItemFormat extends Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
super()
|
super()
|
||||||
this.key = null
|
this.key = null
|
||||||
@ -20,26 +20,19 @@ export default class ItemString extends Item {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_fromBinary (y, decoder) {
|
_fromBinary (y, decoder) {
|
||||||
let missing = super._fromBinary(y, decoder)
|
const missing = super._fromBinary(y, decoder)
|
||||||
this.key = decoder.readVarString()
|
this.key = decoder.readVarString()
|
||||||
this.value = decoder.readVarString()
|
this.value = JSON.parse(decoder.readVarString())
|
||||||
return missing
|
return missing
|
||||||
}
|
}
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
super._toBinary(encoder)
|
super._toBinary(encoder)
|
||||||
encoder.writeVarString(this.key)
|
encoder.writeVarString(this.key)
|
||||||
encoder.writeVarString(this.value)
|
encoder.writeVarString(JSON.stringify(this.value))
|
||||||
}
|
}
|
||||||
_logString () {
|
_logString () {
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
const left = this._left !== null ? this._left._lastId : null
|
||||||
const origin = this._origin !== null ? this._origin._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})`
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,13 @@ import ItemString from '../Struct/ItemString.js'
|
|||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
import YEvent from '../Util/YEvent.js'
|
import YEvent from '../Util/YEvent.js'
|
||||||
|
|
||||||
class YArrayEvent extends YEvent {
|
export class YArrayEvent extends YEvent {
|
||||||
constructor (yarray, remote, transaction) {
|
constructor (yarray, remote, transaction) {
|
||||||
super(yarray)
|
super(yarray)
|
||||||
this.remote = remote
|
this.remote = remote
|
||||||
this._transaction = transaction
|
this._transaction = transaction
|
||||||
this._addedElements = null
|
this._addedElements = null
|
||||||
|
this._removedElements = null
|
||||||
}
|
}
|
||||||
get addedElements () {
|
get addedElements () {
|
||||||
if (this._addedElements === null) {
|
if (this._addedElements === null) {
|
||||||
@ -26,15 +27,18 @@ class YArrayEvent extends YEvent {
|
|||||||
return this._addedElements
|
return this._addedElements
|
||||||
}
|
}
|
||||||
get removedElements () {
|
get removedElements () {
|
||||||
const target = this.target
|
if (this._removedElements === null) {
|
||||||
const transaction = this._transaction
|
const target = this.target
|
||||||
const removedElements = new Set()
|
const transaction = this._transaction
|
||||||
transaction.deletedStructs.forEach(function (struct) {
|
const removedElements = new Set()
|
||||||
if (struct._parent === target && !transaction.newTypes.has(struct)) {
|
transaction.deletedStructs.forEach(function (struct) {
|
||||||
removedElements.add(struct)
|
if (struct._parent === target && !transaction.newTypes.has(struct)) {
|
||||||
}
|
removedElements.add(struct)
|
||||||
})
|
}
|
||||||
return removedElements
|
})
|
||||||
|
this._removedElements = removedElements
|
||||||
|
}
|
||||||
|
return this._removedElements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import ItemString from '../Struct/ItemString.js'
|
import ItemString from '../Struct/ItemString.js'
|
||||||
|
import ItemEmbed from '../Struct/ItemEmbed.js'
|
||||||
import ItemFormat from '../Struct/ItemFormat.js'
|
import ItemFormat from '../Struct/ItemFormat.js'
|
||||||
import YArray from './YArray.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
|
import { YArrayEvent, default as YArray } from './YArray.js'
|
||||||
|
|
||||||
function integrateItem (item, parent, y, left, right) {
|
function integrateItem (item, parent, y, left, right) {
|
||||||
item._origin = left
|
item._origin = left
|
||||||
@ -10,7 +11,7 @@ function integrateItem (item, parent, y, left, right) {
|
|||||||
item._right_origin = right
|
item._right_origin = right
|
||||||
item._parent = parent
|
item._parent = parent
|
||||||
if (y !== null) {
|
if (y !== null) {
|
||||||
item._integrate(this._y)
|
item._integrate(y)
|
||||||
} else if (left === null) {
|
} else if (left === null) {
|
||||||
parent._start = item
|
parent._start = item
|
||||||
} else {
|
} else {
|
||||||
@ -18,49 +19,365 @@ function integrateItem (item, parent, y, left, right) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPosition (parent, pos, attributes) {
|
function findNextPosition (currentAttributes, parent, left, right, count) {
|
||||||
let currentAttributes = new Map()
|
while (right !== null && count > 0) {
|
||||||
let left = null
|
|
||||||
let right = parent._start
|
|
||||||
let count = 0
|
|
||||||
while (right !== null) {
|
|
||||||
switch (right.constructor) {
|
switch (right.constructor) {
|
||||||
// case ItemBlockFormat: do not break..
|
case ItemEmbed:
|
||||||
case ItemString:
|
case ItemString:
|
||||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||||
if (count <= pos && pos <= count + rightLen) {
|
if (count <= rightLen) {
|
||||||
const splitDiff = pos - count
|
right = right._splitAt(parent._y, count)
|
||||||
right = right._splitAt(parent._y, splitDiff)
|
|
||||||
left = right._left
|
left = right._left
|
||||||
count += splitDiff
|
return [left, right, currentAttributes]
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if (!right._deleted) {
|
if (right._deleted === false) {
|
||||||
count += right._length
|
count -= right._length
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case ItemFormat:
|
case ItemFormat:
|
||||||
if (right._deleted === false) {
|
if (right._deleted === false) {
|
||||||
const key = right.key
|
updateCurrentAttributes(currentAttributes, right)
|
||||||
const value = right.value
|
|
||||||
if (value === null) {
|
|
||||||
currentAttributes.delete(key)
|
|
||||||
} else if (attributes.hasOwnProperty(key)) {
|
|
||||||
// only set if relevant
|
|
||||||
currentAttributes.set(key, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
left = right
|
left = right
|
||||||
right = right._right
|
right = right._right
|
||||||
}
|
}
|
||||||
if (pos > count) {
|
|
||||||
throw new Error('Position exceeds array range!')
|
|
||||||
}
|
|
||||||
return [left, right, currentAttributes]
|
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 {
|
export default class YText extends YArray {
|
||||||
constructor (string) {
|
constructor (string) {
|
||||||
super()
|
super()
|
||||||
@ -71,6 +388,9 @@ export default class YText extends YArray {
|
|||||||
this._start = start
|
this._start = start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
|
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
||||||
|
}
|
||||||
toString () {
|
toString () {
|
||||||
let str = ''
|
let str = ''
|
||||||
let n = this._start
|
let n = this._start
|
||||||
@ -82,10 +402,27 @@ export default class YText extends YArray {
|
|||||||
}
|
}
|
||||||
return str
|
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/
|
* As defined by Quilljs - https://quilljs.com/docs/delta/
|
||||||
*/
|
*/
|
||||||
toRichtextDelta () {
|
toDelta () {
|
||||||
let ops = []
|
let ops = []
|
||||||
let currentAttributes = new Map()
|
let currentAttributes = new Map()
|
||||||
let str = ''
|
let str = ''
|
||||||
@ -94,10 +431,16 @@ export default class YText extends YArray {
|
|||||||
if (str.length > 0) {
|
if (str.length > 0) {
|
||||||
// pack str with attributes to ops
|
// pack str with attributes to ops
|
||||||
let attributes = {}
|
let attributes = {}
|
||||||
|
let addAttributes = false
|
||||||
for (let [key, value] of currentAttributes) {
|
for (let [key, value] of currentAttributes) {
|
||||||
|
addAttributes = true
|
||||||
attributes[key] = value
|
attributes[key] = value
|
||||||
}
|
}
|
||||||
ops.push({ insert: str, attributes })
|
let op = { insert: str }
|
||||||
|
if (addAttributes) {
|
||||||
|
op.attributes = attributes
|
||||||
|
}
|
||||||
|
ops.push(op)
|
||||||
str = ''
|
str = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,13 +452,7 @@ export default class YText extends YArray {
|
|||||||
break
|
break
|
||||||
case ItemFormat:
|
case ItemFormat:
|
||||||
packStr()
|
packStr()
|
||||||
const value = n.value
|
updateCurrentAttributes(currentAttributes, n)
|
||||||
const key = n.key
|
|
||||||
if (value === null) {
|
|
||||||
currentAttributes.delete(key)
|
|
||||||
} else {
|
|
||||||
currentAttributes.set(key, value)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,72 +466,35 @@ export default class YText extends YArray {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let [left, right, currentAttributes] = findPosition(this, pos, attributes)
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
||||||
let negatedAttributes = new Map()
|
insertText(y, text, this, left, right, currentAttributes, attributes)
|
||||||
// insert format-start items
|
})
|
||||||
for (let key in attributes) {
|
}
|
||||||
const val = attributes[key]
|
insertEmbed (pos, embed, attributes = {}) {
|
||||||
const currentVal = currentAttributes.get(key)
|
if (embed.constructor !== Object) {
|
||||||
if (currentVal !== val) {
|
throw new Error('Embed must be an Object')
|
||||||
// save negated attribute (set null if currentVal undefined)
|
}
|
||||||
negatedAttributes.set(key, currentVal || null)
|
this._transact(y => {
|
||||||
let format = new ItemFormat()
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
||||||
format.key = key
|
insertText(y, embed, this, left, right, currentAttributes, attributes)
|
||||||
format.value = val
|
})
|
||||||
integrateItem(format, this, y, left, right)
|
}
|
||||||
left = format
|
delete (pos, length) {
|
||||||
}
|
if (length === 0) {
|
||||||
}
|
return
|
||||||
// insert text content
|
}
|
||||||
let item = new ItemString()
|
this._transact(y => {
|
||||||
item._content = text
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
||||||
integrateItem(item, this, y, left, right)
|
deleteText(y, length, this, left, right, currentAttributes)
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
format (pos, length, attributes) {
|
format (pos, length, attributes) {
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let [left, _right, currentAttributes] = findPosition(this, pos, attributes)
|
let [left, right, currentAttributes] = findPosition(this, pos)
|
||||||
if (_right === null) {
|
if (right === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let negatedAttributes = new Map()
|
formatText(y, length, this, left, right, currentAttributes, attributes)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_logString () {
|
_logString () {
|
||||||
|
@ -6,6 +6,8 @@ import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Type/y-xml/y-x
|
|||||||
import Delete from '../Struct/Delete.js'
|
import Delete from '../Struct/Delete.js'
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
import ItemJSON from '../Struct/ItemJSON.js'
|
||||||
import ItemString from '../Struct/ItemString.js'
|
import ItemString from '../Struct/ItemString.js'
|
||||||
|
import ItemFormat from '../Struct/ItemFormat.js'
|
||||||
|
import ItemEmbed from '../Struct/ItemEmbed.js'
|
||||||
|
|
||||||
const structs = new Map()
|
const structs = new Map()
|
||||||
const references = new Map()
|
const references = new Map()
|
||||||
@ -23,8 +25,11 @@ export function getReference (typeConstructor) {
|
|||||||
return references.get(typeConstructor)
|
return references.get(typeConstructor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: reorder (Item* should have low numbers)
|
||||||
addStruct(0, ItemJSON)
|
addStruct(0, ItemJSON)
|
||||||
addStruct(1, ItemString)
|
addStruct(1, ItemString)
|
||||||
|
addStruct(10, ItemFormat)
|
||||||
|
addStruct(11, ItemEmbed)
|
||||||
addStruct(2, Delete)
|
addStruct(2, Delete)
|
||||||
|
|
||||||
addStruct(3, YArray)
|
addStruct(3, YArray)
|
||||||
|
2
src/Y.js
2
src/Y.js
@ -23,6 +23,7 @@ import debug from 'debug'
|
|||||||
import Transaction from './Transaction.js'
|
import Transaction from './Transaction.js'
|
||||||
|
|
||||||
import TextareaBinding from './Binding/TextareaBinding.js'
|
import TextareaBinding from './Binding/TextareaBinding.js'
|
||||||
|
import QuillBinding from './Binding/QuillBinding.js'
|
||||||
|
|
||||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
||||||
|
|
||||||
@ -202,6 +203,7 @@ Y.XmlText = YXmlText
|
|||||||
Y.XmlHook = YXmlHook
|
Y.XmlHook = YXmlHook
|
||||||
|
|
||||||
Y.TextareaBinding = TextareaBinding
|
Y.TextareaBinding = TextareaBinding
|
||||||
|
Y.QuillBinding = QuillBinding
|
||||||
|
|
||||||
Y.utils = {
|
Y.utils = {
|
||||||
BinaryDecoder,
|
BinaryDecoder,
|
||||||
|
102
test/y-text.tests.js
Normal file
102
test/y-text.tests.js
Normal 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)
|
||||||
|
})
|
@ -6,6 +6,7 @@ import Chance from 'chance'
|
|||||||
import ItemJSON from '../src/Struct/ItemJSON.js'
|
import ItemJSON from '../src/Struct/ItemJSON.js'
|
||||||
import ItemString from '../src/Struct/ItemString.js'
|
import ItemString from '../src/Struct/ItemString.js'
|
||||||
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
|
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
|
||||||
|
import Quill from 'quill'
|
||||||
|
|
||||||
export const Y = _Y
|
export const Y = _Y
|
||||||
|
|
||||||
@ -92,9 +93,14 @@ export async function compareUsers (t, users) {
|
|||||||
await wait()
|
await wait()
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
|
|
||||||
var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
|
var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val)))
|
||||||
var userMapValues = users.map(u => u.get('map', Y.Map).toJSON())
|
var userMapValues = users.map(u => u.define('map', Y.Map).toJSON())
|
||||||
var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString())
|
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 => {
|
var data = users.map(u => {
|
||||||
defragmentItemContent(u)
|
defragmentItemContent(u)
|
||||||
@ -124,6 +130,8 @@ export async function compareUsers (t, users) {
|
|||||||
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
||||||
t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
|
t.compare(userMapValues[i], userMapValues[i + 1], 'map types')
|
||||||
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml 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].os, data[i + 1].os, 'os')
|
||||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
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['array' + i] = y.define('array', Y.Array)
|
||||||
result['map' + i] = y.define('map', Y.Map)
|
result['map' + i] = y.define('map', Y.Map)
|
||||||
result['xml' + i] = y.define('xml', Y.XmlElement)
|
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) {
|
y.get('xml').setDomFilter(function (nodeName, attrs) {
|
||||||
if (nodeName === 'HIDDEN') {
|
if (nodeName === 'HIDDEN') {
|
||||||
return null
|
return null
|
||||||
|
Loading…
x
Reference in New Issue
Block a user