|
1 | 1 | console.log("OpenFDD: Extension Context Injected. Security Shield active."); |
2 | 2 |
|
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 | + |
5 | 5 | 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; |
8 | 8 |
|
9 | | - // Clear the raw text DOM |
10 | 9 | document.body.innerHTML = ''; |
11 | | - document.documentElement.style.background = '#e5e5e5'; // default fallback background |
| 10 | + document.documentElement.style.background = '#e5e5e5'; |
12 | 11 |
|
13 | | - // Parse the raw payload |
14 | 12 | const parser = new DOMParser(); |
15 | | - const doc = parser.parseFromString(rawHtml, "text/html"); |
| 13 | + originalDocParser = parser.parseFromString(rawPayloadString, "text/html"); |
16 | 14 |
|
17 | | - // Securely extract only the fdd-container |
18 | | - const fddWrapper = doc.querySelector('fdd-container'); |
| 15 | + const fddWrapper = originalDocParser.querySelector('fdd-container'); |
19 | 16 | 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)); |
25 | 18 | } else { |
26 | | - document.body.innerHTML = '<div style="padding: 2rem; color: #991b1b; font-family: sans-serif;"><h2>Error: Invalid FDD File</h2><p>No valid <fdd-container> 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>'; |
27 | 20 | } |
28 | 21 | } |
29 | 22 |
|
30 | | -// The Extension Execution Logic |
31 | 23 | class ExtensionFDD { |
32 | 24 | constructor(container) { |
33 | 25 | this.container = container; |
34 | 26 | this.template = container.querySelector('template[bind]'); |
35 | 27 | this.vcData = {}; |
36 | 28 | this.mutableData = {}; |
| 29 | + this.fileHandle = null; |
37 | 30 | if (!this.template) throw new Error("No bindings template found"); |
38 | 31 | this.init(); |
39 | 32 | } |
40 | 33 |
|
41 | 34 | init() { |
42 | | - // 1. Extract read-only verifiable data |
43 | 35 | const vcScript = this.container.querySelector('script[type="application/vc+json"]'); |
44 | 36 | if (vcScript) { |
45 | 37 | const payload = JSON.parse(vcScript.textContent || '{}'); |
46 | 38 | this.vcData = payload.credentialSubject ? payload.credentialSubject : payload; |
47 | 39 | } |
48 | 40 |
|
49 | | - // 2. Extract mutable state data |
50 | 41 | const mutableScript = this.container.querySelector('script[type="application/json"]'); |
51 | 42 | if (mutableScript) this.mutableData = JSON.parse(mutableScript.textContent || '{}'); |
52 | 43 |
|
53 | | - // 3. Simple Handlebars-style template binding setup |
54 | 44 | const context = { ...this.vcData, ...this.mutableData }; |
55 | 45 | 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}} |
56 | 60 | html = html.replace(/\{\{(.*?)\}\}/g, (match, key) => { |
57 | 61 | const k = key.trim(); |
58 | 62 | return (context[k] !== undefined && context[k] !== null) ? context[k] : ''; |
59 | 63 | }); |
60 | 64 |
|
61 | | - // 4. Attach to Declarative Shadow DOM for isolated presentation |
62 | 65 | const shadowRoot = this.container.attachShadow({ mode: 'open' }); |
63 | 66 | shadowRoot.innerHTML = html; |
64 | 67 |
|
65 | 68 | 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 | + }); |
66 | 90 | } |
67 | 91 |
|
68 | 92 | setupAutoSave(root) { |
69 | 93 | const inputs = root.querySelectorAll('[autosave]'); |
70 | 94 | 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) => { |
72 | 117 | const path = e.target.name.split('.'); |
73 | | - if (path[0] === 'user-notes' && path[1]) { |
| 118 | + if (path[0] === 'user-notes' && path[1] && this.fileHandle) { |
74 | 119 | this.mutableData[path[1]] = e.target.value; |
75 | | - this.syncFile(); |
| 120 | + await this.syncFile(); |
76 | 121 | } |
77 | 122 | }); |
78 | 123 | }); |
79 | 124 | } |
80 | 125 |
|
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"]'); |
85 | 132 | if (mutableScript) { |
86 | | - mutableScript.textContent = JSON.stringify(this.mutableData, null, 2); |
| 133 | + mutableScript.textContent = "\n " + JSON.stringify(this.mutableData, null, 2) + "\n "; |
87 | 134 | } |
88 | 135 |
|
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 | + } |
95 | 150 | } |
96 | 151 | } |
97 | 152 |
|
98 | | -// Bootstrap after parsing |
99 | 153 | 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); } |
105 | 155 | }); |
0 commit comments