feat: Add vault imports and non-react objects definitions container

This commit is contained in:
Elias 2025-05-13 00:50:16 +01:00
parent 41ae4b0cd7
commit 4ee872e2a3
8 changed files with 500 additions and 170 deletions

View file

@ -52,7 +52,7 @@ export default Greeting;
**Component Requirements:**
- Must include a default export
- Must be self-contained in one code block
- Component name should match default export
- Component ro Render must be the first component within codeblock
### Component Structure
@ -120,12 +120,30 @@ import * as XLSX from 'xlsx'; // Excel files
import * as mathjs from 'mathjs'; // Mathematics
import { format, parseISO } from 'date-fns'; // Date handling
// Interface components
import { Card, Tabs, Switch } from '@/components/ui';
import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons
// Interface components
import { Card, Tabs, Switch } from '@/components/ui';
import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons
```
See the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities.
Even if needed libraries are not provided within this list CDN imports are always an option.
See the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities.
Even if needed libraries are not provided within this list CDN imports are always an option.
### Available in Component Scope
All components have access to these objects and functions:
```javascript
// Note context
noteContext.frontmatter // All frontmatter properties (not just react_data)
noteContext.path // Full path to current note
noteContext.basename // Note filename without extension
// Utilities
getTheme() // Returns 'dark' or 'light'
readFile(path, exts) // File reading utility
getFrontmatter(key?,defaultValue?,notePath?) // If properties are null returns entire frontmatter or current note if path not provided
vaultImport(path, index) // Preprocessor directive to include code from other notes (not a runtime function)
```
## 💻 Core Features
@ -299,6 +317,47 @@ const styles = {
// Or use 'dark:' within your components
```
### 8. Vault Code Imports
Import React code from other notes in your vault:
```javascript
// Import code from another note
vaultImport('Components/SharedComponents.md', 0); // 0 = first code block
vaultImport('Hooks/CustomHooks.md', 1); // 1 = second code block
const MyComponent = () => {
// Use imported components and hooks from these files
const data = useImportedHook();
return <ImportedComponent data={data} />;
};
```
### 9. Definition Storage Blocks
Not all react code blocks need to be valid React you can use React code blocks to store and organize utility functions, hooks, and other definitions:
```javascript
// This displays as a definition storage block instead of rendering
const utilities = () => ({
formatDate: (date) => date.toLocaleDateString(),
calculateSum: (arr) => arr.reduce((a, b) => a + b, 0),
parseData: (raw) => JSON.parse(raw)
});
export default utilities;
```
![Object Definitions Codeblock](assets/codeStorageBox.gif)
When a code block returns an object instead of JSX, it automatically displays as a storage block showing all exported definitions. If you have multiple objects containing definitions only the first object will be previewed.
Perfect for:
Utility function collections
Custom hook libraries
Configuration objects
Shared constants
Import these definitions into other components using vaultImport().
## 🎨 Component Examples
@ -426,6 +485,9 @@ Knowledge progression dashboard using persistant storage and ability to make new
![alt text](assets/IntegratedUsage.gif)
**Games utilising event listeners for key action mapping**
![alt text](assets/gamingObsidian.gif)
## 🛠Development Guide
### Component Structure
```react

BIN
assets/codeStorageBox.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

576
main.tsx
View file

@ -19,7 +19,7 @@ class ReactComponentChild extends MarkdownRenderChild {
private noteFile: TFile;
private ctx: MarkdownPostProcessorContext;
private app: App;
private processedPaths = new Set<string>();
constructor(containerEl: HTMLElement, plugin: Plugin, ctx: MarkdownPostProcessorContext) {
super(containerEl);
@ -50,13 +50,41 @@ class ReactComponentChild extends MarkdownRenderChild {
}
private async getFrontmatterData<T>(key: string, defaultValue: T): Promise<T> {
if (!this.noteFile) return defaultValue;
const cache = this.app.metadataCache.getFileCache(this.noteFile);
const frontmatter = cache?.frontmatter;
return frontmatter?.react_data?.[key] ?? defaultValue;
}
private async getFrontmatter<T>(
key: string | null = null,
defaultValue: T | null = null,
notePath: string | null = null
): Promise<T | Record<string, any> | null> {
try {
// Get the file - either current note or specified path
const file = notePath
? this.app.vault.getAbstractFileByPath(notePath)
: this.noteFile;
if (!file || !(file instanceof TFile)) {
console.error(`Invalid file: ${notePath}`);
return defaultValue;
}
const cache = this.app.metadataCache.getFileCache(file);
const frontmatter = cache?.frontmatter;
if (!frontmatter) {
return defaultValue;
}
// Return specific key if requested
if (key) {
return frontmatter[key] ?? defaultValue;
}
// Otherwise return entire frontmatter
return frontmatter;
} catch (error) {
console.error('Error getting frontmatter:', error);
return defaultValue;
}
}
private async updateFrontmatterData<T>(key: string, value: T): Promise<void> {
if (!this.noteFile) return;
@ -77,24 +105,63 @@ class ReactComponentChild extends MarkdownRenderChild {
throw error;
}
}
// Add wrapper component for error boundary
private RenderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
React.useEffect(() => {
// Capture the original console.error when the component mounts
const originalConsoleError = console.error;
// Override console.error
console.error = (...args: any[]) => {
const errorMessage = args[0]?.toString() || '';
// Check if the error message is the one we want to suppress
if (errorMessage.includes('Objects are not valid as a React child')) {
// Suppress this specific error by not calling the original console.error
return;
}
// For all other errors, call the original console.error
originalConsoleError.apply(console, args);
};
// Cleanup function: Restore the original console.error when the component unmounts
return () => {
console.error = originalConsoleError;
};
}, []); // Empty dependency array ensures this effect runs only once on mount and cleanup on unmount.
return (
<ErrorBoundary
fallback={({ error }) => (
<div className="react-component-error">
<p>Error in component:</p>
<pre className="error-message">
{error.message}
</pre>
</div>
)}
fallback={({ error }) => {
// The ErrorBoundary will catch the error and display a fallback UI.
// The console log for "Objects are not valid as a React child" should have been suppressed.
if (error.message?.includes('Objects are not valid as a React child')) {
const objectMatch = error.message.match(/found: object with keys {([^}]+)}/);
const keys = objectMatch ? objectMatch[1].split(',').map(k => k.trim()) : ['unknown'];
return (
<div style={{ padding: '12px', backgroundColor: 'var(--background-modifier-form-field)', borderRadius: '6px', border: '1px solid var(--background-modifier-border)', fontFamily: 'var(--font-monospace)', margin: '1rem 0' }}>
<div style={{ fontWeight: 'bold', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>🧩</span> <span>Component Rendered an Object</span>
</div>
<div style={{ fontSize: '0.9em', opacity: 0.8, marginBottom: '4px' }}>A React component attempted to render a plain JavaScript object. Objects need to be mapped to renderable elements.</div>
<div style={{ fontSize: '0.9em', opacity: 0.8 }}>Object keys found:</div>
<ul style={{ margin: '8px 0 0 20px', paddingLeft: '0', listStyleType: 'disc' }}>{keys.map(key => <li key={key} style={{fontSize: '0.85em'}}>{key}</li>)}</ul>
</div>
);
}
// Default error display for other types of errors
return (
<div className="react-component-error" style={{ margin: '1rem 0' }}>
<p style={{color: 'var(--text-error)', fontWeight: 'bold'}}>Error in component:</p>
<pre className="error-message" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', backgroundColor: 'var(--background-secondary-alt)', padding: '8px', borderRadius: '4px', color: 'var(--text-error)'}}>{error.message}</pre>
{error.stack && <details style={{marginTop: '8px'}}><summary style={{cursor: 'pointer', fontSize: '0.9em'}}>Stack Trace</summary><pre style={{fontSize: '0.8em', maxHeight: '150px', overflowY: 'auto', backgroundColor: 'var(--background-secondary)', padding: '8px', borderRadius: '4px', marginTop: '4px'}}>{error.stack}</pre></details>}
</div>
);
}}
>
{children}
</ErrorBoundary>
);
};
private needsThree(code: string): boolean {
// Check if the code contains any Three.js specific imports or usage
// More comprehensive detection of THREE usage
@ -123,7 +190,7 @@ class ReactComponentChild extends MarkdownRenderChild {
return hasThreeImports || hasThreeUsage || commonThreeClasses;
}
private preprocessCode(code: string): string {
private async preprocessCode(code: string): Promise<string> {
// Replace CDN imports with script loading
code = code.replace(
@ -138,49 +205,58 @@ class ReactComponentChild extends MarkdownRenderChild {
code = code.replace(/import\s*\([^)]*\);?\s*$/gm, '');
// Handle both JS/TS exports
code = code.replace(/export\s+default\s+/, '');
code = code.replace(/export\s+const\s+/, 'const ');
code = code.replace(/export\s+function\s+/, 'function ');
code = code.replace(/export\s+class\s+/, 'class ');
code = code.replace(/export\s+default\s+/gm, '');
code = code.replace(/export\s+const\s+/gm, 'const ');
code = code.replace(/export\s+function\s+/gm, 'function ');
code = code.replace(/export\s+class\s+/gm, 'class ');
// Remove type annotations
//code = code.replace(/:\s*[A-Za-z<>[\]]+/g, '');
//code = code.replace(/<[A-Za-z,\s]+>/g, '');
// Create a HOC wrapper for chart components
const chartWrapper = `
const withChartContainer = (WrappedComponent) => {
return function ChartContainer(props) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
const timer = setTimeout(() => setMounted(true), 100);
return () => clearTimeout(timer);
}, []);
return React.createElement(
'div',
{ style: { width: '100%', minHeight: '400px' } },
mounted ? React.createElement(WrappedComponent, props) : null
);
};
};
`;
// Find the component name
const componentMatch = code.match(/(?:function\s+(\w+)|(?:const|class)\s+(\w+)\s*=\s*(?:(?:\([^)]*\)|)\s*=>|function\s*\(|React\.memo\(|React\.forwardRef\(|class\s+extends\s+React\.Component))/);
const componentName = componentMatch ? (componentMatch[1] || componentMatch[2]) : 'EmptyComponent';
console.log('Found component:', componentName); // Debug info
if (!componentMatch) {
throw new Error('No React component found');
}
const componentMatch = code.match(/^(?![\s]*\/\/).*?(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:(?:\([^)]*\)|)\s*=>|function\s*\(|React\.memo\(|React\.forwardRef\(|class\s+extends\s+React\.Component))/m);
const storeMatch = !componentMatch ? code.match(/(?:const\s+(\w+)\s*=\s*{|class\s+(\w+)\s*{)/) : null;
// Find component name and rename if it's 'Component'
const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
if (!componentMatch) {
// If no component found, look for object literals or other exports
if (!storeMatch) {
throw new Error('No React component found');
}
}
// Set safeName using either match
const componentName = componentMatch
? (componentMatch[1] || componentMatch[2])
: (storeMatch ? (storeMatch[1]|| storeMatch[2]) : 'EmptyComponent');
console.log('Found component:', componentName);
// Find component name and rename if it's 'WrappedComponent'
const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
if (componentName === 'WrappedComponent') {
code = code.replace(/\bComponent\b/, safeName);
}
if (componentName === 'WrappedComponent') {
code = code.replace(/\bWrappedComponent\b/, safeName);
}
// Create a HOC wrapper for chart components
const chartWrapper = `
const withChartContainer = (WrappedComponent) => {
return function ChartContainer(props) {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
const timer = setTimeout(() => setMounted(true), 100);
return () => clearTimeout(timer);
}, []);
return React.createElement(
'div',
{ style: { width: '100%', minHeight: '400px' } },
mounted ? React.createElement(WrappedComponent, props) : null
);
};
};
`;
// Combine everything
// added Component assignment
code = `
@ -189,14 +265,122 @@ class ReactComponentChild extends MarkdownRenderChild {
const WrappedComponent = (() => {
// Add error handling for component existence
if (typeof ${safeName} === 'undefined') {
throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100)}...\`);
throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100).replace(/`/g, '\\`')}...\`);
}
const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') ||
// Simple check - is it a valid React component?
const isValidComponent = (() => {
// Class component
if (${safeName}.prototype?.isReactComponent) return true;
// Function that returns React elements
if (typeof ${safeName} === 'function') {
try {
const result = ${safeName}({});
return React.isValidElement(result);
} catch {
// If it throws, check source code
return ${safeName}.toString().includes('createElement') ||
${safeName}.toString().includes('<');
}
}
// Direct objects are never valid components
return false;
})();
// If not a valid component, treat as a storage block
if (!isValidComponent) {
return function StorageBlockWrapper() {
// Get the definitions - handle all cases
let definitions;
if (typeof ${safeName} === 'function') {
try {
// Try as function
definitions = ${safeName}({});
} catch (e) {
try {
// Try as class
definitions = new ${safeName}();
} catch (e2) {
// Last resort
definitions = ${safeName};
}
}
} else {
// Already an object (like IIFE or object literal)
definitions = ${safeName};
}
// Ensure it's an object
if (!definitions || typeof definitions !== 'object') {
definitions = { error: "Couldn't extract definitions" };
}
const keys = Object.keys(definitions);
return React.createElement('div', {
style: {
padding: '12px',
backgroundColor: 'var(--background-modifier-form-field)',
borderRadius: '6px',
border: '1px solid var(--background-modifier-border)',
fontFamily: 'var(--font-monospace)'
}
}, [
React.createElement('div', {
style: {
fontWeight: 'bold',
marginBottom: '8px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}
}, [
React.createElement('span', null, '📦'),
React.createElement('span', null, 'Definitions Storage Container:')
]),
React.createElement('div', {
style: {
padding: '8px',
marginBottom: '12px',
color: 'var(--text-on-accent)',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}
}, [
React.createElement('code', {
style: {
padding: '2px 6px',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: '3px',
fontWeight: 'bold'
}
}, '${safeName}')
]),
React.createElement('div', {
style: { fontSize: '0.9em', opacity: 0.8 }
}, 'Exported definitions:'),
React.createElement('ul', {
style: {
margin: '8px 0',
paddingLeft: '20px'
}
}, keys.map(key =>
React.createElement('li', { key },
\`\${key}: \${typeof definitions[key]}\`
)
))
]);
};
}
const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') ||
${safeName}.toString().includes('svg');
return isChartComponent ? withChartContainer(${safeName}) : ${safeName};
return isChartComponent ? withChartContainer(${safeName}) : ${safeName};
})();
`;
code = await this.processVaultImports(code);
// Wrap code in async IIFE to allow for await
return `
(async () => {
@ -222,6 +406,80 @@ private getThreeJs() {
}
}
private async processVaultImports(code: string, depth = 0): Promise<string> {
if (depth > 10) {
console.warn('Maximum import depth reached (10)');
return code;
}
const vaultImportRegex = /vaultImport\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(\d+))?\s*\)/g;
// Create a map to track processed imports (avoid duplicates)
const processedImports = new Map<string, string>();
// Find all vaultImport calls
const matches = [...code.matchAll(vaultImportRegex)];
for (const match of matches) {
const [fullMatch, path, index = '0'] = match;
const importKey = `${path}#${index}`;
// Skip if already processed
if (processedImports.has(importKey)) continue;
try {
const file = this.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) continue;
const content = await this.app.vault.read(file);
const blocks = content.match(/```react\n([\s\S]*?)```/g) || [];
if (blocks[parseInt(index)]) {
let blockCode = blocks[parseInt(index)]
.replace(/```react\n/, '')
.replace(/```$/, '');
// Check for nested imports
const importKey = `${path}#${index}`;
if (!this.processedPaths.has(importKey)) {
this.processedPaths.add(importKey);
// Recursively process nested imports
blockCode = await this.processVaultImports(blockCode, depth + 1);
}
blockCode = blockCode.replace(
/import\s+(\w+)\s+from\s+['"]https:\/\/cdnjs\.cloudflare\.com\/([^'"]+)['"]/g,
(match, importName, cdnPath) => `
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/${cdnPath}';
document.body.appendChild(script);`);
blockCode = blockCode.replace(/import\s+.*?['"]\s*;?\s*$/gm, '')
.replace(/import\s*{[^}]*}\s*from\s*['"][^'"]*['"];?\s*$/gm, '')
.replace(/import\s*\([^)]*\);?\s*$/gm, '');
// Strip exports
blockCode = blockCode
.replace(/export\s+default\s+/gm, '')
.replace(/export\s+const\s+/gm, 'const ')
.replace(/export\s+function\s+/gm, 'function ')
.replace(/export\s+class\s+/gm, 'class ')
.replace(/export\s+{[^}]*}\s*;?/gm, '')
const importedCode = `\n// Imported from ${path}#${index}\n${blockCode}\n`;
processedImports.set(importKey, importedCode);
}
} catch (error) {
console.error(`Error importing from ${path}:`, error);
}
}
// Append all unique imports
const appendedCode = Array.from(processedImports.values()).join('\n');
// Remove ALL vaultImport calls in one pass
const cleanedCode = code.replace(vaultImportRegex, '');
return appendedCode+cleanedCode;
}
async render(code: string) {
console.time('component-render');
try {
@ -241,10 +499,10 @@ private getThreeJs() {
console.time('preprocess');
// Preprocess code
const needsThree=this.needsThree(code);
const processedCode = this.preprocessCode(code);
//console.log('Processed code:', processedCode);
const processedCode = await this.preprocessCode(code);
console.timeEnd('preprocess');
console.time('tailwind');
// Process Tailwind
//Could potentially generate tailwind css based on specific user code using build:css script defined in config
@ -268,14 +526,12 @@ private getThreeJs() {
}).code;
console.timeEnd('babel');
//console.log('Transformed code:', transformedCode);
// Create bound storage hook
// Create bound storage hook with frontmatter support
const boundUseStorage = <T,>(key: string, defaultValue: T) => {
const [value, setValue] = React.useState<T>(defaultValue);
React.useEffect(() => {
this.getFrontmatterData(key, defaultValue).then(setValue);
this.getFrontmatter(key, defaultValue).then(setValue);
}, [key]);
const updateValue = React.useCallback(
@ -312,99 +568,13 @@ private getThreeJs() {
}),
//lazy load THREE if needed
...needsThree ? this.getThreeJs() : undefined,
readFile: async (path=null, extensions = ['txt', 'md', 'json','csv']) => {
return new Promise((resolve) => {
let isResolved = false;
let cancelTimeoutHandle: number | undefined = undefined; // To store timeout handle for cancellation
// Create a SuggestModal subclass that lists vault files
// If a specific path is provided, read that file directly without showing modal
if (path) {
const file = this.app.vault.getAbstractFileByPath(path);
if (file && file instanceof TFile) {
this.app.vault.cachedRead(file).then(content => {
isResolved = true;
resolve({
path: file.path,
name: file.name,
content: content
});
}).catch(err => {
console.error("Failed to read file:", err);
isResolved = true;
resolve(null);
});
} else {
console.error("File not found:", path);
isResolved = true;
resolve(null);
}
return;
}
class FileSuggestModal extends SuggestModal<TFile> {
getSuggestions(query:string) {
// Get all files with the right extension
const files = this.app.vault.getFiles()
.filter(file => extensions.includes(file.extension));
// Filter by query if provided
if (query) {
return files.filter(file =>
file.path.toLowerCase().includes(query.toLowerCase())
);
}
return files;
}
renderSuggestion(file:TFile, el: HTMLElement) {
el.createEl("div", { text: file.name });
el.createEl("small", { text: file.path });
}
onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) {
console.log("File content:");
if (cancelTimeoutHandle !== undefined) {
clearTimeout(cancelTimeoutHandle);
cancelTimeoutHandle = undefined;
}
this.app.vault.cachedRead(file).then(content => {isResolved = true;
resolve({
path: file.path,
name: file.basename,
extension: file.extension,
content: content
});
}).catch(err => {
console.error("Failed to read file:", err);
resolve(null);});}
}
// Show the modal
const modal = new FileSuggestModal(this.app);
modal.setPlaceholder("Select a file");
modal.onClose = () => {
// If onClose is called, and a suggestion hasn't ALREADY been chosen and processed,
// this implies a cancellation or an edge case where onClose fires very quickly.
// We use a setTimeout to yield to the event loop, giving onChooseSuggestion
// a chance to run if it was triggered by the same user action that closed the modal.
if (cancelTimeoutHandle !== undefined) { // Clear previous timeout if any (e.g. rapid open/close)
clearTimeout(cancelTimeoutHandle);
}
cancelTimeoutHandle = window.setTimeout(() => { // window.setTimeout for NodeJS.Timeout type
// After yielding, if the promise hasn't been resolved by onChooseSuggestion
// and a suggestion wasn't flagged as chosen, then resolve as null (cancellation).
if (!isResolved) {
resolve(null);
}
// If suggestionChosen IS true here, it means onChooseSuggestion ran,
// set the flag, and it's responsible for resolving (or has already).
// If promiseResolved IS true, it's already handled.
}, 0); // Yield to the event loop.
};
modal.open();
});
}
readFile: async (path = null, extensions = ['txt', 'md', 'json', 'csv']) => {
return await this.readFile(this.app, path, extensions);
},
// Frontmatter access utility
getFrontmatter: async (notePath = null, key = null, defaultValue = null) => {
return await this.getFrontmatter(notePath, key, defaultValue);
},
};
// Execute the code and get the component
@ -426,6 +596,102 @@ readFile: async (path=null, extensions = ['txt', 'md', 'json','csv']) => {
console.timeEnd('component-render');
}
async readFile(
app: App,
path: string | null = null,
extensions: string[] = ['txt', 'md', 'json', 'csv']
): Promise<{ path: string; name: string; extension?: string; content: string } | null> {
return new Promise((resolve) => {
let isResolved = false;
let cancelTimeoutHandle: number | undefined = undefined; // To store timeout handle for cancellation
// Create a SuggestModal subclass that lists vault files
// If a specific path is provided, read that file directly without showing modal
if (path) {
const file = this.app.vault.getAbstractFileByPath(path);
if (file && file instanceof TFile) {
this.app.vault.cachedRead(file).then(content => {
isResolved = true;
resolve({
path: file.path,
name: file.name,
content: content
});
}).catch(err => {
console.error("Failed to read file:", err);
isResolved = true;
resolve(null);
});
} else {
console.error("File not found:", path);
isResolved = true;
resolve(null);
}
return;
}
class FileSuggestModal extends SuggestModal<TFile> {
getSuggestions(query:string) {
// Get all files with the right extension
const files = this.app.vault.getFiles()
.filter(file => extensions.includes(file.extension));
// Filter by query if provided
if (query) {
return files.filter(file =>
file.path.toLowerCase().includes(query.toLowerCase())
);
}
return files;
}
renderSuggestion(file:TFile, el: HTMLElement) {
el.createEl("div", { text: file.name });
el.createEl("small", { text: file.path });
}
onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) {
console.log("File content:");
if (cancelTimeoutHandle !== undefined) {
clearTimeout(cancelTimeoutHandle);
cancelTimeoutHandle = undefined;
}
this.app.vault.cachedRead(file).then(content => {isResolved = true;
resolve({
path: file.path,
name: file.basename,
extension: file.extension,
content: content
});
}).catch(err => {
console.error("Failed to read file:", err);
resolve(null);});}
}
// Show the modal
const modal = new FileSuggestModal(this.app);
modal.setPlaceholder("Select a file");
modal.onClose = () => {
// We use a setTimeout to yield to the event loop, giving onChooseSuggestion
// a chance to run if it was triggered by the same user action that closed the modal.
if (cancelTimeoutHandle !== undefined) { // Clear previous timeout if any (e.g. rapid open/close)
clearTimeout(cancelTimeoutHandle);
}
cancelTimeoutHandle = window.setTimeout(() => { // window.setTimeout for NodeJS.Timeout type
// After yielding, if the promise hasn't been resolved by onChooseSuggestion
// and a suggestion wasn't flagged as chosen, then resolve as null (cancellation).
if (!isResolved) {
resolve(null);
}
// If suggestionChosen IS true here, it means onChooseSuggestion ran,
// set the flag, and it's responsible for resolving (or has already).
// If promiseResolved IS true, it's already handled.
}, 0); // Yield to the event loop.
};
modal.open();
});
}
onunload() {
this.root.unmount();

View file

@ -1,7 +1,7 @@
{
"id": "reactive-notes",
"name": "Reactive Notes",
"version": "1.0.2",
"version": "1.0.3",
"minAppVersion": "1.4.0",
"description": "Transform your vault into a reactive computational environment. Create dynamic React components directly in your notes.",
"author": "Elias Noesis",
@ -10,4 +10,4 @@
"fundingUrl": {
"Buy Me A Coffee": "https://www.buymeacoffee.com/prodigist"
}
}
}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "reactive-notes",
"version": "1.0.2",
"version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reactive-notes",
"version": "1.0.1",
"version": "1.0.3",
"dependencies": {
"@codemirror/language": "^6.10.6",
"@emotion/css": "^11.13.5",

View file

@ -1,6 +1,6 @@
{
"name": "reactive-notes",
"version": "1.0.2",
"version": "1.0.3",
"description": "Transform your vault into a reactive computational environment with React components.",
"main": "main.js",
"scripts": {

View file

@ -15,7 +15,7 @@ import {
Upload, Activity, AlertCircle, TrendingUp, TrendingDown, Home, BarChart2, BarChart3, Code, Settings, Moon, Sun, ChevronLeft, ChevronRight, Heart, ExternalLink,
Play, Image as ImageIcon, Check, X, Plus, Minus, ArrowRightCircle, ArrowLeftCircle, ArrowUpCircle, ArrowDownCircle,
ChevronUp, ChevronDown, ArrowRight, ArrowLeft, ArrowUp, ArrowDown, PlusCircle, MinusCircle, XCircle, CheckCircle,
Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle,
Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle, Clock, Calendar,
} from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import {
@ -99,7 +99,8 @@ RadialBarChart, RadialBar,
ExternalLink, BarChart2,
Play, ImageIcon, Check, X, Plus, Minus, ArrowRightCircle, ArrowLeftCircle, ArrowUpCircle, ArrowDownCircle,
ChevronUp, ChevronDown, ArrowRight, ArrowLeft, ArrowUp, ArrowDown, PlusCircle, MinusCircle, XCircle, CheckCircle,
Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle,
Info, Search, Trash, Edit, Save, Share, Copy, Pause, Undo, Redo, ZoomIn, ZoomOut, PlayCircle, PauseCircle, Clock, Calendar,
LucideIcons,
//data processing utilities
dateFns: { format, addDays, differenceInDays, parseISO },

View file

@ -1,3 +1,4 @@
{
"1.0.2": "1.4.0"
}
"1.0.2": "1.4.0",
"1.0.3": "1.4.0"
}