@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 +234 -0
- package/README.md +23 -5
- package/bin/buzl-editor.js +3 -2
- package/dist/public/public-live-edit.css +114 -0
- package/dist/public/public-live-edit.js +267 -0
- package/dist/server.js +1290 -1018
- package/docs/Buzl-HTML-Editor-Handbook.pdf +213 -0
- package/lib/cli.js +2 -0
- package/package.json +3 -1
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
|
-
-
|
|
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
|
|
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.
|
package/bin/buzl-editor.js
CHANGED
|
@@ -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
|
|
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, '&')
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.replace(/</g, '<')
|
|
53
|
+
.replace(/>/g, '>');
|
|
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
|
+
})();
|