Skip to content

Commit

Permalink
MINOR: feat: add BIMDataTree component
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasRichel committed Apr 2, 2024
1 parent 883a146 commit 8040368
Show file tree
Hide file tree
Showing 6 changed files with 612 additions and 1 deletion.
8 changes: 7 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
},
"ignorePatterns": ["/dist", "/dist-web", "*.d.ts"],
"rules": {
"prettier/prettier": "error",
"prettier/prettier": [
"error",
{
"arrowParens": "avoid",
"trailingComma": "es5"
}
],
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", "avoid-escape"],
Expand Down
123 changes: 123 additions & 0 deletions src/BIMDataComponents/BIMDataTree/BIMDataTree.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>
<div
class="bimdata-tree"
@mousedown="onTreeMouseDown"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@mouseup.self="onMouseUp"
>
<div class="bimdata-tree__nodes" @mouseleave="onNodesMouseLeave">
<Node v-for="node of data" :key="node.id" :node="node">
<slot
name="node"
:node="node"
:depth="0"
:selected="node.id === state.selectedId"
:hovered="node.id === state.hoveredNode?.id"
:ancestor-selected="state.hasAncestorSelected(node)"
/>
</Node>
</div>
<div v-if="dropHelperDisplayed" class="bimdata-tree__drop-helper"></div>
</div>
</template>

<script>
import { provide, computed, ref } from "vue";
import Node from "./Node.vue";
import useState from "./state.js";
/**
* @typedef { Object } Node
* @property { number } id
* @property { Array } [children=null]
*/
export default {
components: {
Node,
},
provide() {
return {
root: this,
};
},
props: {
/**
* @type { NodeData }
*/
data: {
type: Array,
required: true,
validator: value => {
if (!Array.isArray(value)) return false;
const ids = new Set();
for (const node of value) {
if (!Number.isFinite(node?.id)) return false;
if (node.children && !Array.isArray(node.children)) return false;
ids.add(node.id);
}
return ids.size === value.length;
},
},
selectedId: {
type: Number,
default: null,
},
highlightedId: {
type: Number,
default: null,
},
},
emits: ["drop", "click", "hover"],
setup(props, { emit }) {
const state = useState(props, emit);
provide("state", state);
const onNodesMouseLeave = () => state.onNodesMouseLeave();
const onTreeMouseDown = () => state.onTreeMouseDown();
const hover = ref(false);
const onMouseEnter = () => {
hover.value = true;
};
const onMouseLeave = () => {
hover.value = false;
};
const onMouseUp = () => state.onTreeMouseUp();
const dropHelperDisplayed = computed(
() =>
hover.value && state.dragPosition !== null && state.hoveredNode === null
);
return {
state,
dropHelperDisplayed,
onMouseEnter,
onMouseLeave,
onNodesMouseLeave,
onTreeMouseDown,
onMouseUp,
};
},
};
</script>
<style scoped lang="scss">
.bimdata-tree {
height: 100%;
&__drop-helper {
height: 2px;
background-color: var(--color-primary);
}
}
</style>
207 changes: 207 additions & 0 deletions src/BIMDataComponents/BIMDataTree/Node.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<template>
<div
ref="nodeRef"
class="bimdata-tree__node"
:class="{
'bimdata-tree__node--selected': node.id === state.selectedId,
'bimdata-tree__node--ancestor-selected': hasAncestorSelected(node),
'bimdata-tree__node--highlighted': node.id === state.highlightedId,
}"
@mouseenter="onMouseEnter"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
>
<div
v-if="dropHelperPosition === 'top' || dropHelperPosition === 'bottom'"
class="bimdata-tree__node__drop-helper"
:style="`--drop-helper-top: ${
dropHelperPosition === 'top' ? '0px' : '30px'
};`"
></div>
<div
class="bimdata-tree__node__content"
:style="`padding-left: calc(var(--spacing-unit) * 2 * ${depth});`"
>
<div class="bimdata-tree__node__content__arrow" @mousedown.stop>
<div
v-if="node.children?.length > 0"
class="bimdata-tree__node__content__arrow__content"
@click="expanded = !expanded"
>
<BIMDataIcon name="chevron" size="xxxs" :rotate="expanded ? 90 : 0" />
</div>
</div>

<div class="bimdata-tree__node__content__element">
<slot>
{{ node.label }}
</slot>
</div>
</div>
</div>
<NodeChildren
v-if="node.children?.length > 0 && expanded"
:node="node"
:depth="depth + 1"
/>
</template>

<script>
import { inject, ref, computed, watch, isRef } from "vue";
import NodeChildren from "./NodeChildren.js";
export default {
name: "Node",
components: {
NodeChildren,
},
inject: ["root"],
props: {
node: {
type: Object,
required: true,
},
depth: {
type: Number,
default: 0,
},
},
setup(props) {
const state = inject("state");
let expanded = null;
if (isRef(props.node.expandedRef)) {
expanded = props.node.expandedRef;
} else {
expanded = ref(false);
}
const nodeRef = ref(null);
let expandTimer = null;
const handleExpandTimer = () => {
if (
!props.node.children?.length ||
!state.dragPosition ||
state.hoveredNode?.id !== props.node.id ||
expanded.value
) {
clearTimeout(expandTimer);
expandTimer = null;
return;
}
const position = state.getNodeDropPosition(props.node, nodeRef.value);
if (position !== "center" && position !== "bottom") {
clearTimeout(expandTimer);
expandTimer = null;
return;
}
if (expandTimer !== null) return;
expandTimer = setTimeout(() => {
expanded.value = true;
expandTimer = null;
}, 500);
};
watch(() => state.dragPosition && state.hoveredNode, handleExpandTimer);
const onMouseEnter = () => state.onNodeMouseEnter(props.node);
const onMouseDown = mouseEvent =>
state.onNodeMouseDown(props.node, mouseEvent);
const onMouseMove = mouseEvent => {
state.onNodeMouseMove(mouseEvent);
handleExpandTimer();
};
const dropHelperPosition = computed(() => {
if (!state.dragPosition || state.hoveredNode?.id !== props.node.id)
return null;
return state.getNodeDropPosition(props.node, nodeRef.value);
});
const onMouseUp = () =>
state.onNodeMouseUp(props.node, nodeRef.value, expanded.value);
return {
state,
nodeRef,
expanded,
dropHelperPosition,
hasAncestorSelected: state.hasAncestorSelected,
onMouseEnter,
onMouseDown,
onMouseMove,
onMouseUp,
};
},
};
</script>
<style scoped lang="scss">
.bimdata-tree__node {
height: 32px;
display: flex;
align-items: center;
position: relative;
user-select: none;
&__drop-helper {
position: absolute;
left: 0px;
right: 0px;
height: 2px;
top: var(--drop-helper-top);
background-color: var(--color-primary);
}
&--selected {
background-color: var(--color-secondary-light);
}
&--ancestor-selected {
background-color: var(--color-secondary-lighter);
}
&--highlighted {
outline-offset: -1px;
outline: solid var(--color-primary) 1px;
}
&__content {
width: 100%;
height: 100%;
display: flex;
border-radius: 6px;
padding: 0 calc(var(--spacing-unit) / 2);
&__arrow {
width: 18px;
flex-shrink: 0;
&__content {
cursor: pointer;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
&__element {
padding-left: calc(var(--spacing-unit) / 2);
width: calc(100% - 18px);
height: 100%;
display: flex;
align-items: center;
}
}
}
</style>
35 changes: 35 additions & 0 deletions src/BIMDataComponents/BIMDataTree/NodeChildren.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { h } from "vue";

import Node from "./Node.vue";

export default {
inject: ["root"],
props: {
node: {
type: Object,
required: true,
validator: node => Number.isFinite(node?.id),
},
depth: {
type: Number,
default: 0,
},
},
render() {
const { node, depth, root } = this;

const state = root.state;

return node.children.map(child =>
h(Node, { node: child, depth, key: child.id }, () =>
root.$slots.node?.({
node: child,
depth,
selected: child.id === state.selectedId,
hovered: child.id === state.hoveredNode?.id,
ancestorSelected: state.hasAncestorSelected(child),
})
)
);
},
};
Loading

0 comments on commit 8040368

Please sign in to comment.