PageSourceSearch

https://filament.com/assets/contents/file_tree_component/file_tree_component_controller-ed159eff.js

js filament.com collected 2026-09-25 14:01:25 UTC 4,227 bytes, 107 lines download raw bytes

1import { FileTree } from "pierre-trees"
2import { Turbo } from "@hotwired/turbo-rails"
3import TurboMorphEnabledController from "controllers/turbo_morph_enabled_controller"
4
5// Mirrors Ruby's String#parameterize for ASCII paths.
6function pathToAnchor(path) {
7  return path.toLowerCase().replace(/[^a-z0-9_]+/g, "-").replace(/^-+|-+$/g, "")
8}
9
10export default class extends TurboMorphEnabledController {
11  static values = { files: Array }
12
13  connect() {
14    super.connect()
15
16    const files = this.filesValue
17    const paths = files.map(f => f.path)
18    const gitStatus = files.filter(f => f.status).map(({ path, status }) => ({ path, status }))
19    // A stacked page anchors every diff, so the tree scrolls. The standalone
20    // single-diff page gives each file a page of its own and sends the urls
21    // with them, so there the tree navigates.
22    this.urls = new Map(files.filter(f => f.url).map(({ path, url }) => [path, url]))
23
24    this.fileTree = new FileTree({
25      paths,
26      initialSelectedPaths: this.selectedPaths(),
27      gitStatus,
28      icons: { set: "minimal", colored: false },
29      search: true,
30      fileTreeSearchMode: "hide-non-matches",
31      initialExpansion: "open",
32      onSelectionChange: this.handleSelectionChange.bind(this),
33      // remove permanent padding for scrollbar, don't show filenames in git
34      // status color, and suppress the keyboard-focus ring on the first row
35      // while the search input has real focus.
36      unsafeCSS: `
37        [data-file-tree-virtualized-scroll="true"] { scrollbar-gutter: auto; }
38        [data-file-tree-search-input] { padding-inline: 8px; }
39        [data-file-tree-search-input]:focus-visible { outline-offset: -2px; }
40        [data-item-git-status] > [data-item-section="content"] { color: var(--trees-fg); }
41      `,
42    })
43    this.fileTree.render({ fileTreeContainer: this.element })
44
45    this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this)
46    document.addEventListener("keydown", this.handleGlobalKeydown)
47  }
48
49  disconnect() {
50    super.disconnect()
51    document.removeEventListener("keydown", this.handleGlobalKeydown)
52    this.fileTree?.unmount()
53    this.fileTree?.cleanUp()
54    this.fileTree = null
55  }
56
57  handleGlobalKeydown(event) {
58    if (event.key !== "t" || event.metaKey || event.ctrlKey || event.altKey) return
59
60    // Keys that originate inside a shadow-DOM input (like Pierre's search)
61    // retarget to the host element, so event.target hides the input. Walk
62    // the composed path to see the true originating element.
63    const origin = event.composedPath()[0]
64    if (!origin) return
65    if (origin.tagName === "INPUT" || origin.tagName === "TEXTAREA" || origin.isContentEditable) return
66
67    // Focus the input directly rather than calling fileTree.openSearch(),
68    // which also sets data-item-focused on the first row and paints a
69    // keyboard-nav ring before the user has typed anything.
70    const input = this.element.shadowRoot?.querySelector("[data-file-tree-search-input]")
71    if (!input) return
72
73    event.preventDefault()
74    input.focus()
75  }
76
77  // The row for the file this page is showing, so the tree opens with it
78  // highlighted rather than with nothing selected. Empty on a stacked page,
79  // which shows every file at once and so has no single current one.
80  selectedPaths() {
81    const current = decodeURIComponent(window.location.pathname)
82    for (const [path, url] of this.urls) {
83      if (decodeURIComponent(url) === current) return [path]
84    }
85    return []
86  }
87
88  handleSelectionChange(selectedPaths) {
89    const [path] = selectedPaths
90    if (!path) return
91
92    const url = this.urls.get(path)
93    if (url) return Turbo.visit(url)
94
95    const anchor = pathToAnchor(path)
96    const target = document.getElementById(anchor)
97    if (target) {
98      target.scrollIntoView({ behavior: "smooth", block: "start" })
99      // Pass history.state through rather than null: dropping Turbo's restoration state makes
100      // popstate take the historyPoppedWithEmptyState path, which skips cancelling in-flight
101      // visits and caches the current DOM under this URL.
102      history.replaceState(history.state, "", `#${anchor}`)
103    } else {
104      window.location.hash = anchor
105    }
106  }
107}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.