Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_page_edit.html
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_page_edit.html

added on local at 2026-07-01 13:48:07

Added
+563
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c5b496852951
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Page editor &mdash; in-place WYSIWYG.
3 Left pane: control panel (metadata, formatting toolbar, save).
4 Right pane: live storefront iframe where the user clicks directly
5 in the body to edit. Toolbar commands postMessage into the iframe
6 and the iframe reports body changes back. No third-party libs.
7===================================================================== -->
8
9<style>
10 /* ---- Full-bleed: escape the wrapper's max-width + padding so the
11 editor uses the entire viewport area (next to the sidebar).
12 :has() is supported by every modern browser as of 2024. ---- */
13 .app-content:has(.pe-shell) {
14 max-width: none;
15 padding: 0;
16 }
17
18 /* ---- Page-editor shell ---- */
19 .pe-shell {
20 display: flex; flex-direction: column; gap: 10px;
21 padding: 12px 14px 0;
22 min-height: 100vh;
23 box-sizing: border-box;
24 }
25 .pe-topbar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 10px 14px; background: var(--col-surface-1); border: 1px solid var(--col-border); border-radius: 10px; }
26 .pe-topbar .pe-back { color: var(--col-text-2); text-decoration: none; font-size: 13px; }
27 .pe-topbar .pe-back:hover { color: var(--col-accent-bright); }
28 .pe-topbar .pe-title { font-family: var(--font-display); font-size: 17px; font-weight: 700; color: var(--col-text); }
29 .pe-topbar .pe-meta { font-size: 11px; color: var(--col-text-3); letter-spacing: 1px; text-transform: uppercase; font-family: var(--font-mono, monospace); }
30 .pe-topbar .pe-spacer { flex: 1; }
31 .pe-topbar .pe-status { background: var(--col-surface-2); color: var(--col-text); border: 1px solid var(--col-border); padding: 7px 12px; border-radius: 6px; font-size: 13px; }
32 .pe-topbar .pe-panel-toggle {
33 background: var(--col-surface-2);
34 border: 1px solid var(--col-border);
35 color: var(--col-text);
36 padding: 7px 12px;
37 border-radius: 6px;
38 font-size: 13px;
39 cursor: pointer;
40 display: inline-flex; align-items: center; gap: 6px;
41 }
42 .pe-topbar .pe-panel-toggle:hover { background: var(--col-bg-2); border-color: var(--col-accent); }
43 .pe-topbar .pe-panel-toggle svg { width: 14px; height: 14px; }
44
45 /* ---- Stacked layout: controls row on top, canvas full-width below.
46 When collapsed (.pe-controls-closed) the row collapses to 0
47 height so the canvas claims the entire viewport. ---- */
48 .pe-split { display: flex; flex-direction: column; gap: 12px; min-height: calc(100vh - 110px); }
49 .pe-shell.pe-controls-closed .pe-controls { max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0; pointer-events: none; border-width: 0; }
50
51 /* ---- Controls: a single horizontal row.
52 Each section is a card so the row wraps gracefully at narrow widths. ---- */
53 .pe-controls {
54 background: var(--col-surface-1);
55 border: 1px solid var(--col-border);
56 border-radius: 10px;
57 padding: 14px;
58 display: grid;
59 /* Body-formatting moved to a full-width row above the canvas, so
60 this grid no longer needs a wide column for it. auto-fit lets
61 the remaining columns (metadata, hero images, hints) breathe at
62 the typical desktop width and stack cleanly at narrow widths. */
63 grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
64 gap: 14px;
65 align-items: start;
66 transition: max-height 0.25s ease, opacity 0.18s ease, padding 0.2s ease;
67 overflow: hidden;
68 max-height: 600px;
69 }
70 .pe-controls > div { min-width: 0; }
71 @media (max-width: 760px) { .pe-controls { grid-template-columns: 1fr; } }
72 .pe-section-label { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: var(--col-text-3); font-weight: 700; margin: 0 0 8px; }
73 .pe-hint { background: var(--col-bg-2); border: 1px solid var(--col-border); border-radius: 8px; padding: 12px; font-size: 12px; color: var(--col-text-2); line-height: 1.55; }
74 .pe-hint strong { color: var(--col-accent-bright); }
75 .pe-hint kbd { background: var(--col-surface-2); border: 1px solid var(--col-border); border-radius: 4px; padding: 1px 6px; font-family: var(--font-mono, monospace); font-size: 11px; color: var(--col-text); }
76
77 /* ---- Hero image slots ---- */
78 .pe-hero-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; max-width: 280px; }
79 .pe-hero-slot { display: flex; flex-direction: column; gap: 5px; }
80 .pe-hero-thumb {
81 aspect-ratio: 1;
82 background: var(--col-surface-2);
83 background-size: cover;
84 background-position: center;
85 border: 1px solid var(--col-border);
86 border-radius: 8px;
87 display: grid; place-items: center;
88 color: var(--col-text-3);
89 position: relative;
90 overflow: hidden;
91 max-height: 80px;
92 }
93 .pe-hero-placeholder {
94 font-size: 28px;
95 font-weight: 700;
96 font-family: var(--font-mono, monospace);
97 opacity: 0.6;
98 }
99 .pe-hero-replace { padding: 5px 8px; font-size: 11px; }
100 .pe-hero-saving { opacity: 0.5; pointer-events: none; }
101
102 /* ---- Toolbar ---- */
103 .we-toolbar { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; padding: 8px; background: var(--col-bg-2); border: 1px solid var(--col-border); border-radius: 8px; }
104 .we-tool { background: transparent; border: 1px solid transparent; color: var(--col-text-2); padding: 6px 8px; border-radius: 6px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; transition: background 0.15s, color 0.15s, border-color 0.15s; font-size: 13px; line-height: 1; }
105 .we-tool:hover { background: var(--col-surface-2); color: var(--col-text); }
106 .we-tool.active { background: var(--col-accent); color: #fff; border-color: var(--col-accent); }
107 .we-tool svg { width: 16px; height: 16px; }
108 .we-select { background: var(--col-surface-2); border: 1px solid var(--col-border); color: var(--col-text); padding: 6px 10px; border-radius: 6px; font-size: 13px; cursor: pointer; }
109 .we-divider { width: 1px; background: var(--col-border); align-self: stretch; margin: 0 4px; min-height: 22px; }
110
111 /* ---- Right: live iframe edit pane (grows to fill viewport height) ---- */
112 .pe-canvas { background: var(--col-bg-2); border: 1px solid var(--col-border); border-radius: 10px; overflow: hidden; display: flex; flex-direction: column; min-height: calc(100vh - 110px); }
113 /* Body-formatting toolbar row, full-width across the canvas. Sits
114 above the "Editing in place" head so the seller's eye lands on
115 formatting controls first, then on the page they're shaping. The
116 inner .we-toolbar element loses its own border/background here so
117 the row reads as a single integrated bar of chrome. */
118 .pe-canvas-toolbar { background: var(--col-bg); border-bottom: 1px solid var(--col-border); padding: 8px 12px; }
119 .pe-canvas-toolbar .we-toolbar { background: transparent; border: none; border-radius: 0; padding: 0; gap: 4px; }
120 .pe-canvas-head { background: var(--col-bg); border-bottom: 1px solid var(--col-border); padding: 10px 14px; display: flex; align-items: center; gap: 10px; font-family: var(--font-mono, monospace); font-size: 11px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--col-text-3); }
121 .pe-canvas-head .pe-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--col-accent); box-shadow: 0 0 8px var(--col-accent-glow); }
122 .pe-canvas-head .pe-spacer { flex: 1; }
123 .pe-canvas-head a { color: var(--col-text-2); text-decoration: none; font-size: 11px; }
124 .pe-canvas-head a:hover { color: var(--col-accent-bright); }
125 .pe-canvas-frame { flex: 1; width: 100%; height: 100%; border: none; background: var(--col-bg); display: block; min-height: calc(100vh - 160px); }
126
127 /* Upload progress strip floats over the canvas head */
128 .pe-progress { display: none; padding: 8px 14px; background: var(--col-bg-2); border-bottom: 1px solid var(--col-border); font-size: 13px; color: var(--col-text-2); font-family: var(--font-mono, monospace); }
129 .pe-progress.show { display: block; }
130 .pe-progress.error { color: #f87171; }
131</style>
132
133<form method="POST" action="/page.cgi" id="page-form" class="pe-shell">
134 <input type="hidden" name="action" value="save">
135 <input type="hidden" name="id" value="$page_id">
136 <input type="hidden" name="store" value="$store_id">
137 [if:$is_new]<input type="hidden" name="new" value="1">[/if]
138 <input type="hidden" name="body" id="body-input">
139 <input type="hidden" name="status" id="status-input" value="draft">
140
141 <!-- Sticky top bar: nav, title info, status, save -->
142 <div class="pe-topbar">
143 <a href="/storefront.cgi" class="pe-back">&larr; Back to storefront</a>
144 <button type="button" class="pe-panel-toggle" id="pe-panel-toggle" title="Show/hide editor panel" aria-pressed="true">
145 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
146 <span id="pe-panel-toggle-label">Hide panel</span>
147 </button>
148 <span class="pe-title">[if:$is_new]New page[/if][if:!$is_new]Edit "$title"[/if]</span>
149 <span class="pe-meta">[if:$is_new]Type: $page_type[/if][if:!$is_new]Type: $page_type &middot; ID #$page_id[/if]</span>
150 <span class="pe-spacer"></span>
151 <select class="pe-status" id="pe-status-select">
152 <option value="draft" [if:$is_draft]selected[/if]>Draft</option>
153 <option value="published" [if:$is_published]selected[/if][if:$is_new]selected[/if]>Published</option>
154 <option value="archived" [if:$is_archived]selected[/if]>Archived</option>
155 </select>
156 <button type="submit" class="btn btn-primary">
157 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
158 Save changes
159 </button>
160 </div>
161
162 [if:$saved_flash]
163 <div class="banner success">
164 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
165 <div class="text-xs">Saved.</div>
166 </div>
167 [/if]
168
169 <div class="pe-split">
170
171 <!-- ===== LEFT: control panel ===== -->
172 <aside class="pe-controls">
173
174 <div>
175 <div class="form-group" style="margin-bottom:12px">
176 <label class="form-label">Title <span class="req">*</span></label>
177 <input class="input" id="peTitle" type="text" name="title" value="$title" required maxlength="200">
178 <div class="form-help">Shown as the page heading and the browser tab title.</div>
179 </div>
180 <div class="form-group" style="margin-bottom:12px">
181 <label class="form-label">Menu link label <span class="text-xs text-dim">(optional)</span></label>
182 <input class="input" id="peMenuLabel" type="text" name="menu_label" value="$menu_label" maxlength="120" placeholder="e.g. Maker">
183 <div class="form-help">Short text that appears in the storefront header nav. Leave empty to use the page title.</div>
184 </div>
185 <div class="form-group" style="margin-bottom:0">
186 <label class="form-label">URL slug</label>
187 <input class="input" id="peSlug" type="text" name="slug" value="$slug" pattern="[a-z0-9_-]*" maxlength="120" placeholder="my_page">
188 <div class="form-help">Lowercase only. Spaces become underscores; other characters are stripped. Leave empty and we'll generate one from the title.</div>
189 </div>
190 </div>
191 <script>
192 /* Auto-format the slug field: lowercase, spaces -> underscores,
193 strip anything that is not [a-z0-9_-]. While the slug is
194 empty we also mirror the title into it (slug-ified) so the
195 seller does not have to type it twice. Once they manually
196 edit the slug, we stop auto-syncing. */
197 (function () {
198 var titleEl = document.getElementById('peTitle');
199 var slugEl = document.getElementById('peSlug');
200 if (!titleEl || !slugEl) return;
201
202 function slugify(s) {
203 return (s || '')
204 .toLowerCase()
205 .replace(/\s+/g, '_')
206 .replace(/[^a-z0-9_-]/g, '')
207 .replace(/^[_-]+|[_-]+\$/g, '')
208 .slice(0, 120);
209 }
210
211 var userTouchedSlug = (slugEl.value || '').length > 0;
212 slugEl.addEventListener('input', function () {
213 userTouchedSlug = true;
214 var pos = slugEl.selectionStart;
215 var before = slugEl.value;
216 var after = slugify(before);
217 if (after !== before) {
218 slugEl.value = after;
219 try { slugEl.setSelectionRange(pos - (before.length - after.length), pos - (before.length - after.length)); } catch (e) {}
220 }
221 });
222 titleEl.addEventListener('input', function () {
223 if (userTouchedSlug) return;
224 slugEl.value = slugify(titleEl.value);
225 });
226 })();
227 </script>
228
229 <!-- Body formatting toolbar moved out of this controls aside and
230 down to a full-width row above the canvas. Lives there now
231 because it controls the page CONTENT directly -- placing it
232 inline with the canvas (instead of stuck in a sidebar column)
233 makes the visual cause-and-effect immediate. -->
234
235 [if:$is_home_page]
236 <!-- Hero images panel. Only shown when editing the home page.
237 The catalog/spotlight layouts use 3 floating cards which can
238 hide each other at narrow iframe widths; these slots are the
239 reliable access path to replace any of the 3 images. -->
240 <div>
241 <div class="pe-section-label">Hero images</div>
242 <div class="pe-hero-grid">
243 <div class="pe-hero-slot" data-slot="1">
244 <div class="pe-hero-thumb" id="pe-hero-thumb-1" style="[if:$has_hero_image_1]background:url('$hero_image_1') $hero_image_1_pos / cover[/if]">
245 [if:!$has_hero_image_1]<span class="pe-hero-placeholder">1</span>[/if]
246 </div>
247 <button type="button" class="pe-hero-replace btn btn-secondary btn-sm" data-slot="1">Replace</button>
248 </div>
249 <div class="pe-hero-slot" data-slot="2">
250 <div class="pe-hero-thumb" id="pe-hero-thumb-2" style="[if:$has_hero_image_2]background:url('$hero_image_2') $hero_image_2_pos / cover[/if]">
251 [if:!$has_hero_image_2]<span class="pe-hero-placeholder">2</span>[/if]
252 </div>
253 <button type="button" class="pe-hero-replace btn btn-secondary btn-sm" data-slot="2">Replace</button>
254 </div>
255 <div class="pe-hero-slot" data-slot="3">
256 <div class="pe-hero-thumb" id="pe-hero-thumb-3" style="[if:$has_hero_image_3]background:url('$hero_image_3') $hero_image_3_pos / cover[/if]">
257 [if:!$has_hero_image_3]<span class="pe-hero-placeholder">3</span>[/if]
258 </div>
259 <button type="button" class="pe-hero-replace btn btn-secondary btn-sm" data-slot="3">Replace</button>
260 </div>
261 </div>
262 <div class="pe-hint" style="margin-top:8px">
263 The 3 floating cards on the right are these slots. Replace any one, then drag inside the card in the preview to re-frame.
264 </div>
265 </div>
266 [/if]
267
268 <div class="pe-hint">
269 <strong>Click in the dashed area</strong> on the right to start writing.<br>
270 <strong>Drag and drop</strong> images right onto the page &mdash; they upload automatically.<br>
271 <strong>Paste</strong> screenshots from your clipboard.<br>
272 Click any text in the preview to edit it in place.
273 </div>
274
275 </aside>
276
277 <!-- ===== RIGHT: editing canvas (iframe) ===== -->
278 <div class="pe-canvas">
279 <!-- Body formatting toolbar lives at the top of the canvas now.
280 Full-width, flush against the page, so it visually reads as
281 "the tools for the thing right below me." -->
282 <div class="pe-canvas-toolbar">
283 <div class="we-toolbar" id="we-toolbar">
284 <select class="we-select" id="we-heading" title="Block style">
285 <option value="P">Paragraph</option>
286 <option value="H1">Heading 1</option>
287 <option value="H2">Heading 2</option>
288 <option value="H3">Heading 3</option>
289 </select>
290 <span class="we-divider"></span>
291 <button type="button" class="we-tool" data-cmd="bold" title="Bold (Ctrl+B)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4h7a4 4 0 0 1 0 8H6z"/><path d="M6 12h8a4 4 0 0 1 0 8H6z"/></svg></button>
292 <button type="button" class="we-tool" data-cmd="italic" title="Italic (Ctrl+I)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></button>
293 <button type="button" class="we-tool" data-cmd="underline" title="Underline (Ctrl+U)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 3v7a6 6 0 0 0 12 0V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg></button>
294 <button type="button" class="we-tool" data-cmd="strikeThrough" title="Strikethrough"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="12" x2="20" y2="12"/><path d="M16 6c-2.5-2-9-2-9 2 0 2 2 3 5 3"/><path d="M8 18c2.5 2 9 2 9-2 0-2-2-3-5-3"/></svg></button>
295 <span class="we-divider"></span>
296 <button type="button" class="we-tool" data-cmd="insertUnorderedList" title="Bullet list"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4.5" cy="6" r="1.2" fill="currentColor"/><circle cx="4.5" cy="12" r="1.2" fill="currentColor"/><circle cx="4.5" cy="18" r="1.2" fill="currentColor"/></svg></button>
297 <button type="button" class="we-tool" data-cmd="insertOrderedList" title="Numbered list"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 4h1v4"/><path d="M4 8h2"/><path d="M4 14c0-.6.4-1 1-1s1 .4 1 1c0 1-2 1-2 2h2"/><path d="M4 18h1.5c.5 0 1 .2 1 .8 0 .6-.4.7-.8.7H4"/></svg></button>
298 <button type="button" class="we-tool" data-cmd="blockquote" title="Quote"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.8-2-2-2H4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h.5c0 1.5-.5 2-2 2"/><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.8-2-2-2h-4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h.5c0 1.5-.5 2-2 2"/></svg></button>
299 <button type="button" class="we-tool" data-cmd="codeblock" title="Code block"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></button>
300 <span class="we-divider"></span>
301 <button type="button" class="we-tool" data-cmd="link" title="Add link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
302 <button type="button" class="we-tool" data-cmd="image" title="Insert image"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></button>
303 <span class="we-divider"></span>
304 <button type="button" class="we-tool" data-cmd="removeFormat" title="Clear formatting"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h16v3"/><line x1="5" y1="20" x2="11" y2="20"/><line x1="13" y1="4" x2="8" y2="20"/><line x1="18" y1="14" x2="22" y2="18"/><line x1="22" y1="14" x2="18" y2="18"/></svg></button>
305 </div>
306 </div>
307 <div class="pe-canvas-head">
308 <span class="pe-dot"></span>
309 <span>Editing in place</span>
310 <span class="pe-spacer"></span>
311 [if:!$is_new]<a href="/store.cgi?id=$store_id&page=$slug" target="_blank">View as buyer &rarr;</a>[/if]
312 </div>
313 <div class="pe-progress" id="pe-progress"></div>
314 <iframe class="pe-canvas-frame" id="pe-frame" src="/store.cgi?id=$store_id&page=$slug&editor_page_id=$page_id&editor_live=1" title="Storefront edit canvas"></iframe>
315 </div>
316 </div>
317</form>
318
319<script>
320/*
321 * Page editor controller.
322 *
323 * The actual editing happens in the iframe (which contains the user's
324 * storefront layout). Buttons in this control panel post commands to
325 * the iframe; the iframe reports body changes back. The hidden form
326 * input mirrors the latest body so a normal POST submits everything.
327 */
328(function() {
329 var INITIAL_BODY = $body_json;
330
331 var iframe = document.getElementById('pe-frame');
332 var bodyEl = document.getElementById('body-input');
333 var form = document.getElementById('page-form');
334
335 // Controls panel toggle. Preference persists in localStorage so the
336 // last-used layout is restored on reload. The class on .pe-shell
337 // (set/removed by this button) drives the CSS transition.
338 (function () {
339 var btn = document.getElementById('pe-panel-toggle');
340 var label = document.getElementById('pe-panel-toggle-label');
341 var shell = form;
342 if (!btn || !shell) return;
343 var KEY = 'webstls_pe_panel_closed';
344 function apply(closed) {
345 shell.classList.toggle('pe-controls-closed', closed);
346 btn.setAttribute('aria-pressed', closed ? 'false' : 'true');
347 if (label) label.textContent = closed ? 'Show panel' : 'Hide panel';
348 }
349 apply(localStorage.getItem(KEY) === '1');
350 btn.addEventListener('click', function () {
351 var nowClosed = !shell.classList.contains('pe-controls-closed');
352 apply(nowClosed);
353 try { localStorage.setItem(KEY, nowClosed ? '1' : '0'); } catch (e) {}
354 });
355 })();
356 var heading = document.getElementById('we-heading');
357 var statusSel = document.getElementById('pe-status-select');
358 var statusInp = document.getElementById('status-input');
359 var progress = document.getElementById('pe-progress');
360
361 // Body input starts with the server-rendered body so a save without
362 // any edits still posts the right value.
363 bodyEl.value = INITIAL_BODY || '';
364
365 // Status select -> hidden input
366 function syncStatus() { statusInp.value = statusSel.value; }
367 statusSel.addEventListener('change', syncStatus);
368 syncStatus();
369 form.addEventListener('submit', syncStatus);
370
371 // ====================================================================
372 // Cross-frame messaging
373 // ====================================================================
374 function send(cmd) {
375 if (!iframe || !iframe.contentWindow) return;
376 try { iframe.contentWindow.postMessage(cmd, window.location.origin); } catch (e) {}
377 }
378
379 window.addEventListener('message', function(e) {
380 if (!e || !e.data || typeof e.data !== 'object') return;
381 var d = e.data;
382
383 if (d.type === 'we-preview-ready') {
384 // Iframe is loaded. Push the saved body so the canvas matches.
385 if (INITIAL_BODY) send({ type: 'we-set-body', html: INITIAL_BODY });
386 return;
387 }
388 if (d.type === 'we-body-changed') {
389 bodyEl.value = d.html || '';
390 return;
391 }
392 if (d.type === 'we-state') {
393 // Mirror Bold/Italic/etc. button highlights from the iframe's
394 // current selection state.
395 ['bold','italic','underline','strikeThrough'].forEach(function(c){
396 var btn = document.querySelector('.we-tool[data-cmd="' + c + '"]');
397 if (btn) btn.classList.toggle('active', !!(d.states && d.states[c]));
398 });
399 return;
400 }
401 if (d.type === 'we-upload-progress') {
402 showProgress(d.message || 'Uploading...', false);
403 return;
404 }
405 if (d.type === 'we-upload-done') {
406 if (d.success) hideProgress();
407 else { showProgress('Upload failed: ' + (d.error || 'unknown'), true); setTimeout(hideProgress, 4500); }
408 return;
409 }
410 });
411
412 // ====================================================================
413 // Toolbar -> iframe commands
414 // ====================================================================
415 document.querySelectorAll('.we-tool[data-cmd]').forEach(function(btn) {
416 // Crucial: prevent mousedown from stealing focus from the iframe,
417 // otherwise the iframe's selection collapses and the command
418 // applies to nothing.
419 btn.addEventListener('mousedown', function(e) { e.preventDefault(); });
420 btn.addEventListener('click', function(e) {
421 e.preventDefault();
422 runCommand(btn.dataset.cmd);
423 });
424 });
425
426 heading.addEventListener('change', function() {
427 send({ type: 'we-exec-command', command: 'formatBlock', value: heading.value });
428 heading.blur();
429 });
430
431 function runCommand(cmd) {
432 if (cmd === 'link') {
433 appPrompt({
434 title: 'Insert link',
435 message: 'Enter the URL the selected text should link to.',
436 placeholder: 'https://',
437 default_value: 'https://',
438 input_type: 'url',
439 label: 'Insert link',
440 required: true,
441 validate: function (v) {
442 if (v === 'https://') return 'Enter a real URL.';
443 if (!/^https?:\\/\\//i.test(v)) return 'Must start with http:// or https://';
444 return '';
445 }
446 }).then(function (url) {
447 if (url) send({ type: 'we-exec-command', command: 'createLink', value: url });
448 });
449 return;
450 }
451 if (cmd === 'image') { pickImage(); return; }
452 if (cmd === 'blockquote') { send({ type: 'we-exec-command', command: 'formatBlock', value: 'BLOCKQUOTE' }); return; }
453 if (cmd === 'codeblock') { send({ type: 'we-exec-command', command: 'formatBlock', value: 'PRE' }); return; }
454 send({ type: 'we-exec-command', command: cmd });
455 }
456
457 // ====================================================================
458 // Image upload (toolbar button picker)
459 // Drag/drop and paste are handled INSIDE the iframe -- the iframe
460 // uploads itself and reports progress back here so the strip shows.
461 // ====================================================================
462 function pickImage() {
463 var input = document.createElement('input');
464 input.type = 'file';
465 input.accept = 'image/*';
466 input.onchange = function() {
467 if (!input.files || !input.files[0]) return;
468 uploadFromParent(input.files[0]);
469 };
470 input.click();
471 }
472
473 function uploadFromParent(file) {
474 showProgress('Uploading ' + (file.name || 'image') + '...', false);
475 var fd = new FormData();
476 fd.append('image', file);
477 fetch('/upload_image.cgi', { method: 'POST', body: fd, credentials: 'same-origin' })
478 .then(function(r){ return r.json(); })
479 .then(function(data) {
480 if (data && data.success && data.url) {
481 hideProgress();
482 var safeAlt = (file.name || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;');
483 send({ type: 'we-insert-html', html: '<img src="' + data.url + '" alt="' + safeAlt + '"><br>' });
484 } else {
485 showProgress('Upload failed: ' + ((data && data.error) || 'unknown'), true);
486 setTimeout(hideProgress, 4500);
487 }
488 })
489 .catch(function(err){
490 showProgress('Upload failed: ' + err.message, true);
491 setTimeout(hideProgress, 4500);
492 });
493 }
494
495 function showProgress(msg, isError) { progress.textContent = msg; progress.classList.toggle('error', !!isError); progress.classList.add('show'); }
496 function hideProgress() { progress.classList.remove('show', 'error'); }
497
498 // ----- Hero image slot handlers (left-panel Replace buttons) -----
499 // Each Replace button opens a file picker, uploads the file, then
500 // saves the new URL to the matching storefronts.hero_image_N column.
501 // We update the thumbnail + reload the iframe so the seller sees the
502 // new image immediately in the live preview.
503 var STORE_ID = "$store_id";
504 document.querySelectorAll('.pe-hero-replace').forEach(function (btn) {
505 btn.addEventListener('click', function () {
506 var slot = btn.getAttribute('data-slot');
507 var input = document.createElement('input');
508 input.type = 'file';
509 input.accept = 'image/*';
510 input.onchange = function () {
511 if (!input.files || !input.files[0]) return;
512 uploadHero(slot, input.files[0]);
513 };
514 input.click();
515 });
516 });
517
518 function uploadHero(slot, file) {
519 var slotEl = document.querySelector('.pe-hero-slot[data-slot="' + slot + '"]');
520 if (slotEl) slotEl.classList.add('pe-hero-saving');
521 showProgress('Uploading hero ' + slot + '...', false);
522
523 var fd = new FormData();
524 fd.append('image', file);
525 fetch('/upload_image.cgi', { method: 'POST', body: fd, credentials: 'same-origin' })
526 .then(function (r) { return r.json(); })
527 .then(function (data) {
528 if (!data || !data.success || !data.url) {
529 throw new Error((data && data.error) || 'upload failed');
530 }
531 // Now save the URL to the storefront row.
532 var fd2 = new FormData();
533 fd2.append('storefront_id', STORE_ID);
534 fd2.append('field', 'hero_image_' + slot);
535 fd2.append('value', data.url);
536 return fetch('/update_storefront_field.cgi', {
537 method: 'POST', body: fd2, credentials: 'same-origin'
538 }).then(function (r) { return r.json(); }).then(function (saved) {
539 if (!saved || !saved.success) {
540 throw new Error((saved && saved.error) || 'save failed');
541 }
542 // Update thumbnail in place.
543 var thumb = document.getElementById('pe-hero-thumb-' + slot);
544 if (thumb) {
545 thumb.style.background = "url('" + data.url + "') center / cover";
546 thumb.innerHTML = '';
547 }
548 // Reload the iframe so the live preview shows the new image.
549 if (iframe) iframe.src = iframe.src;
550 showProgress('Hero ' + slot + ' updated.', false);
551 setTimeout(hideProgress, 1500);
552 });
553 })
554 .catch(function (err) {
555 showProgress('Replace failed: ' + err.message, true);
556 setTimeout(hideProgress, 4500);
557 })
558 .finally(function () {
559 if (slotEl) slotEl.classList.remove('pe-hero-saving');
560 });
561 }
562})();
563</script>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help