Merge pull request #267 from yjs/wishlist-259

Implements some features of wishlist #259
This commit is contained in:
Kevin Jahns 2020-12-12 21:38:45 +01:00 committed by GitHub
commit d3b56702ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 258 additions and 10 deletions

View File

@ -242,6 +242,8 @@ necessary.
</p> </p>
<pre>const yarray = new Y.Array()</pre> <pre>const yarray = new Y.Array()</pre>
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b>
<dd></dd>
<b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b> <b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd> <dd>
Insert content at <var>index</var>. Note that content is an array of elements. Insert content at <var>index</var>. Note that content is an array of elements.
@ -313,6 +315,8 @@ or any of its children.
</p> </p>
<pre><code>const ymap = new Y.Map()</code></pre> <pre><code>const ymap = new Y.Map()</code></pre>
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b>
<dd></dd>
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b> <b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
<dd></dd> <dd></dd>
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b> <b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
@ -392,6 +396,8 @@ YTextEvents compute changes as deltas.
</p> </p>
<pre>const ytext = new Y.Text()</pre> <pre>const ytext = new Y.Text()</pre>
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b>
<dd></dd>
<b><code>insert(index:number, content:string, [formattingAttributes:Object&lt;string,string&gt;])</code></b> <b><code>insert(index:number, content:string, [formattingAttributes:Object&lt;string,string&gt;])</code></b>
<dd> <dd>
Insert a string at <var>index</var> and assign formatting attributes to it. Insert a string at <var>index</var> and assign formatting attributes to it.
@ -450,6 +456,10 @@ or any of its children.
</p> </p>
<pre><code>const yxml = new Y.XmlFragment()</code></pre> <pre><code>const yxml = new Y.XmlFragment()</code></pre>
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b>
<dd></dd>
<b><code>firstChild:Y.XmlElement|Y.XmlText|null</code></b>
<dd></dd>
<b><code>insert(index:number, content:Array&lt;Y.XmlElement|Y.XmlText&gt;)</code></b> <b><code>insert(index:number, content:Array&lt;Y.XmlElement|Y.XmlText&gt;)</code></b>
<dd></dd> <dd></dd>
<b><code>delete(index:number, length:number)</code></b> <b><code>delete(index:number, length:number)</code></b>
@ -505,6 +515,14 @@ content and be actually XML compliant.
</p> </p>
<pre><code>const yxml = new Y.XmlElement()</code></pre> <pre><code>const yxml = new Y.XmlElement()</code></pre>
<dl> <dl>
<b><code>parent:Y.AbstractType|null</code></b>
<dd></dd>
<b><code>firstChild:Y.XmlElement|Y.XmlText|null</code></b>
<dd></dd>
<b><code>nextSibling:Y.XmlElement|Y.XmlText|null</code></b>
<dd></dd>
<b><code>prevSibling:Y.XmlElement|Y.XmlText|null</code></b>
<dd></dd>
<b><code>insert(index:number, content:Array&lt;Y.XmlElement|Y.XmlText&gt;)</code></b> <b><code>insert(index:number, content:Array&lt;Y.XmlElement|Y.XmlText&gt;)</code></b>
<dd></dd> <dd></dd>
<b><code>delete(index:number, length:number)</code></b> <b><code>delete(index:number, length:number)</code></b>

View File

@ -287,6 +287,13 @@ export class AbstractType {
this._searchMarker = null this._searchMarker = null
} }
/**
* @return {AbstractType<any>|null}
*/
get parent () {
return this._item ? /** @type {AbstractType<any>} */ (this._item.parent) : null
}
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *

View File

@ -21,6 +21,10 @@ import {
iterateDeletedStructs, iterateDeletedStructs,
iterateStructs, iterateStructs,
findMarker, findMarker,
typeMapDelete,
typeMapSet,
typeMapGet,
typeMapGetAll,
updateMarkerChanges, updateMarkerChanges,
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
@ -512,13 +516,32 @@ export class YTextEvent extends YEvent {
/** /**
* @param {YText} ytext * @param {YText} ytext
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed
*/ */
constructor (ytext, transaction) { constructor (ytext, transaction, subs) {
super(ytext, transaction) super(ytext, transaction)
/** /**
* @type {Array<DeltaItem>|null} * @type {Array<DeltaItem>|null}
*/ */
this._delta = null this._delta = null
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.keysChanged = new Set()
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true
} else {
this.keysChanged.add(sub)
}
})
} }
/** /**
@ -779,7 +802,7 @@ export class YText extends AbstractType {
*/ */
_callObserver (transaction, parentSubs) { _callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs) super._callObserver(transaction, parentSubs)
const event = new YTextEvent(this, transaction) const event = new YTextEvent(this, transaction, parentSubs)
const doc = transaction.doc const doc = transaction.doc
// If a remote change happened, we try to cleanup potential formatting duplicates. // If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local) { if (!transaction.local) {
@ -1111,6 +1134,74 @@ export class YText extends AbstractType {
} }
} }
/**
* Removes an attribute.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that is to be removed.
*
* @public
*/
removeAttribute (attributeName) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.removeAttribute(attributeName))
}
}
/**
* Sets or updates an attribute.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that is to be set.
* @param {any} attributeValue The attribute value that is to be set.
*
* @public
*/
setAttribute (attributeName, attributeValue) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, attributeValue)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue))
}
}
/**
* Returns an attribute value that belongs to the attribute name.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that identifies the
* queried value.
* @return {any} The queried attribute value.
*
* @public
*/
getAttribute (attributeName) {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {Snapshot} [snapshot]
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes (snapshot) {
return typeMapGetAll(this)
}
/** /**
* @param {AbstractUpdateEncoder} encoder * @param {AbstractUpdateEncoder} encoder
*/ */

View File

@ -8,7 +8,7 @@ import {
typeMapGetAll, typeMapGetAll,
typeListForEach, typeListForEach,
YXmlElementRefID, YXmlElementRefID,
AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line YXmlText, ContentType, AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
/** /**
@ -28,6 +28,22 @@ export class YXmlElement extends YXmlFragment {
this._prelimAttrs = new Map() this._prelimAttrs = new Map()
} }
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *

View File

@ -1,7 +1,7 @@
import { import {
YEvent, YEvent,
YXmlElement, YXmlFragment, Transaction // eslint-disable-line YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
/** /**
@ -9,7 +9,7 @@ import {
*/ */
export class YXmlEvent extends YEvent { export class YXmlEvent extends YEvent {
/** /**
* @param {YXmlElement|YXmlFragment} target The target on which the event is created. * @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the * @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
* child list changed. * child list changed.
* @param {Transaction} transaction The transaction instance with wich the * @param {Transaction} transaction The transaction instance with wich the
@ -25,7 +25,7 @@ export class YXmlEvent extends YEvent {
this.childListChanged = false this.childListChanged = false
/** /**
* Set of all changed attributes. * Set of all changed attributes.
* @type {Set<string|null>} * @type {Set<string>}
*/ */
this.attributesChanged = new Set() this.attributesChanged = new Set()
subs.forEach((sub) => { subs.forEach((sub) => {

View File

@ -9,6 +9,7 @@ import {
typeListMap, typeListMap,
typeListForEach, typeListForEach,
typeListInsertGenerics, typeListInsertGenerics,
typeListInsertGenericsAfter,
typeListDelete, typeListDelete,
typeListToArray, typeListToArray,
YXmlFragmentRefID, YXmlFragmentRefID,
@ -19,6 +20,8 @@ import {
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js'
/** /**
* Define the elements to which a set of CSS queries apply. * Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors} * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
@ -130,6 +133,14 @@ export class YXmlFragment extends AbstractType {
this._prelimContent = [] this._prelimContent = []
} }
/**
* @type {YXmlElement|YXmlText|null}
*/
get firstChild () {
const first = this._first
return first ? first.content.getContent()[0] : null
}
/** /**
* Integrate this type into the Yjs instance. * Integrate this type into the Yjs instance.
* *
@ -302,6 +313,32 @@ export class YXmlFragment extends AbstractType {
} }
} }
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insertAfter (ref, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref
typeListInsertGenericsAfter(transaction, this, refItem, content)
})
} else {
const pc = /** @type {Array<any>} */ (this._prelimContent)
const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1
if (index === 0 && ref !== null) {
throw error.create('Reference item not found')
}
pc.splice(index, 0, ...content)
}
}
/** /**
* Deletes elements starting from an index. * Deletes elements starting from an index.
* *

View File

@ -2,7 +2,7 @@
import { import {
YText, YText,
YXmlTextRefID, YXmlTextRefID,
AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line ContentType, YXmlElement, AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
} from '../internals.js' } from '../internals.js'
/** /**
@ -10,6 +10,22 @@ import {
* simple formatting information like bold and italic. * simple formatting information like bold and italic.
*/ */
export class YXmlText extends YText { export class YXmlText extends YText {
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
_copy () { _copy () {
return new YXmlText() return new YXmlText()
} }

View File

@ -264,6 +264,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
/** /**
* @param {RelativePosition|null} a * @param {RelativePosition|null} a
* @param {RelativePosition|null} b * @param {RelativePosition|null} b
* @return {boolean}
* *
* @function * @function
*/ */

View File

@ -119,9 +119,6 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
} }
result = stackItem result = stackItem
if (result != null) {
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
}
} }
transaction.changed.forEach((subProps, type) => { transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary // destroy search marker if necessary
@ -130,6 +127,9 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
}) })
}, undoManager) }, undoManager)
if (result != null) {
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
}
return result return result
} }

View File

@ -40,6 +40,8 @@ export class YEvent {
/** /**
* Computes the path from `y` to the changed type. * Computes the path from `y` to the changed type.
* *
* @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with.
*
* The following property holds: * The following property holds:
* @example * @example
* let type = y * let type = y

View File

@ -73,3 +73,63 @@ export const testTreewalker = tc => {
t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1') t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1')
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testYtextAttributes = tc => {
const ydoc = new Y.Doc()
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
ytext.observe(event => {
t.compare(event.changes.keys.get('test'), { action: 'add', oldValue: undefined })
})
ytext.setAttribute('test', 42)
t.compare(ytext.getAttribute('test'), 42)
t.compare(ytext.getAttributes(), { test: 42 })
}
/**
* @param {t.TestCase} tc
*/
export const testSiblings = tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText()
const second = new Y.XmlElement('p')
yxml.insert(0, [first, second])
t.assert(first.nextSibling === second)
t.assert(second.prevSibling === first)
t.assert(first.parent === yxml)
t.assert(yxml.parent === null)
t.assert(yxml.firstChild === first)
}
/**
* @param {t.TestCase} tc
*/
export const testInsertafter = tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText()
const second = new Y.XmlElement('p')
const third = new Y.XmlElement('p')
const deepsecond1 = new Y.XmlElement('span')
const deepsecond2 = new Y.XmlText()
second.insertAfter(null, [deepsecond1])
second.insertAfter(deepsecond1, [deepsecond2])
yxml.insertAfter(null, [first, second])
yxml.insertAfter(second, [third])
t.assert(yxml.length === 3)
t.assert(second.get(0) === deepsecond1)
t.assert(second.get(1) === deepsecond2)
t.compareArrays(yxml.toArray(), [first, second, third])
t.fails(() => {
const el = new Y.XmlElement('p')
el.insertAfter(deepsecond1, [new Y.XmlText()])
})
}