diff --git a/dist/model/FileTreeController.js b/dist/model/FileTreeController.js index efa8b6a2fd70e0d1f1dec1d90ee87268c72f56c4..8979be3c9549e650b017ace5be2e84c2af3f163f 100644 --- a/dist/model/FileTreeController.js +++ b/dist/model/FileTreeController.js @@ -1056,8 +1056,8 @@ var FileTreeController = class FileTreeController { this.#setExpandedPaths(expandedPaths); return focusCandidate ?? this.#focusedPath; } - #emit() { - for (const listener of this.#listeners) listener(); + #emit(event) { + for (const listener of this.#listeners) listener(event); } #emitMutation(event) { this.#mutationListeners.get(event.operation)?.forEach((listener) => { @@ -1153,7 +1153,7 @@ var FileTreeController = class FileTreeController { const searchFocusCandidate = this.#searchValue != null && this.#searchValue.length > 0 ? this.#refreshActiveSearchState() : this.#searchValue === "" ? this.#focusedPath : focusPathCandidate; const shouldBuildFullProjection = this.#searchValue != null || event.operation !== "expand" && event.operation !== "collapse"; this.#rebuildVisibleProjection(searchFocusCandidate, shouldBuildFullProjection); - this.#emit(); + this.#emit(event); const mutationEvent = toTreesMutationEvent(event); if (mutationEvent != null) this.#emitMutation(mutationEvent); }); diff --git a/dist/model/internalTypes.d.ts b/dist/model/internalTypes.d.ts index 5b0fdd5a3b483439ae256795f3dad5a32182ab78..84ba867b12003fb57602c19234c262385f6ee704 100644 --- a/dist/model/internalTypes.d.ts +++ b/dist/model/internalTypes.d.ts @@ -4,7 +4,10 @@ import { FileTreeCompositionOptions, FileTreePublicId, FileTreeRenderOptions, Fi import { FileTreeController } from "./FileTreeController.js"; //#region src/model/internalTypes.d.ts -type FileTreeControllerListener = () => void; +type FileTreeControllerListener = (event?: { + operation: 'expand' | 'collapse'; + path: string; +}) => void; interface FileTreeStickyRowCandidate { row: FileTreeVisibleRow; subtreeEndIndex: number; diff --git a/dist/model/publicTypes.d.ts b/dist/model/publicTypes.d.ts index 7c851604626d34fcb838cd0381abfccfcc7728cf..4e93d2072d3550af080bc98a36e293b05c1a6a10 100644 --- a/dist/model/publicTypes.d.ts +++ b/dist/model/publicTypes.d.ts @@ -170,6 +170,11 @@ type FileTreeOptionSurface = FileTreeRenderOptions & { gitStatus?: readonly GitStatusEntry[]; id?: string; icons?: FileTreeIcons; + middleTruncate?: boolean; + onExpansionChange?: (change: { + expanded: boolean; + path: FileTreePublicId; + }) => void; onSelectionChange?: FileTreeSelectionChangeListener; renderRowDecoration?: FileTreeRowDecorationRenderer; search?: boolean; diff --git a/dist/render/FileTree.js b/dist/render/FileTree.js index 305714319d9d988bfc025ecaaca662562a3a0ff7..8ab6a7b9b7734b1fa2e17c96743eeb013d4d838c 100644 --- a/dist/render/FileTree.js +++ b/dist/render/FileTree.js @@ -62,6 +62,7 @@ var FileTree = class { #composition; #controller; #id; + #onExpansionChange; #onSelectionChange; #renderRowDecoration; #renamingEnabled; @@ -79,16 +80,18 @@ var FileTree = class { #appliedUnsafeCSS; #selectionVersion; #selectionSubscription = null; + #expansionSubscription = null; #wrapper; #wroteHostItemHeight = false; #wroteHostDensityFactor = false; constructor(options) { - const { composition, density, fileTreeSearchMode, gitStatus, id, initialSearchQuery, icons, itemHeight, onSearchChange, onSelectionChange, overscan, renderRowDecoration, renaming, search, searchBlurBehavior, searchFakeFocus, stickyFolders, unsafeCSS, initialVisibleRowCount, ...controllerOptions } = options; + const { composition, density, fileTreeSearchMode, gitStatus, id, initialSearchQuery, icons, itemHeight, middleTruncate, onExpansionChange, onSearchChange, onSelectionChange, overscan, renderRowDecoration, renaming, search, searchBlurBehavior, searchFakeFocus, stickyFolders, unsafeCSS, initialVisibleRowCount, ...controllerOptions } = options; this.#composition = composition; this.#id = createClientId(id); this.#gitStatusState = resolveFileTreeGitStatusState(gitStatus); this.#icons = icons; this.#unsafeCSS = unsafeCSS; + this.#onExpansionChange = onExpansionChange; this.#onSelectionChange = onSelectionChange; this.#renderRowDecoration = renderRowDecoration; this.#renamingEnabled = renaming != null && renaming !== false; @@ -98,6 +101,7 @@ var FileTree = class { this.#density = resolveFileTreeDensity(density, itemHeight); this.#viewOptions = { itemHeight: this.#density.itemHeight, + middleTruncate, overscan, stickyFolders, initialVisibleRowCount @@ -113,6 +117,10 @@ var FileTree = class { this.#selectionSubscription = this.#onSelectionChange == null ? null : this.subscribe(() => { this.#emitSelectionChange(); }); + this.#expansionSubscription = this.#onExpansionChange == null ? null : this.#controller.subscribe((event) => { + if (event?.operation !== "expand" && event?.operation !== "collapse") return; + this.#onExpansionChange?.({ path: event.path, expanded: event.operation === "expand" }); + }); } unmount() { if (this.#wrapper != null) { @@ -132,6 +140,8 @@ var FileTree = class { this.unmount(); this.#selectionSubscription?.(); this.#selectionSubscription = null; + this.#expansionSubscription?.(); + this.#expansionSubscription = null; this.#controller.destroy(); } getFileTreeContainer() { @@ -296,6 +306,7 @@ var FileTree = class { itemHeight: this.#viewOptions.itemHeight }), itemHeight: this.#viewOptions.itemHeight, + middleTruncate: this.#viewOptions.middleTruncate, overscan: this.#viewOptions.overscan, stickyFolders: this.#viewOptions.stickyFolders }; diff --git a/dist/render/FileTreeView.js b/dist/render/FileTreeView.js index 7d3b818c43a7621ab9170da32b180232ed3c9472..222e49fe7130452d028f4204fde57b0398ce2d34 100644 --- a/dist/render/FileTreeView.js +++ b/dist/render/FileTreeView.js @@ -360,7 +360,7 @@ function focusFirstMenuElement(menuElement) { "[tabindex]:not([tabindex=\"-1\"])" ].join(", ")) ?? menuElement); } -function renderFileTreeRowContent(row, resolveIcon, { actionLaneEnabled = false, customDecoration = null, decorationLaneEnabled = false, dragTargetFlattenedSegmentPath = null, gitDecoration = null, gitLaneActive = false, renameInput = null, showDecorativeActionAffordance = false } = {}) { +function renderFileTreeRowContent(row, resolveIcon, { actionLaneEnabled = false, customDecoration = null, decorationLaneEnabled = false, dragTargetFlattenedSegmentPath = null, gitDecoration = null, gitLaneActive = false, middleTruncate = true, renameInput = null, showDecorativeActionAffordance = false } = {}) { const targetPath = getFileTreeRowPath(row); return /* @__PURE__ */ jsxs(Fragment, { children: [ row.depth > 0 ? /* @__PURE__ */ jsx("div", { @@ -376,11 +376,12 @@ function renderFileTreeRowContent(row, resolveIcon, { actionLaneEnabled = false, }), /* @__PURE__ */ jsx("div", { "data-item-section": "content", - children: row.isFlattened ? formatFlattenedSegments(row, renameInput, dragTargetFlattenedSegmentPath) : renameInput ?? /* @__PURE__ */ jsx(MiddleTruncate, { + title: middleTruncate ? void 0 : row.name, + children: row.isFlattened ? formatFlattenedSegments(row, renameInput, dragTargetFlattenedSegmentPath) : renameInput ?? (middleTruncate ? /* @__PURE__ */ jsx(MiddleTruncate, { minimumLength: 5, split: "extension", children: row.name - }) + }) : row.name) }), decorationLaneEnabled ? /* @__PURE__ */ jsx("div", { "data-item-section": "decoration", @@ -401,7 +402,7 @@ function renderFileTreeRowContent(row, resolveIcon, { actionLaneEnabled = false, ] }); } function renderStyledRow(frame, row, key, options = {}) { - const { controller, renameView, visualFocusPath, contextHoverPath, draggedPathSet, dragTarget, dragAndDropEnabled, shouldSuppressContextMenu, handleRowDragStart, handleRowDragEnd, handleRowTouchStart, instanceId, itemHeight, gitStatusByPath, ignoredGitDirectories, ignoredInheritanceCache, directoriesWithGitChanges, gitLaneActive, contextMenuEnabled, contextMenuTriggerMode, contextMenuButtonTriggerEnabled, contextMenuButtonVisibility, contextMenuRightClickEnabled, registerRenameInput, registerButton, resolveIcon, renderDecorationForRow, openContextMenuForRow, onRowClick, onKeyDown } = frame; + const { controller, renameView, visualFocusPath, contextHoverPath, draggedPathSet, dragTarget, dragAndDropEnabled, shouldSuppressContextMenu, handleRowDragStart, handleRowDragEnd, handleRowTouchStart, instanceId, itemHeight, middleTruncate, gitStatusByPath, ignoredGitDirectories, ignoredInheritanceCache, directoriesWithGitChanges, gitLaneActive, contextMenuEnabled, contextMenuTriggerMode, contextMenuButtonTriggerEnabled, contextMenuButtonVisibility, contextMenuRightClickEnabled, registerRenameInput, registerButton, resolveIcon, renderDecorationForRow, openContextMenuForRow, onRowClick, onKeyDown } = frame; const targetPath = getFileTreeRowPath(row); const { isParked = false, mode = "flow", style } = options; const isSticky = mode === "sticky"; @@ -433,6 +434,7 @@ function renderStyledRow(frame, row, key, options = {}) { dragTargetFlattenedSegmentPath: dragTarget?.flattenedSegmentPath ?? null, gitDecoration, gitLaneActive, + middleTruncate, renameInput, showDecorativeActionAffordance }); @@ -517,7 +519,7 @@ function renderRangeChildren(frame, range, hiddenRowPaths) { if (range.end < range.start) return []; return frame.controller.getVisibleRows(range.start, range.end).filter((row) => !hiddenRowPaths.has(getFileTreeRowPath(row))).map((row, slotIndex) => renderStyledRow(frame, row, range.start + slotIndex)); } -function FileTreeView({ composition, controller, gitStatusByPath, ignoredGitDirectories, directoriesWithGitChanges, icons, instanceId, itemHeight = FILE_TREE_DEFAULT_ITEM_HEIGHT, overscan = 10, renamingEnabled = false, renderRowDecoration, searchBlurBehavior = "close", searchEnabled = false, searchFakeFocus = false, slotHost, stickyFolders = false, initialViewportHeight = 420 }) { +function FileTreeView({ composition, controller, gitStatusByPath, ignoredGitDirectories, directoriesWithGitChanges, icons, instanceId, itemHeight = FILE_TREE_DEFAULT_ITEM_HEIGHT, middleTruncate = true, overscan = 10, renamingEnabled = false, renderRowDecoration, searchBlurBehavior = "close", searchEnabled = false, searchFakeFocus = false, slotHost, stickyFolders = false, initialViewportHeight = 420 }) { "use no memo"; const contextMenuAnchorRef = useRef(null); const contextMenuTriggerRef = useRef(null); @@ -653,7 +655,7 @@ function FileTreeView({ composition, controller, gitStatusByPath, ignoredGitDire const getTriggerAnchorButton = useCallback((path) => { return getContextMenuAnchorButton(path, stickyRowButtonRefs.current, rowButtonRefs.current); }, []); - const gitLaneActive = gitStatusByPath != null || ignoredGitDirectories != null || directoriesWithGitChanges != null; + const gitLaneActive = true; const { resolveIcon } = useMemo(() => createFileTreeIconResolver(icons), [icons]); const renameView = controller[FILE_TREE_RENAME_VIEW](); const renamingPath = renameView.getPath(); @@ -1272,7 +1274,7 @@ function FileTreeView({ composition, controller, gitStatusByPath, ignoredGitDire const update = () => { const nextItemCount = controller.getVisibleCount(); const nextViewportHeight = getCachedViewportHeight(measuredViewportHeightRef.current, initialViewportHeight); - const maxScrollTop = Math.max(0, nextItemCount * itemHeight - nextViewportHeight); + const maxScrollTop = Math.max(0, nextItemCount * itemHeight - nextViewportHeight, scrollElement.scrollHeight - scrollElement.clientHeight); if (scrollElement.scrollTop > maxScrollTop) scrollElement.scrollTop = maxScrollTop; setLayoutState(computeFileTreeViewLayoutState({ controller, @@ -1752,6 +1754,7 @@ function FileTreeView({ composition, controller, gitStatusByPath, ignoredGitDire ignoredInheritanceCache, instanceId, itemHeight, + middleTruncate, onKeyDown: handleTreeKeyDown, onRowClick: handleRowClick, openContextMenuForRow,