Skip to content

Commit cd7333b

Browse files
author
Corey B
committed
feat: integrate real file system persistence testing and enhanced interactivity
1 parent 622f5b4 commit cd7333b

2 files changed

Lines changed: 133 additions & 62 deletions

File tree

examples/bank_statement.fdd

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8">
55
<title>Bank Statement - OpenFDD</title>
66
</head>
7-
<body>
7+
<body style="background: #e2e8f0; margin: 0; padding: 2rem;">
88
<fdd-container extension=".fdd">
99
<script type="application/vc+json" id="fdd-data">
1010
{
@@ -13,7 +13,14 @@
1313
"accountHolder": "Jane Doe",
1414
"accountNumber": "****-1234",
1515
"balance": "4,321.00",
16-
"statementPeriod": "December 2025"
16+
"statementPeriod": "December 2025",
17+
"transactions": [
18+
{"date": "Dec 01", "vendor": "Starbucks", "amount": "-$4.50", "category": "Food"},
19+
{"date": "Dec 05", "vendor": "Target", "amount": "-$45.00", "category": "Shopping"},
20+
{"date": "Dec 10", "vendor": "Tech Corp Salary", "amount": "+$2,000.00", "category": "Income"},
21+
{"date": "Dec 14", "vendor": "Shell Gas", "amount": "-$35.00", "category": "Auto"},
22+
{"date": "Dec 20", "vendor": "Amazon", "amount": "-$125.00", "category": "Shopping"}
23+
]
1724
},
1825
"proof": { "signature": "mock-signature-123" }
1926
}
@@ -24,20 +31,21 @@
2431
<template shadowrootmode="open" bind="#fdd-data, #user-notes">
2532
<style>
2633
:host {
27-
display: block; font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 0 auto; color: #1e293b;
28-
}
29-
.header {
30-
display: flex; justify-content: space-between; border-bottom: 2px solid #cbd5e1; padding-bottom: 1rem; margin-bottom: 2rem;
31-
}
32-
.verified-badge {
33-
background: #dcfce7; color: #166534; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;
34+
display: block; font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 0 auto; color: #1e293b; background: white; padding: 3rem; border-radius: 12px; box-shadow: 0 10px 20px rgba(0,0,0,0.05);
3435
}
36+
.header { display: flex; justify-content: space-between; border-bottom: 2px solid #cbd5e1; padding-bottom: 1rem; margin-bottom: 2rem; }
37+
.verified-badge { background: #dcfce7; color: #166534; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold; }
3538
h1, h2, h3 { margin: 0; }
36-
.summary {
37-
background: #f8fafc; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; border: 1px solid #e2e8f0;
38-
display: flex; justify-content: space-between; align-items: center;
39-
}
39+
.summary { background: #f8fafc; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; border: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; }
4040
.balance { font-size: 2.5rem; font-weight: bold; color: #0f172a; }
41+
42+
.interactive-bar { margin-bottom: 1rem; }
43+
.search-input { width: 100%; padding: 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-size: 1rem; box-sizing: border-box; }
44+
.search-input:focus { outline: 2px solid #3b82f6; border-color: transparent; }
45+
46+
.row { display: grid; grid-template-columns: 100px 2fr 1fr 100px; padding: 1rem; border-bottom: 1px solid #e2e8f0; transition: background 0.2s;}
47+
.row:hover { background: #f8fafc; }
48+
.row-header { font-weight: bold; color: #64748b; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px; border-bottom: 2px solid #cbd5e1; }
4149
</style>
4250
<div class="header">
4351
<div>
@@ -49,15 +57,28 @@
4957
<p>Account: {{accountNumber}}</p>
5058
</div>
5159
</div>
60+
5261
<div class="summary">
53-
<div>
54-
<h2>Account Summary</h2>
55-
<p>Account Holder: {{accountHolder}}</p>
56-
</div>
57-
<div>
58-
<span style="font-size:1rem; color:#64748b;">Current Balance</span><br>
59-
<span class="balance">${{balance}}</span>
60-
</div>
62+
<div><h2>Account Summary</h2><p>Account Holder: {{accountHolder}}</p></div>
63+
<div><span style="font-size:1rem; color:#64748b;">Current Balance</span><br><span class="balance">${{balance}}</span></div>
64+
</div>
65+
66+
<div class="interactive-bar">
67+
<input type="text" class="search-input" placeholder="Search transactions locally..." data-filter-target=".row.transaction" data-filter-attr="data-vendor">
68+
</div>
69+
70+
<div class="transactions">
71+
<div class="row row-header">
72+
<div>Date</div><div>Vendor</div><div>Category</div><div style="text-align: right;">Amount</div>
73+
</div>
74+
{{#each transactions}}
75+
<div class="row transaction" data-vendor="{{vendor}} {{category}}">
76+
<div style="color: #64748b;">{{date}}</div>
77+
<div style="font-weight: 500;">{{vendor}}</div>
78+
<div><span style="background: #e2e8f0; padding: 2px 8px; border-radius: 99px; font-size: 0.8rem;">{{category}}</span></div>
79+
<div style="text-align: right; font-family: monospace; font-size: 1.1rem;">{{amount}}</div>
80+
</div>
81+
{{/each}}
6182
</div>
6283
</template>
6384
</fdd-container>

extension/content.js

Lines changed: 91 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,155 @@
11
console.log("OpenFDD: Extension Context Injected. Security Shield active.");
22

3-
// If Chrome loaded this as raw text (because .fdd isn't an OS recognized HTML mimetype),
4-
// the document body will just be a <pre> tag wrapping the text.
3+
let originalDocParser = null;
4+
55
if (document.contentType === 'text/plain' && document.body.firstElementChild?.tagName === 'PRE') {
6-
console.log("OpenFDD: Detected raw text presentation. Rebuilding FDD DOM dynamically.");
7-
const rawHtml = document.body.firstElementChild.innerText;
6+
console.log("OpenFDD: Detected raw text presentation.");
7+
const rawPayloadString = document.body.firstElementChild.innerText;
88

9-
// Clear the raw text DOM
109
document.body.innerHTML = '';
11-
document.documentElement.style.background = '#e5e5e5'; // default fallback background
10+
document.documentElement.style.background = '#e5e5e5';
1211

13-
// Parse the raw payload
1412
const parser = new DOMParser();
15-
const doc = parser.parseFromString(rawHtml, "text/html");
13+
originalDocParser = parser.parseFromString(rawPayloadString, "text/html");
1614

17-
// Securely extract only the fdd-container
18-
const fddWrapper = doc.querySelector('fdd-container');
15+
const fddWrapper = originalDocParser.querySelector('fdd-container');
1916
if (fddWrapper) {
20-
document.body.appendChild(fddWrapper);
21-
22-
// Attempt to carry over any inline body styles the template author might have used safely
23-
const bodyStyle = doc.body.getAttribute('style');
24-
if (bodyStyle) document.body.setAttribute('style', bodyStyle);
17+
document.body.appendChild(fddWrapper.cloneNode(true));
2518
} else {
26-
document.body.innerHTML = '<div style="padding: 2rem; color: #991b1b; font-family: sans-serif;"><h2>Error: Invalid FDD File</h2><p>No valid &lt;fdd-container&gt; element was found in the document.</p></div>';
19+
document.body.innerHTML = '<div style="padding: 2rem; color: #991b1b; font-family: sans-serif;"><h2>Error: Invalid FDD File</h2></div>';
2720
}
2821
}
2922

30-
// The Extension Execution Logic
3123
class ExtensionFDD {
3224
constructor(container) {
3325
this.container = container;
3426
this.template = container.querySelector('template[bind]');
3527
this.vcData = {};
3628
this.mutableData = {};
29+
this.fileHandle = null;
3730
if (!this.template) throw new Error("No bindings template found");
3831
this.init();
3932
}
4033

4134
init() {
42-
// 1. Extract read-only verifiable data
4335
const vcScript = this.container.querySelector('script[type="application/vc+json"]');
4436
if (vcScript) {
4537
const payload = JSON.parse(vcScript.textContent || '{}');
4638
this.vcData = payload.credentialSubject ? payload.credentialSubject : payload;
4739
}
4840

49-
// 2. Extract mutable state data
5041
const mutableScript = this.container.querySelector('script[type="application/json"]');
5142
if (mutableScript) this.mutableData = JSON.parse(mutableScript.textContent || '{}');
5243

53-
// 3. Simple Handlebars-style template binding setup
5444
const context = { ...this.vcData, ...this.mutableData };
5545
let html = this.template.innerHTML;
46+
47+
// Advanced Handlebars-style Arrays: {{#each list}}...{{/each}}
48+
html = html.replace(/\{\{#each\s+(.*?)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, arrayKey, loopBody) => {
49+
const arr = context[arrayKey.trim()];
50+
if (!Array.isArray(arr)) return '';
51+
return arr.map(item => {
52+
return loopBody.replace(/\{\{(.*?)\}\}/g, (m, k) => {
53+
const key = k.trim();
54+
return (item[key] !== undefined && item[key] !== null) ? item[key] : '';
55+
});
56+
}).join('');
57+
});
58+
59+
// Basic Handlebars Binding: {{field}}
5660
html = html.replace(/\{\{(.*?)\}\}/g, (match, key) => {
5761
const k = key.trim();
5862
return (context[k] !== undefined && context[k] !== null) ? context[k] : '';
5963
});
6064

61-
// 4. Attach to Declarative Shadow DOM for isolated presentation
6265
const shadowRoot = this.container.attachShadow({ mode: 'open' });
6366
shadowRoot.innerHTML = html;
6467

6568
this.setupAutoSave(shadowRoot);
69+
this.setupInteractivity(shadowRoot);
70+
}
71+
72+
setupInteractivity(root) {
73+
// Allows declarative CSS filtering by driving native filtering through data attributes over the Shadow Root
74+
const searchInputs = root.querySelectorAll('input[data-filter-target]');
75+
searchInputs.forEach(input => {
76+
input.addEventListener('input', e => {
77+
const query = e.target.value.toLowerCase();
78+
const targetSelector = input.getAttribute('data-filter-target');
79+
const searchAttr = input.getAttribute('data-filter-attr');
80+
const rows = root.querySelectorAll(targetSelector);
81+
rows.forEach(row => {
82+
if (!query || row.getAttribute(searchAttr).toLowerCase().includes(query)) {
83+
row.style.display = '';
84+
} else {
85+
row.style.display = 'none';
86+
}
87+
});
88+
});
89+
});
6690
}
6791

6892
setupAutoSave(root) {
6993
const inputs = root.querySelectorAll('[autosave]');
7094
inputs.forEach(input => {
71-
input.addEventListener('blur', (e) => {
95+
96+
// Request persistent file write-handle purely on interaction intent
97+
input.addEventListener('focus', async () => {
98+
if (!this.fileHandle && window.showOpenFilePicker) {
99+
try {
100+
const [handle] = await window.showOpenFilePicker({
101+
types: [{ description: 'Formatted Data Document', accept: {'text/html': ['.fdd']} }],
102+
multiple: false
103+
});
104+
if (handle) {
105+
if ((await handle.queryPermission({mode: 'readwrite'})) !== 'granted') {
106+
await handle.requestPermission({mode: 'readwrite'});
107+
}
108+
this.fileHandle = handle;
109+
console.log("FDD: Auto-save File Handle persistently bound!");
110+
}
111+
} catch(e) { console.warn("FDD Interaction missing valid write handle confirmation."); }
112+
}
113+
});
114+
115+
// Commit to persistent disk layer via handler mechanism
116+
input.addEventListener('blur', async (e) => {
72117
const path = e.target.name.split('.');
73-
if (path[0] === 'user-notes' && path[1]) {
118+
if (path[0] === 'user-notes' && path[1] && this.fileHandle) {
74119
this.mutableData[path[1]] = e.target.value;
75-
this.syncFile();
120+
await this.syncFile();
76121
}
77122
});
78123
});
79124
}
80125

81-
syncFile() {
82-
console.log("OpenFDD Extension: Triggering Auto-Save to Local File", this.mutableData);
83-
84-
const mutableScript = this.container.querySelector('script[type="application/json"]');
126+
async syncFile() {
127+
console.log("Deploying local disk persistent save...", this.mutableData);
128+
if (!originalDocParser) return;
129+
130+
// Isolate mutable script injection to avoid blowing up VC parameters
131+
const mutableScript = originalDocParser.querySelector('script[type="application/json"]');
85132
if (mutableScript) {
86-
mutableScript.textContent = JSON.stringify(this.mutableData, null, 2);
133+
mutableScript.textContent = "\n " + JSON.stringify(this.mutableData, null, 2) + "\n ";
87134
}
88135

89-
// UX Indicator that the file was preserved locally
90-
const badge = document.createElement('div');
91-
badge.style.cssText = "position:fixed;bottom:20px;right:20px;background:#10b981;color:white;padding:8px 16px;border-radius:999px;z-index:9999;font-family:system-ui;box-shadow:0 4px 6px rgba(0,0,0,0.1);font-weight:bold;";
92-
badge.innerText = "✓ FDD File Updated Locally";
93-
document.body.appendChild(badge);
94-
setTimeout(() => badge.remove(), 2500);
136+
try {
137+
const writable = await this.fileHandle.createWritable();
138+
const content = "<!DOCTYPE html>\n" + originalDocParser.documentElement.outerHTML;
139+
await writable.write(content);
140+
await writable.close();
141+
142+
const badge = document.createElement('div');
143+
badge.style.cssText = "position:fixed;bottom:20px;right:20px;background:#10b981;color:white;padding:8px 16px;border-radius:999px;z-index:9999;font-family:system-ui;box-shadow:0 4px 6px rgba(0,0,0,0.1);font-weight:bold;";
144+
badge.innerText = "✓ FDD Synced to Disk";
145+
document.body.appendChild(badge);
146+
setTimeout(() => badge.remove(), 2500);
147+
} catch (e) {
148+
console.error("FDD Persist Error", e);
149+
}
95150
}
96151
}
97152

98-
// Bootstrap after parsing
99153
document.querySelectorAll('fdd-container').forEach(c => {
100-
try {
101-
new ExtensionFDD(c);
102-
} catch (e) {
103-
console.error("OpenFDD Rendering Error:", e);
104-
}
154+
try { new ExtensionFDD(c); } catch (e) { console.error("OpenFDD Error:", e); }
105155
});

0 commit comments

Comments
 (0)