Site Builder
Editing:
businessPromptinator-html.js
writable 0666
/* Promptinator – 2025‑07‑17 ────────────────────────────────────────────────────────────────────── BUSINESS edition ➜ E‑button support ➜ Safe‑markup display -------------------------------------------------------------- • Safe on‑screen tags: <b> <strong> <i> <em> <u> <br> <hr> • Outbound prompt, mail‑to & POST body are all plain text • All earlier business helpers (auto‑fill, tag lists, etc.) retained */ export default class Promptinator { /* ───────── static fields ───────── */ static _lastMeta = []; // snapshot of last‑rendered meta (for E button) static _lastState = []; // snapshot of last‑rendered state static _hasE = false; /* ───────── helpers ───────── */ static _escAttr (s = '') { return String(s) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/</g, '<') .replace(/>/g, '>'); } /** Strip/convert HTML → plain text (for outbound prompt & mail‑to) */ static _stripHtml (html = '') { return html .replace(/<\s*(br|hr)\s*\/?>/gi, '\n') // keep line breaks .replace(/<\/?\s*(b|strong|i|em|u)\b[^>]*>/gi, '') // drop inline fmt .replace(/<[^>]+>/g, ''); // drop everything else } /** Keep only whitelisted tags for display */ static _sanitizeDisplay (html = '') { return html.replace( /<(\/?)([a-z0-9-]+)([^>]*)>/gi, (_all, slash, tag, attr) => /^(b|strong|i|em|u|br|hr)$/i.test(tag) ? `<${slash}${tag}${attr}>` : '' ); } /* ───────────── init ───────────── */ static init ({ mount, defaultPrompt }) { /* 1️⃣ read whatever HTML the author put inside the block */ const prompt = mount.dataset.prompt || defaultPrompt || mount.innerHTML.trim().replace(/<!--[\s\S]*?-->/g, ''); let template = prompt; // editable version (unsanitised) /* tokens: A B C D **E** I */ const RX = /\[(?:([A-DEI]?)-)?([^~|\-\]]*)(?:-([^~\]]*))?(?:~([^~]+)~)?\]/ig; const state = {}; const meta = []; const allowEdit = mount.getAttribute('edit') !== '0' && mount.getAttribute('edit') !== 'false'; const allowThink = !mount.hasAttribute('think') || (mount.getAttribute('think') !== '0' && mount.getAttribute('think') !== 'false'); /* ── business helpers (unchanged) ── */ const biz = window.businessData || null; const phParam = new URLSearchParams(window.location.search).get('ph'); const isBiz = biz && phParam && biz.phone && biz.phone === phParam; function applyBiz (stateArr, metaArr) { if (!isBiz) return; metaArr.forEach((m, i) => { const lab = (m.lab || '').toLowerCase(); switch (lab) { case 'name': stateArr[i] = biz.name; break; case 'slogan': stateArr[i] = biz.slogan; break; case 'description': stateArr[i] = biz.description; break; case 'address': stateArr[i] = biz.address; break; case 'city': stateArr[i] = biz.city; break; case 'state': stateArr[i] = biz.state; break; case 'zip': stateArr[i] = biz.zip; break; case 'phone': stateArr[i] = biz.phone; break; case 'website': stateArr[i] = biz.website; break; case 'email': stateArr[i] = biz.email; break; default: { if (m.cmd === 'C') { if (lab.startsWith('service')) { const arr = Array.isArray(biz.tags) ? biz.tags : []; stateArr[i] = arr.length ? [arr[0]] : []; m.opts = arr; } if (lab.startsWith('location')) { const arr = Array.isArray(biz.location_tags) ? biz.location_tags : []; stateArr[i] = arr.length ? [arr[0]] : []; m.opts = arr; } } } } }); } /* ── renderer ── */ function render () { /* sanitise the author‑provided markup for *display* */ const safeTemplate = Promptinator._sanitizeDisplay(template); meta.length = 0; let idx = 0; /* tokenise & seed state */ safeTemplate.replace(RX, (_, cmd = '', lab = '', ops = '', def = '') => { lab = lab.trim(); const opts = ops.split('|').filter(Boolean); if (!(idx in state)) { state[idx] = cmd === 'C' ? (def ? def.split(',').map(v => v.trim()) : opts.slice(0, 1)) : (def || opts[0] || lab); } meta.push({ cmd: cmd || 'A', lab, opts, def }); idx++; return ''; }); /* one‑time business autofill */ if (!render._bizApplied) { applyBiz(state, meta); render._bizApplied = true; } /* build HTML with tokens replaced */ idx = 0; const html = safeTemplate.replace(RX, (_, cmd = '', lab = '') => { lab = lab.trim(); const val = state[idx]; let out; if (cmd === 'C') { out = (Array.isArray(val) ? val : [val]) .map(v => `<span class="token" data-i="${idx}" data-label="${lab}">[${v}]</span>`).join(', '); } else if (cmd === 'E') { const isEmail = /^\S+@\S+\.\S+$/.test(val); out = `<button class="token e-btn" data-i="${idx}" data-label="${lab}">${isEmail ? 'Send' : 'Submit'}</button>`; } else { out = `<span class="token" data-i="${idx}" data-label="${lab}">[${val}]</span>`; } idx++; return out; }); /* E‑token presence controls AI buttons & placeholder logic */ const hasE = meta.some(m => m.cmd === 'E'); Promptinator._hasE = hasE; const aiButtons = (allowThink && !hasE) ? ` <div class="ai-buttons" style="margin-top:1em;display:flex;gap:.6em;"> <a class="ai-btn chatgpt" target="_blank" style="display:inline-flex;align-items:center;gap:.3em;background:#5c3bff;color:#fff;font-weight:600;border-radius:7px;padding:.4em 1.2em;text-decoration:none;">🧠 ChatGPT</a> <a class="ai-btn perplexity" target="_blank" style="display:inline-flex;align-items:center;gap:.3em;background:#22a8e5;color:#fff;font-weight:600;border-radius:7px;padding:.4em 1.2em;text-decoration:none;">🔎 Perplexity</a> <a class="ai-btn copilot" target="_blank" style="display:inline-flex;align-items:center;gap:.3em;background:#10c775;color:#fff;font-weight:600;border-radius:7px;padding:.4em 1.2em;text-decoration:none;">🤖 Copilot</a> </div>` : ''; promptText.innerHTML = html.replace(/\n/g, '<br>') + aiButtons; /* build plain‑text prompt for AI links */ if (allowThink && !hasE) { let i = 0; const raw = template.replace(RX, () => { const v = state[i++]; return Array.isArray(v) ? v.join(', ') : v; }); const fullPrompt = Promptinator._stripHtml(raw); promptText.querySelector('.chatgpt') .href = 'https://chat.openai.com/?prompt=' + encodeURIComponent(fullPrompt); promptText.querySelector('.perplexity').href = 'https://www.perplexity.ai/search?q=' + encodeURIComponent(fullPrompt); promptText.querySelector('.copilot') .href = 'https://copilot.microsoft.com/?q=' + encodeURIComponent(fullPrompt); } } /* render */ /* ── editor scaffold ── */ const editor = document.createElement('div'); editor.innerHTML = ` <div style="display:flex;align-items:baseline;gap:.5em;"> ${allowEdit ? '<span id="edit-link" style="cursor:pointer">✏️</span>' : ''} <div class="prompt-text" style="flex:1 1 auto;"></div> </div> ${allowEdit ? `<form id="edit-form" style="display:none;margin-top:.6em;"> <textarea style="width:100%;min-height:90px;font-family:monospace;"></textarea> <div style="text-align:right;margin-top:.7em;"><button type="submit">Save & Update</button></div> </form>` : ''}`; mount.innerHTML = ''; mount.append(editor); const promptText = editor.querySelector('.prompt-text'); /* edit toggle */ if (allowEdit) { const link = editor.querySelector('#edit-link'); const form = editor.querySelector('#edit-form'); const ta = form.querySelector('textarea'); link.onclick = () => { form.style.display = form.style.display ? '' : 'block'; ta.value = template; ta.focus(); }; form.onsubmit = e => { e.preventDefault(); template = ta.value.trim(); Object.keys(state).forEach(k => delete state[k]); // reset render._bizApplied = false; render(); form.style.display = 'none'; }; } render(); // initial paint /* ── click handler (open popup) ── */ mount.addEventListener('click', e => { if (!e.target.matches('.token')) return; const i = Number(e.target.dataset.i); const info = meta[i]; const value = state[i]; /* snapshot for E‑button’s mail/POST build */ if (info.cmd === 'E') { Promptinator._lastMeta = meta.slice(); Promptinator._lastState = Object.assign([], state); } Promptinator._popup( info, Array.isArray(value) ? value.join(', ') : value, v => { // commit callback state[i] = info.cmd === 'C' ? v.split(',').map(s => s.trim()).filter(Boolean) : v; render(); } ); }); } /* init */ /* ───────────── popup ───────────── */ static _popup (meta, cur, done) { /* singleton <dialog> */ let dlg = document.getElementById('promptinator-dlg'); if (!dlg) { dlg = document.createElement('dialog'); dlg.id = 'promptinator-dlg'; document.body.append(dlg); dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); }); dlg.addEventListener('cancel', e => { e.preventDefault(); dlg.close(); }); } const prettify = s => s.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); const niceLabel = meta.lab && meta.lab.trim() ? prettify(meta.lab) : (cur || { A:'Text Input', B:'Choice', C:'Options', D:'Dropdown', I:'URL Picker', E:'Send' }[meta.cmd] || 'Input'); /* placeholder swap applies when any E‑token exists */ const showPlaceholder = Promptinator._hasE && cur === meta.def; const phAttr = showPlaceholder ? ` placeholder="${Promptinator._escAttr(cur)}"` : ''; const valAttr = showPlaceholder ? '' : Promptinator._escAttr(cur); let inner = ''; switch (meta.cmd) { /* A – multiline text */ case 'A': inner = `<label>${niceLabel} <textarea style="width:100%;font-size:1rem;" rows="4"${phAttr}>${valAttr}</textarea> </label> <div style="text-align:right;margin-top:.7em;"><button type="button">Done</button></div>`; break; /* B – radios */ case 'B': inner = `<fieldset><legend>${niceLabel}</legend>` + meta.opts.map(o => `<label><input type="radio" name="r" value="${o}" ${o === cur ? 'checked' : ''}> ${o}</label>` ).join('') + `</fieldset>`; break; /* C – checkboxes */ case 'C': inner = `<fieldset><legend>${niceLabel}</legend>` + meta.opts.map(o => `<label><input type="checkbox" value="${o}" ${cur.split(', ').includes(o) ? 'checked' : ''}> ${o}</label>` ).join('') + `</fieldset> <div style="text-align:right;margin-top:.7em;"><button type="button">Done</button></div>`; break; /* D – dropdown */ case 'D': { let opts = meta.opts; if (opts.length === 1 && /^\d+$/.test(opts[0])) { opts = Array.from({ length: +opts[0] }, (_, i) => String(i + 1)); } inner = `<label>${niceLabel} <select>` + opts.map(o => `<option${o === cur ? ' selected' : ''}>${o}</option>`).join('') + `</select> </label>`; break; } /* I – URL picker */ case 'I': inner = `<label>${niceLabel} <select>` + (showPlaceholder ? `<option disabled selected>${Promptinator._escAttr(cur)}</option>` : '') + meta.opts.map(u => `<option${u === cur ? ' selected' : ''}>${u}</option>`).join('') + `</select> </label> <iframe hidden style="width:100%;height:300px;border:1px solid #ccc;margin-top:.6em;"></iframe> <div style="text-align:right;margin-top:.7em;"><button type="button">Use site</button></div>`; break; /* E – Send / Submit */ case 'E': { const isEmail = /^\S+@\S+\.\S+$/.test(cur); inner = `<div style="padding:1em 0;text-align:center;font-size:1.1rem;font-weight:600;">${niceLabel}</div> <div style="text-align:right;margin-top:.7em;"><button type="button">${isEmail ? 'Send' : 'Submit'}</button></div>`; break; } /* default – single‑line input */ default: inner = `<label>${niceLabel} <input type="text" style="width:100%;font-size:1rem;"${phAttr} value="${valAttr}"> </label> <div style="text-align:right;margin-top:.7em;"><button type="button">Done</button></div>`; } dlg.innerHTML = `<div class="popup-body">${inner}</div>`; dlg.showModal(); const pb = dlg.querySelector('.popup-body'); /* command‑specific behaviour */ /* E‑button handler (mailto / POST) */ if (meta.cmd === 'E') { const btn = pb.querySelector('button'); const isEmail = /^\S+@\S+\.\S+$/.test(cur); if (isEmail) { const body = Promptinator._stripHtml( Promptinator._lastMeta .map((m, i) => `${prettify(m.lab)}: ${Promptinator._lastState[i]}`) .join('\n') ); btn.onclick = () => { window.location.href = `mailto:${cur}?subject=${encodeURIComponent(niceLabel)}&body=${encodeURIComponent(body)}`; dlg.close(); }; } else { /* auto‑POST form */ const form = document.createElement('form'); form.method = 'post'; form.action = cur; const titleInp = document.createElement('input'); titleInp.type = 'hidden'; titleInp.name = '_formTitle'; titleInp.value = niceLabel; form.append(titleInp); Promptinator._lastMeta.forEach((m, i) => { const inp = document.createElement('input'); inp.type = 'hidden'; inp.name = prettify(m.lab).replace(/\s+/g, '-').toLowerCase(); inp.value = Promptinator._stripHtml(String(Promptinator._lastState[i])); form.append(inp); }); pb.append(form); btn.onclick = () => { form.submit(); dlg.close(); }; } return; // E handled entirely } const commit = v => { done(v); dlg.close(); }; /* A */ if (meta.cmd === 'A') { const ta = pb.querySelector('textarea'); ta.focus(); pb.querySelector('button').onclick = () => commit(ta.value); return; } /* B */ if (meta.cmd === 'B') { pb.onclick = e => { if (e.target.name === 'r') commit(e.target.value); }; return; } /* C */ if (meta.cmd === 'C') { pb.querySelector('button').onclick = () => { const vals = [...pb.querySelectorAll('input:checked')].map(i => i.value); commit(vals.join(', ') || cur); }; return; } /* D */ if (meta.cmd === 'D') { pb.querySelector('select').onchange = e => commit(e.target.value); return; } /* I */ if (meta.cmd === 'I') { const sel = pb.querySelector('select'); const fr = pb.querySelector('iframe'); sel.onchange = () => { fr.hidden = false; fr.src = sel.value; }; pb.querySelector('button').onclick = () => commit(sel.value || cur); return; } /* default single‑line */ const inp = pb.querySelector('input'); inp.focus(); pb.querySelector('button').onclick = () => commit(inp.value); inp.onkeydown = e => { if (e.key === 'Enter') commit(inp.value); }; } /* popup */ }
Save changes
Create folder
writable 0777
Create
Cancel