chore(client): prototype implementation to communicate data through note context

This commit is contained in:
Elian Doran 2025-12-29 21:26:52 +02:00
parent fffab73061
commit 9098bfb63a
No known key found for this signature in database
4 changed files with 373 additions and 0 deletions

194
CONTEXT_DATA_EXAMPLE.md Normal file
View File

@ -0,0 +1,194 @@
# Context Data Pattern - Usage Examples
This document shows how to use the new context data pattern to communicate between type widgets (inside splits) and shared components (sidebars, toolbars).
## Architecture
Data is stored directly on the `NoteContext` object:
- **Type widgets** (PDF, Text, Code) publish data using `useSetContextData()`
- **Sidebar/toolbar components** read data using `useGetContextData()`
- Data is automatically cleared when switching notes
- Components automatically re-render when data changes
## Example 1: PDF Page Navigation
### Publishing from PDF Viewer (inside split)
```tsx
// In Pdf.tsx
import { useSetContextData } from "../../react/hooks";
interface PdfPageInfo {
pageNumber: number;
title: string;
}
export default function PdfPreview({ note, blob }) {
const { noteContext } = useActiveNoteContext();
const [pages, setPages] = useState<PdfPageInfo[]>([]);
useEffect(() => {
// When PDF loads, extract page information
const pageInfo = extractPagesFromPdf();
setPages(pageInfo);
}, [blob]);
// Publish pages to context data
useSetContextData(noteContext, "pdfPages", pages);
return <iframe className="pdf-preview" ... />;
}
```
### Consuming in Sidebar (outside split)
```tsx
// In PdfPageNavigation.tsx (sidebar widget)
import { useGetContextData } from "../../react/hooks";
export default function PdfPageNavigation() {
const pages = useGetContextData<PdfPageInfo[]>("pdfPages");
if (!pages || pages.length === 0) {
return null; // Don't show if no PDF is active
}
return (
<RightPanelWidget id="pdf-nav" title="PDF Pages">
<ul>
{pages.map(page => (
<li key={page.pageNumber}>
Page {page.pageNumber}: {page.title}
</li>
))}
</ul>
</RightPanelWidget>
);
}
```
## Example 2: Table of Contents
### Publishing from Text Editor
```tsx
// In TextTypeWidget.tsx
import { useSetContextData } from "../../react/hooks";
interface Heading {
id: string;
level: number;
text: string;
}
function TextTypeWidget() {
const { noteContext } = useActiveNoteContext();
const [headings, setHeadings] = useState<Heading[]>([]);
const extractHeadings = useCallback((editor: CKTextEditor) => {
const extractedHeadings = [];
// Extract headings from editor...
setHeadings(extractedHeadings);
}, []);
// Publish headings to context
useSetContextData(noteContext, "toc", headings);
return <div className="text-editor">...</div>;
}
```
### Consuming in Sidebar
```tsx
// In TableOfContents.tsx
import { useGetContextData } from "../../react/hooks";
export default function TableOfContents() {
const headings = useGetContextData<Heading[]>("toc");
if (!headings || headings.length === 0) {
return <div className="no-headings">No headings available</div>;
}
return (
<RightPanelWidget id="toc" title="Table of Contents">
<ul>
{headings.map(h => (
<li key={h.id} style={{ marginLeft: `${(h.level - 1) * 20}px` }}>
{h.text}
</li>
))}
</ul>
</RightPanelWidget>
);
}
```
## Example 3: Code Outline
### Publishing from Code Editor
```tsx
// In CodeTypeWidget.tsx
interface CodeSymbol {
name: string;
kind: 'function' | 'class' | 'variable';
line: number;
}
function CodeTypeWidget() {
const { noteContext } = useActiveNoteContext();
const [symbols, setSymbols] = useState<CodeSymbol[]>([]);
// When code changes, extract symbols
const onCodeChange = useCallback((editor: CodeMirror) => {
const extractedSymbols = parseSymbols(editor.getValue());
setSymbols(extractedSymbols);
}, []);
useSetContextData(noteContext, "codeOutline", symbols);
return <div className="code-editor">...</div>;
}
```
### Consuming in Sidebar
```tsx
// In CodeOutline.tsx
function CodeOutline() {
const symbols = useGetContextData<CodeSymbol[]>("codeOutline");
if (!symbols) return null;
return (
<RightPanelWidget id="code-outline" title="Outline">
{symbols.map((symbol, i) => (
<div key={i} className={`symbol-${symbol.kind}`}>
{symbol.name} (Line {symbol.line})
</div>
))}
</RightPanelWidget>
);
}
```
## Benefits
1. **Simple & Direct**: Data lives where it belongs (on the note context)
2. **Automatic Cleanup**: Data cleared when switching notes
3. **Reactive**: Components automatically re-render when data changes
4. **Type-Safe**: Full TypeScript support with generics
5. **No Global State**: Each context has its own data
6. **Works with Splits**: Each split can have different data
## Data Keys Convention
Use descriptive, namespaced keys:
- `"toc"` - Table of contents headings
- `"pdfPages"` - PDF page information
- `"codeOutline"` - Code symbols/outline
- `"searchResults"` - Search results within note
- `"imageGallery"` - Image list for gallery view
- `"noteStats"` - Statistics about the note

View File

@ -473,6 +473,11 @@ type EventMappings = {
noteContextRemoved: {
ntxIds: string[];
};
contextDataChanged: {
noteContext: NoteContext;
key: string;
value: unknown;
};
exportSvg: { ntxId: string | null | undefined; };
exportPng: { ntxId: string | null | undefined; };
geoMapCreateChildNote: {

View File

@ -32,6 +32,13 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
parentNoteId?: string | null;
viewScope?: ViewScope;
/**
* Metadata storage for UI components (e.g., table of contents, PDF page list, code outline).
* This allows type widgets to publish data that sidebar/toolbar components can consume.
* Data is automatically cleared when navigating to a different note.
*/
private contextData: Map<string, unknown> = new Map();
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
super();
@ -91,6 +98,17 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
this.viewScope = opts.viewScope;
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
// Clear context data when switching notes and notify subscribers
const oldKeys = Array.from(this.contextData.keys());
this.contextData.clear();
for (const key of oldKeys) {
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value: undefined
});
}
this.saveToRecentNotes(resolvedNotePath);
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
@ -443,6 +461,52 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return title;
}
/**
* Set metadata for this note context (e.g., table of contents, PDF pages, code outline).
* This data can be consumed by sidebar/toolbar components.
*
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages", "codeOutline")
* @param value - The data to store (will be cleared when switching notes)
*/
setContextData<T>(key: string, value: T): void {
this.contextData.set(key, value);
// Trigger event so subscribers can react
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value
});
}
/**
* Get metadata for this note context.
*
* @param key - The data key to retrieve
* @returns The stored data, or undefined if not found
*/
getContextData<T>(key: string): T | undefined {
return this.contextData.get(key) as T | undefined;
}
/**
* Check if context data exists for a given key.
*/
hasContextData(key: string): boolean {
return this.contextData.has(key);
}
/**
* Clear specific context data.
*/
clearContextData(key: string): void {
this.contextData.delete(key);
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value: undefined
});
}
}
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {

View File

@ -1192,3 +1192,113 @@ export function useContentElement(noteContext: NoteContext | null | undefined) {
return contentElement;
}
/**
* Set context data on the current note context.
* This allows type widgets to publish data (e.g., table of contents, PDF pages)
* that can be consumed by sidebar/toolbar components.
*
* Data is automatically cleared when navigating to a different note.
*
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages")
* @param value - The data to publish
*
* @example
* // In a PDF viewer widget:
* const { noteContext } = useActiveNoteContext();
* useSetContextData(noteContext, "pdfPages", pages);
*/
export function useSetContextData<T>(
noteContext: NoteContext | null | undefined,
key: string,
value: T | undefined
) {
const valueRef = useRef<T | undefined>(value);
valueRef.current = value;
useEffect(() => {
if (!noteContext) return;
noteContext.setContextData(key, valueRef.current);
return () => {
noteContext.clearContextData(key);
};
}, [noteContext, key]);
// Update when value changes
useEffect(() => {
if (!noteContext) return;
noteContext.setContextData(key, value);
}, [noteContext, key, value]);
}
/**
* Get context data from the active note context.
* This is typically used in sidebar/toolbar components that need to display
* data published by type widgets.
*
* The component will automatically re-render when the data changes.
*
* @param key - The data key to retrieve (e.g., "toc", "pdfPages")
* @returns The current data, or undefined if not available
*
* @example
* // In a Table of Contents sidebar widget:
* function TableOfContents() {
* const headings = useGetContextData<Heading[]>("toc");
* if (!headings) return <div>No headings available</div>;
* return <ul>{headings.map(h => <li>{h.text}</li>)}</ul>;
* }
*/
export function useGetContextData<T = unknown>(key: string): T | undefined {
const { noteContext } = useActiveNoteContext();
const [data, setData] = useState<T | undefined>(() =>
noteContext?.getContextData<T>(key)
);
// Update initial value when noteContext changes
useEffect(() => {
setData(noteContext?.getContextData<T>(key));
}, [noteContext, key]);
// Subscribe to changes via Trilium event system
useTriliumEvent("contextDataChanged", ({ noteContext: eventNoteContext, key: changedKey, value }) => {
if (eventNoteContext === noteContext && changedKey === key) {
setData(value as T);
}
});
return data;
}
/**
* Get context data from a specific note context (not necessarily the active one).
*
* @param noteContext - The specific note context to get data from
* @param key - The data key to retrieve
* @returns The current data, or undefined if not available
*/
export function useGetContextDataFrom<T = unknown>(
noteContext: NoteContext | null | undefined,
key: string
): T | undefined {
const [data, setData] = useState<T | undefined>(() =>
noteContext?.getContextData<T>(key)
);
// Update initial value when noteContext changes
useEffect(() => {
setData(noteContext?.getContextData<T>(key));
}, [noteContext, key]);
// Subscribe to changes via Trilium event system
useTriliumEvent("contextDataChanged", ({ noteContext: eventNoteContext, key: changedKey, value }) => {
if (eventNoteContext === noteContext && changedKey === key) {
setData(value as T);
}
});
return data;
}