PageSourceSearch

https://holofin.ai/static/js/common.34af32fd574d.js

js holofin.ai collected 2026-10-05 14:11:57 UTC 14,548 bytes, 360 lines download raw bytes

1/* global posthog, Alpine, CustomEvent, htmx, localStorage, requestAnimationFrame */
2
3/**
4 * PostHog event tracking
5 * Ensure we capture page views and page leaves for all pages
6 */
7document.addEventListener('DOMContentLoaded', function () {
8  htmx.config.historyCacheSize = 0
9  // Check if PostHog is available and initialized
10  if (typeof posthog !== 'undefined' && posthog.get_distinct_id) {
11    // Capture initial page view
12    posthog.capture('$pageview', {
13      $current_url: window.location.href,
14      path: window.location.pathname,
15      referrer: document.referrer,
16      load_type: 'initial'
17    })
18
19    // Set up page leave event
20    window.addEventListener('beforeunload', function () {
21      posthog.capture('$pageleave', {
22        $current_url: window.location.href,
23        path: window.location.pathname
24      })
25    })
26  }
27})
28
29// Handle HTMX navigation to prevent sidebar flicker
30document.body.addEventListener('htmx:beforeSwap', function (event) {
31  // Only for boosted FULL-PAGE navigation (target = <body>). Boosted links that
32  // swap a scoped region (e.g. the docs nav targeting #docs-content) leave the
33  // rest of the page untouched, so none of this teardown work should run.
34  if (event.detail.boosted && event.detail.target === document.body) {
35    const leftNav = document.getElementById('left-nav')
36    if (leftNav) {
37      // Temporarily disable transitions to prevent animation during swap
38      leftNav.style.transition = 'none'
39    }
40
41    // Hide cookie icon during navigation to prevent it from appearing during transitions
42    const cookieIcon = document.getElementById('silktide-cookie-icon')
43    if (cookieIcon) {
44      cookieIcon.classList.remove('show-cookie-icon')
45    }
46
47    // Prevent scrollbar flash during swap by hiding overflow
48    document.documentElement.style.overflow = 'hidden'
49
50    // Clean up Alpine teleported elements (they live in <body> and survive hx-boost swaps)
51    document.querySelectorAll('[x-anchor]').forEach(el => el.remove())
52  }
53})
54
55// For HTMX-based navigation
56document.body.addEventListener('htmx:afterSettle', function (event) {
57  if (event.detail.target.id === 'modal') {
58    const _event = new CustomEvent('show-modal-event', { })
59    window.dispatchEvent(_event)
60  }
61
62  // Handle partial swaps (hx-select pattern) for agent content areas
63  // These don't trigger boosted navigation, so we need to manually init Alpine
64  // Check if the request used hx-select targeting one of our content areas
65  const requestConfig = event.detail.requestConfig
66  const hxSelect = requestConfig?.elt?.getAttribute('hx-select')
67  const hxTarget = requestConfig?.elt?.getAttribute('hx-target')
68  const contentTargets = ['#segmenter-content', '#classifier-content', '#extractor-content']
69
70  // Check both hx-select and hx-target to catch content swaps
71  const targetSelector = hxSelect || hxTarget
72  if (targetSelector && contentTargets.includes(targetSelector) && !event.detail.boosted) {
73    const contentEl = document.querySelector(targetSelector)
74    if (contentEl && typeof Alpine !== 'undefined') {
75      // Re-trigger alpine:init event to ensure all components are re-registered
76      document.dispatchEvent(new CustomEvent('alpine:init'))
77      // Initialize Alpine only on the swapped content
78      Alpine.initTree(contentEl)
79      // Re-process HTMX attributes on the new content
80      if (typeof htmx !== 'undefined') {
81        htmx.process(contentEl)
82      }
83    }
84  }
85
86  // Ensure sidebar state is synced with data attribute during navigation
87  if (event.detail.boosted) {
88    const sidebarFolded = localStorage.getItem('sidebarIsFolded') === 'true'
89    document.documentElement.setAttribute('data-sidebar-folded', sidebarFolded)
90  }
91
92  // Capture page view after HTMX content is loaded for PostHog analytics
93  if (typeof posthog !== 'undefined' && posthog.get_distinct_id &&
94      event.detail.pathInfo && event.detail.pathInfo.path) {
95    // Use a small timeout to ensure the page has fully rendered for analytics
96    setTimeout(() => {
97      posthog.capture('$pageview', {
98        $current_url: window.location.href,
99        path: event.detail.pathInfo.path,
100        load_type: 'htmx'
101      })
102    }, 100) // Timeout ensures analytics capture happens after rendering
103  }
104
105  // Handle boosted FULL-PAGE changes (target = <body>) - Alpine auto-initializes
106  // new content, so we only need to handle sidebar state and transitions.
107  // Scoped boosted swaps (docs nav → #docs-content) skip this: Alpine's
108  // MutationObserver picks up the swapped subtree and htmx processes it natively.
109  if (event.detail.boosted && event.detail.target === document.body) {
110    if (typeof Alpine !== 'undefined') {
111      // Store and restore sidebar state
112      const currentSidebarState = Alpine.$data(document.documentElement)
113      const preservedIsFolded = currentSidebarState?.isFolded
114
115      // Re-trigger alpine:init to ensure components are registered for new content
116      // Alpine will auto-init the new DOM, but we need components to be registered first
117      document.dispatchEvent(new CustomEvent('alpine:init'))
118
119      // Wait for Alpine's auto-initialization to complete
120      setTimeout(() => {
121        // Re-process HTMX attributes after Alpine reinit
122        // This ensures hx-* attributes on new DOM elements are recognized
123        if (typeof htmx !== 'undefined') {
124          htmx.process(document.body)
125        }
126
127        // After re-initialization, ensure the sidebar state is immediately restored
128        // This prevents the visual flicker by setting the state before the next frame
129        if (preservedIsFolded !== undefined) {
130          const rootData = Alpine.$data(document.documentElement)
131          if (rootData && 'isFolded' in rootData) {
132            rootData.isFolded = preservedIsFolded
133          }
134        }
135
136        // Re-enable transitions after Alpine is re-initialized
137        const leftNav = document.getElementById('left-nav')
138        if (leftNav) {
139          // Use requestAnimationFrame to ensure the DOM has settled
140          requestAnimationFrame(() => {
141            // Remove opacity-0 after Alpine init to prevent layout flash during hydration
142            document.body.classList.remove('opacity-0')
143            // Restore overflow after layout has settled
144            document.documentElement.style.overflow = ''
145
146            leftNav.style.transition = ''
147
148            // Show cookie icon again after navigation is complete
149            const cookieIcon = document.getElementById('silktide-cookie-icon')
150            if (cookieIcon) {
151              // Only show if the user has already made a cookie choice
152              const hasInitialChoice = localStorage.getItem('silktideCookieBanner_InitialChoice') !== null
153              if (hasInitialChoice) {
154                cookieIcon.classList.add('show-cookie-icon')
155              }
156            }
157          })
158        } else {
159          // No left nav, still need to reveal body
160          requestAnimationFrame(() => {
161            document.body.classList.remove('opacity-0')
162            document.documentElement.style.overflow = ''
163          })
164        }
165      }, 0)
166    } else {
167      console.error('Alpine is not defined. Cannot re-initialize after htmx:afterSettle.')
168      // Still reveal body even if Alpine is not available
169      document.body.classList.remove('opacity-0')
170      document.documentElement.style.overflow = ''
171    }
172  }
173})
174
175/**
176 * Modal helper.
177 * Ensure we show backdrop and loading before modal content is loaded.
178 */
179document.body.addEventListener('htmx:beforeRequest', function (event) {
180  // Don't show loading bar for boosted elements
181  if (event.detail.boosted) {
182    return
183  }
184
185  // Check if loading bar exists first
186  const loadingBar = document.getElementById('loadingBar')
187  if (loadingBar) {
188    loadingBar.classList.remove('hidden')
189    loadingBar.classList.add('block')
190  }
191})
192
193/**
194 * Catch and display 500 with HTMx
195 */
196document.body.addEventListener('htmx:responseError', function (event) {
197  if (event.detail.xhr.status === 500) {
198    const body = document.querySelector('body')
199    body.innerHTML = event.detail.xhr.responseText
200  }
201  // if 503 close modals and reload page
202  if (event.detail.xhr.status === 503) {
203    console.info('HTMx 503, reloading page')
204    window.location.reload()
205  }
206})
207
208document.body.addEventListener('htmx:afterRequest', function (event) {
209  // Only hide loading bar if it wasn't a boosted request
210  if (!event.detail.boosted) {
211    const loadingBar = document.getElementById('loadingBar')
212    if (loadingBar) {
213      loadingBar.classList.add('hidden')
214      loadingBar.classList.remove('show')
215    }
216  }
217})
218
219/**
220 * Reveal body after a short delay to prevent FOUC with Alpine.
221 *
222 * base.html no longer ships `opacity-0` on <body> — hiding every page until the
223 * whole script waterfall resolved cost a visible white flash on first paint.
224 * This stays as an opt-in: a page that genuinely needs a reveal adds `opacity-0`
225 * via {% block body_class %} and gets uncloaked here. Prefer x-cloak on the
226 * specific Alpine elements instead.
227 */
228document.addEventListener('DOMContentLoaded', () => {
229  // Wait briefly for Alpine to initialize and apply x-cloak/localStorage states
230  setTimeout(() => {
231    document.body.classList.remove('opacity-0')
232
233    // Show cookie icon after page is fully loaded to prevent it from appearing during navigation
234    const cookieIcon = document.getElementById('silktide-cookie-icon')
235    if (cookieIcon && window.silktideCookieBannerManager) {
236      // Only show if the user has already made a cookie choice and the icon should be visible
237      const hasInitialChoice = localStorage.getItem('silktideCookieBanner_InitialChoice') !== null
238      if (hasInitialChoice) {
239        cookieIcon.classList.add('show-cookie-icon')
240      }
241    }
242  }, 50) // 50ms delay, adjust if needed
243})
244
245// Ensure holofin namespace exists (without overwriting existing properties)
246window.holofin = window.holofin || {}
247
248// Track the last-seen path so the popstate handler can tell real page
249// traversal apart from same-document fragment navigation: clicking a #anchor
250// link fires popstate in Chrome/Firefox, and the reload below would turn every
251// in-page table-of-contents click into a full white-flash page reload.
252let lastNavigationPath = window.location.pathname + window.location.search
253
254// htmx pushes URLs without firing popstate — keep the snapshot current so the
255// next popstate compares against where htmx actually left us.
256document.body.addEventListener('htmx:pushedIntoHistory', function () {
257  lastNavigationPath = window.location.pathname + window.location.search
258})
259
260// Listen for popstate event (browser back/forward button)
261window.addEventListener('popstate', function (event) {
262  const navigationPath = window.location.pathname + window.location.search
263  const isFragmentNavigation = navigationPath === lastNavigationPath
264  lastNavigationPath = navigationPath
265  if (isFragmentNavigation) {
266    // Only the #hash changed: the browser scrolls natively and all JS state
267    // (Alpine, Vue) is still valid — resetting or reloading here is what used
268    // to cause the blink on anchor links.
269    return
270  }
271
272  // Check if we're on extraction or document pages
273  const currentPath = window.location.pathname
274  const isExtractionPage = currentPath.includes('/extractor/')
275  const isDocumentPage = currentPath.includes('/documents/')
276
277  if (isExtractionPage || isDocumentPage) {
278    // For these pages, hide content immediately to prevent visual glitches
279    const extractionApp = document.getElementById('document-extraction-app')
280    const documentViewerApp = document.getElementById('document-viewer-app-container')
281
282    if (extractionApp) {
283      extractionApp.style.display = 'none'
284    }
285    if (documentViewerApp) {
286      documentViewerApp.style.display = 'none'
287    }
288
289    // Clean up Vue/Pinia apps if they exist
290    if (window.holofin?.extractionApp?.destroy) {
291      try {
292        window.holofin.extractionApp.destroy()
293      } catch (e) {
294        console.warn('Error destroying extraction app:', e)
295      }
296    }
297
298    // Allow a small delay for cleanup then reload
299    setTimeout(() => {
300      window.location.reload()
301    }, 10)
302    return
303  }
304
305  // For any page with Alpine components, ensure proper cleanup and reinitialization
306  // This provides the "dumbest Alpine context reset" to avoid stale state issues
307  if (typeof Alpine !== 'undefined') {
308    // Hide content containers immediately to prevent flash of stale content
309    const segmenterContent = document.getElementById('segmenter-content')
310    const classifierContent = document.getElementById('classifier-content')
311    const extractorContent = document.getElementById('extractor-content')
312
313    if (segmenterContent) segmenterContent.style.visibility = 'hidden'
314    if (classifierContent) classifierContent.style.visibility = 'hidden'
315    if (extractorContent) extractorContent.style.visibility = 'hidden'
316
317    // Destroy the current Alpine tree to clean up all components
318    Alpine.destroyTree(document.body)
319
320    // Force a full page reload to ensure clean state
321    // This is the safest approach to avoid any stale Alpine state
322    window.location.reload()
323  }
324})
325
326// Additional safeguard for browser navigation
327window.addEventListener('pageshow', function (event) {
328  // Skip for extraction and document pages - they handle their own cleanup
329  const currentPath = window.location.pathname
330  const skipPaths = ['/extractor/', '/documents/']
331  const shouldSkip = skipPaths.some(path => currentPath.includes(path))
332
333  if (shouldSkip) {
334    return
335  }
336
337  // If the page is being shown from the back-forward cache (persisted)
338  if (event.persisted) {
339    // Force a reload to ensure clean Alpine state
340    window.location.reload()
341  }
342})
343
344// Handle tab visibility change (user returns to tab after being away)
345// This fixes sidebar state desync when browser freezes/unfreezes tabs
346document.addEventListener('visibilitychange', function () {
347  if (document.visibilityState === 'visible') {
348    // Sync Alpine sidebar state with localStorage when tab becomes visible
349    if (typeof Alpine !== 'undefined') {
350      const storedFolded = localStorage.getItem('sidebarIsFolded') === 'true'
351      const rootData = Alpine.$data(document.documentElement)
352
353      if (rootData && 'isFolded' in rootData && rootData.isFolded !== storedFolded) {
354        // Only update if there's a mismatch
355        rootData.isFolded = storedFolded
356        document.documentElement.setAttribute('data-sidebar-folded', storedFolded)
357      }
358    }
359  }
360})

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.