undergroundwires/privacy.sexy

View on GitHub
src/presentation/components/Scripts/View/Tree/TreeView/TreeView.vue

Summary

Maintainability
Test Coverage
<template>
  <div
    ref="treeContainerElement"
    class="tree"
  >
    <TreeRoot :tree-root="tree" :rendering-strategy="renderingStrategy">
      <template #default="slotProps">
        <slot name="node-content" v-bind="slotProps" />
      </template>
    </TreeRoot>
  </div>
</template>

<script lang="ts">
import {
  defineComponent, onMounted, watch,
  shallowRef, toRef, shallowReadonly,
  nextTick,
} from 'vue';
import { TreeRootManager } from './TreeRoot/TreeRootManager';
import TreeRoot from './TreeRoot/TreeRoot.vue';
import { useTreeQueryFilter } from './UseTreeQueryFilter';
import { useTreeKeyboardNavigation } from './UseTreeKeyboardNavigation';
import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator';
import { useLeafNodeCheckedStateUpdater } from './UseLeafNodeCheckedStateUpdater';
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
import { useGradualNodeRendering, type NodeRenderingControl } from './Rendering/UseGradualNodeRendering';
import type { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChangedEmittedEvent';
import type { TreeInputNodeData } from './Bindings/TreeInputNodeData';
import type { TreeViewFilterEvent } from './Bindings/TreeInputFilterEvent';
import type { PropType } from 'vue';

export default defineComponent({
  components: {
    TreeRoot,
  },
  props: {
    nodes: {
      type: Array as PropType<readonly TreeInputNodeData[]>,
      default: () => [],
    },
    latestFilterEvent: {
      type: Object as PropType<TreeViewFilterEvent | undefined>,
      default: () => undefined,
    },
    selectedLeafNodeIds: {
      type: Array as PropType<ReadonlyArray<string>>,
      default: () => [],
    },
  },
  emits: {
    /* eslint-disable @typescript-eslint/no-unused-vars */
    nodeStateChanged: (node: TreeNodeStateChangedEmittedEvent) => true,
    /* eslint-enable @typescript-eslint/no-unused-vars */
  },
  setup(props, { emit }) {
    const treeContainerElement = shallowRef<HTMLElement | undefined>();

    const tree = new TreeRootManager();

    const treeRef = shallowReadonly(shallowRef(tree));

    useTreeKeyboardNavigation(treeRef, treeContainerElement);
    useTreeQueryFilter(toRef(props, 'latestFilterEvent'), treeRef);
    useLeafNodeCheckedStateUpdater(treeRef, toRef(props, 'selectedLeafNodeIds'));
    useAutoUpdateParentCheckState(treeRef);
    useAutoUpdateChildrenCheckState(treeRef);
    const nodeRenderer = useGradualNodeRendering(treeRef);

    const { onNodeStateChange } = useNodeStateChangeAggregator(treeRef);

    onNodeStateChange((change) => {
      emit('nodeStateChanged', {
        node: change.node,
        newState: change.newState,
        oldState: change.oldState,
      });
    });

    onMounted(() => {
      watch(() => props.nodes, async (nodes) => {
        await forceRerenderNodes(
          nodeRenderer,
          () => tree.collection.updateRootNodes(nodes),
        );
      }, { immediate: true });
    });

    return {
      treeContainerElement,
      renderingStrategy: nodeRenderer.renderingStrategy,
      tree,
    };
  },
});

/**
 * This function is used to manually trigger a re-render of the tree nodes.
 * In Vue, manually controlling the rendering process is typically an anti-pattern,
 * as Vue's reactivity system is designed to handle updates efficiently. However,
 * in this specific case, it's necessary to ensure the correct order of rendering operations.
 * This function first clears the rendering queue and the currently rendered nodes,
 * ensuring that UI elements relying on outdated node states are removed. This is needed
 * in scenarios where the collection is updated before the nodes, which can lead to errors
 * if nodes that no longer exist in the collection are still being rendered.
 * Using this function, we ensure a clean state before updating the nodes, aligning with
 * the updated collection.
 */
async function forceRerenderNodes(
  renderer: NodeRenderingControl,
  nodeUpdater: () => void,
) {
  renderer.clearRenderingStates();
  renderer.notifyRenderingUpdates();
  await nextTick();
  nodeUpdater();
}

</script>

<style scoped lang="scss">
@use "./tree-colors" as *;

.tree {
  background: $color-tree-bg;
  overflow: auto; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
  flex: 1; // Expands the node horizontally, allowing its content to utilize full width for child item alignment, such as icons and text.
}
</style>