@mahe_pkm/buzl-html-editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HANDBOOK.md ADDED
@@ -0,0 +1,234 @@
1
+ # Buzl HTML Editor Handbook
2
+
3
+ This handbook explains how to install and run Buzl HTML Editor in the root of an ordinary static HTML website. It is for Windows, macOS, and Linux users.
4
+
5
+ ## What Buzl does
6
+
7
+ `buzl-editor` starts one local server with two ways to work:
8
+
9
+ | Address | Use it for |
10
+ | --- | --- |
11
+ | `http://localhost:4000/` | Normal website preview with the floating **Edit Text** toolbar |
12
+ | `http://localhost:4000/admin/` | The full visual editor, page controls, and image upload tools |
13
+
14
+ `buzl-site` starts a clean website-only preview. It has no editor, edit toolbar, or editor API routes.
15
+
16
+ ## Before you start
17
+
18
+ You need Node.js 18 or newer. Node.js 20 LTS or newer is recommended.
19
+
20
+ ```powershell
21
+ node --version
22
+ npm --version
23
+ ```
24
+
25
+ Your website root is the folder that contains `index.html`. It normally also contains folders such as `assets`, `images`, `css`, or `js`.
26
+
27
+ ```text
28
+ client-website/
29
+ ├── index.html
30
+ ├── about.html
31
+ ├── assets/
32
+ ├── images/
33
+ └── css/
34
+ ```
35
+
36
+ Before editing, make a copy of the complete website folder or commit the current work to Git. Buzl makes page backups on save, but a full-site backup is still the simplest recovery point.
37
+
38
+ ## Install Buzl
39
+
40
+ ### Recommended: install for one website
41
+
42
+ Open a terminal in the website root, then run:
43
+
44
+ ```powershell
45
+ npm install --save-dev @mahe_pkm/buzl-html-editor
46
+ ```
47
+
48
+ Run it with `npx`, which always uses the version installed for this website:
49
+
50
+ ```powershell
51
+ npx buzl-editor
52
+ ```
53
+
54
+ ### Optional: install globally
55
+
56
+ Use this only when you want one command available for many websites on the same computer:
57
+
58
+ ```powershell
59
+ npm install -g @mahe_pkm/buzl-html-editor
60
+ ```
61
+
62
+ Then use `buzl-editor` and `buzl-site` directly instead of `npx buzl-editor` and `npx buzl-site`.
63
+
64
+ ## First-time checks
65
+
66
+ From the website root, create Buzl's optional local configuration and run diagnostics:
67
+
68
+ ```powershell
69
+ npx buzl-editor init
70
+ npx buzl-editor doctor
71
+ ```
72
+
73
+ `init` creates `.buzl/config.json`, `.env.example`, and safe Git-ignore rules only when they are missing. It does not replace website HTML. `doctor` checks that Buzl can find HTML pages and start safely.
74
+
75
+ ## Start the website and editor
76
+
77
+ ```powershell
78
+ npx buzl-editor
79
+ ```
80
+
81
+ The default addresses are:
82
+
83
+ ```text
84
+ Website preview: http://localhost:4000/
85
+ Advanced editor: http://localhost:4000/admin/
86
+ ```
87
+
88
+ Use a different port when `4000` is already in use:
89
+
90
+ ```powershell
91
+ npx buzl-editor --port 4173
92
+ ```
93
+
94
+ Then open:
95
+
96
+ ```text
97
+ http://localhost:4173/
98
+ http://localhost:4173/admin/
99
+ ```
100
+
101
+ To run a site while your terminal is in another folder, provide its root explicitly:
102
+
103
+ ```powershell
104
+ npx buzl-editor --root "C:\Websites\client-website" --port 4000
105
+ ```
106
+
107
+ By default, Buzl opens the normal website preview. To open the advanced editor instead:
108
+
109
+ ```powershell
110
+ npx buzl-editor --open-admin
111
+ ```
112
+
113
+ To prevent a browser tab opening automatically:
114
+
115
+ ```powershell
116
+ npx buzl-editor --no-open
117
+ ```
118
+
119
+ Press `Ctrl+C` in the same terminal window to stop the server.
120
+
121
+ ## Edit text from the normal website preview
122
+
123
+ 1. Open the normal preview URL, such as `http://localhost:4000/`.
124
+ 2. Use the floating toolbar in the lower-right corner.
125
+ 3. Select **Edit Text**.
126
+ 4. Click highlighted text, type your change, and inspect the result.
127
+ 5. Select **Undo** to reverse the latest edit, or **Cancel** to discard all unsaved edits.
128
+ 6. Select **Save** only when the page is correct.
129
+
130
+ The toolbar is available only from `buzl-editor` while it is bound to your local machine. Simply opening the preview or turning on Edit Text does not write the toolbar into your website files. Saving writes only the approved page content and creates a recoverable backup under `.buzl/backups/`.
131
+
132
+ Use the advanced editor when you need image upload, optimization, page controls, or richer editing tools.
133
+
134
+ ## Use the advanced editor
135
+
136
+ Open `http://localhost:4000/admin/`.
137
+
138
+ Use the page list to choose an HTML page. Make changes, preview the page, and save only after checking text, links, mobile layout, and images. Uploaded raster images are optimized to AVIF and saved under `assets/images/`; safe SVG images remain SVG.
139
+
140
+ ## Run a clean website-only preview
141
+
142
+ Use this when you want to show or test the local website without any editing controls:
143
+
144
+ ```powershell
145
+ npx buzl-site
146
+ ```
147
+
148
+ For a different port:
149
+
150
+ ```powershell
151
+ npx buzl-site --port 4173
152
+ ```
153
+
154
+ Website-only mode deliberately blocks `/admin/`, editor APIs, local configuration, dependencies, package files, and the Preview Live Edit toolbar.
155
+
156
+ ## macOS and Linux
157
+
158
+ The commands are the same in Terminal. Change into the website root first:
159
+
160
+ ```bash
161
+ cd "/path/to/client-website"
162
+ npm install --save-dev @mahe_pkm/buzl-html-editor
163
+ npx buzl-editor
164
+ ```
165
+
166
+ Stop the server with `Ctrl+C`.
167
+
168
+ ## Backups and rollback
169
+
170
+ Every saved HTML change gets a timestamped backup in `.buzl/backups/`. To recover, stop the editor, copy the required backup over the affected HTML file, then restart and check the page in a browser.
171
+
172
+ For an entire website rollback, restore the full copy or Git commit made before editing. Do not delete current work until the restored copy has been checked.
173
+
174
+ ## Troubleshooting
175
+
176
+ ### `buzl-editor` is not recognized
177
+
178
+ Use the project-local command:
179
+
180
+ ```powershell
181
+ npx buzl-editor
182
+ ```
183
+
184
+ Or reinstall the global package and reopen the terminal:
185
+
186
+ ```powershell
187
+ npm install -g @mahe_pkm/buzl-html-editor
188
+ ```
189
+
190
+ ### The browser shows `Cannot GET /`
191
+
192
+ Confirm you started Buzl from the folder containing `index.html`. If the website has no root `index.html`, open the actual page URL or add the correct entry page before starting.
193
+
194
+ ### Port already in use
195
+
196
+ Stop the earlier server with `Ctrl+C`, or choose another port:
197
+
198
+ ```powershell
199
+ npx buzl-editor --port 4173
200
+ ```
201
+
202
+ ### The Edit Text toolbar is missing
203
+
204
+ Use `buzl-editor`, not `buzl-site`, and open the local website URL from the terminal output. The toolbar is intentionally disabled on a network-bound server.
205
+
206
+ ### Images or styles are missing
207
+
208
+ Start from the true website root and use the local HTTP address. Do not open pages directly with `file://`, because relative asset paths and scripts can behave differently.
209
+
210
+ ## Update Buzl
211
+
212
+ For a website-local installation:
213
+
214
+ ```powershell
215
+ npm update @mahe_pkm/buzl-html-editor
216
+ ```
217
+
218
+ For a global installation:
219
+
220
+ ```powershell
221
+ npm update -g @mahe_pkm/buzl-html-editor
222
+ ```
223
+
224
+ Back up the website, run `doctor`, start the editor, and test the website preview and `/admin/` after every update.
225
+
226
+ ## Safety checklist before deployment
227
+
228
+ - Confirm every changed page in a browser.
229
+ - Test navigation, forms, images, and mobile layout.
230
+ - Keep a dated full-site backup.
231
+ - Deploy with your existing hosting or Git workflow only after local review.
232
+ - Never upload `.env` files or API keys.
233
+
234
+ This version supports ordinary static HTML, CSS, JavaScript, and multi-page websites. Server-rendered and build-system websites require a separate integration workflow.
package/README.md CHANGED
@@ -1,12 +1,17 @@
1
1
  # Buzl HTML Editor
2
2
 
3
3
  A local visual editor and static-site server for ordinary multi-page HTML, CSS,
4
- and JavaScript websites. It includes Live Edit, image uploads, AVIF image
4
+ and JavaScript websites. It includes on-page Live Edit, image uploads, AVIF image
5
5
  optimization, AI-assisted text, and AI image generation.
6
6
 
7
7
  The server is local-only by default. It edits the website folder from which it
8
8
  is started; the editor's own files stay inside the npm package.
9
9
 
10
+ ## Documentation
11
+
12
+ - Full setup handbook: [HANDBOOK.md](./HANDBOOK.md)
13
+ - Printable handbook: [docs/Buzl-HTML-Editor-Handbook.pdf](./docs/Buzl-HTML-Editor-Handbook.pdf)
14
+
10
15
  ## Requirements
11
16
 
12
17
  - Node.js 18 or newer
@@ -29,8 +34,19 @@ npx buzl-editor
29
34
 
30
35
  Default addresses:
31
36
 
32
- - Website: `http://localhost:4000/`
33
- - Editor: `http://localhost:4000/admin/`
37
+ - Website with a floating **Edit Text** toolbar: `http://localhost:4000/`
38
+ - Advanced editor: `http://localhost:4000/admin/`
39
+
40
+ `buzl-editor` opens the website preview by default. Select **Edit Text** to edit
41
+ visible text directly on any page, then use **Save**, **Undo**, or **Cancel**.
42
+ The toolbar is injected only while the local editor server is running and is
43
+ never added to the saved website.
44
+
45
+ Open the advanced editor by default when needed:
46
+
47
+ ```bash
48
+ npx buzl-editor --open-admin
49
+ ```
34
50
 
35
51
  Use another port or website folder when needed:
36
52
 
@@ -52,8 +68,8 @@ npx buzl-site
52
68
  npx buzl-site --port 3500
53
69
  ```
54
70
 
55
- Website-only mode blocks the editor, APIs, dependencies, configuration,
56
- development files, and local secrets.
71
+ Website-only mode does not inject the editing toolbar and blocks the editor,
72
+ APIs, dependencies, configuration, development files, and local secrets.
57
73
 
58
74
  ## Setup and diagnostics
59
75
 
@@ -106,6 +122,8 @@ actions require them.
106
122
  - Non-local binding requires both `--host` and `--allow-network`.
107
123
  - The editor can only load and save HTML files inside the selected website.
108
124
  - Each save creates a recoverable copy under `.buzl/backups/`.
125
+ - Preview Live Edit uses short-lived page tokens, rejects stale-page saves, and
126
+ is disabled when the editor binds to a non-local network address.
109
127
  - Uploaded raster images are optimized to AVIF under `assets/images/`.
110
128
  - `.env`, `.git`, `.buzl`, `node_modules`, tests, package metadata, and server
111
129
  source are not publicly served.
@@ -31,7 +31,8 @@ Options:
31
31
  --port, -p Local port (default: 4000)
32
32
  --host <address> Bind address (default: 127.0.0.1)
33
33
  --allow-network Required for a non-local bind address
34
- --open Open the editor in the default browser
34
+ --open Open the website preview in the default browser
35
+ --open-admin Open the advanced /admin editor instead
35
36
  --no-open Do not open a browser
36
37
  --version, -v Show package version
37
38
  --help, -h Show this help
@@ -87,7 +88,7 @@ async function main() {
87
88
  console.log(`Editor: ${editorUrl}`);
88
89
  console.log(`Root: ${options.root}`);
89
90
  console.log('Press Ctrl+C to stop.\n');
90
- if (options.open) openBrowser(editorUrl);
91
+ if (options.open) openBrowser(options.openAdmin ? editorUrl : siteUrl);
91
92
  });
92
93
 
93
94
  const stop = () => server.close(() => process.exit(0));
@@ -0,0 +1,114 @@
1
+ :host {
2
+ all: initial;
3
+ }
4
+
5
+ .buzl-toolbar {
6
+ align-items: center;
7
+ background: #211717;
8
+ border: 1px solid rgba(212, 175, 55, 0.55);
9
+ border-radius: 14px;
10
+ bottom: 18px;
11
+ box-shadow: 0 14px 42px rgba(0, 0, 0, 0.35);
12
+ color: #f8f4eb;
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ font: 600 13px/1.2 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
16
+ gap: 8px;
17
+ max-width: calc(100vw - 32px);
18
+ padding: 9px;
19
+ position: fixed;
20
+ right: 18px;
21
+ z-index: 2147483647;
22
+ }
23
+
24
+ .buzl-toolbar button,
25
+ .buzl-toolbar a {
26
+ align-items: center;
27
+ background: transparent;
28
+ border: 1px solid rgba(255, 255, 255, 0.2);
29
+ border-radius: 9px;
30
+ box-sizing: border-box;
31
+ color: inherit;
32
+ cursor: pointer;
33
+ display: inline-flex;
34
+ font: inherit;
35
+ min-height: 36px;
36
+ padding: 8px 12px;
37
+ text-decoration: none;
38
+ }
39
+
40
+ .buzl-toolbar button:hover,
41
+ .buzl-toolbar a:hover {
42
+ border-color: #d4af37;
43
+ color: #f5d975;
44
+ }
45
+
46
+ .buzl-toolbar button:focus-visible,
47
+ .buzl-toolbar a:focus-visible {
48
+ outline: 3px solid rgba(212, 175, 55, 0.35);
49
+ outline-offset: 2px;
50
+ }
51
+
52
+ .buzl-toolbar button:disabled {
53
+ cursor: not-allowed;
54
+ opacity: 0.45;
55
+ }
56
+
57
+ .buzl-toolbar .buzl-primary {
58
+ background: #d4af37;
59
+ border-color: #d4af37;
60
+ color: #211717;
61
+ }
62
+
63
+ .buzl-toolbar .buzl-primary:hover {
64
+ background: #ebca5b;
65
+ color: #211717;
66
+ }
67
+
68
+ .buzl-toolbar .buzl-status {
69
+ color: #d9d0c7;
70
+ font-weight: 500;
71
+ max-width: 260px;
72
+ overflow: hidden;
73
+ padding: 0 5px;
74
+ text-overflow: ellipsis;
75
+ white-space: nowrap;
76
+ }
77
+
78
+ .buzl-toolbar .buzl-status[data-tone="success"] {
79
+ color: #89e6aa;
80
+ }
81
+
82
+ .buzl-toolbar .buzl-status[data-tone="warning"] {
83
+ color: #f5d975;
84
+ }
85
+
86
+ .buzl-toolbar .buzl-status[data-tone="error"] {
87
+ color: #ff9c9c;
88
+ }
89
+
90
+ @media (max-width: 680px) {
91
+ .buzl-toolbar {
92
+ bottom: 10px;
93
+ left: 10px;
94
+ right: 10px;
95
+ }
96
+
97
+ .buzl-toolbar .buzl-status {
98
+ flex: 1 1 100%;
99
+ max-width: none;
100
+ order: -1;
101
+ }
102
+ }
103
+
104
+ body.buzl-public-editing [data-buzl-public-edit="true"] {
105
+ cursor: text !important;
106
+ outline: 2px dashed rgba(212, 175, 55, 0.85) !important;
107
+ outline-offset: 3px !important;
108
+ }
109
+
110
+ body.buzl-public-editing [data-buzl-public-edit="true"]:focus {
111
+ background: rgba(212, 175, 55, 0.13) !important;
112
+ outline-style: solid !important;
113
+ }
114
+
@@ -0,0 +1,267 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const runtime = document.currentScript
5
+ || document.querySelector('script[data-buzl-live-edit-runtime]');
6
+ if (!runtime || !runtime.dataset.buzlPageToken) return;
7
+
8
+ const pageToken = runtime.dataset.buzlPageToken;
9
+ const pagePath = runtime.dataset.buzlPagePath || window.location.pathname;
10
+ const editableSelector = '[data-buzl-public-edit-key]';
11
+ const originals = new Map();
12
+ const histories = new Map();
13
+ const dirtyKeys = new Set();
14
+ let editMode = false;
15
+ let activeKey = null;
16
+ let lastChangedKey = null;
17
+ let saving = false;
18
+
19
+ const host = document.createElement('div');
20
+ host.setAttribute('data-buzl-public-editor-ui', 'true');
21
+ const shadow = host.attachShadow({ mode: 'open' });
22
+ const stylesheet = document.createElement('link');
23
+ stylesheet.rel = 'stylesheet';
24
+ stylesheet.href = '/__buzl/live-edit.css';
25
+ shadow.appendChild(stylesheet);
26
+
27
+ const toolbar = document.createElement('div');
28
+ toolbar.className = 'buzl-toolbar';
29
+ toolbar.setAttribute('role', 'toolbar');
30
+ toolbar.setAttribute('aria-label', 'Buzl page editor');
31
+ toolbar.innerHTML = `
32
+ <span class="buzl-status" data-status title="${escapeAttribute(pagePath)}">Ready to edit this page</span>
33
+ <button class="buzl-primary" type="button" data-toggle aria-pressed="false">Edit Text</button>
34
+ <button type="button" data-save disabled>Save</button>
35
+ <button type="button" data-undo disabled>Undo</button>
36
+ <button type="button" data-cancel disabled>Cancel</button>
37
+ <a href="/admin/" data-advanced>Advanced Editor</a>
38
+ `;
39
+ shadow.appendChild(toolbar);
40
+ document.documentElement.appendChild(host);
41
+
42
+ const toggleButton = shadow.querySelector('[data-toggle]');
43
+ const saveButton = shadow.querySelector('[data-save]');
44
+ const undoButton = shadow.querySelector('[data-undo]');
45
+ const cancelButton = shadow.querySelector('[data-cancel]');
46
+ const statusLabel = shadow.querySelector('[data-status]');
47
+
48
+ function escapeAttribute(value) {
49
+ return String(value)
50
+ .replace(/&/g, '&amp;')
51
+ .replace(/"/g, '&quot;')
52
+ .replace(/</g, '&lt;')
53
+ .replace(/>/g, '&gt;');
54
+ }
55
+
56
+ function setStatus(message, tone = '') {
57
+ statusLabel.textContent = message;
58
+ if (tone) statusLabel.dataset.tone = tone;
59
+ else delete statusLabel.dataset.tone;
60
+ }
61
+
62
+ function visibleEditableElements() {
63
+ return Array.from(document.querySelectorAll(editableSelector)).filter((element) => {
64
+ if (!element.textContent || !element.textContent.trim()) return false;
65
+ if (element.closest('[data-buzl-public-editor-ui], [aria-hidden="true"], svg, script, style, noscript, input, textarea, select')) {
66
+ return false;
67
+ }
68
+ const styles = window.getComputedStyle(element);
69
+ const box = element.getBoundingClientRect();
70
+ return styles.display !== 'none'
71
+ && styles.visibility !== 'hidden'
72
+ && box.width > 0
73
+ && box.height > 0;
74
+ });
75
+ }
76
+
77
+ function ensureOriginal(element) {
78
+ const key = element.dataset.buzlPublicEditKey;
79
+ if (!originals.has(key)) originals.set(key, element.innerHTML);
80
+ if (!histories.has(key)) histories.set(key, []);
81
+ return key;
82
+ }
83
+
84
+ function changedElements() {
85
+ return Array.from(document.querySelectorAll(editableSelector)).filter((element) => (
86
+ dirtyKeys.has(element.dataset.buzlPublicEditKey)
87
+ ));
88
+ }
89
+
90
+ function updateControls() {
91
+ const changed = changedElements();
92
+ const dirty = changed.length > 0;
93
+ toggleButton.textContent = editMode ? 'Editing On' : 'Edit Text';
94
+ toggleButton.setAttribute('aria-pressed', String(editMode));
95
+ saveButton.disabled = !dirty || saving;
96
+ cancelButton.disabled = !dirty || saving;
97
+ const history = activeKey ? histories.get(activeKey) : histories.get(lastChangedKey);
98
+ undoButton.disabled = !history || history.length === 0 || saving;
99
+ if (dirty && !saving) setStatus(`${changed.length} unsaved change${changed.length === 1 ? '' : 's'}`, 'warning');
100
+ if (!dirty && !saving) setStatus(editMode ? 'Click highlighted text to edit' : 'Ready to edit this page');
101
+ }
102
+
103
+ function setEditMode(nextMode) {
104
+ editMode = Boolean(nextMode);
105
+ document.body.classList.toggle('buzl-public-editing', editMode);
106
+ for (const element of document.querySelectorAll(editableSelector)) {
107
+ if (editMode) {
108
+ element.setAttribute('contenteditable', 'true');
109
+ element.setAttribute('spellcheck', 'true');
110
+ element.setAttribute('data-buzl-public-edit', 'true');
111
+ } else {
112
+ element.removeAttribute('contenteditable');
113
+ element.removeAttribute('spellcheck');
114
+ element.removeAttribute('data-buzl-public-edit');
115
+ }
116
+ }
117
+ sessionStorage.setItem('buzl-live-edit-enabled', editMode ? '1' : '0');
118
+ updateControls();
119
+ }
120
+
121
+ function restoreOriginals() {
122
+ for (const key of dirtyKeys) {
123
+ const element = document.querySelector(`[data-buzl-public-edit-key="${CSS.escape(key)}"]`);
124
+ if (element && originals.has(key)) element.innerHTML = originals.get(key);
125
+ histories.set(key, []);
126
+ }
127
+ dirtyKeys.clear();
128
+ activeKey = null;
129
+ lastChangedKey = null;
130
+ }
131
+
132
+ async function saveChanges() {
133
+ if (saving) return;
134
+ const changed = changedElements();
135
+ if (!changed.length) return;
136
+
137
+ saving = true;
138
+ updateControls();
139
+ setStatus('Saving…', 'warning');
140
+ try {
141
+ const response = await fetch('/__buzl/live-edit/save', {
142
+ method: 'POST',
143
+ credentials: 'same-origin',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({
146
+ pageToken,
147
+ changes: changed.map((element) => ({
148
+ key: element.dataset.buzlPublicEditKey,
149
+ html: element.innerHTML,
150
+ })),
151
+ }),
152
+ });
153
+ const result = await response.json().catch(() => ({}));
154
+ if (!response.ok) throw new Error(result.error || 'Could not save this page');
155
+
156
+ const savedChanges = new Map(
157
+ Array.isArray(result.savedChanges)
158
+ ? result.savedChanges.map((change) => [String(change.key), String(change.html)])
159
+ : [],
160
+ );
161
+ for (const element of changed) {
162
+ const key = ensureOriginal(element);
163
+ if (savedChanges.has(key)) element.innerHTML = savedChanges.get(key);
164
+ originals.set(key, element.innerHTML);
165
+ histories.set(key, []);
166
+ dirtyKeys.delete(key);
167
+ }
168
+ activeKey = null;
169
+ lastChangedKey = null;
170
+ const successMessage = `Saved ${result.savedCount || changed.length} change${changed.length === 1 ? '' : 's'}`;
171
+ setStatus(successMessage, 'success');
172
+ setTimeout(() => {
173
+ if (!changedElements().length) updateControls();
174
+ }, 1800);
175
+ } catch (error) {
176
+ setStatus(error.message || 'Save failed', 'error');
177
+ } finally {
178
+ saving = false;
179
+ updateControls();
180
+ if (!changedElements().length) {
181
+ setStatus('Saved successfully', 'success');
182
+ }
183
+ }
184
+ }
185
+
186
+ toggleButton.addEventListener('click', () => setEditMode(!editMode));
187
+ saveButton.addEventListener('click', saveChanges);
188
+ cancelButton.addEventListener('click', () => {
189
+ if (!changedElements().length || window.confirm('Discard all unsaved text changes on this page?')) {
190
+ restoreOriginals();
191
+ setEditMode(false);
192
+ setStatus('Changes cancelled');
193
+ }
194
+ });
195
+ undoButton.addEventListener('click', () => {
196
+ const key = activeKey || lastChangedKey;
197
+ if (!key) return;
198
+ const element = document.querySelector(`[data-buzl-public-edit-key="${CSS.escape(key)}"]`);
199
+ const history = histories.get(key) || [];
200
+ if (!element || !history.length) return;
201
+ element.innerHTML = history.pop();
202
+ activeKey = key;
203
+ if (element.innerHTML === originals.get(key)) dirtyKeys.delete(key);
204
+ else dirtyKeys.add(key);
205
+ updateControls();
206
+ element.focus();
207
+ });
208
+
209
+ document.addEventListener('focusin', (event) => {
210
+ const element = event.target.closest && event.target.closest('[data-buzl-public-edit="true"]');
211
+ if (element) {
212
+ const key = element.dataset.buzlPublicEditKey;
213
+ if (!dirtyKeys.has(key)) {
214
+ originals.set(key, element.innerHTML);
215
+ histories.set(key, []);
216
+ }
217
+ activeKey = key;
218
+ }
219
+ }, true);
220
+
221
+ document.addEventListener('beforeinput', (event) => {
222
+ const element = event.target.closest && event.target.closest('[data-buzl-public-edit="true"]');
223
+ if (!editMode || !element) return;
224
+ const key = ensureOriginal(element);
225
+ const history = histories.get(key);
226
+ if (history.at(-1) !== element.innerHTML) history.push(element.innerHTML);
227
+ if (history.length > 30) history.shift();
228
+ activeKey = key;
229
+ lastChangedKey = key;
230
+ }, true);
231
+
232
+ document.addEventListener('input', (event) => {
233
+ const element = event.target.closest && event.target.closest('[data-buzl-public-edit="true"]');
234
+ if (!editMode || !element) return;
235
+ lastChangedKey = ensureOriginal(element);
236
+ if (element.innerHTML === originals.get(lastChangedKey)) dirtyKeys.delete(lastChangedKey);
237
+ else dirtyKeys.add(lastChangedKey);
238
+ updateControls();
239
+ }, true);
240
+
241
+ document.addEventListener('click', (event) => {
242
+ if (!editMode) return;
243
+ const editable = event.target.closest && event.target.closest('[data-buzl-public-edit="true"]');
244
+ if (!editable) return;
245
+ if (editable.closest('a, button, summary')) event.preventDefault();
246
+ event.stopPropagation();
247
+ }, true);
248
+
249
+ document.addEventListener('keydown', (event) => {
250
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
251
+ event.preventDefault();
252
+ saveChanges();
253
+ }
254
+ if (editMode && event.key === 'Escape' && document.activeElement) {
255
+ document.activeElement.blur();
256
+ }
257
+ }, true);
258
+
259
+ window.addEventListener('beforeunload', (event) => {
260
+ if (!changedElements().length) return;
261
+ event.preventDefault();
262
+ event.returnValue = '';
263
+ });
264
+
265
+ if (sessionStorage.getItem('buzl-live-edit-enabled') === '1') setEditMode(true);
266
+ else updateControls();
267
+ })();