diff --git a/README.md b/README.md
index 01eea0e1..d19a48b0 100644
--- a/README.md
+++ b/README.md
@@ -242,6 +242,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.
@@ -313,6 +315,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)
@@ -392,6 +396,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.
@@ -450,6 +456,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)
@@ -505,6 +515,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/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/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/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/src/types/YXmlFragment.js b/src/types/YXmlFragment.js
index 711c9687..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}
@@ -130,6 +133,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.
*
@@ -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} 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/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/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
}
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
diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js
index 8c2023fc..31513091 100644
--- a/tests/y-xml.tests.js
+++ b/tests/y-xml.tests.js
@@ -73,3 +73,63 @@ 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 })
+}
+
+/**
+ * @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()])
+ })
+}