@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.
Files changed (91) hide show
  1. package/lib/assets/css/_colors.pcss +125 -0
  2. package/lib/assets/css/_context.pcss +99 -0
  3. package/lib/assets/css/_fonts.pcss +6 -0
  4. package/lib/assets/css/_reset.pcss +42 -0
  5. package/lib/assets/css/_shadows.pcss +36 -0
  6. package/lib/assets/css/_typography.pcss +6 -0
  7. package/lib/assets/css/base.pcss +91 -0
  8. package/lib/assets/css/typography/_legacy.pcss +123 -0
  9. package/lib/assets/css/typography/_letter-spacing.pcss +27 -0
  10. package/lib/assets/css/typography/_line-height.pcss +19 -0
  11. package/lib/assets/css/typography/_size.pcss +95 -0
  12. package/lib/assets/css/typography/_style.pcss +34 -0
  13. package/lib/assets/css/typography/_weight.pcss +57 -0
  14. package/lib/assets/user.png +0 -0
  15. package/lib/components/PowerStateIcon.vue +46 -0
  16. package/lib/components/StatusPill.vue +32 -0
  17. package/lib/components/UiCounter.vue +89 -0
  18. package/lib/components/UiSpinner.vue +48 -0
  19. package/lib/components/UiTag.vue +97 -0
  20. package/lib/components/button/ButtonGroup.vue +33 -0
  21. package/lib/components/button/ButtonIcon.vue +199 -0
  22. package/lib/components/button/UiButton.vue +207 -0
  23. package/lib/components/chip/ChipIcon.vue +29 -0
  24. package/lib/components/chip/ChipRemoveIcon.vue +13 -0
  25. package/lib/components/chip/UiChip.vue +138 -0
  26. package/lib/components/dropdown/DropdownItem.vue +192 -0
  27. package/lib/components/dropdown/DropdownList.vue +31 -0
  28. package/lib/components/dropdown/DropdownTitle.vue +65 -0
  29. package/lib/components/icon/ComplexIcon.vue +51 -0
  30. package/lib/components/icon/ObjectIcon.vue +243 -0
  31. package/lib/components/icon/UiIcon.vue +47 -0
  32. package/lib/components/icon/VmIcon.vue +30 -0
  33. package/lib/components/layout/LayoutSidebar.vue +100 -0
  34. package/lib/components/menu/MenuItem.vue +82 -0
  35. package/lib/components/menu/MenuList.vue +104 -0
  36. package/lib/components/menu/MenuSeparator.vue +27 -0
  37. package/lib/components/menu/MenuTrigger.vue +52 -0
  38. package/lib/components/tab/TabItem.vue +100 -0
  39. package/lib/components/tab/TabList.vue +32 -0
  40. package/lib/components/tooltip/TooltipItem.vue +80 -0
  41. package/lib/components/tooltip/TooltipList.vue +15 -0
  42. package/lib/components/tree/TreeItem.vue +34 -0
  43. package/lib/components/tree/TreeItemError.vue +13 -0
  44. package/lib/components/tree/TreeItemLabel.vue +128 -0
  45. package/lib/components/tree/TreeLine.vue +51 -0
  46. package/lib/components/tree/TreeList.vue +14 -0
  47. package/lib/components/tree/TreeLoadingItem.vue +64 -0
  48. package/lib/components/user/UserLink.vue +72 -0
  49. package/lib/components/user/UserLogo.vue +57 -0
  50. package/lib/composables/context.composable.ts +34 -0
  51. package/lib/composables/tree/branch-definition.ts +17 -0
  52. package/lib/composables/tree/branch.ts +143 -0
  53. package/lib/composables/tree/build-nodes.ts +20 -0
  54. package/lib/composables/tree/define-branch.ts +23 -0
  55. package/lib/composables/tree/define-leaf.ts +16 -0
  56. package/lib/composables/tree/define-tree.ts +65 -0
  57. package/lib/composables/tree/leaf-definition.ts +8 -0
  58. package/lib/composables/tree/leaf.ts +34 -0
  59. package/lib/composables/tree/tree-node-base.ts +103 -0
  60. package/lib/composables/tree/tree-node-definition-base.ts +12 -0
  61. package/lib/composables/tree/types.ts +92 -0
  62. package/lib/composables/tree-filter.composable.ts +12 -0
  63. package/lib/composables/tree.composable.ts +85 -0
  64. package/lib/context.ts +10 -0
  65. package/lib/directives/tooltip.directive.md +117 -0
  66. package/lib/directives/tooltip.directive.ts +52 -0
  67. package/lib/i18n.ts +158 -0
  68. package/lib/layouts/CoreLayout.vue +182 -0
  69. package/lib/locales/de.json +6 -0
  70. package/lib/locales/en.json +15 -0
  71. package/lib/locales/fr.json +15 -0
  72. package/lib/stores/panel.store.ts +12 -0
  73. package/lib/stores/sidebar.store.ts +63 -0
  74. package/lib/stores/tooltip.store.ts +74 -0
  75. package/lib/stores/ui.store.ts +34 -0
  76. package/lib/types/button.type.ts +3 -0
  77. package/lib/types/color.type.ts +5 -0
  78. package/lib/types/object-icon.type.ts +43 -0
  79. package/lib/types/power-state.type.ts +1 -0
  80. package/lib/types/size.type.ts +3 -0
  81. package/lib/types/subscribable-store.type.ts +21 -0
  82. package/lib/types/utility.type.ts +1 -0
  83. package/lib/utils/create-subscribable-store-context.util.ts +66 -0
  84. package/lib/utils/has-ellipsis.util.ts +11 -0
  85. package/lib/utils/if-else.utils.ts +27 -0
  86. package/lib/utils/injection-keys.util.ts +17 -0
  87. package/lib/utils/sort-by-name-label.util.ts +6 -0
  88. package/lib/utils/to-array.utils.ts +9 -0
  89. package/lib/utils/unique-id.util.ts +8 -0
  90. package/package.json +45 -0
  91. 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
+ }