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

Hide/show child nodes of a branch #325

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8f8e65a
add: hidden field to mmpnode
sorenjohanson Jul 5, 2024
bdfcfc5
add: functionality to show/hide nodes
sorenjohanson Jul 5, 2024
c867398
add: tooltip and translations
sorenjohanson Jul 5, 2024
12ff66d
changed: use hasHiddenChildren instead
sorenjohanson Jul 10, 2024
7f5f35b
Merge branch 'main' into 105-hide-show-child-nodes-of-a-branch
sorenjohanson Jul 18, 2024
204583d
changed: use client-side hidden variable, add key function to dom nodes
sorenjohanson Jul 19, 2024
068e26a
fix: do not send websocket update when hiding nodes
sorenjohanson Jul 24, 2024
32d1e1e
fix: only hide direct children, not all descendants
sorenjohanson Jul 25, 2024
b8dfccf
changed: toolbar icon depending on node visibility, removed unnecessa…
sorenjohanson Jul 25, 2024
fc9fe67
changed: remove unnecessary UserNodeProperties attribute
sorenjohanson Jul 25, 2024
68141bc
fix: hide all descendants properly based on their parent status
sorenjohanson Jul 26, 2024
84a6c25
add: hidden eye icon when updating nodes
sorenjohanson Jul 26, 2024
ea2dd7d
add: disable hide node button if no node/root node is selected
sorenjohanson Jul 26, 2024
5d4e097
changed: rename methods
sorenjohanson Jul 29, 2024
ea706ff
removed: unnecessary class
sorenjohanson Jul 29, 2024
4f197de
fix: remove text like visibility_off and link from exported image
sorenjohanson Jul 30, 2024
9ccde3b
Merge branch 'main' into 105-hide-show-child-nodes-of-a-branch
sorenjohanson Aug 9, 2024
36b5319
add: comments to explain slightly more complicated branch visibility …
sorenjohanson Aug 9, 2024
4f2288c
fix: lint
sorenjohanson Aug 9, 2024
0d010d5
changed: history should not include visibility changes, fixed: toolba…
sorenjohanson Aug 9, 2024
04e5176
changed: visibility hidden instead of filtering out nodes, set proper…
sorenjohanson Aug 10, 2024
bef9113
fix: eye icon not rendering during undo/redo
sorenjohanson Aug 12, 2024
250a84c
added: method to recursively hide child nodes and retain proper hidde…
sorenjohanson Aug 13, 2024
587016f
fix: lint
sorenjohanson Aug 13, 2024
5beaf21
changed: small changes to variable names
sorenjohanson Aug 13, 2024
d0be27c
fix: link icon disappearing
sorenjohanson Aug 13, 2024
85454a7
fix: refactoring
sorenjohanson Aug 14, 2024
ee483a7
fix: refactor updateNodeHidden
sorenjohanson Aug 14, 2024
63f58ec
changed: add comments
sorenjohanson Aug 14, 2024
de316ce
Merge branch 'main' into 105-hide-show-child-nodes-of-a-branch
sorenjohanson Aug 14, 2024
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
51 changes: 45 additions & 6 deletions teammapper-frontend/mmp/src/map/handlers/draw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,23 @@ export default class Draw {
* Update the dom of the map with the (new) nodes.
*/
public update() {
const nodes = this.map.nodes.getNodes(),
dom = {
nodes: this.map.dom.g.selectAll('.' + this.map.id + '_node').data(nodes),
branches: this.map.dom.g.selectAll('.' + this.map.id + '_branch').data(nodes.slice(1))
}
let nodes = this.map.nodes.getNodes().filter(x => !x.hidden)

const dom = {
nodes: this.map.dom.g.selectAll('.' + this.map.id + '_node').data(nodes, (d) => d.id),
branches: this.map.dom.g.selectAll('.' + this.map.id + '_branch').data(nodes.slice(1), (d) => d.id)
}
let tapedTwice = false

dom.nodes.each((node: Node) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldnt it be easier to set the hidden field directly when the button is pressed for all nodes, and not inside this render method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean by this? There's practically no difference whether you actually set a field on the node itself or do it like this, as you'd still have to call both methods (setHiddenChildrenIcon and removeHiddenChildrenIcon) separately whenever you add/remove the icon to make it work reliably, at least in my testing.

You also can't avoid calling dom.nodes.each as outer.each (which is based on the enter method of D3.js) only seems to include nodes that were changed, ie those that were hidden, whereas we explicitly want the node that wasn't hidden.

const hasHiddenChildren = this.map.nodes.nodeChildren(node.id)?.filter(x => x.hidden).length > 0
if (hasHiddenChildren) {
this.setHiddenChildrenIcon(node)
} else {
this.removeHiddenChildrenIcon(node)
}
})

const outer = dom.nodes.enter().append('g')
.style('cursor', 'pointer')
.style('touch-action', 'none')
Expand Down Expand Up @@ -97,7 +107,6 @@ export default class Draw {

this.enableNodeNameEditing(node)
})

if (this.map.options.drag === true) {
outer.call(this.map.drag.getDragBehavior())
} else {
Expand All @@ -119,6 +128,7 @@ export default class Draw {
.style('stroke-width', 3)
.attr('d', (node: Node) => this.drawNodeBackground(node))


// Set image and link of the node
outer.each((node: Node) => {
this.setImage(node)
Expand Down Expand Up @@ -288,6 +298,35 @@ export default class Draw {
}
}

/**
* Set a hidden eye icon if child nodes are hidden.
* @param {Node} node
*/
public setHiddenChildrenIcon(node: Node) {
let domText = node.getHiddenChildIconDOM()
if (!domText) {
domText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
domText.textContent = 'visibility_off'
domText.classList.add('link-text')
domText.classList.add('material-icons')
domText.style.setProperty('fill', DOMPurify.sanitize(node.colors.name))
domText.setAttribute('y', (-node.dimensions.height + 30).toString())
domText.setAttribute('x', '-60')
node.dom.appendChild(domText)
}
}

/**
* Explicitly remove the hidden eye icon even if not set
* @param {Node} node
*/
public removeHiddenChildrenIcon(node) {
const domText = node.getHiddenChildIconDOM()
if (domText) {
domText.remove()
}
}

/**
* Update the node image position.
* @param {Node} node
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/mmp/src/map/handlers/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export default class History {
link: Utils.cloneObject(mergedProperty.link) as Link,
locked: mergedProperty.locked,
detached: mergedProperty.detached,
hidden: mergedProperty.hidden,
isRoot: mergedProperty.isRoot
}

Expand Down
55 changes: 54 additions & 1 deletion teammapper-frontend/mmp/src/map/handlers/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default class Nodes {
id: rootId,
parent: null,
detached: false,
hidden: false,
isRoot: true
}) as NodeProperties

Expand Down Expand Up @@ -203,6 +204,35 @@ export default class Nodes {
}
}

/**
* Toggle (hide/show) all child nodes of selected node
*/
public toggleBranch = () => {
if (this.selectedNode) {
const children = this.getChildren(this.selectedNode);

const descendants = this.getDescendants(this.selectedNode).filter(x => !children.includes(x));

if (children) {
children.forEach(x => this.updateNode('hidden', !x.hidden, false, false, x.id))
}

if (descendants) {
descendants.forEach(x => {
if (x.parent.hidden && !x.hidden) {
this.updateNode('hidden', true, false, false, x.id)
}

if (!x.parent.hidden && x.hidden) {
this.updateNode('hidden', false, false, false, x.id)
}
})
}

this.map.draw.update()
}
}

/**
* Deselect the current selected node.
*/
Expand Down Expand Up @@ -283,6 +313,9 @@ export default class Nodes {
case 'nameColor':
updated = this.updateNodeNameColor(node, value, graphic)
break
case 'hidden':
updated = this.updateNodeHidden(node, value)
break
default:
Log.error('The property does not exist')
}
Expand Down Expand Up @@ -371,6 +404,7 @@ export default class Nodes {
locked: node.locked,
isRoot: node.isRoot,
detached: node.detached,
hidden: node.hidden,
k: node.k
}
}
Expand Down Expand Up @@ -882,6 +916,24 @@ export default class Nodes {
}
}

/**
* Update the node hidden value
* @param {Node} node
* @param {boolean} hidden
* @returns {boolean}
*/
private updateNodeHidden = (node: Node, hidden: boolean) => {
if (hidden && typeof hidden !== 'boolean') {
Log.error('The hidden value must be boolean', 'type')
}

if (node.hidden !== hidden) {
node.hidden = hidden;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check to return a consistent return type. here, you either return false or you assign a variable and return undefined (which is also false).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the other methods also either assign a variable or return false, so I was just orienting myself on those. Maybe a general refactoring idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho only because the other methods follow a bad practice, this doesn't mean we have to continue this way. The return value is useless if it's always false? But yes, the other methods could be refactored in a separate refactoring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I've refactored this one to return undefined, I'll open an issue for the other ones.

} else {
return false
}
}

/**
* Update the node font style.
* @param {Node} node
Expand Down Expand Up @@ -1048,5 +1100,6 @@ export const PropertyMapping = {
textDecoration: [],
fontStyle: ['font', 'style'],
fontSize: ['font', 'size'],
nameColor: ['colors', 'name']
nameColor: ['colors', 'name'],
hidden: ['hidden']
} as const
4 changes: 4 additions & 0 deletions teammapper-frontend/mmp/src/map/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export default class MmpMap {
copyNode: this.copyPaste.copy,
cutNode: this.copyPaste.cut,
deselectNode: this.nodes.deselectNode,
getSelectedNode: this.nodes.getSelectedNode,
editNode: this.nodes.editNode,
toggleBranch: this.nodes.toggleBranch,
existNode: this.nodes.existNode,
exportAsImage: this.export.asImage,
exportAsJSON: this.export.asJSON,
Expand Down Expand Up @@ -125,7 +127,9 @@ export interface MmpInstance {
copyNode: Function
cutNode: Function
deselectNode: Function
getSelectedNode: Function
editNode: Function
toggleBranch: Function
existNode: Function
exportAsImage: Function
exportAsJSON: Function
Expand Down
12 changes: 12 additions & 0 deletions teammapper-frontend/mmp/src/map/models/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default class Node implements NodeProperties {
public dom: SVGGElement
public isRoot: boolean
public detached: boolean
public hidden: boolean

/**
* Initialize the node properties, the dimensions and the k coefficient.
Expand All @@ -37,6 +38,7 @@ export default class Node implements NodeProperties {
this.locked = properties.locked
this.isRoot = properties.isRoot
this.detached = properties.detached
this.hidden = properties.hidden

this.dimensions = {
width: 0,
Expand Down Expand Up @@ -94,6 +96,14 @@ export default class Node implements NodeProperties {
return this.dom.querySelector('a > text') as any
}

/**
* Returns the SVG text of the hidden child icon.
* @returns {SVGITextElement} text
*/
public getHiddenChildIconDOM(): SVGTextElement {
return this.dom.querySelector('text') as any
}

}

export interface UserNodeProperties {
Expand All @@ -106,6 +116,8 @@ export interface UserNodeProperties {
locked?: boolean
isRoot?: boolean
detached?: boolean
hidden?: boolean
hasHiddenChildNodes?: boolean
}

export interface NodeProperties extends UserNodeProperties {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ export class MapSyncService implements OnDestroy {
const existingNode = this.mmpService.getNode(newNode.id);
const propertyPath = NodePropertyMapping[result.property];
const changedValue = UtilsService.get(newNode, propertyPath);

this.mmpService.updateNode(
result.property,
changedValue,
Expand Down
16 changes: 15 additions & 1 deletion teammapper-frontend/src/app/core/services/mmp/mmp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ export class MmpService implements OnDestroy {
this.currentMap.instance.editNode();
}

/**
* Get the currently selected node
*/
public getSelectedNode() {
return this.currentMap.instance.getSelectedNode();
}

/**
* Deselect the current node.
*/
Expand Down Expand Up @@ -377,11 +384,18 @@ export class MmpService implements OnDestroy {
this.currentMap.instance.pasteNode(nodeId);
}

/**
* Toggle (hide/show) all child nodes of the selected node
*/
public toggleBranch() {
this.currentMap.instance.toggleBranch();
}

/**
* Return the children of the current node.
*/
public nodeChildren(): ExportNodeProperties[] {
return this.currentMap.instance.nodeChildren();
return this.currentMap?.instance.nodeChildren();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@
mat-icon-button>
<mat-icon>content_paste</mat-icon>
</button>
<button
(click)="mmpService.toggleBranch()"
[title]="'TOOLTIPS.HIDE_CHILD_NODES' | translate"
[disabled]="editDisabled || !canHideNodes"
color="primary"
mat-icon-button>
<mat-icon>{{ hasHiddenNodes ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>

<div class="vertical-line">
<div></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export class ToolbarComponent {
);
}

get hasHiddenNodes() {
return this.mmpService.nodeChildren()?.filter(x => x.hidden).length > 0
}

get canHideNodes() {
const selectedNode = this.mmpService.getSelectedNode()
return (selectedNode && !selectedNode.isRoot)
}

public async share() {
this.dialogService.openShareDialog();
}
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "Aktuellen Knoten kopieren",
"CUT_NODE": "Aktuellen Knoten ausschneiden",
"PASTE_NODE": "Kopierten Knoten einfügen",
"HIDE_CHILD_NODES": "Untergeordnete Knoten ausblenden",
"MOVE_NODE_TO_THE_LEFT": "Knoten nach links verschieben",
"MOVE_NODE_TO_THE_RIGHT": "Knoten nach rechts verschieben",
"MOVE_NODE_DOWN": "Knoten nach unten verschieben",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "Copies the current node",
"CUT_NODE": "Cuts the current node",
"PASTE_NODE": "Paste the copied node",
"HIDE_CHILD_NODES": "Hide child nodes",
"MOVE_NODE_TO_THE_LEFT": "Moves the node to the left",
"MOVE_NODE_TO_THE_RIGHT": "Moves the node to the right",
"MOVE_NODE_DOWN": "Moves the node down",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "Copiar el nodo actual",
"CUT_NODE": "Cortar el nodo actual",
"PASTE_NODE": "Pegar el nodo copiado",
"HIDE_CHILD_NODES": "Ocultar nodos hijos",
"MOVE_NODE_TO_THE_LEFT": "Mover el nodo a la izquierda",
"MOVE_NODE_TO_THE_RIGHT": "Mover el nodo a la derecha",
"MOVE_NODE_DOWN": "Mover el nodo hacia abajo",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "Copie le nœud actuel",
"CUT_NODE": "Coupe le nœud actuel",
"PASTE_NODE": "Collez le noeud copié",
"HIDE_CHILD_NODES": "Masquer les nœuds enfants",
"MOVE_NODE_TO_THE_LEFT": "Déplace le nœud vers la gauche",
"MOVE_NODE_TO_THE_RIGHT": "Déplace le noeud vers la droite",
"MOVE_NODE_DOWN": "Déplace le nœud vers le bas",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "Copia il nodo selezionato",
"CUT_NODE": "Taglia il nodo selezionato",
"PASTE_NODE": "Incolla il nodo copiato",
"HIDE_CHILD_NODES": "Nascondere i nodi figli",
"MOVE_NODE_TO_THE_LEFT": "Muove il nodo a sinistra",
"MOVE_NODE_TO_THE_RIGHT": "Muove il nodo a destra",
"MOVE_NODE_DOWN": "Muove il nodo in basso",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/pt-br.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "Copiar o nó atual",
"CUT_NODE": "Recortar o nó atual",
"PASTE_NODE": "Colar o nó copiado",
"HIDE_CHILD_NODES": "Ocultar nós filhos",
"MOVE_NODE_TO_THE_LEFT": "Mover o nó para a esquerda",
"MOVE_NODE_TO_THE_RIGHT": "Mover o nó para a direita",
"MOVE_NODE_DOWN": "Mover o nó para baixo",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "复制节点",
"CUT_NODE": "剪切节点",
"PASTE_NODE": "粘贴节点",
"HIDE_CHILD_NODES": "隐藏子节点",
"MOVE_NODE_TO_THE_LEFT": "左移节点",
"MOVE_NODE_TO_THE_RIGHT": "右移节点",
"MOVE_NODE_DOWN": "下移节点",
Expand Down
1 change: 1 addition & 0 deletions teammapper-frontend/src/assets/i18n/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"COPY_NODE": "複製選取的節點",
"CUT_NODE": "剪下選取的節點",
"PASTE_NODE": "貼上已複製的節點",
"HIDE_CHILD_NODES": "Hide child nodes",
"MOVE_NODE_TO_THE_LEFT": "將節點向左移動",
"MOVE_NODE_TO_THE_RIGHT": "將節點向右移動",
"MOVE_NODE_DOWN": "將節點向下移動",
Expand Down