mirror of
https://github.com/zadam/trilium.git
synced 2026-01-06 22:54:23 +01:00
chore(client): prototype implementation to communicate data through note context
This commit is contained in:
parent
fffab73061
commit
9098bfb63a
194
CONTEXT_DATA_EXAMPLE.md
Normal file
194
CONTEXT_DATA_EXAMPLE.md
Normal 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
|
||||
@ -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: {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user