webspresso 0.0.76 → 0.0.78

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.
Files changed (35) hide show
  1. package/README.md +6 -1
  2. package/bin/commands/db-scaffold.js +81 -0
  3. package/bin/utils/model-migrations.js +211 -0
  4. package/bin/webspresso.js +2 -0
  5. package/core/content/cache.js +64 -0
  6. package/core/content/field-types.js +180 -0
  7. package/core/content/index.js +30 -0
  8. package/core/content/renderer.js +84 -0
  9. package/core/content/schema.js +75 -0
  10. package/core/content/service.js +400 -0
  11. package/core/content/types.js +59 -0
  12. package/index.d.ts +17 -0
  13. package/index.js +7 -0
  14. package/package.json +1 -1
  15. package/plugins/admin-panel/app.js +7 -7
  16. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +41 -0
  17. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +99 -15
  18. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +2 -2
  19. package/plugins/admin-panel/field-renderers/file-upload.js +108 -27
  20. package/plugins/admin-panel/index.js +17 -18
  21. package/plugins/admin-panel/modules/menu.js +1 -0
  22. package/plugins/content/admin/content-entries-component.js +291 -0
  23. package/plugins/content/admin/content-types-component.js +250 -0
  24. package/plugins/content/api-handlers.js +157 -0
  25. package/plugins/content/client/inline-edit.css +296 -0
  26. package/plugins/content/client/inline-edit.js +366 -0
  27. package/plugins/content/helpers.js +77 -0
  28. package/plugins/content/index.js +231 -0
  29. package/plugins/content/migration-template.js +54 -0
  30. package/plugins/content/models/content-entry.js +45 -0
  31. package/plugins/content/models/content-type.js +36 -0
  32. package/plugins/index.js +2 -0
  33. package/src/file-router.js +21 -1
  34. package/templates/skills/webspresso-usage/REFERENCE-framework.md +1 -1
  35. package/templates/skills/webspresso-usage/SKILL.md +5 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Inline edit — popover editor for admin users on public pages.
3
+ */
4
+ .ws-content-host,
5
+ .ws-content-block {
6
+ position: relative;
7
+ margin-block: 0.25rem;
8
+ }
9
+
10
+ .ws-content-toolbar {
11
+ display: flex;
12
+ justify-content: flex-end;
13
+ margin-bottom: 0.375rem;
14
+ min-height: 1.75rem;
15
+ }
16
+
17
+ .ws-content-edit-btn {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ gap: 0.35rem;
21
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
22
+ font-size: 0.75rem;
23
+ font-weight: 600;
24
+ line-height: 1;
25
+ padding: 0.35rem 0.65rem;
26
+ border-radius: 9999px;
27
+ border: 1px solid #bfdbfe;
28
+ background: #eff6ff;
29
+ color: #1d4ed8;
30
+ cursor: pointer;
31
+ box-shadow: 0 1px 2px rgba(37, 99, 235, 0.12);
32
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
33
+ }
34
+
35
+ .ws-content-edit-btn:hover {
36
+ background: #dbeafe;
37
+ border-color: #93c5fd;
38
+ box-shadow: 0 2px 6px rgba(37, 99, 235, 0.18);
39
+ }
40
+
41
+ .ws-content-edit-btn:focus-visible {
42
+ outline: 2px solid #2563eb;
43
+ outline-offset: 2px;
44
+ }
45
+
46
+ .ws-content-edit-btn-icon {
47
+ font-size: 0.85em;
48
+ line-height: 1;
49
+ }
50
+
51
+ .ws-content-editable {
52
+ outline: 1px dashed transparent;
53
+ outline-offset: 3px;
54
+ border-radius: 0.2rem;
55
+ transition: outline-color 0.15s, background-color 0.15s;
56
+ }
57
+
58
+ .ws-content-host:hover .ws-content-editable,
59
+ .ws-content-host.is-editing .ws-content-editable {
60
+ outline-color: rgba(37, 99, 235, 0.4);
61
+ background: rgba(37, 99, 235, 0.05);
62
+ }
63
+
64
+ /* Editor shell — id kept for compatibility */
65
+ #ws-content-modal {
66
+ display: none;
67
+ position: fixed;
68
+ inset: 0;
69
+ z-index: 99999;
70
+ pointer-events: none;
71
+ }
72
+
73
+ #ws-content-modal.is-open {
74
+ display: block;
75
+ }
76
+
77
+ .ws-content-modal-backdrop {
78
+ position: absolute;
79
+ inset: 0;
80
+ background: rgba(15, 23, 42, 0.2);
81
+ pointer-events: auto;
82
+ }
83
+
84
+ .ws-content-modal-panel {
85
+ position: fixed;
86
+ z-index: 1;
87
+ width: min(22rem, calc(100vw - 1.5rem));
88
+ max-height: min(24rem, calc(100vh - 2rem));
89
+ display: flex;
90
+ flex-direction: column;
91
+ background: #fff;
92
+ border: 1px solid #e2e8f0;
93
+ border-radius: 0.75rem;
94
+ box-shadow:
95
+ 0 4px 6px -1px rgba(15, 23, 42, 0.08),
96
+ 0 16px 32px -8px rgba(15, 23, 42, 0.18);
97
+ pointer-events: auto;
98
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
99
+ font-size: 0.875rem;
100
+ color: #0f172a;
101
+ box-sizing: border-box;
102
+ }
103
+
104
+ .ws-content-modal-panel *,
105
+ .ws-content-modal-panel *::before,
106
+ .ws-content-modal-panel *::after {
107
+ box-sizing: border-box;
108
+ }
109
+
110
+ .ws-content-popover-arrow {
111
+ position: absolute;
112
+ width: 12px;
113
+ height: 12px;
114
+ background: #fff;
115
+ border: 1px solid #e2e8f0;
116
+ transform: rotate(45deg);
117
+ top: -7px;
118
+ left: 50%;
119
+ margin-left: -6px;
120
+ pointer-events: none;
121
+ }
122
+
123
+ .ws-content-modal-panel.is-above .ws-content-popover-arrow {
124
+ top: auto;
125
+ bottom: -7px;
126
+ border-top-color: transparent;
127
+ border-left-color: transparent;
128
+ }
129
+
130
+ .ws-content-modal-panel:not(.is-above) .ws-content-popover-arrow {
131
+ border-bottom-color: transparent;
132
+ border-right-color: transparent;
133
+ }
134
+
135
+ .ws-content-modal-header {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ gap: 0.75rem;
140
+ padding: 0.75rem 1rem;
141
+ border-bottom: 1px solid #f1f5f9;
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ .ws-content-modal-title {
146
+ margin: 0;
147
+ font-size: 0.875rem;
148
+ font-weight: 600;
149
+ line-height: 1.3;
150
+ color: #0f172a;
151
+ }
152
+
153
+ .ws-content-modal-close {
154
+ flex-shrink: 0;
155
+ width: 1.75rem;
156
+ height: 1.75rem;
157
+ display: inline-flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ border: none;
161
+ border-radius: 0.375rem;
162
+ background: transparent;
163
+ font-size: 1.25rem;
164
+ line-height: 1;
165
+ cursor: pointer;
166
+ color: #64748b;
167
+ }
168
+
169
+ .ws-content-modal-close:hover {
170
+ background: #f1f5f9;
171
+ color: #334155;
172
+ }
173
+
174
+ .ws-content-modal-body {
175
+ padding: 0.75rem 1rem;
176
+ overflow: auto;
177
+ flex: 1;
178
+ min-height: 0;
179
+ }
180
+
181
+ .ws-content-field {
182
+ margin-bottom: 0.75rem;
183
+ }
184
+
185
+ .ws-content-field:last-child {
186
+ margin-bottom: 0;
187
+ }
188
+
189
+ .ws-content-field label {
190
+ display: block;
191
+ font-size: 0.75rem;
192
+ font-weight: 600;
193
+ margin-bottom: 0.3rem;
194
+ color: #475569;
195
+ letter-spacing: 0.01em;
196
+ }
197
+
198
+ .ws-content-field input,
199
+ .ws-content-field textarea,
200
+ .ws-content-field select {
201
+ width: 100%;
202
+ border: 1px solid #cbd5e1;
203
+ border-radius: 0.5rem;
204
+ padding: 0.5rem 0.625rem;
205
+ font: inherit;
206
+ font-size: 0.875rem;
207
+ color: #0f172a;
208
+ background: #fff;
209
+ transition: border-color 0.15s, box-shadow 0.15s;
210
+ }
211
+
212
+ .ws-content-field input:focus,
213
+ .ws-content-field textarea:focus,
214
+ .ws-content-field select:focus {
215
+ outline: none;
216
+ border-color: #3b82f6;
217
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
218
+ }
219
+
220
+ .ws-content-field textarea {
221
+ resize: vertical;
222
+ min-height: 4.5rem;
223
+ }
224
+
225
+ .ws-content-modal-error {
226
+ padding: 0 1rem 0.5rem;
227
+ color: #b91c1c;
228
+ font-size: 0.8125rem;
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ .ws-content-modal-footer {
233
+ display: flex;
234
+ justify-content: flex-end;
235
+ gap: 0.5rem;
236
+ padding: 0.625rem 1rem 0.875rem;
237
+ border-top: 1px solid #f1f5f9;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ .ws-content-btn {
242
+ border-radius: 0.5rem;
243
+ padding: 0.45rem 0.875rem;
244
+ font-size: 0.8125rem;
245
+ font-weight: 600;
246
+ cursor: pointer;
247
+ border: 1px solid transparent;
248
+ font-family: inherit;
249
+ }
250
+
251
+ .ws-content-btn-muted {
252
+ background: #f8fafc;
253
+ border-color: #e2e8f0;
254
+ color: #475569;
255
+ }
256
+
257
+ .ws-content-btn-muted:hover {
258
+ background: #f1f5f9;
259
+ }
260
+
261
+ .ws-content-btn-primary {
262
+ background: #2563eb;
263
+ color: #fff;
264
+ }
265
+
266
+ .ws-content-btn-primary:hover:not(:disabled) {
267
+ background: #1d4ed8;
268
+ }
269
+
270
+ .ws-content-btn-primary:disabled {
271
+ opacity: 0.6;
272
+ cursor: not-allowed;
273
+ }
274
+
275
+ /* Mobile: bottom sheet */
276
+ @media (max-width: 639px) {
277
+ .ws-content-modal-panel.is-sheet {
278
+ left: 0 !important;
279
+ right: 0 !important;
280
+ bottom: 0 !important;
281
+ top: auto !important;
282
+ width: 100% !important;
283
+ max-width: none;
284
+ max-height: min(88vh, 32rem);
285
+ border-radius: 1rem 1rem 0 0;
286
+ border-bottom: none;
287
+ }
288
+
289
+ .ws-content-modal-panel.is-sheet .ws-content-popover-arrow {
290
+ display: none;
291
+ }
292
+
293
+ .ws-content-modal-backdrop {
294
+ background: rgba(15, 23, 42, 0.35);
295
+ }
296
+ }
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Inline content editing for admin users on public pages.
3
+ * Popover anchored to the edit trigger — responsive bottom sheet on small screens.
4
+ */
5
+ (function () {
6
+ 'use strict';
7
+
8
+ var config = window.__WS_CONTENT__;
9
+ if (!config || !config.enabled) return;
10
+
11
+ var API = config.adminPath + '/api/content';
12
+ var modalEl = null;
13
+ var activeEntry = null;
14
+ var activeSchema = null;
15
+ var activeAnchor = null;
16
+ var activeHost = null;
17
+ var formState = {};
18
+ var repositionBound = false;
19
+
20
+ function qs(sel, root) {
21
+ return (root || document).querySelector(sel);
22
+ }
23
+
24
+ function qsa(sel, root) {
25
+ return Array.prototype.slice.call((root || document).querySelectorAll(sel));
26
+ }
27
+
28
+ function ensureEditor() {
29
+ if (modalEl) return modalEl;
30
+ modalEl = document.createElement('div');
31
+ modalEl.id = 'ws-content-modal';
32
+ modalEl.innerHTML =
33
+ '<div class="ws-content-modal-backdrop" data-close="1"></div>' +
34
+ '<div class="ws-content-modal-panel" role="dialog" aria-modal="true" aria-labelledby="ws-content-modal-title">' +
35
+ '<div class="ws-content-popover-arrow" aria-hidden="true"></div>' +
36
+ '<div class="ws-content-modal-header">' +
37
+ '<h3 class="ws-content-modal-title" id="ws-content-modal-title">Edit content</h3>' +
38
+ '<button type="button" class="ws-content-modal-close" data-close="1" aria-label="Close">&times;</button>' +
39
+ '</div>' +
40
+ '<div class="ws-content-modal-body"></div>' +
41
+ '<div class="ws-content-modal-error" hidden></div>' +
42
+ '<div class="ws-content-modal-footer">' +
43
+ '<button type="button" class="ws-content-btn ws-content-btn-muted" data-close="1">Cancel</button>' +
44
+ '<button type="button" class="ws-content-btn ws-content-btn-primary" data-save="1">Save</button>' +
45
+ '</div></div>';
46
+ document.body.appendChild(modalEl);
47
+
48
+ modalEl.addEventListener('click', function (e) {
49
+ if (e.target && e.target.getAttribute('data-close')) closeEditor();
50
+ });
51
+ qs('[data-save="1"]', modalEl).addEventListener('click', saveEditor);
52
+
53
+ document.addEventListener('keydown', function (e) {
54
+ if (e.key === 'Escape' && modalEl.classList.contains('is-open')) {
55
+ e.preventDefault();
56
+ closeEditor();
57
+ }
58
+ });
59
+
60
+ if (!repositionBound) {
61
+ repositionBound = true;
62
+ window.addEventListener('resize', function () {
63
+ if (modalEl.classList.contains('is-open')) positionPopover();
64
+ });
65
+ window.addEventListener(
66
+ 'scroll',
67
+ function () {
68
+ if (modalEl.classList.contains('is-open')) positionPopover();
69
+ },
70
+ true
71
+ );
72
+ }
73
+
74
+ return modalEl;
75
+ }
76
+
77
+ function closeEditor() {
78
+ if (modalEl) modalEl.classList.remove('is-open');
79
+ if (activeHost) activeHost.classList.remove('is-editing');
80
+ activeEntry = null;
81
+ activeSchema = null;
82
+ activeAnchor = null;
83
+ activeHost = null;
84
+ formState = {};
85
+ }
86
+
87
+ function showError(msg) {
88
+ var el = qs('.ws-content-modal-error', modalEl);
89
+ if (!el) return;
90
+ if (msg) {
91
+ el.textContent = msg;
92
+ el.hidden = false;
93
+ } else {
94
+ el.textContent = '';
95
+ el.hidden = true;
96
+ }
97
+ }
98
+
99
+ function positionPopover() {
100
+ if (!modalEl || !activeAnchor) return;
101
+ var panel = qs('.ws-content-modal-panel', modalEl);
102
+ if (!panel) return;
103
+
104
+ if (window.innerWidth < 640) {
105
+ panel.classList.add('is-sheet');
106
+ panel.style.top = '';
107
+ panel.style.left = '';
108
+ panel.style.visibility = 'visible';
109
+ return;
110
+ }
111
+
112
+ panel.classList.remove('is-sheet');
113
+ panel.style.visibility = 'hidden';
114
+ panel.style.display = 'flex';
115
+
116
+ var panelRect = panel.getBoundingClientRect();
117
+ var anchorRect = activeAnchor.getBoundingClientRect();
118
+ var gap = 10;
119
+ var pad = 12;
120
+ var vw = window.innerWidth;
121
+ var vh = window.innerHeight;
122
+
123
+ var top = anchorRect.bottom + gap;
124
+ var left = anchorRect.left + anchorRect.width / 2 - panelRect.width / 2;
125
+ var above = false;
126
+
127
+ if (top + panelRect.height > vh - pad && anchorRect.top - panelRect.height - gap > pad) {
128
+ top = anchorRect.top - panelRect.height - gap;
129
+ above = true;
130
+ }
131
+
132
+ top = Math.max(pad, Math.min(top, vh - panelRect.height - pad));
133
+ left = Math.max(pad, Math.min(left, vw - panelRect.width - pad));
134
+
135
+ panel.style.top = top + 'px';
136
+ panel.style.left = left + 'px';
137
+ panel.style.visibility = 'visible';
138
+ panel.classList.toggle('is-above', above);
139
+
140
+ var arrow = qs('.ws-content-popover-arrow', panel);
141
+ if (arrow) {
142
+ var arrowLeft = anchorRect.left + anchorRect.width / 2 - left;
143
+ arrow.style.left = Math.max(16, Math.min(arrowLeft, panelRect.width - 16)) + 'px';
144
+ }
145
+ }
146
+
147
+ function focusFirstField() {
148
+ var body = qs('.ws-content-modal-body', modalEl);
149
+ if (!body) return;
150
+ var input = body.querySelector('input, textarea, select');
151
+ if (input) input.focus();
152
+ }
153
+
154
+ function renderField(field, container) {
155
+ var wrap = document.createElement('div');
156
+ wrap.className = 'ws-content-field';
157
+ var label = document.createElement('label');
158
+ label.textContent = field.label || field.name;
159
+ wrap.appendChild(label);
160
+
161
+ var value = formState[field.name];
162
+ var input;
163
+
164
+ if (field.type === 'boolean') {
165
+ input = document.createElement('input');
166
+ input.type = 'checkbox';
167
+ input.checked = !!value;
168
+ input.addEventListener('change', function () {
169
+ formState[field.name] = input.checked;
170
+ });
171
+ } else if (field.type === 'textarea' || field.type === 'rich-text') {
172
+ input = document.createElement('textarea');
173
+ input.rows = field.type === 'rich-text' ? 6 : 3;
174
+ input.value = value || '';
175
+ input.addEventListener('input', function () {
176
+ formState[field.name] = input.value;
177
+ });
178
+ } else if (field.type === 'select') {
179
+ input = document.createElement('select');
180
+ (field.options || []).forEach(function (opt) {
181
+ var o = document.createElement('option');
182
+ o.value = opt;
183
+ o.textContent = opt;
184
+ if (value === opt) o.selected = true;
185
+ input.appendChild(o);
186
+ });
187
+ input.addEventListener('change', function () {
188
+ formState[field.name] = input.value;
189
+ });
190
+ } else {
191
+ input = document.createElement('input');
192
+ input.type = field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text';
193
+ input.value = value != null ? value : '';
194
+ input.addEventListener('input', function () {
195
+ formState[field.name] = field.type === 'number' ? Number(input.value) : input.value;
196
+ });
197
+ }
198
+
199
+ wrap.appendChild(input);
200
+ container.appendChild(wrap);
201
+ }
202
+
203
+ function openEditor(anchorEl, hostEl, entryId, typeSlug) {
204
+ ensureEditor();
205
+ showError('');
206
+ activeAnchor = anchorEl;
207
+ activeHost = hostEl;
208
+ if (activeHost) activeHost.classList.add('is-editing');
209
+
210
+ var body = qs('.ws-content-modal-body', modalEl);
211
+ body.innerHTML = '<p>Loading…</p>';
212
+ modalEl.classList.add('is-open');
213
+ positionPopover();
214
+
215
+ Promise.all([
216
+ fetch(API + '/types/' + encodeURIComponent(typeSlug) + '/schema', { credentials: 'include' }).then(function (r) {
217
+ return r.json();
218
+ }),
219
+ fetch(API + '/entries/' + entryId, { credentials: 'include' }).then(function (r) {
220
+ return r.json();
221
+ }),
222
+ ])
223
+ .then(function (results) {
224
+ if (!results[0].data || !results[1].data) throw new Error('Failed to load content');
225
+ activeSchema = results[0].data.schema;
226
+ activeEntry = results[1].data;
227
+ formState = Object.assign({}, activeEntry.data || {});
228
+ body.innerHTML = '';
229
+ var title = qs('.ws-content-modal-title', modalEl);
230
+ if (title) title.textContent = 'Edit: ' + (activeEntry.title || activeEntry.slug);
231
+ (activeSchema.fields || []).forEach(function (field) {
232
+ renderField(field, body);
233
+ });
234
+ requestAnimationFrame(function () {
235
+ positionPopover();
236
+ focusFirstField();
237
+ });
238
+ })
239
+ .catch(function (err) {
240
+ body.innerHTML = '';
241
+ showError(err.message || 'Load failed');
242
+ positionPopover();
243
+ });
244
+ }
245
+
246
+ function updateDomFields(entryId, data) {
247
+ qsa('[data-ws-content-entry="' + entryId + '"][data-ws-content-field]').forEach(function (el) {
248
+ var field = el.getAttribute('data-ws-content-field');
249
+ if (!field || !(field in data)) return;
250
+ var val = data[field];
251
+ var isHtml = el.getAttribute('data-ws-content-html') === 'true';
252
+ if (isHtml) {
253
+ el.innerHTML = val || '';
254
+ } else {
255
+ el.textContent = val != null ? val : '';
256
+ }
257
+ });
258
+ }
259
+
260
+ function saveEditor() {
261
+ if (!activeEntry) return;
262
+ showError('');
263
+ var saveBtn = qs('[data-save="1"]', modalEl);
264
+ saveBtn.disabled = true;
265
+ saveBtn.textContent = 'Saving…';
266
+
267
+ fetch(API + '/entries/' + activeEntry.id, {
268
+ method: 'PUT',
269
+ credentials: 'include',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ data: formState }),
272
+ })
273
+ .then(function (r) {
274
+ return r.json().then(function (json) {
275
+ if (!r.ok) throw new Error(json.error || 'Save failed');
276
+ return json;
277
+ });
278
+ })
279
+ .then(function (res) {
280
+ var data = (res.data && res.data.data) || formState;
281
+ updateDomFields(activeEntry.id, data);
282
+ closeEditor();
283
+ })
284
+ .catch(function (err) {
285
+ showError(err.message || 'Save failed');
286
+ })
287
+ .finally(function () {
288
+ saveBtn.disabled = false;
289
+ saveBtn.textContent = 'Save';
290
+ });
291
+ }
292
+
293
+ function lowestCommonAncestor(nodes) {
294
+ if (!nodes.length) return null;
295
+ var ancestor = nodes[0].parentElement;
296
+ while (ancestor && ancestor !== document.body) {
297
+ var ok = nodes.every(function (n) {
298
+ return ancestor.contains(n);
299
+ });
300
+ if (ok) return ancestor;
301
+ ancestor = ancestor.parentElement;
302
+ }
303
+ return nodes[0].parentElement;
304
+ }
305
+
306
+ function getEntryHosts() {
307
+ var byEntry = {};
308
+ qsa('[data-ws-content-entry]').forEach(function (el) {
309
+ var entryId = el.getAttribute('data-ws-content-entry');
310
+ var typeSlug = el.getAttribute('data-ws-content-type');
311
+ if (!entryId || !typeSlug) return;
312
+ if (!byEntry[entryId]) byEntry[entryId] = { typeSlug: typeSlug, nodes: [] };
313
+ byEntry[entryId].nodes.push(el);
314
+ });
315
+
316
+ var hosts = [];
317
+ Object.keys(byEntry).forEach(function (entryId) {
318
+ var info = byEntry[entryId];
319
+ var block = qs('.ws-content-block[data-ws-content-entry="' + entryId + '"]');
320
+ var host = block || lowestCommonAncestor(info.nodes);
321
+ if (!host) return;
322
+ hosts.push({ host: host, entryId: entryId, typeSlug: info.typeSlug });
323
+ });
324
+ return hosts;
325
+ }
326
+
327
+ function attachToolbar(host, entryId, typeSlug) {
328
+ if (host.querySelector('.ws-content-toolbar')) return;
329
+ host.classList.add('ws-content-host');
330
+
331
+ var toolbar = document.createElement('div');
332
+ toolbar.className = 'ws-content-toolbar';
333
+
334
+ var btn = document.createElement('button');
335
+ btn.type = 'button';
336
+ btn.className = 'ws-content-edit-btn';
337
+ btn.title = 'Edit content';
338
+ btn.setAttribute('aria-haspopup', 'dialog');
339
+ btn.innerHTML = '<span class="ws-content-edit-btn-icon" aria-hidden="true">✎</span> Edit';
340
+ btn.addEventListener('click', function (e) {
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ openEditor(btn, host, entryId, typeSlug);
344
+ });
345
+
346
+ toolbar.appendChild(btn);
347
+ host.insertBefore(toolbar, host.firstChild);
348
+ }
349
+
350
+ function attachEditButtons() {
351
+ getEntryHosts().forEach(function (item) {
352
+ attachToolbar(item.host, item.entryId, item.typeSlug);
353
+ });
354
+ }
355
+
356
+ function init() {
357
+ attachEditButtons();
358
+ document.addEventListener('DOMContentLoaded', attachEditButtons);
359
+ }
360
+
361
+ if (document.readyState === 'loading') {
362
+ document.addEventListener('DOMContentLoaded', init);
363
+ } else {
364
+ init();
365
+ }
366
+ })();