|
1 | 1 | (function () { |
2 | 2 | 'use strict'; |
3 | 3 |
|
| 4 | + /* Shared helpers (/js/shared.js): 0.1.0 output, old-key migration, safe embedding */ |
| 5 | + var buildHTML = DTD.buildHTML; |
| 6 | + |
4 | 7 | /* ── STATE ────────────────────────────────────────────────────── */ |
5 | 8 | var cfg = null; // active config object |
6 | 9 | var activeId = null; // localStorage project id being edited (null = unsaved) |
|
23 | 26 | var saveBtn = document.getElementById('save-btn'); |
24 | 27 | var copyBtn = document.getElementById('copy-btn'); |
25 | 28 | var exportBtn = document.getElementById('export-btn'); |
| 29 | + var deviceBtn = document.getElementById('device-btn'); |
26 | 30 |
|
27 | 31 | var projCount = document.getElementById('proj-count'); |
28 | 32 |
|
|
48 | 52 | function uid() { |
49 | 53 | return 'p' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); |
50 | 54 | } |
51 | | - function fmt(ts) { |
52 | | - var d = new Date(ts); |
53 | | - return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + |
54 | | - ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); |
55 | | - } |
56 | 55 |
|
57 | 56 | /* ── PROJECTS UI ──────────────────────────────────────────────── */ |
58 | 57 | function updateProjCount() { |
59 | | - var n = loadProjects().length; |
60 | | - projCount.textContent = n; |
| 58 | + projCount.textContent = loadProjects().length; |
61 | 59 | } |
62 | 60 |
|
63 | 61 | /* ── SAVE FLOW ────────────────────────────────────────────────── */ |
|
116 | 114 | modalTitle.textContent = name || 'Preview'; |
117 | 115 | modalFrame.srcdoc = buildHTML(c); |
118 | 116 | previewModal.classList.add('open'); |
119 | | - // store config for export |
120 | | - modalFrame._cfg = c; |
| 117 | + modalFrame._cfg = c; // store config for export |
121 | 118 | } |
122 | 119 | modalClose.addEventListener('click', function () { |
123 | 120 | previewModal.classList.remove('open'); |
|
126 | 123 | modalExport.addEventListener('click', function () { |
127 | 124 | var c = modalFrame._cfg; |
128 | 125 | if (!c) return; |
129 | | - var html = buildHTML(c); |
130 | | - var name = (c.logoName || 'page').toLowerCase().replace(/\s+/g, '-'); |
131 | | - var blob = new Blob([html], { type: 'text/html' }); |
132 | | - var a = document.createElement('a'); |
133 | | - a.href = URL.createObjectURL(blob); a.download = name + '.html'; a.click(); |
134 | | - URL.revokeObjectURL(a.href); |
| 126 | + download(buildHTML(c), c.logoName); |
135 | 127 | }); |
136 | | - // close modal on backdrop click |
137 | 128 | previewModal.addEventListener('click', function (e) { |
138 | 129 | if (e.target === previewModal) modalClose.click(); |
139 | 130 | }); |
140 | | - // ESC closes modal or dialog |
141 | 131 | document.addEventListener('keydown', function (e) { |
142 | 132 | if (e.key === 'Escape') { |
143 | 133 | if (previewModal.classList.contains('open')) modalClose.click(); |
144 | 134 | if (saveDialog.classList.contains('open')) saveDialog.classList.remove('open'); |
145 | 135 | } |
146 | 136 | }); |
147 | 137 |
|
| 138 | + function download(html, logoName) { |
| 139 | + var name = (logoName || 'page').toLowerCase().replace(/\s+/g, '-'); |
| 140 | + var blob = new Blob([html], { type: 'text/html' }); |
| 141 | + var a = document.createElement('a'); |
| 142 | + a.href = URL.createObjectURL(blob); a.download = name + '.html'; a.click(); |
| 143 | + URL.revokeObjectURL(a.href); |
| 144 | + } |
| 145 | + |
148 | 146 | /* ── THEME ────────────────────────────────────────────────────── */ |
149 | 147 | document.getElementById('btn-dark').addEventListener('click', function () { |
150 | 148 | document.documentElement.removeAttribute('data-theme'); |
|
183 | 181 | }); |
184 | 182 |
|
185 | 183 | /* ── PREVIEW ──────────────────────────────────────────────────── */ |
| 184 | + var frameLive = false; |
| 185 | + previewFrame.addEventListener('load', function () { frameLive = true; }); |
| 186 | + |
186 | 187 | function schedulePreview() { |
187 | 188 | clearTimeout(previewTimer); |
188 | 189 | previewTimer = setTimeout(doPreview, 280); |
189 | 190 | } |
| 191 | + // First call loads the page; later calls update it in place (keeps scroll, no flicker) |
190 | 192 | function doPreview() { |
191 | 193 | if (!cfg) return; |
192 | | - previewFrame.srcdoc = buildHTML(cfg); |
193 | 194 | previewEmpty.style.display = 'none'; |
194 | 195 | previewFrame.style.display = 'block'; |
| 196 | + if (frameLive && previewFrame.contentWindow) { |
| 197 | + previewFrame.contentWindow.postMessage({ dtdCfg: DTD.migrate(cfg) }, '*'); |
| 198 | + } else { |
| 199 | + previewFrame.srcdoc = buildHTML(cfg, true); |
| 200 | + } |
195 | 201 | } |
196 | 202 |
|
| 203 | + if (deviceBtn) deviceBtn.addEventListener('click', function () { |
| 204 | + var m = previewFrame.classList.toggle('m'); |
| 205 | + deviceBtn.innerHTML = m |
| 206 | + ? '<i class="fas fa-desktop"></i> Desktop' |
| 207 | + : '<i class="fas fa-mobile-screen"></i> Mobile'; |
| 208 | + }); |
| 209 | + |
197 | 210 | /* ── WIRE SIMPLE INPUTS (once) ────────────────────────────────── */ |
198 | 211 | function wireSimpleInputs() { |
199 | 212 | function w(id, path) { |
|
211 | 224 | schedulePreview(); |
212 | 225 | }); |
213 | 226 | } |
| 227 | + // Old key names are kept on purpose: DTD.buildHTML maps them to 0.1.0 names |
214 | 228 | w('e-logoName', 'logoName'); |
215 | 229 | w('e-logoUrl', 'logo'); |
216 | 230 | w('e-heroTag', 'heroTag'); |
|
247 | 261 | } |
248 | 262 |
|
249 | 263 | /* ── HTML ESCAPE ──────────────────────────────────────────────── */ |
250 | | - function esc(s) { |
251 | | - return String(s || '').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); |
252 | | - } |
| 264 | + var esc = DTD.esc; |
253 | 265 |
|
254 | 266 | /* ── RENDER LISTS ─────────────────────────────────────────────── */ |
255 | 267 | function renderNavLinks() { |
256 | | - var el = document.getElementById('navLinksList'); |
257 | | - if (!el || !cfg) return; |
258 | | - el.innerHTML = (cfg.navLinks || []).map(function (lnk, i) { |
259 | | - return '<div class="list-item"><div class="li-row">' + |
260 | | - '<input type="text" value="' + esc(lnk.label) + '" data-nll="' + i + '" placeholder="Label">' + |
261 | | - '<input type="text" value="' + esc(lnk.href) + '" data-nlh="' + i + '" placeholder="#section">' + |
262 | | - '<button class="btn-rm" data-rnl="' + i + '" title="Remove"><i class="fas fa-times"></i></button>' + |
263 | | - '</div></div>'; |
264 | | - }).join(''); |
265 | | - el.querySelectorAll('[data-nll]').forEach(function (inp) { |
266 | | - inp.addEventListener('input', function () { cfg.navLinks[+inp.dataset.nll].label = inp.value; schedulePreview(); }); |
267 | | - }); |
268 | | - el.querySelectorAll('[data-nlh]').forEach(function (inp) { |
269 | | - inp.addEventListener('input', function () { cfg.navLinks[+inp.dataset.nlh].href = inp.value; schedulePreview(); }); |
270 | | - }); |
271 | | - el.querySelectorAll('[data-rnl]').forEach(function (btn) { |
272 | | - btn.addEventListener('click', function () { cfg.navLinks.splice(+btn.dataset.rnl, 1); renderNavLinks(); schedulePreview(); }); |
273 | | - }); |
274 | | -} |
275 | | - |
| 268 | + var el = document.getElementById('navLinksList'); |
| 269 | + if (!el || !cfg) return; |
| 270 | + el.innerHTML = (cfg.navLinks || []).map(function (lnk, i) { |
| 271 | + return '<div class="list-item"><div class="li-row">' + |
| 272 | + '<input type="text" value="' + esc(lnk.label) + '" data-nll="' + i + '" placeholder="Label">' + |
| 273 | + '<input type="text" value="' + esc(lnk.href) + '" data-nlh="' + i + '" placeholder="#section">' + |
| 274 | + '<button class="btn-rm" data-rnl="' + i + '" title="Remove"><i class="fas fa-times"></i></button>' + |
| 275 | + '</div></div>'; |
| 276 | + }).join(''); |
| 277 | + el.querySelectorAll('[data-nll]').forEach(function (inp) { |
| 278 | + inp.addEventListener('input', function () { cfg.navLinks[+inp.dataset.nll].label = inp.value; schedulePreview(); }); |
| 279 | + }); |
| 280 | + el.querySelectorAll('[data-nlh]').forEach(function (inp) { |
| 281 | + inp.addEventListener('input', function () { cfg.navLinks[+inp.dataset.nlh].href = inp.value; schedulePreview(); }); |
| 282 | + }); |
| 283 | + el.querySelectorAll('[data-rnl]').forEach(function (btn) { |
| 284 | + btn.addEventListener('click', function () { cfg.navLinks.splice(+btn.dataset.rnl, 1); renderNavLinks(); schedulePreview(); }); |
| 285 | + }); |
| 286 | + } |
| 287 | + |
276 | 288 | document.getElementById('addNavLinkBtn').addEventListener('click', function () { |
277 | | - if (!cfg) return; |
278 | | - cfg.navLinks.push({ label: 'New link', href: '#' }); |
279 | | - renderNavLinks(); |
280 | | - schedulePreview(); |
281 | | -}); |
| 289 | + if (!cfg) return; |
| 290 | + if (!cfg.navLinks) cfg.navLinks = []; |
| 291 | + cfg.navLinks.push({ label: 'New link', href: '#' }); |
| 292 | + renderNavLinks(); |
| 293 | + schedulePreview(); |
| 294 | + }); |
282 | 295 |
|
283 | 296 | function renderBrands() { |
284 | 297 | var el = document.getElementById('brandsList'); |
|
301 | 314 | '<div class="li-row"><input type="text" value="' + esc(f.icon) + '" data-fi="' + i + '" placeholder="Icon HTML"></div>' + |
302 | 315 | '</div>'; |
303 | 316 | }).join(''); |
304 | | - |
305 | 317 | el.querySelectorAll('[data-ft]').forEach(function (inp) { inp.addEventListener('input', function () { cfg.features[+inp.dataset.ft].title = inp.value; schedulePreview(); }); }); |
306 | 318 | el.querySelectorAll('[data-fd]').forEach(function (inp) { inp.addEventListener('input', function () { cfg.features[+inp.dataset.fd].desc = inp.value; schedulePreview(); }); }); |
307 | 319 | el.querySelectorAll('[data-fi]').forEach(function (inp) { inp.addEventListener('input', function () { cfg.features[+inp.dataset.fi].icon = inp.value; schedulePreview(); }); }); |
|
380 | 392 | el.querySelectorAll('[data-rl]').forEach(function (btn) { btn.addEventListener('click', function () { cfg.footer.legal.splice(+btn.dataset.rl,1); renderLegal(); schedulePreview(); }); }); |
381 | 393 | } |
382 | 394 |
|
383 | | - |
384 | | - |
385 | 395 | function renderAllLists() { |
386 | | - renderNavLinks(); // added |
387 | | - renderBrands(); |
388 | | - renderFeatures(); |
389 | | - renderPanels(); |
390 | | - renderFooterCols(); |
391 | | - renderSocial(); |
392 | | - renderLegal(); |
393 | | -} |
| 396 | + renderNavLinks(); |
| 397 | + renderBrands(); |
| 398 | + renderFeatures(); |
| 399 | + renderPanels(); |
| 400 | + renderFooterCols(); |
| 401 | + renderSocial(); |
| 402 | + renderLegal(); |
| 403 | + } |
394 | 404 |
|
395 | 405 | /* ── ADD BUTTONS ──────────────────────────────────────────────── */ |
396 | 406 | document.getElementById('addBrandBtn').addEventListener('click', function () { if (!cfg) return; cfg.brands.push('New Brand'); renderBrands(); schedulePreview(); }); |
|
488 | 498 | } |
489 | 499 | return { |
490 | 500 | template: tmpl, logo: 'https://fscss.devtem.org/assets/images/fscss.png', logoName: brand, |
491 | | - navLinks: [{ label: 'Work', href: '#work' }, { label: 'Services', href: '#services' }, { label: 'About', href: '#about' }, { label: 'Contact', href: '#contact' }], |
| 501 | + // 0.1.0 anchors: the features section renders as #features |
| 502 | + navLinks: [{ label: 'Features', href: '#features' }, { label: 'Contact', href: '#' }], |
492 | 503 | ctaText: 'Get started', heroTag: heroTag, heroTitleLine1: heroLine1, heroTitleLine2: heroLine2, |
493 | 504 | heroSubtitle: short, primaryBtnText: 'Learn more', secondaryBtnText: 'Contact us', |
494 | 505 | brands: brands, brandsSubtitle: 'technologies & platforms I work with daily', |
|
507 | 518 | }; |
508 | 519 | } |
509 | 520 |
|
510 | | - /* ── BUILD HTML ───────────────────────────────────────────────── */ |
511 | | - function buildHTML(c) { |
512 | | - return '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>' + (c.logoName || 'devtem') + '</title>\n <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/devtemdesign@0.0.2/style.css">\n <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">\n <script src="https://cdn.jsdelivr.net/npm/devtemdesign@0.0.2/script.js"><\/script>\n</head>\n<body>\n <div id="app"></div>\n <script>devtemDesign("app", ' + JSON.stringify(c, null, 2) + ');<\/script>\n</body>\n</html>'; |
513 | | - } |
514 | | - |
515 | 521 | /* ── GENERATE ─────────────────────────────────────────────────── */ |
516 | 522 | generateBtn.addEventListener('click', function () { |
517 | 523 | var desc = descEl.value.trim(); |
|
551 | 557 | /* ── EXPORT ───────────────────────────────────────────────────── */ |
552 | 558 | exportBtn.addEventListener('click', function () { |
553 | 559 | if (!cfg) return; |
554 | | - var html = buildHTML(cfg); |
555 | | - var name = (cfg.logoName || 'page').toLowerCase().replace(/\s+/g, '-'); |
556 | | - var blob = new Blob([html], { type: 'text/html' }); |
557 | | - var a = document.createElement('a'); |
558 | | - a.href = URL.createObjectURL(blob); a.download = name + '.html'; a.click(); |
559 | | - URL.revokeObjectURL(a.href); |
| 560 | + download(buildHTML(cfg), cfg.logoName); |
560 | 561 | }); |
561 | 562 |
|
562 | 563 | /* ── INIT: URL PARAM LOAD ─────────────────────────────────────── */ |
|
565 | 566 | var params = new URLSearchParams(window.location.search); |
566 | 567 | var id = params.get('id'); |
567 | 568 | if (!id) return; |
568 | | - var list = loadProjects(); |
569 | | - var p = list.find(function (x) { return x.id === id; }); |
| 569 | + var p = loadProjects().find(function (x) { return x.id === id; }); |
570 | 570 | if (!p) return; |
571 | | - // Load the project into editor |
572 | 571 | activeId = p.id; |
573 | 572 | cfg = JSON.parse(JSON.stringify(p.config)); |
574 | 573 | template = cfg.template || 'figsh'; |
575 | | - // Sync template buttons |
576 | 574 | document.querySelectorAll('.tpl-btn').forEach(function (b) { |
577 | 575 | b.classList.toggle('active', b.dataset.tpl === template); |
578 | 576 | }); |
579 | | - // Update generate btn label |
580 | 577 | generateBtn.innerHTML = '<i class="fas fa-rotate"></i> Regenerate'; |
581 | 578 | generateBtn.disabled = false; |
582 | | - // Open editor |
583 | 579 | openEditor(true); |
584 | | - // Update browser title |
585 | 580 | document.title = 'Editing: ' + p.name + ' · devtemDesign'; |
586 | 581 | }()); |
587 | 582 |
|
588 | 583 | })(); |
| 584 | + |
0 commit comments