@xen-orchestra/web-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/assets/css/_colors.pcss +125 -0
- package/lib/assets/css/_context.pcss +99 -0
- package/lib/assets/css/_fonts.pcss +6 -0
- package/lib/assets/css/_reset.pcss +42 -0
- package/lib/assets/css/_shadows.pcss +36 -0
- package/lib/assets/css/_typography.pcss +6 -0
- package/lib/assets/css/base.pcss +91 -0
- package/lib/assets/css/typography/_legacy.pcss +123 -0
- package/lib/assets/css/typography/_letter-spacing.pcss +27 -0
- package/lib/assets/css/typography/_line-height.pcss +19 -0
- package/lib/assets/css/typography/_size.pcss +95 -0
- package/lib/assets/css/typography/_style.pcss +34 -0
- package/lib/assets/css/typography/_weight.pcss +57 -0
- package/lib/assets/user.png +0 -0
- package/lib/components/PowerStateIcon.vue +46 -0
- package/lib/components/StatusPill.vue +32 -0
- package/lib/components/UiCounter.vue +89 -0
- package/lib/components/UiSpinner.vue +48 -0
- package/lib/components/UiTag.vue +97 -0
- package/lib/components/button/ButtonGroup.vue +33 -0
- package/lib/components/button/ButtonIcon.vue +199 -0
- package/lib/components/button/UiButton.vue +207 -0
- package/lib/components/chip/ChipIcon.vue +29 -0
- package/lib/components/chip/ChipRemoveIcon.vue +13 -0
- package/lib/components/chip/UiChip.vue +138 -0
- package/lib/components/dropdown/DropdownItem.vue +192 -0
- package/lib/components/dropdown/DropdownList.vue +31 -0
- package/lib/components/dropdown/DropdownTitle.vue +65 -0
- package/lib/components/icon/ComplexIcon.vue +51 -0
- package/lib/components/icon/ObjectIcon.vue +243 -0
- package/lib/components/icon/UiIcon.vue +47 -0
- package/lib/components/icon/VmIcon.vue +30 -0
- package/lib/components/layout/LayoutSidebar.vue +100 -0
- package/lib/components/menu/MenuItem.vue +82 -0
- package/lib/components/menu/MenuList.vue +104 -0
- package/lib/components/menu/MenuSeparator.vue +27 -0
- package/lib/components/menu/MenuTrigger.vue +52 -0
- package/lib/components/tab/TabItem.vue +100 -0
- package/lib/components/tab/TabList.vue +32 -0
- package/lib/components/tooltip/TooltipItem.vue +80 -0
- package/lib/components/tooltip/TooltipList.vue +15 -0
- package/lib/components/tree/TreeItem.vue +34 -0
- package/lib/components/tree/TreeItemError.vue +13 -0
- package/lib/components/tree/TreeItemLabel.vue +128 -0
- package/lib/components/tree/TreeLine.vue +51 -0
- package/lib/components/tree/TreeList.vue +14 -0
- package/lib/components/tree/TreeLoadingItem.vue +64 -0
- package/lib/components/user/UserLink.vue +72 -0
- package/lib/components/user/UserLogo.vue +57 -0
- package/lib/composables/context.composable.ts +34 -0
- package/lib/composables/tree/branch-definition.ts +17 -0
- package/lib/composables/tree/branch.ts +143 -0
- package/lib/composables/tree/build-nodes.ts +20 -0
- package/lib/composables/tree/define-branch.ts +23 -0
- package/lib/composables/tree/define-leaf.ts +16 -0
- package/lib/composables/tree/define-tree.ts +65 -0
- package/lib/composables/tree/leaf-definition.ts +8 -0
- package/lib/composables/tree/leaf.ts +34 -0
- package/lib/composables/tree/tree-node-base.ts +103 -0
- package/lib/composables/tree/tree-node-definition-base.ts +12 -0
- package/lib/composables/tree/types.ts +92 -0
- package/lib/composables/tree-filter.composable.ts +12 -0
- package/lib/composables/tree.composable.ts +85 -0
- package/lib/context.ts +10 -0
- package/lib/directives/tooltip.directive.md +117 -0
- package/lib/directives/tooltip.directive.ts +52 -0
- package/lib/i18n.ts +158 -0
- package/lib/layouts/CoreLayout.vue +182 -0
- package/lib/locales/de.json +6 -0
- package/lib/locales/en.json +15 -0
- package/lib/locales/fr.json +15 -0
- package/lib/stores/panel.store.ts +12 -0
- package/lib/stores/sidebar.store.ts +63 -0
- package/lib/stores/tooltip.store.ts +74 -0
- package/lib/stores/ui.store.ts +34 -0
- package/lib/types/button.type.ts +3 -0
- package/lib/types/color.type.ts +5 -0
- package/lib/types/object-icon.type.ts +43 -0
- package/lib/types/power-state.type.ts +1 -0
- package/lib/types/size.type.ts +3 -0
- package/lib/types/subscribable-store.type.ts +21 -0
- package/lib/types/utility.type.ts +1 -0
- package/lib/utils/create-subscribable-store-context.util.ts +66 -0
- package/lib/utils/has-ellipsis.util.ts +11 -0
- package/lib/utils/if-else.utils.ts +27 -0
- package/lib/utils/injection-keys.util.ts +17 -0
- package/lib/utils/sort-by-name-label.util.ts +6 -0
- package/lib/utils/to-array.utils.ts +9 -0
- package/lib/utils/unique-id.util.ts +8 -0
- package/package.json +45 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- v1.0 -->
|
|
2
|
+
<template>
|
|
3
|
+
<span :class="size" class="user-logo" />
|
|
4
|
+
</template>
|
|
5
|
+
|
|
6
|
+
<script lang="ts" setup>
|
|
7
|
+
import type { UserLogoSize } from '@core/types/size.type'
|
|
8
|
+
|
|
9
|
+
withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
size?: UserLogoSize
|
|
12
|
+
}>(),
|
|
13
|
+
{ size: 'extra-small' }
|
|
14
|
+
)
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<style lang="postcss" scoped>
|
|
18
|
+
/* COLOR VARIANTS */
|
|
19
|
+
.user-logo {
|
|
20
|
+
--border-color: var(--color-purple-base);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* SIZE VARIANTS */
|
|
24
|
+
.user-logo {
|
|
25
|
+
&.extra-small {
|
|
26
|
+
--size: 1.6rem;
|
|
27
|
+
--border-size: 0.1rem;
|
|
28
|
+
--background-position: -0.8rem -0.493rem;
|
|
29
|
+
--background-size: 2.88rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&.small {
|
|
33
|
+
--size: 2.4rem;
|
|
34
|
+
--border-size: 0.1rem;
|
|
35
|
+
--background-position: -1.2rem -0.739rem;
|
|
36
|
+
--background-size: 4.32rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&.medium {
|
|
40
|
+
--size: 4rem;
|
|
41
|
+
--border-size: 0.2rem;
|
|
42
|
+
--background-position: -2rem -1.232rem;
|
|
43
|
+
--background-size: 7.2rem;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* IMPLEMENTATION */
|
|
48
|
+
.user-logo {
|
|
49
|
+
display: block;
|
|
50
|
+
width: var(--size);
|
|
51
|
+
height: var(--size);
|
|
52
|
+
background: var(--color-grey-100) url('../../assets/user.png') no-repeat var(--background-position) /
|
|
53
|
+
var(--background-size);
|
|
54
|
+
border: var(--border-size) solid var(--border-color);
|
|
55
|
+
border-radius: 20rem;
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey, MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { computed, inject, provide, toValue } from 'vue'
|
|
3
|
+
|
|
4
|
+
export const createContext = <T, Output = ComputedRef<T>>(
|
|
5
|
+
initialValue: MaybeRefOrGetter<T>,
|
|
6
|
+
customBuilder?: (value: ComputedRef<T>, previousValue: ComputedRef<T>) => Output
|
|
7
|
+
) => {
|
|
8
|
+
return {
|
|
9
|
+
id: Symbol('CONTEXT_ID') as InjectionKey<MaybeRefOrGetter<T>>,
|
|
10
|
+
initialValue,
|
|
11
|
+
builder: customBuilder ?? (value => value as Output),
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Context<T = any, Output = any> = ReturnType<typeof createContext<T, Output>>
|
|
16
|
+
|
|
17
|
+
type ContextOutput<Ctx extends Context> = Ctx extends Context<any, infer Output> ? Output : never
|
|
18
|
+
|
|
19
|
+
type ContextValue<Ctx extends Context> = Ctx extends Context<infer T> ? T : never
|
|
20
|
+
|
|
21
|
+
export const useContext = <Ctx extends Context, T extends ContextValue<Ctx>>(
|
|
22
|
+
context: Ctx,
|
|
23
|
+
newValue?: MaybeRefOrGetter<T | undefined>
|
|
24
|
+
): ContextOutput<Ctx> => {
|
|
25
|
+
const currentValue = inject(context.id, undefined)
|
|
26
|
+
|
|
27
|
+
const updatedValue = () => toValue(newValue) ?? toValue(currentValue) ?? context.initialValue
|
|
28
|
+
provide(context.id, updatedValue)
|
|
29
|
+
|
|
30
|
+
return context.builder(
|
|
31
|
+
computed(() => toValue(updatedValue)),
|
|
32
|
+
computed(() => toValue(currentValue))
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TreeNodeDefinitionBase } from '@core/composables/tree/tree-node-definition-base'
|
|
2
|
+
import type { TreeNodeDefinition, TreeNodeOptions } from '@core/composables/tree/types'
|
|
3
|
+
|
|
4
|
+
export class BranchDefinition<
|
|
5
|
+
TData extends object = any,
|
|
6
|
+
TChildDefinition extends TreeNodeDefinition = TreeNodeDefinition,
|
|
7
|
+
const TDiscriminator = any,
|
|
8
|
+
> extends TreeNodeDefinitionBase<TData, TDiscriminator> {
|
|
9
|
+
readonly isBranch = true
|
|
10
|
+
children: TChildDefinition[]
|
|
11
|
+
|
|
12
|
+
constructor(data: TData, options: TreeNodeOptions<TData, TDiscriminator>, children: TChildDefinition[]) {
|
|
13
|
+
super(data, options)
|
|
14
|
+
|
|
15
|
+
this.children = children
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { TreeNodeBase } from '@core/composables/tree/tree-node-base'
|
|
2
|
+
import type {
|
|
3
|
+
BranchStatuses,
|
|
4
|
+
ChildTreeGetter,
|
|
5
|
+
TreeContext,
|
|
6
|
+
TreeNode,
|
|
7
|
+
TreeNodeOptions,
|
|
8
|
+
} from '@core/composables/tree/types'
|
|
9
|
+
|
|
10
|
+
export class Branch<
|
|
11
|
+
TData extends object = any,
|
|
12
|
+
TChildNode extends TreeNode = TreeNode,
|
|
13
|
+
const TDiscriminator = any,
|
|
14
|
+
> extends TreeNodeBase<TData, TDiscriminator> {
|
|
15
|
+
readonly isBranch = true
|
|
16
|
+
readonly rawChildren: TChildNode[]
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
data: TData,
|
|
20
|
+
parent: Branch | undefined,
|
|
21
|
+
context: TreeContext,
|
|
22
|
+
depth: number,
|
|
23
|
+
options: TreeNodeOptions<TData, TDiscriminator> | undefined,
|
|
24
|
+
getChildTree: ChildTreeGetter<TData, TChildNode, TDiscriminator>
|
|
25
|
+
) {
|
|
26
|
+
super(data, parent, context, depth, options)
|
|
27
|
+
this.rawChildren = getChildTree(this)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get children() {
|
|
31
|
+
return this.rawChildren.filter(child => !child.isExcluded)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get hasChildren() {
|
|
35
|
+
return this.rawChildren.length > 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get passesFilterDownwards(): boolean {
|
|
39
|
+
return this.passesFilter || this.rawChildren.some(child => child.passesFilterDownwards)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get failsFilterDownwards(): boolean {
|
|
43
|
+
return this.passesFilter !== true && this.rawChildren.every(child => child.failsFilterDownwards)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get isExcluded() {
|
|
47
|
+
if (this.parent?.isExpanded === false) {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!this.hasChildren) {
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (this.passesFilterUpwards || this.passesFilterDownwards) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this.failsFilterDownwards
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get isExpanded() {
|
|
63
|
+
return this.context.expandedIds.has(this.id) || this.passesFilterDownwards || this.passesFilterUpwards
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get areChildrenFullySelected(): boolean {
|
|
67
|
+
if (!this.context.allowMultiSelect) {
|
|
68
|
+
console.warn('allowMultiSelect must be enabled to use areChildrenFullySelected')
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this.rawChildren.every(child => (child.isBranch ? child.areChildrenFullySelected : child.isSelected))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get areChildrenPartiallySelected(): boolean {
|
|
76
|
+
if (!this.context.allowMultiSelect) {
|
|
77
|
+
console.warn('allowMultiSelect must be enabled to use areChildrenPartiallySelected')
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.areChildrenFullySelected) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.rawChildren.some(child => (child.isBranch ? child.areChildrenPartiallySelected : child.isSelected))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get statuses(): BranchStatuses {
|
|
89
|
+
return {
|
|
90
|
+
active: this.isActive,
|
|
91
|
+
selected: this.isSelected,
|
|
92
|
+
matches: this.passesFilter === true,
|
|
93
|
+
'selected-partial': this.context.allowMultiSelect && this.areChildrenPartiallySelected,
|
|
94
|
+
'selected-full': this.context.allowMultiSelect && this.areChildrenFullySelected,
|
|
95
|
+
expanded: this.isExpanded,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get childrenSelectedState() {
|
|
100
|
+
console.warn('allowMultiSelect must be enabled to use childrenSelectedState')
|
|
101
|
+
return this.areChildrenFullySelected ? 'all' : this.areChildrenPartiallySelected ? 'some' : 'none'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
toggleExpand(forcedValue?: boolean, recursive?: boolean) {
|
|
105
|
+
const nextExpanded = forcedValue ?? !this.isExpanded
|
|
106
|
+
|
|
107
|
+
if (nextExpanded) {
|
|
108
|
+
this.context.expandedIds.add(this.id)
|
|
109
|
+
} else {
|
|
110
|
+
this.context.expandedIds.delete(this.id)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const shouldPropagate = recursive ?? !nextExpanded
|
|
114
|
+
|
|
115
|
+
if (shouldPropagate) {
|
|
116
|
+
this.rawChildren.forEach(child => {
|
|
117
|
+
if (child.isBranch) {
|
|
118
|
+
child.toggleExpand(nextExpanded, recursive)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get shouldSelectChildren(): boolean {
|
|
125
|
+
return this.children.some(child => (child.isBranch ? child.shouldSelectChildren : !child.isSelected))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toggleChildrenSelect(forcedValue?: boolean) {
|
|
129
|
+
if (!this.context.allowMultiSelect) {
|
|
130
|
+
console.warn('allowMultiSelect must be enabled to use toggleChildrenSelect')
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const shouldSelect = forcedValue ?? this.shouldSelectChildren
|
|
135
|
+
this.rawChildren.forEach(child => {
|
|
136
|
+
if (child.isBranch) {
|
|
137
|
+
child.toggleChildrenSelect(shouldSelect)
|
|
138
|
+
} else if (!child.isExcluded) {
|
|
139
|
+
child.toggleSelect(shouldSelect)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Branch } from '@core/composables/tree/branch'
|
|
2
|
+
import { Leaf } from '@core/composables/tree/leaf'
|
|
3
|
+
import type { DefinitionToTreeNode, TreeContext, TreeNode, TreeNodeDefinition } from '@core/composables/tree/types'
|
|
4
|
+
|
|
5
|
+
export function buildNodes<TDefinition extends TreeNodeDefinition, TTreeNode extends DefinitionToTreeNode<TDefinition>>(
|
|
6
|
+
definitions: TDefinition[],
|
|
7
|
+
context: TreeContext
|
|
8
|
+
): TTreeNode[] {
|
|
9
|
+
function create(definitions: TreeNodeDefinition[], parent: Branch | undefined, depth: number): TreeNode[] {
|
|
10
|
+
return definitions.map(definition =>
|
|
11
|
+
definition.isBranch
|
|
12
|
+
? new Branch(definition.data, parent, context, depth, definition.options, thisBranch =>
|
|
13
|
+
create(definition.children, thisBranch, depth + 1)
|
|
14
|
+
)
|
|
15
|
+
: new Leaf(definition.data, parent, context, depth, definition.options)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return create(definitions, undefined, 0) as TTreeNode[]
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BranchDefinition } from '@core/composables/tree/branch-definition'
|
|
2
|
+
import type { Identifiable, Labeled, TreeNodeDefinition, TreeNodeOptions } from '@core/composables/tree/types'
|
|
3
|
+
|
|
4
|
+
export function defineBranch<
|
|
5
|
+
TData extends Identifiable & Labeled,
|
|
6
|
+
TChildDefinition extends TreeNodeDefinition,
|
|
7
|
+
const TDiscriminator,
|
|
8
|
+
>(data: TData, children: TChildDefinition[]): BranchDefinition<TData, TChildDefinition, TDiscriminator>
|
|
9
|
+
export function defineBranch<TData extends object, TChildDefinition extends TreeNodeDefinition, const TDiscriminator>(
|
|
10
|
+
data: TData,
|
|
11
|
+
options: TreeNodeOptions<TData, TDiscriminator>,
|
|
12
|
+
children: TChildDefinition[]
|
|
13
|
+
): BranchDefinition<TData, TChildDefinition, TDiscriminator>
|
|
14
|
+
export function defineBranch<TData extends object, TChildDefinition extends TreeNodeDefinition, const TDiscriminator>(
|
|
15
|
+
data: TData,
|
|
16
|
+
optionsOrChildren: TreeNodeOptions<TData, TDiscriminator> | TChildDefinition[],
|
|
17
|
+
childrenOrNone?: TChildDefinition[]
|
|
18
|
+
): BranchDefinition<TData, TChildDefinition, TDiscriminator> {
|
|
19
|
+
const options = Array.isArray(optionsOrChildren) ? ({} as TreeNodeOptions<TData, TDiscriminator>) : optionsOrChildren
|
|
20
|
+
const children = Array.isArray(optionsOrChildren) ? optionsOrChildren : childrenOrNone!
|
|
21
|
+
|
|
22
|
+
return new BranchDefinition(data, options, children)
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { LeafDefinition } from '@core/composables/tree/leaf-definition'
|
|
2
|
+
import type { Identifiable, Labeled, TreeNodeOptions } from '@core/composables/tree/types'
|
|
3
|
+
|
|
4
|
+
export function defineLeaf<TData extends Identifiable & Labeled, const TDiscriminator>(
|
|
5
|
+
data: TData
|
|
6
|
+
): LeafDefinition<TData, TDiscriminator>
|
|
7
|
+
export function defineLeaf<TData extends object, const TDiscriminator>(
|
|
8
|
+
data: TData,
|
|
9
|
+
options: TreeNodeOptions<TData, TDiscriminator>
|
|
10
|
+
): LeafDefinition<TData, TDiscriminator>
|
|
11
|
+
export function defineLeaf<TData extends object, const TDiscriminator>(
|
|
12
|
+
data: TData,
|
|
13
|
+
options?: TreeNodeOptions<TData, TDiscriminator>
|
|
14
|
+
): LeafDefinition<TData, TDiscriminator> {
|
|
15
|
+
return new LeafDefinition(data, options ?? ({} as TreeNodeOptions<TData, TDiscriminator>))
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BranchDefinition } from '@core/composables/tree/branch-definition'
|
|
2
|
+
import { LeafDefinition } from '@core/composables/tree/leaf-definition'
|
|
3
|
+
import type {
|
|
4
|
+
ChildTreeDefinitionGetter,
|
|
5
|
+
Identifiable,
|
|
6
|
+
Labeled,
|
|
7
|
+
TreeNodeDefinition,
|
|
8
|
+
TreeNodeOptions,
|
|
9
|
+
} from '@core/composables/tree/types'
|
|
10
|
+
|
|
11
|
+
// Overload 1: Leaf with no options
|
|
12
|
+
export function defineTree<TData extends Identifiable & Labeled, const TDiscriminator = any>(
|
|
13
|
+
entries: TData[]
|
|
14
|
+
): LeafDefinition<TData, TDiscriminator>[]
|
|
15
|
+
|
|
16
|
+
// Overload 2: Leaf with options
|
|
17
|
+
export function defineTree<TData extends object, const TDiscriminator = any>(
|
|
18
|
+
entries: TData[],
|
|
19
|
+
options: TreeNodeOptions<TData, TDiscriminator>
|
|
20
|
+
): LeafDefinition<TData, TDiscriminator>[]
|
|
21
|
+
|
|
22
|
+
// Overload 3: Branch with no options
|
|
23
|
+
export function defineTree<
|
|
24
|
+
TData extends Identifiable & Labeled,
|
|
25
|
+
TChildDefinition extends TreeNodeDefinition,
|
|
26
|
+
const TDiscriminator = any,
|
|
27
|
+
>(
|
|
28
|
+
entries: TData[],
|
|
29
|
+
getChildTree: ChildTreeDefinitionGetter<TData, TChildDefinition>
|
|
30
|
+
): BranchDefinition<TData, TChildDefinition, TDiscriminator>[]
|
|
31
|
+
|
|
32
|
+
// Overload 4: Branch with options
|
|
33
|
+
export function defineTree<
|
|
34
|
+
TData extends object,
|
|
35
|
+
TChildDefinition extends TreeNodeDefinition = TreeNodeDefinition,
|
|
36
|
+
const TDiscriminator = any,
|
|
37
|
+
>(
|
|
38
|
+
entries: TData[],
|
|
39
|
+
options: TreeNodeOptions<TData, TDiscriminator>,
|
|
40
|
+
getChildTree: ChildTreeDefinitionGetter<TData, TChildDefinition>
|
|
41
|
+
): BranchDefinition<TData, TChildDefinition, TDiscriminator>[]
|
|
42
|
+
|
|
43
|
+
// Implementation
|
|
44
|
+
export function defineTree<
|
|
45
|
+
TData extends object,
|
|
46
|
+
TChildDefinition extends TreeNodeDefinition = TreeNodeDefinition,
|
|
47
|
+
const TDiscriminator = any,
|
|
48
|
+
>(
|
|
49
|
+
entries: TData[],
|
|
50
|
+
optionsOrGetChildTree?: TreeNodeOptions<TData, TDiscriminator> | ChildTreeDefinitionGetter<TData, TChildDefinition>,
|
|
51
|
+
getChildTreeOrNone?: ChildTreeDefinitionGetter<TData, TChildDefinition>
|
|
52
|
+
) {
|
|
53
|
+
const options = (typeof optionsOrGetChildTree === 'function' ? {} : optionsOrGetChildTree ?? {}) as TreeNodeOptions<
|
|
54
|
+
TData,
|
|
55
|
+
TDiscriminator
|
|
56
|
+
>
|
|
57
|
+
|
|
58
|
+
const getChildTree = typeof optionsOrGetChildTree === 'function' ? optionsOrGetChildTree : getChildTreeOrNone
|
|
59
|
+
|
|
60
|
+
if (getChildTree !== undefined) {
|
|
61
|
+
return entries.map(data => new BranchDefinition(data, options, getChildTree(data)))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return entries.map(data => new LeafDefinition(data, options))
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TreeNodeDefinitionBase } from '@core/composables/tree/tree-node-definition-base'
|
|
2
|
+
|
|
3
|
+
export class LeafDefinition<TData extends object = any, const TDiscriminator = any> extends TreeNodeDefinitionBase<
|
|
4
|
+
TData,
|
|
5
|
+
TDiscriminator
|
|
6
|
+
> {
|
|
7
|
+
readonly isBranch = false
|
|
8
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TreeNodeBase } from '@core/composables/tree/tree-node-base'
|
|
2
|
+
import type { LeafStatuses } from '@core/composables/tree/types'
|
|
3
|
+
|
|
4
|
+
export class Leaf<TData extends object = any, const TDiscriminator = any> extends TreeNodeBase<TData, TDiscriminator> {
|
|
5
|
+
readonly isBranch = false
|
|
6
|
+
|
|
7
|
+
get passesFilterDownwards(): boolean {
|
|
8
|
+
return this.passesFilter ?? false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get failsFilterDownwards(): boolean {
|
|
12
|
+
return this.passesFilter === false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get isExcluded() {
|
|
16
|
+
if (this.parent?.isExpanded === false) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.passesFilterUpwards) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return this.passesFilter === false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get statuses(): LeafStatuses {
|
|
28
|
+
return {
|
|
29
|
+
active: this.isActive,
|
|
30
|
+
selected: this.isSelected,
|
|
31
|
+
matches: this.passesFilter === true,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Branch } from '@core/composables/tree/branch'
|
|
2
|
+
import type { Identifiable, Labeled, TreeContext, TreeNodeOptions } from '@core/composables/tree/types'
|
|
3
|
+
|
|
4
|
+
export abstract class TreeNodeBase<TData extends object = any, TDiscriminator = any> {
|
|
5
|
+
abstract readonly isBranch: boolean
|
|
6
|
+
abstract passesFilterDownwards: boolean
|
|
7
|
+
abstract isExcluded: boolean
|
|
8
|
+
abstract statuses: Record<string, boolean>
|
|
9
|
+
|
|
10
|
+
readonly data: TData
|
|
11
|
+
readonly depth: number
|
|
12
|
+
readonly parent: Branch | undefined
|
|
13
|
+
readonly context: TreeContext
|
|
14
|
+
readonly options: TreeNodeOptions<TData, TDiscriminator>
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
data: TData,
|
|
18
|
+
parent: Branch | undefined,
|
|
19
|
+
context: TreeContext,
|
|
20
|
+
depth: number,
|
|
21
|
+
options?: TreeNodeOptions<TData, TDiscriminator>
|
|
22
|
+
) {
|
|
23
|
+
this.data = data
|
|
24
|
+
this.parent = parent
|
|
25
|
+
this.context = context
|
|
26
|
+
this.depth = depth
|
|
27
|
+
this.options = options ?? ({} as TreeNodeOptions<TData, TDiscriminator>)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get id() {
|
|
31
|
+
if (this.options.getId === undefined) {
|
|
32
|
+
return (this.data as Identifiable).id
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof this.options.getId === 'function') {
|
|
36
|
+
return this.options.getId(this.data)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.data[this.options.getId]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get label() {
|
|
43
|
+
if (this.options.getLabel === undefined) {
|
|
44
|
+
return (this.data as Labeled).label
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof this.options.getLabel === 'function') {
|
|
48
|
+
return this.options.getLabel(this.data)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return this.data[this.options.getLabel]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get discriminator() {
|
|
55
|
+
return this.options.discriminator
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get passesFilter() {
|
|
59
|
+
return this.options.predicate?.(this)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get isSelected() {
|
|
63
|
+
return this.context.selectedIds.has(this.id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get isActive() {
|
|
67
|
+
return this.context.activeId === this.id
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get passesFilterUpwards(): boolean {
|
|
71
|
+
return this.passesFilter || (this.parent?.passesFilterUpwards ?? false)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get isSelectable() {
|
|
75
|
+
return this.options.selectable?.(this.data) ?? !this.isBranch
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
activate() {
|
|
79
|
+
if (!this.isSelectable) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.context.activeId = this.id
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toggleSelect(forcedValue?: boolean) {
|
|
87
|
+
if (!this.isSelectable) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const shouldSelect = forcedValue ?? !this.isSelected
|
|
92
|
+
|
|
93
|
+
if (shouldSelect) {
|
|
94
|
+
if (!this.context.allowMultiSelect) {
|
|
95
|
+
this.context.selectedIds.clear()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.context.selectedIds.add(this.id)
|
|
99
|
+
} else {
|
|
100
|
+
this.context.selectedIds.delete(this.id)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TreeNodeOptions } from '@core/composables/tree/types'
|
|
2
|
+
|
|
3
|
+
export abstract class TreeNodeDefinitionBase<TData extends object, TDiscriminator> {
|
|
4
|
+
abstract readonly isBranch: boolean
|
|
5
|
+
data: TData
|
|
6
|
+
options: TreeNodeOptions<TData, TDiscriminator>
|
|
7
|
+
|
|
8
|
+
constructor(data: TData, options: TreeNodeOptions<TData, TDiscriminator>) {
|
|
9
|
+
this.data = data
|
|
10
|
+
this.options = options
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useTree } from '@core/composables/tree.composable'
|
|
2
|
+
import type { Branch } from '@core/composables/tree/branch'
|
|
3
|
+
import type { BranchDefinition } from '@core/composables/tree/branch-definition'
|
|
4
|
+
import type { Leaf } from '@core/composables/tree/leaf'
|
|
5
|
+
import type { LeafDefinition } from '@core/composables/tree/leaf-definition'
|
|
6
|
+
import type { TreeNodeBase } from '@core/composables/tree/tree-node-base'
|
|
7
|
+
|
|
8
|
+
export type TreeNodeId = string | number
|
|
9
|
+
|
|
10
|
+
export type Identifiable = { id: TreeNodeId }
|
|
11
|
+
|
|
12
|
+
export type Labeled = { label: string }
|
|
13
|
+
|
|
14
|
+
type AcceptableKeys<TData, TAccepted> = {
|
|
15
|
+
[K in keyof TData]: TData[K] extends TAccepted ? K : never
|
|
16
|
+
}[keyof TData]
|
|
17
|
+
|
|
18
|
+
type AcceptableGetter<TData, TAccepted> = AcceptableKeys<TData, TAccepted> | ((data: TData) => TAccepted)
|
|
19
|
+
|
|
20
|
+
export type TreeNode<
|
|
21
|
+
TData extends object = any,
|
|
22
|
+
TChildNode extends TreeNode = TreeNode<any, any>,
|
|
23
|
+
TDiscriminator = any,
|
|
24
|
+
> = Leaf<TData, TDiscriminator> | Branch<TData, TChildNode, TDiscriminator>
|
|
25
|
+
|
|
26
|
+
export type BaseTreeNodeOptions<TData extends object, TDiscriminator> = {
|
|
27
|
+
discriminator?: TDiscriminator
|
|
28
|
+
predicate?: (node: TreeNodeBase<TData, TDiscriminator>) => boolean | undefined
|
|
29
|
+
selectable?: (data: TData) => boolean
|
|
30
|
+
meta?: any
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type GetIdOption<TData extends object> = TData extends Identifiable
|
|
34
|
+
? { getId?: AcceptableGetter<TData, TreeNodeId> }
|
|
35
|
+
: { getId: AcceptableGetter<TData, TreeNodeId> }
|
|
36
|
+
|
|
37
|
+
type GetLabelOption<TData extends object> = TData extends Labeled
|
|
38
|
+
? { getLabel?: AcceptableGetter<TData, string> }
|
|
39
|
+
: { getLabel: AcceptableGetter<TData, string> }
|
|
40
|
+
|
|
41
|
+
export type TreeNodeOptions<TData extends object, TDiscriminator> = BaseTreeNodeOptions<TData, TDiscriminator> &
|
|
42
|
+
GetIdOption<TData> &
|
|
43
|
+
GetLabelOption<TData>
|
|
44
|
+
|
|
45
|
+
export type TreeNodeDefinition = LeafDefinition | BranchDefinition
|
|
46
|
+
|
|
47
|
+
export type DefinitionToTreeNode<TDefinition> =
|
|
48
|
+
TDefinition extends BranchDefinition<infer TData, infer TChildDefinition, infer TDiscriminator>
|
|
49
|
+
? Branch<TData, DefinitionToTreeNode<TChildDefinition>, TDiscriminator>
|
|
50
|
+
: TDefinition extends LeafDefinition<infer TData, infer TDiscriminator>
|
|
51
|
+
? Leaf<TData, TDiscriminator>
|
|
52
|
+
: never
|
|
53
|
+
|
|
54
|
+
export type ChildTreeGetter<TData extends object, TChildNode extends TreeNode, TDiscriminator> = (
|
|
55
|
+
thisBranch: Branch<TData, TChildNode, TDiscriminator>
|
|
56
|
+
) => TChildNode[]
|
|
57
|
+
|
|
58
|
+
export type ChildTreeDefinitionGetter<TData extends object, TChildDefinition extends TreeNodeDefinition> = (
|
|
59
|
+
data: TData
|
|
60
|
+
) => TChildDefinition[]
|
|
61
|
+
|
|
62
|
+
export type TreeContext = {
|
|
63
|
+
allowMultiSelect: boolean
|
|
64
|
+
selectedIds: Set<TreeNodeId>
|
|
65
|
+
expandedIds: Set<TreeNodeId>
|
|
66
|
+
activeId: TreeNodeId | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type UseTreeOptions = {
|
|
70
|
+
allowMultiSelect?: boolean
|
|
71
|
+
expand?: boolean
|
|
72
|
+
selectedLabel?:
|
|
73
|
+
| ((nodes: TreeNode[]) => string)
|
|
74
|
+
| {
|
|
75
|
+
max: number
|
|
76
|
+
fn: (count: number) => string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type Tree = ReturnType<typeof useTree>
|
|
81
|
+
|
|
82
|
+
export type LeafStatuses = {
|
|
83
|
+
active: boolean
|
|
84
|
+
selected: boolean
|
|
85
|
+
matches: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type BranchStatuses = LeafStatuses & {
|
|
89
|
+
'selected-partial': boolean
|
|
90
|
+
'selected-full': boolean
|
|
91
|
+
expanded: boolean
|
|
92
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TreeNodeBase } from '@core/composables/tree/tree-node-base'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useTreeFilter() {
|
|
5
|
+
const filter = ref('')
|
|
6
|
+
const hasFilter = computed(() => filter.value.trim().length > 0)
|
|
7
|
+
|
|
8
|
+
const predicate = (node: TreeNodeBase) =>
|
|
9
|
+
hasFilter.value ? node.label.toLocaleLowerCase().includes(filter.value.toLocaleLowerCase()) : undefined
|
|
10
|
+
|
|
11
|
+
return { filter, predicate }
|
|
12
|
+
}
|