|
1 | | -import type { TreeNode, TreeSkipFn } from "@zag-js/collection" |
2 | | -import { createGuards, createMachine, type Params } from "@zag-js/core" |
| 1 | +import type { TreeNode } from "@zag-js/collection" |
| 2 | +import { createGuards, createMachine } from "@zag-js/core" |
3 | 3 | import { getByTypeahead } from "@zag-js/dom-query" |
4 | | -import { add, addOrRemove, diff, ensure, first, isArray, isEqual, last, remove, toArray, uniq } from "@zag-js/utils" |
| 4 | +import { addOrRemove, diff, first, isArray, isEqual, last, remove, toArray, uniq } from "@zag-js/utils" |
5 | 5 | import { collection } from "./tree-view.collection" |
6 | 6 | import * as dom from "./tree-view.dom" |
7 | 7 | import type { TreeLoadingStatusMap, TreeViewSchema } from "./tree-view.types" |
| 8 | +import { toggleBranchChecked } from "./utils/checked-state" |
| 9 | +import { expandBranches } from "./utils/expand-branch" |
| 10 | +import { skipFn } from "./utils/visit-skip" |
8 | 11 |
|
9 | 12 | const { and } = createGuards<TreeViewSchema>() |
10 | 13 |
|
@@ -56,6 +59,14 @@ export const machine = createMachine<TreeViewSchema>({ |
56 | 59 | loadingStatus: bindable<TreeLoadingStatusMap>(() => ({ |
57 | 60 | defaultValue: {}, |
58 | 61 | })), |
| 62 | + checkedValue: bindable(() => ({ |
| 63 | + defaultValue: prop("defaultCheckedValue") || [], |
| 64 | + value: prop("checkedValue"), |
| 65 | + isEqual, |
| 66 | + onChange(value) { |
| 67 | + prop("onCheckedChange")?.({ checkedValue: value }) |
| 68 | + }, |
| 69 | + })), |
59 | 70 | } |
60 | 71 | }, |
61 | 72 |
|
@@ -119,6 +130,15 @@ export const machine = createMachine<TreeViewSchema>({ |
119 | 130 | "NODE.DESELECT": { |
120 | 131 | actions: ["deselectNode"], |
121 | 132 | }, |
| 133 | + "CHECKED.TOGGLE": { |
| 134 | + actions: ["toggleChecked"], |
| 135 | + }, |
| 136 | + "CHECKED.SET": { |
| 137 | + actions: ["setChecked"], |
| 138 | + }, |
| 139 | + "CHECKED.CLEAR": { |
| 140 | + actions: ["clearChecked"], |
| 141 | + }, |
122 | 142 | }, |
123 | 143 |
|
124 | 144 | exit: ["clearPendingAborts"], |
@@ -241,14 +261,12 @@ export const machine = createMachine<TreeViewSchema>({ |
241 | 261 | expandOnClick: ({ prop }) => !!prop("expandOnClick"), |
242 | 262 | }, |
243 | 263 | actions: { |
244 | | - selectNode({ context, event, prop }) { |
245 | | - const value = toArray(event.id || event.value) |
| 264 | + selectNode({ context, event }) { |
| 265 | + const value = (event.id || event.value) as string | string[] | undefined |
246 | 266 | context.set("selectedValue", (prev) => { |
247 | | - if (prop("selectionMode") === "single") { |
248 | | - return [last(value)].filter(Boolean) |
249 | | - } else { |
250 | | - return uniq([...prev, ...value]) |
251 | | - } |
| 267 | + if (value == null) return prev |
| 268 | + if (!event.isTrusted && isArray(value)) return prev.concat(...value) |
| 269 | + return [isArray(value) ? last(value) : value].filter(Boolean) as string[] |
252 | 270 | }) |
253 | 271 | }, |
254 | 272 | deselectNode({ context, event }) { |
@@ -480,114 +498,18 @@ export const machine = createMachine<TreeViewSchema>({ |
480 | 498 | aborts.forEach((abort) => abort.abort()) |
481 | 499 | aborts.clear() |
482 | 500 | }, |
| 501 | + toggleChecked({ context, event, prop }) { |
| 502 | + const collection = prop("collection") |
| 503 | + context.set("checkedValue", (prev) => |
| 504 | + event.isBranch ? toggleBranchChecked(collection, event.value, prev) : addOrRemove(prev, event.value), |
| 505 | + ) |
| 506 | + }, |
| 507 | + setChecked({ context, event }) { |
| 508 | + context.set("checkedValue", event.value) |
| 509 | + }, |
| 510 | + clearChecked({ context }) { |
| 511 | + context.set("checkedValue", []) |
| 512 | + }, |
483 | 513 | }, |
484 | 514 | }, |
485 | 515 | }) |
486 | | - |
487 | | -function skipFn(params: Pick<Params<TreeViewSchema>, "prop" | "context">): TreeSkipFn<any> { |
488 | | - const { prop, context } = params |
489 | | - return function skip({ indexPath }) { |
490 | | - const paths = prop("collection").getValuePath(indexPath).slice(0, -1) |
491 | | - return paths.some((value) => !context.get("expandedValue").includes(value)) |
492 | | - } |
493 | | -} |
494 | | - |
495 | | -function partition<T>(array: T[], predicate: (value: T) => boolean) { |
496 | | - const pass: T[] = [] |
497 | | - const fail: T[] = [] |
498 | | - array.forEach((value) => { |
499 | | - if (predicate(value)) pass.push(value) |
500 | | - else fail.push(value) |
501 | | - }) |
502 | | - return [pass, fail] |
503 | | -} |
504 | | - |
505 | | -function expandBranches(params: Params<TreeViewSchema>, ids: string[]) { |
506 | | - const { context, prop, refs } = params |
507 | | - |
508 | | - if (!prop("loadChildren")) { |
509 | | - context.set("expandedValue", (prev) => uniq(add(prev, ...ids))) |
510 | | - return |
511 | | - } |
512 | | - |
513 | | - const [loadedValues, loadingValues] = partition(ids, (id) => context.get("loadingStatus")[id] === "loaded") |
514 | | - |
515 | | - if (loadedValues.length > 0) { |
516 | | - context.set("expandedValue", (prev) => uniq(add(prev, ...loadedValues))) |
517 | | - } |
518 | | - |
519 | | - if (loadingValues.length === 0) return |
520 | | - |
521 | | - const collection = prop("collection") |
522 | | - const [nodeWithChildren, nodeWithoutChildren] = partition(loadingValues, (id) => { |
523 | | - const node = collection.findNode(id) |
524 | | - return collection.getNodeChildren(node).length > 0 |
525 | | - }) |
526 | | - |
527 | | - // Check if node already has children (skip loading) |
528 | | - if (nodeWithChildren.length > 0) { |
529 | | - context.set("expandedValue", (prev) => uniq(add(prev, ...nodeWithChildren))) |
530 | | - } |
531 | | - |
532 | | - if (nodeWithoutChildren.length === 0) return |
533 | | - |
534 | | - context.set("loadingStatus", (prev) => ({ |
535 | | - ...prev, |
536 | | - ...nodeWithoutChildren.reduce((acc, id) => ({ ...acc, [id]: "loading" }), {}), |
537 | | - })) |
538 | | - |
539 | | - const nodesToLoad = nodeWithoutChildren.map((id) => { |
540 | | - const indexPath = collection.getIndexPath(id)! |
541 | | - const valuePath = collection.getValuePath(indexPath)! |
542 | | - const node = collection.findNode(id)! |
543 | | - return { id, indexPath, valuePath, node } |
544 | | - }) |
545 | | - |
546 | | - const pendingAborts = refs.get("pendingAborts") |
547 | | - |
548 | | - // load children asynchronously |
549 | | - const loadChildren = prop("loadChildren") |
550 | | - ensure(loadChildren, () => "[zag-js/tree-view] `loadChildren` is required for async expansion") |
551 | | - |
552 | | - const proms = nodesToLoad.map(({ id, indexPath, valuePath, node }) => { |
553 | | - const existingAbort = pendingAborts.get(id) |
554 | | - if (existingAbort) { |
555 | | - existingAbort.abort() |
556 | | - pendingAborts.delete(id) |
557 | | - } |
558 | | - const abortController = new AbortController() |
559 | | - pendingAborts.set(id, abortController) |
560 | | - return loadChildren({ |
561 | | - valuePath, |
562 | | - indexPath, |
563 | | - node, |
564 | | - signal: abortController.signal, |
565 | | - }) |
566 | | - }) |
567 | | - |
568 | | - // prefer `Promise.allSettled` over `Promise.all` to avoid early termination |
569 | | - Promise.allSettled(proms).then((results) => { |
570 | | - const loadedValues: string[] = [] |
571 | | - const nextLoadingStatus = context.get("loadingStatus") |
572 | | - let collection = prop("collection") |
573 | | - |
574 | | - results.forEach((result, index) => { |
575 | | - const { id, indexPath, node } = nodesToLoad[index] |
576 | | - if (result.status === "fulfilled") { |
577 | | - nextLoadingStatus[id] = "loaded" |
578 | | - loadedValues.push(id) |
579 | | - collection = collection.replace(indexPath, { ...node, children: result.value }) |
580 | | - } else { |
581 | | - pendingAborts.delete(id) |
582 | | - Reflect.deleteProperty(nextLoadingStatus, id) |
583 | | - } |
584 | | - }) |
585 | | - |
586 | | - context.set("loadingStatus", nextLoadingStatus) |
587 | | - |
588 | | - if (loadedValues.length) { |
589 | | - context.set("expandedValue", (prev) => uniq(add(prev, ...loadedValues))) |
590 | | - prop("onLoadChildrenComplete")?.({ collection }) |
591 | | - } |
592 | | - }) |
593 | | -} |
0 commit comments