Skip to content

Add filtering #307

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

Merged
merged 1 commit into from
Apr 16, 2023
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grapoza/vue-tree",
"version": "5.1.0",
"version": "5.2.0",
"description": "Tree components for Vue 3",
"author": "Gregg Rapoza <[email protected]>",
"license": "MIT",
Expand Down
17 changes: 14 additions & 3 deletions src/components/TreeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
@treeNodeAdd="(t, p)=>$emit(TreeEvent.Add, t, p)"
@treeNodeDelete="handleChildDeletion"
@treeNodeAriaFocusableChange="handleFocusableChange"
@treeNodeAriaRequestFirstFocus="focusFirst(model)"
@treeNodeAriaRequestFirstFocus="(keepCurrentDomFocus) => focusFirst(model, keepCurrentDomFocus)"
@treeNodeAriaRequestLastFocus="focusLast(model)"
@treeNodeAriaRequestPreviousFocus="(t) => focusPrevious(model, t)"
@treeNodeAriaRequestNextFocus="(t, f) => focusNext(model, t, f)"
Expand All @@ -55,13 +55,14 @@
</template>

<script setup>
import { computed, nextTick, ref, readonly, onMounted, toRef } from 'vue'
import { computed, nextTick, ref, readonly, onMounted, provide, toRef } from 'vue'
import SelectionMode from '../enums/selectionMode.js';
import { useIdGeneration } from '../composables/idGeneration.js'
import { useTreeViewTraversal } from '../composables/treeViewTraversal.js'
import { useFocus } from '../composables/focus/focus.js';
import { useTreeViewFocus } from '../composables/focus/treeViewFocus.js';
import { useSelection } from '../composables/selection/selection.js';
import { useTreeViewFilter } from '../composables/filter/treeViewFilter.js';
import { useTreeViewSelection } from '../composables/selection/treeViewSelection.js';
import { useTreeViewDragAndDrop } from '../composables/dragDrop/treeViewDragAndDrop.js';
import { useTreeViewConvenienceMethods } from '../composables/treeViewConvenienceMethods.js';
Expand All @@ -87,6 +88,11 @@ const props = defineProps({
return true;
}
},
filterMethod: {
type: Function,
required: false,
default: null
},
initialModel: {
type: Array,
required: false,
Expand Down Expand Up @@ -186,7 +192,6 @@ const {
handleNodeSelectedChange,
} = useTreeViewSelection(model, toRef(props, "selectionMode"), focusableNodeModel, emit);


const {
isSelectable,
isSelected,
Expand All @@ -204,6 +209,8 @@ const {

const { dragMoveNode, drop } = useTreeViewDragAndDrop(model, uniqueId, findById, removeById);

useTreeViewFilter(model);

// COMPUTED

const areNodesLoaded = computed(() => {
Expand Down Expand Up @@ -323,6 +330,10 @@ function handleNodeDeletion(node) {
}
}

// PROVIDE/INJECT

provide("filterMethod", toRef(props, 'filterMethod'));

// CREATION LOGIC

// Force a unique tree ID. This will generate a unique ID internally, but on mount
Expand Down
7 changes: 6 additions & 1 deletion src/components/TreeViewNode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ async function createWrapper(customPropsData, slotsData) {
sync: false,
props: customPropsData || getDefaultPropsData(),
slots: slotsData,
attachTo: '#root'
attachTo: '#root',
global: {
provide: {
filterMethod: null
}
}
});

await w.setProps({ isMounted: true });
Expand Down
29 changes: 21 additions & 8 deletions src/components/TreeViewNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
<li :id="nodeId"
ref="nodeElement"
class="grtvn"
:class="[customClasses.treeViewNode,
tns._.dragging ? 'grtvn-dragging' : '']"
:class="[
customClasses.treeViewNode,
tns._.dragging ? 'grtvn-dragging' : '',
filterIncludesNode ? '' : 'grtvn-hidden'
]"
role="treeitem"
:tabindex="tabIndex"
:aria-expanded="ariaExpanded"
Expand Down Expand Up @@ -197,7 +200,7 @@
@treeNodeDelete="handleChildDeletion"
@treeNodeAriaFocusableChange="(t)=>$emit(TreeEvent.FocusableChange, t)"
@treeNodeAriaRequestParentFocus="()=>focusNode()"
@treeNodeAriaRequestFirstFocus="()=>$emit(TreeEvent.RequestFirstFocus)"
@treeNodeAriaRequestFirstFocus="(keepCurrentDomFocus)=>$emit(TreeEvent.RequestFirstFocus, keepCurrentDomFocus)"
@treeNodeAriaRequestLastFocus="()=>$emit(TreeEvent.RequestLastFocus)"
@treeNodeAriaRequestPreviousFocus="focusPreviousNode"
@treeNodeAriaRequestNextFocus="focusNextNode"
Expand Down Expand Up @@ -231,6 +234,7 @@ import { useFocus } from '../composables/focus/focus.js';
import { useTreeViewNodeFocus } from '../composables/focus/treeViewNodeFocus.js';
import { useTreeViewNodeSelection } from '../composables/selection/treeViewNodeSelection.js';
import { useTreeViewNodeExpansion } from '../composables/expansion/treeViewNodeExpansion.js';
import { useTreeViewNodeFilter } from '../composables/filter/treeViewNodeFilter.js';
import SelectionMode from '../enums/selectionMode.js';
import TreeEvent from '../enums/event.js';

Expand Down Expand Up @@ -349,9 +353,14 @@ const {
children,
deleteChild,
hasChildren,
mayHaveChildren,
} = useTreeViewNodeChildren(model, emit);

const {
filteredChildren,
filterIncludesNode,
mayHaveFilteredChildren
} = useTreeViewNodeFilter(model, emit);

const {
focus,
isFocused,
Expand Down Expand Up @@ -484,9 +493,9 @@ function onKeyDown(event) {
// When focus is on a closed node, opens the node; focus does not move.
// When focus is on a open node, moves focus to the first child node.
// When focus is on an end node, does nothing.
if (mayHaveChildren.value && !areChildrenLoading.value) {
if (mayHaveFilteredChildren.value && !areChildrenLoading.value) {
if (!expandNode() && isNodeExpanded()) {
focus(children.value[0]);
focus(filteredChildren.value[0]);
}
}
}
Expand Down Expand Up @@ -551,12 +560,12 @@ function onKeyDown(event) {
function handleChildDeletion(node) {
// Remove the node from the array of children if this is an immediate child.
// Note that only the node that was deleted fires these, not any subnode.
let targetIndex = children.value.indexOf(node);
let targetIndex = filteredChildren.value.indexOf(node);
if (targetIndex > -1) {
if (isFocused(node)) {
// When this is the first of several siblings, focus the next node.
// Otherwise, focus the previous node.
if (children.value.length > 1 && children.value.indexOf(node) === 0) {
if (filteredChildren.value.length > 1 && filteredChildren.value.indexOf(node) === 0) {
focusNextNode(node);
}
else {
Expand Down Expand Up @@ -736,4 +745,8 @@ if (!label.value || typeof label.value !== 'string') {
padding: 0;
list-style: none;
}

.grtv-wrapper.grtv-default-skin .grtvn.grtvn-hidden {
display: none;
}
</style>
16 changes: 16 additions & 0 deletions src/composables/children/children.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { unref } from 'vue';

/**
* Composable dealing with children on an arbitrary node.
* @returns {Object} Methods to deal with children arbitrary nodes
*/
export function useChildren() {

function getChildren(targetNodeModel) {
return unref(targetNodeModel)[unref(targetNodeModel).treeNodeSpec.childrenProperty ?? 'children'];
}

return {
getChildren,
};
}
33 changes: 33 additions & 0 deletions src/composables/children/children.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect, describe, it } from 'vitest';
import { useChildren } from './children.js';
import { generateNodes } from 'tests/data/node-generator.js';

const { getChildren } = useChildren();

describe('children.js', () => {

describe('when getting children', () => {

describe('and the node model has a specified childrenProperty', () => {

it('should get the children from that property', () => {
const node = generateNodes(['e', ['e', 'e']])[0];
node.newChildrenProp = node.children;
node.treeNodeSpec.childrenProperty = 'newChildrenProp';
delete node.children;

const children = getChildren(node);
expect(children.length).to.equal(2);
});
});

describe('and the node model does not have a specified childrenProperty', () => {

it('should get the children from the children property', () => {
const node = generateNodes(['e', ['e', 'e']])[0];
const children = getChildren(node);
expect(children.length).to.equal(node.children.length);
});
});
});
});
7 changes: 6 additions & 1 deletion src/composables/children/treeViewNodeChildren.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { computed } from 'vue';
import { useChildren } from './children.js';
import TreeEvent from '../../enums/event.js';

/**
Expand All @@ -9,11 +10,15 @@ import TreeEvent from '../../enums/event.js';
*/
export function useTreeViewNodeChildren(nodeModel, emit) {

const {
getChildren
} = useChildren();

const areChildrenLoaded = computed(() => typeof nodeModel.value.treeNodeSpec.loadChildrenAsync !== 'function' || nodeModel.value.treeNodeSpec._.state.areChildrenLoaded);

const areChildrenLoading = computed(() => nodeModel.value.treeNodeSpec._.state.areChildrenLoading);

const children = computed(() => nodeModel.value[nodeModel.value.treeNodeSpec.childrenProperty ?? 'children']);
const children = computed(() => getChildren(nodeModel));

const hasChildren = computed(() => children.value && children.value.length > 0);

Expand Down
9 changes: 7 additions & 2 deletions src/composables/dragDrop/TreeViewNodeDragAndDrop.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { generateNodes } from '../../../tests/data/node-generator.js';
import { dropEffect as DropEffect, effectAllowed as EffectAllowed } from '../../enums/dragDrop';
import MimeType from '../../enums/mimeType';

const serializedNodeData = '{"id":"n0","label":"Node 0","children":[],"treeNodeSpec":{"_":{"dragging":false,"state":{"areChildrenLoaded":true,"areChildrenLoading":false}},"childrenProperty":"children","idProperty":"id","labelProperty":"label","loadChildrenAsync":null,"expandable":false,"selectable":true,"deletable":false,"focusable":false,"input":{"type":"checkbox","name":"n0-cbx"},"state":{"expanded":false,"selected":false,"input":{"disabled":false,"value":false}},"addChildCallback":null,"draggable":false,"allowDrop":false,"dataTransferEffectAllowed":"copyMove","title":null,"expanderTitle":null,"addChildTitle":null,"deleteTitle":null,"customizations":{}}}';
const serializedNodeData = '{"id":"n0","label":"Node 0","children":[],"treeNodeSpec":{"_":{"dragging":false,"state":{"areChildrenLoaded":true,"areChildrenLoading":false,"matchesFilter":true,"subnodeMatchesFilter":false}},"childrenProperty":"children","idProperty":"id","labelProperty":"label","loadChildrenAsync":null,"expandable":false,"selectable":true,"deletable":false,"focusable":false,"input":{"type":"checkbox","name":"n0-cbx"},"state":{"expanded":false,"selected":false,"input":{"disabled":false,"value":false}},"addChildCallback":null,"draggable":false,"allowDrop":false,"dataTransferEffectAllowed":"copyMove","title":null,"expanderTitle":null,"addChildTitle":null,"deleteTitle":null,"customizations":{}}}';

const getDefaultPropsData = function () {
return {
Expand Down Expand Up @@ -43,7 +43,12 @@ function createWrapper(customPropsData, customAttrs) {
sync: false,
props: customPropsData || getDefaultPropsData(),
attrs: customAttrs,
attachTo: elem
attachTo: elem,
global: {
provide: {
filterMethod: null
}
}
});
};

Expand Down
8 changes: 8 additions & 0 deletions src/composables/dragDrop/treeViewDragAndDrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ const { resolveNodeIdConflicts } = useIdGeneration();
const { cheapCopyObject } = useObjectMethods();
const { unfocus } = useFocus();

/**
* Composable dealing with drag-and-drop handling at the top level of the tree view.
* @param {Ref<TreeViewNode[]>} treeModel A Ref to the top level model of the tree
* @param {Ref<string>} uniqueId A Ref to the unique ID for the tree.
* @param {Function} findById A function to find a node by ID
* @param {Function} removeById A function to remove a node by ID
* @returns {Object} Methods to deal with tree view level drag-and-drop
*/
export function useTreeViewDragAndDrop(treeModel, uniqueId, findById, removeById) {
/**
* Removes the given node from this node's children
Expand Down
8 changes: 8 additions & 0 deletions src/composables/dragDrop/treeViewNodeDragAndDrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import { useFocus } from '../focus/focus.js';

const { closest } = useDomMethods();

/**
* Composable dealing with drag-and-drop handling at the tree view node.
* @param {Ref<TreeViewNode>} model A Ref to the model of the node
* @param {Ref<TreeViewNode[]>} children A Ref to the children of the node
* @param {Ref<string>} treeId A Ref to the tree ID
* @param {Function} emit The TreeViewNode's emit function, used to emit selection events on the node's behalf
* @returns {Object} Methods to deal with tree view node level drag-and-drop
*/
export function useTreeViewNodeDragAndDrop(model, children, treeId, emit) {

const { unfocus } = useFocus();
Expand Down
2 changes: 1 addition & 1 deletion src/composables/expansion/expansion.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, expect, describe, it, vi } from 'vitest';
import { expect, describe, it } from 'vitest';
import { ref } from 'vue';
import { useExpansion } from './expansion.js';
import { generateNodes } from '../../../tests/data/node-generator.js';
Expand Down
8 changes: 6 additions & 2 deletions src/composables/expansion/treeViewNodeExpansion.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { computed, watch } from 'vue';
import { useExpansion } from './expansion.js';
import { useTreeViewNodeChildren } from '../children/treeViewNodeChildren.js';
import { useTreeViewNodeFilter } from '../filter/treeViewNodeFilter.js';
import TreeEvent from '../../enums/event.js';

/**
Expand All @@ -18,13 +19,16 @@ export function useTreeViewNodeExpansion(nodeModel, emit) {

const {
loadChildren,
mayHaveChildren,
} = useTreeViewNodeChildren(nodeModel, emit);

const {
mayHaveFilteredChildren,
} = useTreeViewNodeFilter(nodeModel, emit);

const ariaExpanded = computed(() => canExpand.value ? isNodeExpanded() : null);

const canExpand = computed(() => {
return mayHaveChildren.value && isNodeExpandable();
return isNodeExpandable() && mayHaveFilteredChildren.value;
});

watch(() => nodeModel.value.treeNodeSpec.state.expanded, async function () {
Expand Down
Loading