Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve NodeHelpers.descendants() #86

Merged
merged 4 commits into from
Jun 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 44 additions & 33 deletions packages/core/src/editor/NodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
ROOT_NODE,
} from '@craftjs/utils';
import { serializeNode } from '../utils/serializeNode';
import { mergeTrees } from '../utils/mergeTrees';

export function NodeHelpers(state: EditorState, id: NodeId) {
invariant(typeof id == 'string', ERROR_INVALID_NODE_ID);
Expand Down Expand Up @@ -55,66 +54,71 @@ export function NodeHelpers(state: EditorState, id: NodeId) {
get() {
return node;
},
ancestors(deep = false) {
ancestors(deep = false): NodeId[] {
function appendParentNode(
id: NodeId,
result: NodeId[] = [],
ancestors: NodeId[] = [],
depth: number = 0
) {
const node = state.nodes[id];
if (!node) {
return result;
return ancestors;
}

result.push(id);
ancestors.push(id);

if (!node.data.parent) {
return result;
return ancestors;
}

if (deep || (!deep && depth === 0)) {
result = appendParentNode(node.data.parent, result, depth + 1);
ancestors = appendParentNode(node.data.parent, ancestors, depth + 1);
}
return result;
return ancestors;
}
return appendParentNode(node.data.parent);
},
descendants(deep = false) {
descendants(
deep = false,
includeOnly?: 'linkedNodes' | 'childNodes'
): NodeId[] {
function appendChildNode(
id: NodeId,
result: NodeId[] = [],
descendants: NodeId[] = [],
depth: number = 0
) {
if (deep || (!deep && depth === 0)) {
const node = state.nodes[id];

if (!node) {
return result;
return descendants;
}

// Include linkedNodes if any
const linkedNodes = nodeHelpers(id).linkedNodes();
if (includeOnly !== 'childNodes') {
// Include linkedNodes if any
const linkedNodes = nodeHelpers(id).linkedNodes();

linkedNodes.forEach((nodeId) => {
result.push(nodeId);
result = appendChildNode(nodeId, result, depth + 1);
});
linkedNodes.forEach((nodeId) => {
descendants.push(nodeId);
descendants = appendChildNode(nodeId, descendants, depth + 1);
});
}

const childNodes = node.data.nodes;
if (includeOnly !== 'linkedNodes') {
const childNodes = node.data.nodes;

if (!childNodes) {
return result;
// Include child Nodes if any
if (childNodes) {
childNodes.forEach((nodeId) => {
descendants.push(nodeId);
descendants = appendChildNode(nodeId, descendants, depth + 1);
});
}
}

// Include child Nodes if any
if (childNodes) {
childNodes.forEach((nodeId) => {
result.push(nodeId);
result = appendChildNode(nodeId, result, depth + 1);
});
}
return descendants;
}
return result;
return descendants;
}
return appendChildNode(id);
},
Expand Down Expand Up @@ -202,12 +206,19 @@ export function NodeHelpers(state: EditorState, id: NodeId) {
toSerializedNode() {
return serializeNode(node.data, state.options.resolver);
},
toNodeTree() {
const childNodes = (node.data.nodes || []).map((childNodeId) => {
return NodeHelpers(state, childNodeId).toNodeTree();
});
toNodeTree(includeOnly?: 'linkedNodes' | 'childNodes') {
const nodes = [id, ...this.descendants(true, includeOnly)].reduce(
(accum, descendantId) => {
accum[descendantId] = nodeHelpers(descendantId).get();
return accum;
},
{}
);

return mergeTrees(node, childNodes);
return {
rootNodeId: id,
nodes,
};
},

/**
Expand Down
87 changes: 57 additions & 30 deletions packages/core/src/editor/tests/NodeHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,49 @@ describe('NodeHelpers', () => {
});

describe('descendants', () => {
it('should return immediate child node ids', () => {
expect(helper(rootNode.id).descendants()).toStrictEqual([card.id]);
it('should return immediate child and linked node ids', () => {
expect(helper('canvas-node-reject-dnd').descendants()).toStrictEqual(
helper('canvas-node-reject-dnd').get().data.nodes
);
});
it('should return all child nodes', () => {
expect(helper(rootNode.id).descendants(true)).toStrictEqual([
card.id,
...documentWithVariousNodes.nodes[card.id].data.nodes,
]);
describe('when "includeOnly" is unset', () => {
it('should return all child and linked nodes', () => {
expect(
helper('canvas-node-reject-dnd').descendants(true)
).toStrictEqual([
...documentWithVariousNodes.nodes['canvas-node-reject-dnd'].data
.nodes,
...Object.values(
documentWithVariousNodes.nodes['parent-of-linked-node'].data
.linkedNodes || {}
),
]);
});
});
describe('when "includeOnly" is set to childNodes', () => {
it('should return all child nodes only', () => {
expect(
helper('canvas-node-reject-dnd').descendants(true, 'childNodes')
).toStrictEqual([
...documentWithVariousNodes.nodes['canvas-node-reject-dnd'].data
.nodes,
]);
});
});
describe('when "includeOnly" is set to linkedNodes', () => {
it('should return all linked nodes only', () => {
expect(
helper('canvas-node-reject-dnd').descendants(true, 'linkedNodes')
).toStrictEqual([]);
expect(
helper('parent-of-linked-node').descendants(true, 'linkedNodes')
).toStrictEqual(
Object.values(
documentWithVariousNodes.nodes['parent-of-linked-node'].data
.linkedNodes || {}
)
);
});
});
});

Expand Down Expand Up @@ -142,33 +177,25 @@ describe('NodeHelpers', () => {
});
describe('toNodeTree', () => {
let tree;
let testHelper;
let testDescendants = jest.fn().mockImplementation(() => []);
let descendantType;
beforeEach(() => {
tree = helper('ROOT').toNodeTree();
testHelper = jest.fn().mockImplementation(function (...args) {
return {
...helper(...args),
descendants: testDescendants,
};
});

tree = testHelper('canvas-node-reject-dnd').toNodeTree(descendantType);
});

it('should have correct rootNodeId', () => {
expect(tree.rootNodeId).toEqual('ROOT');
});
it('should contain root and child nodes', () => {
const { nodes } = tree;

expect(nodes).toStrictEqual({
ROOT: documentWithVariousNodes.nodes['ROOT'],
...documentWithVariousNodes.nodes['ROOT'].data.nodes.reduce(
(accum, key) => {
accum[key] = documentWithVariousNodes.nodes[key];
return accum;
},
{}
),
...documentWithVariousNodes.nodes[card.id].data.nodes.reduce(
(accum, key) => {
accum[key] = documentWithVariousNodes.nodes[key];
return accum;
},
{}
),
});
expect(tree.rootNodeId).toEqual('canvas-node-reject-dnd');
});
it('should have called .descendants', () => {
expect(testDescendants).toHaveBeenCalledWith(true, descendantType);
});
});
});
94 changes: 93 additions & 1 deletion packages/docs/docs/api/NodeHelpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,98 @@ Get `Node` object from id

Returns an array of Node ids of all child Nodes of a given Node.

#### Parameters
<API items={[
["deep", "boolean", "If set to true, retrieve all descendants in nested levels. Default is false"],
["includeOnly?", "'childNodes' | 'linkedNodes'", "Get descendants that are either childNodes or linkedNodes. If unset, get all descendants"]
]} />


#### Returns
<API items={[
["NodeId[]"]
]} />

```jsx
// The descendants of `div` when deep=false
<div>
// highlight-next-line
<h2>Yo</h2>
// highlight-next-line
<Element is={Container}>
<h3>Child</h3>
// highlight-next-line
</Element>
</div>
```

```jsx
// The descendants of `div` when deep=true
<div>
// highlight-start
<h2>Yo</h2>
<Element is={Container}>
<h3>Child</h3>
</Element>
// highlight-end
</div>

const Container = () => {
return (
<div>
// highlight-start
<Element id="linked-div">
<h1>Hello</h1>
<Element>
// highlight-end
</div>
)
}
```

```jsx
// The descendants of `div` when deep=true and includeOnly="childNodes" only
<div>
// highlight-start
<h2>Yo</h2>
<Element is={Container}>
<h3>Child</h3>
</Element>
// highlight-end
</div>

const Container = () => {
return (
<div>
<Element id="linked-div">
<h1>Hello</h1>
<Element>
</div>
)
}
```

```jsx
// The descendants of `div` when deep=true and includeOnly="linkedNodes" only
<div>
<h2>Yo</h2>
<Element is={Container}>
<h3>Child</h3>
</Element>
</div>

const Container = () => {
return (
<div>
// highlight-start
<Element id="linked-div">
<h1>Hello</h1>
<Element>
// highlight-end
</div>
)
}
```

### ancestors
<Badge type="function" />
Expand Down Expand Up @@ -417,7 +503,13 @@ Gets the current Node in it's `SerializedNode` form
<Badge type="function" noMargin={true} />


Gets the current Node in it's `NodeTree` form
Gets the current Node and its descendants in its `NodeTree` form

#### Parameters
<API items={[
["includeOnly?", "'childNodes' | 'linkedNodes'", "Get descendants that are either childNodes or linkedNodes. If unset, get all descendants"]
]} />


#### Returns
<API items={[
Expand Down