2025-08-30 15:44:49 +03:00
import { allViewTypes , ViewModeProps , ViewTypeOptions } from "./interface" ;
2025-08-30 15:11:49 +03:00
import { useNoteContext , useNoteLabel , useTriliumEvent } from "../react/hooks" ;
import FNote from "../../entities/fnote" ;
import "./NoteList.css" ;
2025-08-30 19:21:26 +03:00
import { ListView , GridView } from "./legacy/ListOrGridView" ;
2025-09-03 23:57:38 +03:00
import { useEffect , useMemo , useRef , useState } from "preact/hooks" ;
2025-09-03 23:17:35 +03:00
import GeoView from "./geomap" ;
2025-09-03 23:57:38 +03:00
import ViewModeStorage from "../view_widgets/view_mode_storage" ;
2025-09-05 16:02:35 +03:00
import CalendarView from "./calendar" ;
2025-09-06 18:48:58 +03:00
import TableView from "./table" ;
2025-08-30 15:11:49 +03:00
2025-09-03 23:57:38 +03:00
interface NoteListProps < T extends object > {
2025-08-30 18:48:34 +03:00
note? : FNote | null ;
2025-08-30 19:50:20 +03:00
/** if set to `true` then only collection-type views are displayed such as geo-map and the calendar. The original book types grid and list will be ignored. */
2025-08-30 14:29:54 +03:00
displayOnlyCollections? : boolean ;
2025-08-30 18:48:34 +03:00
highlightedTokens? : string [ ] | null ;
2025-09-03 23:57:38 +03:00
viewStorage : ViewModeStorage < T > ;
2025-08-30 14:29:54 +03:00
}
2025-09-03 23:57:38 +03:00
export default function NoteList < T extends object > ( { note : providedNote , highlightedTokens , displayOnlyCollections } : NoteListProps < T > ) {
2025-08-30 19:42:16 +03:00
const widgetRef = useRef < HTMLDivElement > ( null ) ;
2025-09-07 20:38:16 +03:00
const { note : contextNote , noteContext , notePath } = useNoteContext ( ) ;
2025-08-30 18:48:34 +03:00
const note = providedNote ? ? contextNote ;
2025-08-30 15:11:49 +03:00
const viewType = useNoteViewType ( note ) ;
const noteIds = useNoteIds ( note , viewType ) ;
2025-08-30 19:24:32 +03:00
const isFullHeight = ( viewType !== "list" && viewType !== "grid" ) ;
2025-08-30 19:42:16 +03:00
const [ isIntersecting , setIsIntersecting ] = useState ( false ) ;
2025-08-30 19:50:20 +03:00
const shouldRender = ( isFullHeight || isIntersecting || note ? . type === "book" ) ;
2025-08-30 19:48:05 +03:00
const isEnabled = ( note && noteContext ? . hasNoteList ( ) && ! ! viewType && shouldRender ) ;
2025-08-30 19:42:16 +03:00
useEffect ( ( ) = > {
2025-08-30 19:50:20 +03:00
if ( isFullHeight || displayOnlyCollections || note ? . type === "book" ) {
// Double role: no need to check if the note list is visible if the view is full-height or book, but also prevent legacy views if `displayOnlyCollections` is true.
2025-08-30 19:48:05 +03:00
return ;
}
2025-08-30 19:42:16 +03:00
const observer = new IntersectionObserver (
( entries ) = > {
if ( ! isIntersecting ) {
setIsIntersecting ( entries [ 0 ] . isIntersecting ) ;
}
observer . disconnect ( ) ;
} ,
{
rootMargin : "50px" ,
threshold : 0.1
}
) ;
// there seems to be a race condition on Firefox which triggers the observer only before the widget is visible
// (intersection is false). https://github.com/zadam/trilium/issues/4165
setTimeout ( ( ) = > widgetRef . current && observer . observe ( widgetRef . current ) , 10 ) ;
return ( ) = > observer . disconnect ( ) ;
} , [ ] ) ;
2025-08-30 15:11:49 +03:00
2025-09-04 15:13:48 +03:00
// Preload the configuration.
let props : ViewModeProps < any > | undefined | null = null ;
const viewModeConfig = useViewModeConfig ( note , viewType ) ;
2025-09-07 20:38:16 +03:00
if ( note && notePath && viewModeConfig ) {
2025-09-04 15:13:48 +03:00
props = {
2025-09-07 20:38:16 +03:00
note , noteIds , notePath ,
2025-09-04 15:13:48 +03:00
highlightedTokens ,
viewConfig : viewModeConfig [ 0 ] ,
saveConfig : viewModeConfig [ 1 ]
}
}
2025-09-03 23:57:38 +03:00
2025-08-30 15:11:49 +03:00
return (
2025-09-07 20:38:16 +03:00
< div ref = { widgetRef } className = { ` note-list-widget component ${ isFullHeight ? "full-height" : "" } ` } >
2025-09-04 15:13:48 +03:00
{ props && isEnabled && (
2025-08-30 15:11:49 +03:00
< div className = "note-list-widget-content" >
2025-09-04 15:13:48 +03:00
{ getComponentByViewType ( viewType , props ) }
2025-08-30 15:11:49 +03:00
< / div >
) }
< / div >
) ;
}
2025-09-04 15:13:48 +03:00
function getComponentByViewType ( viewType : ViewTypeOptions , props : ViewModeProps < any > ) {
2025-08-30 15:11:49 +03:00
switch ( viewType ) {
case "list" :
2025-08-30 15:44:49 +03:00
return < ListView { ...props } / > ;
2025-08-30 17:21:22 +03:00
case "grid" :
return < GridView { ...props } / > ;
2025-09-03 23:17:35 +03:00
case "geoMap" :
return < GeoView { ...props } / > ;
2025-09-05 16:02:35 +03:00
case "calendar" :
return < CalendarView { ...props } / >
2025-09-06 18:48:58 +03:00
case "table" :
return < TableView { ...props } / >
2025-08-30 15:11:49 +03:00
}
}
function useNoteViewType ( note? : FNote | null ) : ViewTypeOptions | undefined {
const [ viewType ] = useNoteLabel ( note , "viewType" ) ;
2025-08-30 17:21:22 +03:00
2025-08-30 15:11:49 +03:00
if ( ! note ) {
return undefined ;
} else if ( ! ( allViewTypes as readonly string [ ] ) . includes ( viewType || "" ) ) {
// when not explicitly set, decide based on the note type
return note . type === "search" ? "list" : "grid" ;
} else {
return viewType as ViewTypeOptions ;
}
}
function useNoteIds ( note : FNote | null | undefined , viewType : ViewTypeOptions | undefined ) {
const [ noteIds , setNoteIds ] = useState < string [ ] > ( [ ] ) ;
2025-08-30 17:21:22 +03:00
2025-08-30 15:11:49 +03:00
async function refreshNoteIds() {
if ( ! note ) {
setNoteIds ( [ ] ) ;
} else if ( viewType === "list" || viewType === "grid" ) {
2025-08-30 15:44:49 +03:00
console . log ( "Refreshed note IDs" ) ;
2025-08-30 15:11:49 +03:00
setNoteIds ( note . getChildNoteIds ( ) ) ;
} else {
2025-08-30 15:44:49 +03:00
console . log ( "Refreshed note IDs" ) ;
2025-08-30 15:11:49 +03:00
setNoteIds ( await note . getSubtreeNoteIds ( ) ) ;
}
}
// Refresh on note switch.
useEffect ( ( ) = > { refreshNoteIds ( ) } , [ note ] ) ;
// Refresh on alterations to the note subtree.
useTriliumEvent ( "entitiesReloaded" , ( { loadResults } ) = > {
if ( note && loadResults . getBranchRows ( ) . some ( branch = >
branch . parentNoteId === note . noteId
|| noteIds . includes ( branch . parentNoteId ? ? "" ) ) ) {
2025-08-30 17:21:22 +03:00
refreshNoteIds ( ) ;
2025-08-30 15:11:49 +03:00
}
} )
return noteIds ;
2025-08-30 17:21:22 +03:00
}
2025-09-04 15:13:48 +03:00
function useViewModeConfig < T extends object > ( note : FNote | null | undefined , viewType : ViewTypeOptions | undefined ) {
const [ viewConfig , setViewConfig ] = useState < [ T | undefined , ( data : T ) = > void ] > ( ) ;
useEffect ( ( ) = > {
if ( ! note || ! viewType ) return ;
const viewStorage = new ViewModeStorage < T > ( note , viewType ) ;
viewStorage . restore ( ) . then ( config = > {
const storeFn = ( config : T ) = > viewStorage . store ( config ) ;
setViewConfig ( [ config , storeFn ] ) ;
} ) ;
} , [ note , viewType ] ) ;
return viewConfig ;
}