diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts index f4833ea8..7da63ff7 100644 --- a/src/brsTypes/components/RoSGNode.ts +++ b/src/brsTypes/components/RoSGNode.ts @@ -372,6 +372,7 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable { this.callfunc, this.issubtype, this.parentsubtype, + this.clone, ], ifSGNodeBoundingRect: [this.boundingRect], }); @@ -1695,6 +1696,17 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable { }, }); + /* Returns a copy of the entire node tree or just a shallow copy. */ + private clone = new Callable("clone", { + signature: { + args: [new StdlibArgument("isDeepCopy", ValueKind.Boolean)], + returns: ValueKind.Object, + }, + impl: (interpreter: Interpreter, isDeepCopy: BrsBoolean) => { + return cloneNode(interpreter, this, isDeepCopy.toBoolean()); + }, + }); + /* Returns the subtype of this node as specified when it was created */ private subtype = new Callable("subtype", { signature: { @@ -1914,3 +1926,55 @@ function addChildren( } }); } + +function cloneNode( + interpreter: Interpreter, + toBeCloned: RoSGNode, + isDeepCopy: Boolean +): RoSGNode | BrsInvalid { + let copy = createNodeByType(interpreter, new BrsString(toBeCloned.nodeSubtype)); + + if (copy instanceof RoSGNode) { + let originalFields = toBeCloned.getFields(); + let copiedFields = new RoAssociativeArray([]); + let addReplace = copiedFields.getMethod("addreplace"); + + // Copy the fields. + for (let [key, value] of originalFields) { + if (addReplace) { + addReplace.call( + interpreter, + new BrsString(key), + getBrsValueFromFieldType(value.getType(), value.toString()) + ); + } + } + + let update = copy.getMethod("update"); + + if (update) { + update.call(interpreter, copiedFields, BrsBoolean.True); + } + + // A deep clone also copies children. + if (isDeepCopy) { + let appendChild = copy.getMethod("appendchild"); + let getChildren = toBeCloned.getMethod("getchildren"); + + if (getChildren && appendChild) { + let children = getChildren.call(interpreter, new Int32(-1), new Int32(0)); + + if (children instanceof RoArray) { + for (let child of children.getElements()) { + if (child instanceof RoSGNode) { + let childCopy = cloneNode(interpreter, child, isDeepCopy); + appendChild.call(interpreter, childCopy); + } + } + } + } + } + } + + return copy; +} diff --git a/test/brsTypes/components/RoSGNode.test.js b/test/brsTypes/components/RoSGNode.test.js index 24523f7d..b877bd58 100644 --- a/test/brsTypes/components/RoSGNode.test.js +++ b/test/brsTypes/components/RoSGNode.test.js @@ -2514,6 +2514,109 @@ describe("RoSGNode", () => { expect(result).toBe(BrsInvalid.Instance); }); }); + + describe("clone", () => { + let interpreter, originalNode; + + beforeEach(() => { + interpreter = new Interpreter(); + originalNode = new MarkupGrid([ + { name: new BrsString("field1"), value: new BrsString("a string") }, + { name: new BrsString("field2"), value: new Int32(-19999) }, + { name: new BrsString("field3"), value: BrsBoolean.False }, + { name: new BrsString("<33"), value: new BrsString("") }, + ]); + + child1 = new RoSGNode([ + { name: new BrsString("child1name"), value: new BrsString("1") }, + ]); + child2 = new RoSGNode([ + { name: new BrsString("child2name"), value: new Int32(2) }, + ]); + child3 = new RoSGNode([ + { name: new BrsString("child3name"), value: BrsBoolean.False }, + ]); + grandchild1 = new RoSGNode([ + { name: new BrsString("grandchild1name"), value: new Int32(1.2) }, + ]); + grandchild2 = new RoSGNode([ + { + name: new BrsString("grandchild2name"), + value: new BrsString("second grandchild"), + }, + ]); + + let appendGrandchildren = child1.getMethod("appendchildren"); + appendGrandchildren.call(interpreter, new RoArray([grandchild1, grandchild2])); + + let appendChildren = originalNode.getMethod("appendchildren"); + appendChildren.call(interpreter, new RoArray([child1, child2, child3])); + }); + + it("return type is same as of subject", () => { + let cloneMethod = originalNode.getMethod("clone"); + let copyNode = cloneMethod.call(interpreter, new BrsBoolean(true)); + + expect(copyNode).toBeInstanceOf(RoSGNode); + expect(copyNode).toBeInstanceOf(MarkupGrid); + }); + + it("copies field values", () => { + let cloneMethod = originalNode.getMethod("clone"); + let copyNode = cloneMethod.call(interpreter, new BrsBoolean(true)); + + expect(copyNode.get(new BrsString("field1"))).toEqual( + new BrsString("a string") + ); + expect(copyNode.get(new BrsString("field2"))).toEqual(new Int32(-19999)); + expect(copyNode.get(new BrsString("field3"))).toEqual(BrsBoolean.False); + expect(copyNode.get(new BrsString("<33"))).toEqual(new BrsString("")); + + let isSameNode = originalNode.getMethod("isSameNode"); + expect(isSameNode.call(interpreter, copyNode)).toEqual(BrsBoolean.False); + }); + + it("shallow copy doesn't copy children", () => { + let cloneMethod = originalNode.getMethod("clone"); + let copyNode = cloneMethod.call(interpreter, new BrsBoolean(false)); + + let getChildCount = copyNode.getMethod("getchildcount"); + let childCount = getChildCount.call(interpreter); + + expect(childCount).toEqual(new Int32(0)); + }); + + it("deep clone copies children and grandchildren", () => { + let cloneMethod = originalNode.getMethod("clone"); + let copyNode = cloneMethod.call(interpreter, new BrsBoolean(true)); + + let getChildCount = copyNode.getMethod("getchildcount"); + let childCount = getChildCount.call(interpreter); + + expect(childCount).toEqual(new Int32(3)); + + getChild = copyNode.getMethod("getchild"); + let child1copy = getChild.call(interpreter, new Int32(0)); + + expect(child1copy.get(new BrsString("child1name"))).toEqual(new BrsString("1")); + + let getGrandchildCount = child1copy.getMethod("getchildcount"); + let grandchildCount = getGrandchildCount.call(interpreter); + + expect(grandchildCount).toEqual(new Int32(2)); + + getChild = child1copy.getMethod("getchild"); + let grandchild1copy = getChild.call(interpreter, new Int32(0)); + let grandchild2copy = getChild.call(interpreter, new Int32(1)); + + expect(grandchild1copy.get(new BrsString("grandchild1name"))).toEqual( + new Int32(1.2) + ); + expect(grandchild2copy.get(new BrsString("grandchild2name"))).toEqual( + new BrsString("second grandchild") + ); + }); + }); }); }); diff --git a/test/e2e/RoSGNode.test.js b/test/e2e/RoSGNode.test.js index bdc35420..8b5223a2 100644 --- a/test/e2e/RoSGNode.test.js +++ b/test/e2e/RoSGNode.test.js @@ -209,6 +209,8 @@ describe("components/roSGNode", () => { "invalid", "33", "37", + "updatedId", + "newValue", "0", "0", "0", diff --git a/test/e2e/resources/components/roSGNode/roSGNode.brs b/test/e2e/resources/components/roSGNode/roSGNode.brs index f490522b..c25988fa 100644 --- a/test/e2e/resources/components/roSGNode/roSGNode.brs +++ b/test/e2e/resources/components/roSGNode/roSGNode.brs @@ -214,6 +214,16 @@ sub init() node.ClipStart = 37 ?node.ClipStart + ' it clones nodes + node = createObject("roSGNode", "ContentNode") + node.update({ + id: "updatedId", + newField: "newValue" + }, true) + clonedNode = node.clone(true) + print clonedNode.id + print clonedNode.newField + ' ifSGNodeBoundingRect rect = node.boundingRect() print rect.x ' => 0