From e1f0324840455fcedba10deedf3685af429e3b02 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 13 Nov 2020 12:05:53 +0100 Subject: [PATCH 1/6] call UndoManager pop-stack-item after transaction --- src/utils/RelativePosition.js | 1 + src/utils/UndoManager.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/RelativePosition.js b/src/utils/RelativePosition.js index a4af4af7..dc435359 100644 --- a/src/utils/RelativePosition.js +++ b/src/utils/RelativePosition.js @@ -264,6 +264,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => { /** * @param {RelativePosition|null} a * @param {RelativePosition|null} b + * @return {boolean} * * @function */ diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 0710ef11..ce906be3 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -119,9 +119,6 @@ const popStackItem = (undoManager, stack, eventType) => { } } result = stackItem - if (result != null) { - undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager]) - } } transaction.changed.forEach((subProps, type) => { // destroy search marker if necessary @@ -130,6 +127,9 @@ const popStackItem = (undoManager, stack, eventType) => { } }) }, undoManager) + if (result != null) { + undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager]) + } return result } From 0aca7bbefaa6bc7e9d48a492c3d48acfa9873e10 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 13 Nov 2020 12:40:53 +0100 Subject: [PATCH 2/6] implement attributes on Y.Text --- src/types/YText.js | 95 +++++++++++++++++++++++++++++++++++++++++- src/types/YXmlEvent.js | 6 +-- tests/y-xml.tests.js | 14 +++++++ 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/src/types/YText.js b/src/types/YText.js index 2c129385..a12d0747 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -21,6 +21,10 @@ import { iterateDeletedStructs, iterateStructs, findMarker, + typeMapDelete, + typeMapSet, + typeMapGet, + typeMapGetAll, updateMarkerChanges, ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line } from '../internals.js' @@ -512,13 +516,32 @@ export class YTextEvent extends YEvent { /** * @param {YText} ytext * @param {Transaction} transaction + * @param {Set} subs The keys that changed */ - constructor (ytext, transaction) { + constructor (ytext, transaction, subs) { super(ytext, transaction) /** * @type {Array|null} */ this._delta = null + /** + * Whether the children changed. + * @type {Boolean} + * @private + */ + this.childListChanged = false + /** + * Set of all changed attributes. + * @type {Set} + */ + 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) { super._callObserver(transaction, parentSubs) - const event = new YTextEvent(this, transaction) + const event = new YTextEvent(this, transaction, parentSubs) const doc = transaction.doc // If a remote change happened, we try to cleanup potential formatting duplicates. 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} */ (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} */ (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} A JSON Object that describes the attributes. + * + * @public + */ + getAttributes (snapshot) { + return typeMapGetAll(this) + } + /** * @param {AbstractUpdateEncoder} encoder */ diff --git a/src/types/YXmlEvent.js b/src/types/YXmlEvent.js index c41ba3d4..535e55d6 100644 --- a/src/types/YXmlEvent.js +++ b/src/types/YXmlEvent.js @@ -1,7 +1,7 @@ import { YEvent, - YXmlElement, YXmlFragment, Transaction // eslint-disable-line + YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line } from '../internals.js' /** @@ -9,7 +9,7 @@ import { */ 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} subs The set of changed attributes. `null` is included if the * child list changed. * @param {Transaction} transaction The transaction instance with wich the @@ -25,7 +25,7 @@ export class YXmlEvent extends YEvent { this.childListChanged = false /** * Set of all changed attributes. - * @type {Set} + * @type {Set} */ this.attributesChanged = new Set() subs.forEach((sub) => { diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index 8c2023fc..d59ee6c8 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -73,3 +73,17 @@ export const testTreewalker = tc => { t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1') 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 }) +} From 1ed58909d3053f3ce9775ad0f9bd1dfc6721b0a4 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sat, 14 Nov 2020 13:33:43 +0100 Subject: [PATCH 3/6] implement prev/nextSibling&firstChild & parent - #259 --- README.md | 18 ++++++++++++++++++ src/types/AbstractType.js | 7 +++++++ src/types/YXmlElement.js | 18 +++++++++++++++++- src/types/YXmlFragment.js | 8 ++++++++ src/types/YXmlText.js | 18 +++++++++++++++++- tests/y-xml.tests.js | 16 ++++++++++++++++ 6 files changed, 83 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60dcf892..d36b15b8 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,8 @@ necessary.

const yarray = new Y.Array()
+ parent:Y.AbstractType|null +
insert(index:number, content:Array<object|boolean|Array|string|number|Uint8Array|Y.Type>)
Insert content at index. Note that content is an array of elements. @@ -312,6 +314,8 @@ or any of its children.

const ymap = new Y.Map()
+ parent:Y.AbstractType|null +
get(key:string):object|boolean|string|number|Uint8Array|Y.Type
set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type) @@ -391,6 +395,8 @@ YTextEvents compute changes as deltas.

const ytext = new Y.Text()
+ parent:Y.AbstractType|null +
insert(index:number, content:string, [formattingAttributes:Object<string,string>])
Insert a string at index and assign formatting attributes to it. @@ -449,6 +455,10 @@ or any of its children.

const yxml = new Y.XmlFragment()
+ parent:Y.AbstractType|null +
+ firstChild:Y.XmlElement|Y.XmlText|null +
insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)
delete(index:number, length:number) @@ -504,6 +514,14 @@ content and be actually XML compliant.

const yxml = new Y.XmlElement()
+ parent:Y.AbstractType|null +
+ firstChild:Y.XmlElement|Y.XmlText|null +
+ nextSibling:Y.XmlElement|Y.XmlText|null +
+ prevSibling:Y.XmlElement|Y.XmlText|null +
insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)
delete(index:number, length:number) diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index c3513d68..3b6aa581 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -287,6 +287,13 @@ export class AbstractType { this._searchMarker = null } + /** + * @return {AbstractType|null} + */ + get parent () { + return this._item ? /** @type {AbstractType} */ (this._item.parent) : null + } + /** * Integrate this type into the Yjs instance. * diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index 0757e1b2..3b64fa51 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -8,7 +8,7 @@ import { typeMapGetAll, typeListForEach, YXmlElementRefID, - AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line + YXmlText, ContentType, AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line } from '../internals.js' /** @@ -28,6 +28,22 @@ export class YXmlElement extends YXmlFragment { 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. * diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js index 711c9687..41c581e1 100644 --- a/src/types/YXmlFragment.js +++ b/src/types/YXmlFragment.js @@ -130,6 +130,14 @@ export class YXmlFragment extends AbstractType { 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. * diff --git a/src/types/YXmlText.js b/src/types/YXmlText.js index 697e31c9..dd8d892d 100644 --- a/src/types/YXmlText.js +++ b/src/types/YXmlText.js @@ -2,7 +2,7 @@ import { YText, YXmlTextRefID, - AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line + ContentType, YXmlElement, AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line } from '../internals.js' /** @@ -10,6 +10,22 @@ import { * simple formatting information like bold and italic. */ 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 () { return new YXmlText() } diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index d59ee6c8..48132175 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -87,3 +87,19 @@ export const testYtextAttributes = tc => { 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) +} From cb705922b41d62b401d3af23cea4285f810865a0 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sun, 15 Nov 2020 14:57:45 +0100 Subject: [PATCH 4/6] implement insertAfter - #259 --- src/types/YXmlFragment.js | 29 +++++++++++++++++++++++++++++ tests/y-xml.tests.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js index 41c581e1..21c6a3a0 100644 --- a/src/types/YXmlFragment.js +++ b/src/types/YXmlFragment.js @@ -9,6 +9,7 @@ import { typeListMap, typeListForEach, typeListInsertGenerics, + typeListInsertGenericsAfter, typeListDelete, typeListToArray, YXmlFragmentRefID, @@ -19,6 +20,8 @@ import { AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line } from '../internals.js' +import * as error from 'lib0/error.js' + /** * 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} @@ -310,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} 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} */ (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. * diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index 48132175..31513091 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -103,3 +103,33 @@ export const testSiblings = tc => { 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()]) + }) +} From 7bcd4a828daab1bb67e58f2b140bea5253110ac4 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Mon, 16 Nov 2020 12:40:18 +0100 Subject: [PATCH 5/6] Create new Pos API - #259 --- src/utils/Pos.js | 120 ++++++++++++++++++++++++++++++++++++++++++++ src/utils/YEvent.js | 2 + 2 files changed, 122 insertions(+) create mode 100644 src/utils/Pos.js diff --git a/src/utils/Pos.js b/src/utils/Pos.js new file mode 100644 index 00000000..d404bf2c --- /dev/null +++ b/src/utils/Pos.js @@ -0,0 +1,120 @@ +import { AbstractType, Doc } from '../internals.js' + +/** + * - [ ] Write sample documentation on gitbook + * - [ ] Fill out functions + * - [ ] Add pos functionality to insertAfter + * - [ ] Think about walking api: pos.walk(1) pos.walk(-3) + * - [ ] Should only recompute path if necessary. + * - [ ] types should have property type.pos + * - [ ] should type have api ytext.getPos(3) & ytext.getRange(3, 65) ?? + */ + +/** + * @param {Doc} ydoc + * @param {Uint8Array} buf + * @return {Pos} + */ +export const decodePos = (ydoc, buf) => { + +} + +/** + * @param {Doc} ydoc + * @param {any} json + * @return {Pos} + */ +export const decodePosJSON = (ydoc, json) => { + +} + +/** + * The new position API. It implements the same functionality as Relative Pos, but has convenient methods to work with positions. + * + */ +export class Pos { + constructor () { + + } + + /** + * @param {AbstractType} type + * @param {number|string} index + * @return {Pos} + */ + static from (type, index) { + + } + + /** + * @type {boolean} + */ + get deleted () { + + } + + /** + * @type {Array<{parent: AbstractType, index: number}|{ parent: AbstractType, key: string }>} + */ + get path () { + + } + + /** + * Delete the pointed content. + * + * If this points to list content, it will delete len=1 characters starting from the pointed position. + * + * If this points to a map-attribute, it will delete the curent value. + * + * @param {number?} len + */ + delete (len) { + + } + + /** + * Retrieves the current value of the pointed position. + * + * @return {any} + */ + get () { + + } + + /** + * Encode the position to a uint8array that you can send to other peers. + * + * @return {Uint8Array} + */ + encode () { + + } + + /** + * Encode the position to a JSON object that you can send to other peers. + * + * @return {any} + */ + encodeJSON () { + + } +} + +/** + * @todo decide if we want a range API. + */ +class Range { + constructor (anchor, head) { + this.start = + this.end = + this.forward = + } + + delete () {} + + encode () {} + + +} + diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 76be076b..ff4e7339 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -40,6 +40,8 @@ export class YEvent { /** * 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: * @example * let type = y From e497f07f7a496fab219aef5b62490a1c69055b60 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sat, 12 Dec 2020 21:33:14 +0100 Subject: [PATCH 6/6] remove new pos api template --- src/utils/Pos.js | 120 ----------------------------------------------- 1 file changed, 120 deletions(-) delete mode 100644 src/utils/Pos.js diff --git a/src/utils/Pos.js b/src/utils/Pos.js deleted file mode 100644 index d404bf2c..00000000 --- a/src/utils/Pos.js +++ /dev/null @@ -1,120 +0,0 @@ -import { AbstractType, Doc } from '../internals.js' - -/** - * - [ ] Write sample documentation on gitbook - * - [ ] Fill out functions - * - [ ] Add pos functionality to insertAfter - * - [ ] Think about walking api: pos.walk(1) pos.walk(-3) - * - [ ] Should only recompute path if necessary. - * - [ ] types should have property type.pos - * - [ ] should type have api ytext.getPos(3) & ytext.getRange(3, 65) ?? - */ - -/** - * @param {Doc} ydoc - * @param {Uint8Array} buf - * @return {Pos} - */ -export const decodePos = (ydoc, buf) => { - -} - -/** - * @param {Doc} ydoc - * @param {any} json - * @return {Pos} - */ -export const decodePosJSON = (ydoc, json) => { - -} - -/** - * The new position API. It implements the same functionality as Relative Pos, but has convenient methods to work with positions. - * - */ -export class Pos { - constructor () { - - } - - /** - * @param {AbstractType} type - * @param {number|string} index - * @return {Pos} - */ - static from (type, index) { - - } - - /** - * @type {boolean} - */ - get deleted () { - - } - - /** - * @type {Array<{parent: AbstractType, index: number}|{ parent: AbstractType, key: string }>} - */ - get path () { - - } - - /** - * Delete the pointed content. - * - * If this points to list content, it will delete len=1 characters starting from the pointed position. - * - * If this points to a map-attribute, it will delete the curent value. - * - * @param {number?} len - */ - delete (len) { - - } - - /** - * Retrieves the current value of the pointed position. - * - * @return {any} - */ - get () { - - } - - /** - * Encode the position to a uint8array that you can send to other peers. - * - * @return {Uint8Array} - */ - encode () { - - } - - /** - * Encode the position to a JSON object that you can send to other peers. - * - * @return {any} - */ - encodeJSON () { - - } -} - -/** - * @todo decide if we want a range API. - */ -class Range { - constructor (anchor, head) { - this.start = - this.end = - this.forward = - } - - delete () {} - - encode () {} - - -} -