@@ -212,72 +212,125 @@ document.addEventListener('DOMContentLoaded', async () => {
212212 return ;
213213 }
214214
215- dataCollection . forEach ( file => {
216- const documentCard = document . createElement ( 'div' ) ;
217- documentCard . className = 'genre-card' ;
218- documentCard . style . flexDirection = 'column' ;
219- documentCard . style . padding = '25px 20px' ;
220- documentCard . style . gap = '10px' ;
221- documentCard . style . position = 'relative' ;
222-
223- let iconTypeMap = 'fa-file-shield' ;
224- if ( file . type . startsWith ( 'image/' ) ) iconTypeMap = 'fa-file-image' ;
225- else if ( file . type === 'application/pdf' || file . name . endsWith ( '.pdf' ) ) iconTypeMap = 'fa-file-pdf' ;
226- else if ( file . type . startsWith ( 'text/' ) ) iconTypeMap = 'fa-file-lines' ;
227-
228- let localizedObjectURL = "#" ;
229- if ( file . binaryData instanceof Blob ) {
230- localizedObjectURL = URL . createObjectURL ( file . binaryData ) ;
231- activeObjectUrlsPool . push ( localizedObjectURL ) ;
215+ let vaultJsRenderIndex = 0 ;
216+ const vaultJsChunkSize = window . CONFIG ?. CHUNK_SIZE || 20 ;
217+
218+ if ( vaultGrid . _vaultObserver ) {
219+ vaultGrid . _vaultObserver . disconnect ( ) ;
220+ }
221+
222+ const sentinel = document . createElement ( 'div' ) ;
223+ sentinel . style . width = '100%' ;
224+ sentinel . style . height = '20px' ;
225+ sentinel . style . gridColumn = '1 / -1' ;
226+
227+ vaultGrid . _vaultObserver = new IntersectionObserver ( ( entries ) => {
228+ if ( entries [ 0 ] . isIntersecting ) {
229+ renderChunk ( ) ;
232230 }
231+ } ) ;
233232
234- const computedSizeInMB = ( file . size / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
235- const safeName = DOMPurify . sanitize ( file . name ) ;
236- const safeGenre = DOMPurify . sanitize ( file . genre ) ;
237- const safeDescription = DOMPurify . sanitize ( file . description || 'No annotation summary log notes provided.' ) ;
238- const safeMetadataString = DOMPurify . sanitize ( `${ computedSizeInMB } MB • ${ file . uploadedAt } ` ) ;
239-
240- const badgeClassMap = file . privacy === 'private' ? 'status-is-private' : 'status-is-public' ;
241- const iconClassMap = file . privacy === 'private' ? 'fa-user-lock' : 'fa-share-nodes' ;
242-
243- documentCard . innerHTML = `
244- <button class="purge-item-trigger" data-row-id="${ file . id } " title="Delete document registry entry"
245- style="position: absolute; top: 12px; right: 12px; background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 1.1rem; opacity: 0.6; transition: opacity 0.2s;">
246- <i class="fa-solid fa-trash-can"></i>
247- </button>
248-
249- <i class="fa-solid ${ iconTypeMap } " style="font-size: 2.6rem; color: var(--text-main); margin-bottom: 3px;"></i>
250-
251- <span style="font-weight: 600; text-align: center; word-break: break-word; font-family: 'Georgia', serif; font-size: 1.05rem;">${ safeName } </span>
252-
253- <span style="font-size: 0.78rem; font-weight: 500; font-style: italic; opacity: 0.85; background: rgba(255,255,255,0.04); padding: 3px 10px; border-radius: 12px; display: inline-block;">
254- Classification: ${ safeGenre }
255- </span>
233+ function renderChunk ( ) {
234+ const chunk = dataCollection . slice ( vaultJsRenderIndex , vaultJsRenderIndex + vaultJsChunkSize ) ;
235+ if ( chunk . length === 0 ) return ;
256236
257- <p style="font-size: 0.85rem; opacity: 0.7; text-align: center; margin: 4px 0; max-height: 50px; overflow-y: auto; width: 100%; word-break: break-word; line-height: 1.35; padding: 0 4px;">
258- ${ safeDescription }
259- </p>
237+ if ( sentinel . parentNode ) {
238+ vaultGrid . removeChild ( sentinel ) ;
239+ }
240+
241+ chunk . forEach ( file => {
242+ const documentCard = document . createElement ( 'div' ) ;
243+ documentCard . className = 'genre-card' ;
244+ documentCard . style . flexDirection = 'column' ;
245+ documentCard . style . padding = '25px 20px' ;
246+ documentCard . style . gap = '10px' ;
247+ documentCard . style . position = 'relative' ;
248+
249+ let iconTypeMap = 'fa-file-shield' ;
250+ if ( file . type . startsWith ( 'image/' ) ) iconTypeMap = 'fa-file-image' ;
251+ else if ( file . type === 'application/pdf' || file . name . endsWith ( '.pdf' ) ) iconTypeMap = 'fa-file-pdf' ;
252+ else if ( file . type . startsWith ( 'text/' ) ) iconTypeMap = 'fa-file-lines' ;
253+
254+ let localizedObjectURL = "#" ;
255+ if ( file . binaryData instanceof Blob ) {
256+ localizedObjectURL = URL . createObjectURL ( file . binaryData ) ;
257+ activeObjectUrlsPool . push ( localizedObjectURL ) ;
258+ }
260259
261- <span style="font-size: 0.72rem; opacity: 0.45; text-align: center; letter-spacing: 0.3px;">${ safeMetadataString } </span>
260+ const computedSizeInMB = ( file . size / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
261+ const safeName = DOMPurify . sanitize ( file . name ) ;
262+ const safeGenre = DOMPurify . sanitize ( file . genre ) ;
263+ const safeDescription = DOMPurify . sanitize ( file . description || 'No annotation summary log notes provided.' ) ;
264+ const safeMetadataString = DOMPurify . sanitize ( `${ computedSizeInMB } MB • ${ file . uploadedAt } ` ) ;
262265
263- <div>
264- <span class="visibility-status-badge ${ badgeClassMap } "><i class="fa-solid ${ iconClassMap } "></i> Settings: ${ file . privacy } </span>
265- </div>
266+ const badgeClassMap = file . privacy === 'private' ? 'status-is-private' : 'status-is-public' ;
267+ const iconClassMap = file . privacy === 'private' ? 'fa-user-lock' : 'fa-share-nodes' ;
268+
269+ documentCard . innerHTML = `
270+ <button class="purge-item-trigger" data-row-id="${ file . id } " title="Delete document registry entry"
271+ style="position: absolute; top: 12px; right: 12px; background: none; border: none; color: #e74c3c; cursor: pointer; font-size: 1.1rem; opacity: 0.6; transition: opacity 0.2s;">
272+ <i class="fa-solid fa-trash-can"></i>
273+ </button>
274+
275+ <i class="fa-solid ${ iconTypeMap } " style="font-size: 2.6rem; color: var(--text-main); margin-bottom: 3px;"></i>
276+
277+ <span style="font-weight: 600; text-align: center; word-break: break-word; font-family: 'Georgia', serif; font-size: 1.05rem;">${ safeName } </span>
278+
279+ <span style="font-size: 0.78rem; font-weight: 500; font-style: italic; opacity: 0.85; background: rgba(255,255,255,0.04); padding: 3px 10px; border-radius: 12px; display: inline-block;">
280+ Classification: ${ safeGenre }
281+ </span>
282+
283+ <p style="font-size: 0.85rem; opacity: 0.7; text-align: center; margin: 4px 0; max-height: 50px; overflow-y: auto; width: 100%; word-break: break-word; line-height: 1.35; padding: 0 4px;">
284+ ${ safeDescription }
285+ </p>
286+
287+ <span style="font-size: 0.72rem; opacity: 0.45; text-align: center; letter-spacing: 0.3px;">${ safeMetadataString } </span>
288+
289+ <div>
290+ <span class="visibility-status-badge ${ badgeClassMap } "><i class="fa-solid ${ iconClassMap } "></i> Settings: ${ file . privacy } </span>
291+ </div>
266292
267- <div style="display: flex; gap: 10px; width: 100%; margin-top: 10px;">
268- <a href="${ localizedObjectURL } " target="_blank" class="btn-preview"
269- style="flex: 1; text-align: center; font-size: 12px; padding: 8px 0; text-decoration: none; border-radius: 6px; display: inline-block; background: var(--border-focus, #9b59b6); color: white; font-weight: 500;">
270- <i class="fa-solid fa-up-right-from-square"></i> Open
271- </a>
272- <a href="${ localizedObjectURL } " download="${ safeName } " class="btn-secondary"
273- style="flex: 1; text-align: center; font-size: 12px; padding: 8px 0; text-decoration: none; border-radius: 6px; border: 1px solid currentColor; display: inline-block;">
274- <i class="fa-solid fa-download"></i> Extract
275- </a>
276- </div>
277- ` ;
293+ <div style="display: flex; gap: 10px; width: 100%; margin-top: 10px;">
294+ <a href="${ localizedObjectURL } " target="_blank" class="btn-preview"
295+ style="flex: 1; text-align: center; font-size: 12px; padding: 8px 0; text-decoration: none; border-radius: 6px; display: inline-block; background: var(--border-focus, #9b59b6); color: white; font-weight: 500;">
296+ <i class="fa-solid fa-up-right-from-square"></i> Open
297+ </a>
298+ <a href="${ localizedObjectURL } " download="${ safeName } " class="btn-secondary"
299+ style="flex: 1; text-align: center; font-size: 12px; padding: 8px 0; text-decoration: none; border-radius: 6px; border: 1px solid currentColor; display: inline-block;">
300+ <i class="fa-solid fa-download"></i> Extract
301+ </a>
302+ </div>
303+ ` ;
304+
305+ const dropBtn = documentCard . querySelector ( '.purge-item-trigger' ) ;
306+ if ( dropBtn ) {
307+ dropBtn . addEventListener ( 'mouseover' , ( ) => dropBtn . style . opacity = '1' ) ;
308+ dropBtn . addEventListener ( 'mouseout' , ( ) => dropBtn . style . opacity = '0.6' ) ;
309+ dropBtn . addEventListener ( 'click' , async ( event ) => {
310+ if ( confirm ( "Confirm Deletion Action: This file tracking record will be cleared from your local storage system cache completely. Proceed?" ) ) {
311+ try {
312+ await vaultDb . securedFiles . delete ( file . id ) ;
313+ displayVaultFiles ( document . getElementById ( 'vault-search-query' ) ? document . getElementById ( 'vault-search-query' ) . value : '' ) ;
314+ } catch ( err ) {
315+ console . error ( "Dexie processing dropped during record destruction processing phase:" , err ) ;
316+ }
317+ }
318+ } ) ;
319+ }
278320
279- vaultGrid . appendChild ( documentCard ) ;
280- } ) ;
321+ vaultGrid . appendChild ( documentCard ) ;
322+ } ) ;
323+
324+ vaultJsRenderIndex += vaultJsChunkSize ;
325+ if ( vaultJsRenderIndex < dataCollection . length ) {
326+ vaultGrid . appendChild ( sentinel ) ;
327+ vaultGrid . _vaultObserver . observe ( sentinel ) ;
328+ } else if ( vaultGrid . _vaultObserver ) {
329+ vaultGrid . _vaultObserver . disconnect ( ) ;
330+ }
331+ }
332+
333+ renderChunk ( ) ;
281334
282335 initializeDestructionTriggers ( ) ;
283336
@@ -346,25 +399,7 @@ document.addEventListener('DOMContentLoaded', async () => {
346399 }
347400 }
348401 function initializeDestructionTriggers ( ) {
349- const dropButtons = document . querySelectorAll ( '.purge-item-trigger' ) ;
350- dropButtons . forEach ( button => {
351- button . addEventListener ( 'mouseover' , ( ) => button . style . opacity = '1' ) ;
352- button . addEventListener ( 'mouseout' , ( ) => button . style . opacity = '0.6' ) ;
353-
354- button . addEventListener ( 'click' , async ( event ) => {
355- const elementTarget = event . target . closest ( '.purge-item-trigger' ) ;
356- const uniqueRowID = parseInt ( elementTarget . getAttribute ( 'data-row-id' ) , 10 ) ;
357-
358- if ( confirm ( "Confirm Deletion Action: This file tracking record will be cleared from your local storage system cache completely. Proceed?" ) ) {
359- try {
360- await vaultDb . securedFiles . delete ( uniqueRowID ) ;
361- displayVaultFiles ( searchInput ? searchInput . value : '' ) ;
362- } catch ( err ) {
363- console . error ( "Dexie processing dropped during record destruction processing phase:" , err ) ;
364- }
365- }
366- } ) ;
367- } ) ;
402+ // Obsolete: event listeners are now attached per-element in displayVaultFiles
368403 }
369404
370405 if ( searchInput ) {
0 commit comments