158 lines
6.3 KiB
Vue
158 lines
6.3 KiB
Vue
<template>
|
||
<span>
|
||
<span v-if="initialLiquourTreeNodes != null && initialLiquourTreeNodes.length > 0">
|
||
<tree :options="liquorTreeOptions"
|
||
:data="initialLiquourTreeNodes"
|
||
v-on:node:checked="nodeSelected($event)"
|
||
v-on:node:unchecked="nodeSelected($event)"
|
||
ref="treeElement"
|
||
>
|
||
<span class="tree-text" slot-scope="{ node }">
|
||
<Node :data="convertExistingToNode(node)" />
|
||
</span>
|
||
</tree>
|
||
</span>
|
||
<span v-else>Nooo 😢</span>
|
||
</span>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
|
||
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeOptions } from 'liquor-tree';
|
||
import Node from './Node.vue';
|
||
import { INode } from './INode';
|
||
import { convertExistingToNode, toNewLiquorTreeNode } from './NodeTranslator';
|
||
export type FilterPredicate = (node: INode) => boolean;
|
||
|
||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||
@Component({
|
||
components: {
|
||
LiquorTree,
|
||
Node,
|
||
},
|
||
})
|
||
export default class SelectableTree extends Vue {
|
||
@Prop() public filterPredicate?: FilterPredicate;
|
||
@Prop() public filterText?: string;
|
||
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
|
||
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
||
|
||
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
|
||
public liquorTreeOptions = this.getDefaults();
|
||
public convertExistingToNode = convertExistingToNode;
|
||
|
||
public mounted() {
|
||
if (this.initialNodes) {
|
||
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
|
||
if (this.selectedNodeIds) {
|
||
recurseDown(initialNodes,
|
||
(node) => node.state.checked = this.selectedNodeIds.includes(node.id));
|
||
}
|
||
this.initialLiquourTreeNodes = initialNodes;
|
||
} else {
|
||
throw new Error('Initial nodes are null or empty');
|
||
}
|
||
if (this.filterText) {
|
||
this.updateFilterText(this.filterText);
|
||
}
|
||
}
|
||
|
||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||
this.$emit('nodeSelected', convertExistingToNode(node));
|
||
return;
|
||
}
|
||
|
||
@Watch('filterText')
|
||
public updateFilterText(filterText: |string) {
|
||
const api = this.getLiquorTreeApi();
|
||
if (!filterText) {
|
||
api.clearFilter();
|
||
} else {
|
||
api.filter('filtered'); // text does not matter, it'll trigger the filterPredicate
|
||
}
|
||
}
|
||
|
||
@Watch('selectedNodeIds')
|
||
public setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
|
||
if (!selectedNodeIds) {
|
||
throw new Error('Selected nodes are undefined');
|
||
}
|
||
const newNodes = updateCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
|
||
this.getLiquorTreeApi().setModel(newNodes);
|
||
/* Alternative:
|
||
this.getLiquorTreeApi().recurseDown((node) => {
|
||
node.states.checked = selectedNodeIds.includes(node.id);
|
||
});
|
||
Problem: Does not check their parent if all children are checked, because it does not
|
||
trigger update on parent as we work with scripts not categories. */
|
||
/* Alternative:
|
||
this.getLiquorTreeApi().recurseDown((node) => {
|
||
if(selectedNodeIds.includes(node.id)) { node.select(); } else { node.unselect(); }
|
||
});
|
||
Problem: Emits nodeSelected() event again which will cause an infinite loop. */
|
||
}
|
||
|
||
private getLiquorTreeApi(): ILiquorTree {
|
||
if (!this.$refs.treeElement) {
|
||
throw new Error('Referenced tree element cannot be found. Probably it\'s not rendered?');
|
||
}
|
||
return (this.$refs.treeElement as any).tree;
|
||
}
|
||
|
||
private getDefaults(): ILiquorTreeOptions {
|
||
return {
|
||
multiple: true,
|
||
checkbox: true,
|
||
checkOnSelect: true,
|
||
autoCheckChildren: true,
|
||
parentSelect: false,
|
||
keyboardNavigation: true,
|
||
deletion: (node) => !node.children || node.children.length === 0,
|
||
filter: {
|
||
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
||
if (!this.filterPredicate) {
|
||
throw new Error('Cannot filter as predicate is null');
|
||
}
|
||
return this.filterPredicate(convertExistingToNode(node));
|
||
},
|
||
emptyText: '🕵️Hmm.. Can not see one 🧐',
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
function recurseDown(
|
||
nodes: ReadonlyArray<ILiquorTreeNewNode>,
|
||
handler: (node: ILiquorTreeNewNode) => void) {
|
||
for (const node of nodes) {
|
||
handler(node);
|
||
if (node.children) {
|
||
recurseDown(node.children, handler);
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateCheckedState(
|
||
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
|
||
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
|
||
const result = new Array<ILiquorTreeNewNode>();
|
||
for (const oldNode of oldNodes) {
|
||
const newState = oldNode.states;
|
||
newState.checked = selectedNodeIds.some((id) => id === oldNode.id);
|
||
const newNode: ILiquorTreeNewNode = {
|
||
id: oldNode.id,
|
||
text: oldNode.data.text,
|
||
data: {
|
||
documentationUrls: oldNode.data.documentationUrls,
|
||
isReversible: oldNode.data.isReversible,
|
||
},
|
||
children: oldNode.children == null ? [] :
|
||
updateCheckedState(oldNode.children, selectedNodeIds),
|
||
state: newState,
|
||
};
|
||
result.push(newNode);
|
||
}
|
||
return result;
|
||
}
|
||
</script>
|