, same patterns as lib/utils/checkout-link.ts (never
// /system/checkout/complete|upsell|free) — so we never leak params to third-party
// iframes (YouTube, Stripe, etc.). UTM/attribution are intentionally left
// alone here — they keep flowing through the sandbox document only.
var CHECKOUT_PATH_RE =
/^\/(?:system\/checkout\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|checkout\/[a-z0-9]{8})\/?$/i
function propagateCouponToCheckoutIframe(f) {
var rawSrc = f.getAttribute('src')
if (!rawSrc) return
try {
var target = new URL(rawSrc, location.href)
if (!CHECKOUT_PATH_RE.test(target.pathname)) return
var sandboxParams = new URLSearchParams(location.search)
var coupon = sandboxParams.get('coupon') || sandboxParams.get('COUPON')
if (!coupon) return
if (target.searchParams.has('coupon') || target.searchParams.has('COUPON')) return
target.searchParams.set('coupon', coupon)
f.setAttribute('src', target.toString())
} catch (_) {}
}
// A product checkout link clicked in here (a Tiny Block's buttons, a creator's own
// link) gets what the page's own buttons add to it (ButtonBlockRender): the page's
// coupon and its attribution (srcPageId / variant of an A/B test), sent by the parent
// in the init. Set at click time, so links written by a script are covered too; a
// param the link already carries is kept. Same checkout paths as above.
var checkoutParams = {}
window.addEventListener('click', function (event) {
if (event.defaultPrevented) return
var link = event.target && event.target.closest && event.target.closest('a[href]')
if (!link) return
try {
var target = new URL(link.getAttribute('href'), location.href)
if (!CHECKOUT_PATH_RE.test(target.pathname)) return
var changed = false
for (var key in checkoutParams) {
if (!checkoutParams[key] || target.searchParams.has(key)) continue
target.searchParams.set(key, checkoutParams[key])
changed = true
}
if (changed) link.setAttribute('href', target.toString())
} catch (_) {}
})
// Tiny Block in the editor: its texts (marked data-tp-text by the AI) are edited right
// here. Each one is numbered from the static HTML, before the scripts run, so the
// number matches the parent's own count; a change is posted back, never re-mounted.
// Only on the editor's explicit `editTexts` flag: public pages never get any of this.
var TEXT_SEND_DELAY_MS = 400
function sendText(el) {
if (!parentOrigin) return
var text = el.textContent || ''
if (el.__tpSent === text) return
el.__tpSent = text
parent.postMessage(
{
type: 'tinypages-codehtmlblock-text',
id: id,
index: Number(el.getAttribute('data-tp-text-index')),
text: text,
},
parentOrigin
)
}
// Chrome leaves a in an emptied field, so :empty can't be relied on.
function markEmpty(el) {
if ((el.textContent || '').trim()) el.removeAttribute('data-tp-empty')
else el.setAttribute('data-tp-empty', '')
}
function enableTextEditing(root) {
var style = document.createElement('style')
// !important: the block's own CSS (user-select: none, outline: none, pointer-events…)
// must not make a text impossible to edit.
style.textContent =
// A thin solid line 4 px around the text, like the block's own frame. On a filled
// element (button, badge), its own text color can match nothing around it (white on
// green): the line takes the page's text color instead.
'[data-tp-editable]{cursor:text!important;-webkit-user-select:text!important;user-select:text!important;pointer-events:auto!important;outline:1px solid transparent!important;outline-offset:4px!important}' +
'[data-tp-editable]:hover{outline-color:color-mix(in srgb,currentColor 30%,transparent)!important}' +
'[data-tp-editable]:focus{outline-color:color-mix(in srgb,currentColor 60%,transparent)!important}' +
'[data-tp-editable][data-tp-filled]:hover{outline-color:color-mix(in srgb,var(--tp-text,#1d1c1a) 35%,transparent)!important}' +
'[data-tp-editable][data-tp-filled]:focus{outline-color:color-mix(in srgb,var(--tp-text,#1d1c1a) 65%,transparent)!important}' +
// An emptied text keeps something to click: its label, faded.
'[data-tp-empty]::before{content:attr(data-tp-text);opacity:.4}'
document.head.appendChild(style)
// Pencil on the hovered text's corner, so it reads as editable. It looks like a button
// and is one: a click opens the text with the caret at its end (clicking in the text
// works too). Outside the text: nothing moves. Hidden while the text is being edited.
var badge = document.createElement('div')
badge.setAttribute('aria-hidden', 'true')
badge.style.cssText =
'position:absolute;z-index:2147483647;width:22px;height:22px;border-radius:999px;background:#fff;color:#1d1c1a;box-shadow:0 1px 4px rgba(0,0,0,.3);display:none;align-items:center;justify-content:center;cursor:pointer'
badge.innerHTML =
' '
document.body.appendChild(badge)
var badgeFor = null
function hideBadge() {
badge.style.display = 'none'
badgeFor = null
}
// mousedown, not click: the text gets the focus before anything else can take it.
badge.addEventListener('mousedown', function (event) {
event.preventDefault()
var el = badgeFor
hideBadge()
if (!el) return
el.focus()
var range = document.createRange()
range.selectNodeContents(el)
range.collapse(false)
var selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
})
document.addEventListener('mouseover', function (event) {
// Moving from the text onto its pencil keeps the pencil.
if (event.target === badge || badge.contains(event.target)) return
var el = event.target && event.target.closest && event.target.closest('[data-tp-editable]')
if (!el || document.activeElement === el) return hideBadge()
badgeFor = el
var r = el.getBoundingClientRect()
var left = Math.min(r.right + window.scrollX - 8, document.documentElement.clientWidth - 24)
var top = Math.max(r.top + window.scrollY - 14, 0)
badge.style.left = left + 'px'
badge.style.top = top + 'px'
badge.style.display = 'flex'
})
document.addEventListener('mouseleave', hideBadge)
document.addEventListener('focusin', hideBadge)
// The editor keeps the block framed while one of its texts is being typed.
function sendEditing(editing) {
if (!parentOrigin) return
parent.postMessage({type: 'tinypages-codehtmlblock-editing', id: id, editing: editing}, parentOrigin)
}
document.addEventListener('focusin', function (event) {
var el = event.target
if (el && el.hasAttribute && el.hasAttribute('data-tp-editable')) sendEditing(true)
})
document.addEventListener('focusout', function (event) {
var el = event.target
if (el && el.hasAttribute && el.hasAttribute('data-tp-editable')) sendEditing(false)
})
// Links: numbered like the texts (static HTML, before the scripts). The hovered one
// is reported with its position, so the editor shows its link bar under it; the
// editor then sends the new address back (tinypages-codehtmlblock-set-link).
var links = root.querySelectorAll('a[href]')
for (var j = 0; j < links.length; j++) links[j].setAttribute('data-tp-link-index', String(j))
var hoveredLink = null
function postLink(a) {
if (!parentOrigin) return
var message = {type: 'tinypages-codehtmlblock-link', id: id, index: -1}
if (a) {
var r = a.getBoundingClientRect()
message.index = Number(a.getAttribute('data-tp-link-index'))
message.href = a.getAttribute('href') || ''
message.left = r.left
message.top = r.top
message.bottom = r.bottom
}
parent.postMessage(message, parentOrigin)
}
// Every pass over a link is reported, not only a change: the editor may have hidden
// its bar (link window opened, then closed) while the pointer never left the link.
document.addEventListener('mouseover', function (event) {
var a = event.target && event.target.closest && event.target.closest('a[data-tp-link-index]')
if (!a && !hoveredLink) return
hoveredLink = a || null
postLink(hoveredLink)
})
document.addEventListener('mouseleave', function () {
if (!hoveredLink) return
hoveredLink = null
postLink(null)
})
var marked = root.querySelectorAll('[data-tp-text]')
for (var i = 0; i < marked.length; i++) {
var el = marked[i]
el.setAttribute('data-tp-text-index', String(i))
// Text only: an element holding other elements would lose them.
if (el.children.length > 0) continue
el.setAttribute('data-tp-editable', '')
// Filled element (button, badge): outline in the page's text color. Plain text:
// rounded corners, so its outline matches the block's rounded frame.
var cs = window.getComputedStyle(el)
var bg = cs.backgroundColor
var filled =
(bg && bg !== 'transparent' && bg !== 'rgba(0, 0, 0, 0)') ||
(cs.backgroundImage && cs.backgroundImage !== 'none') ||
el.tagName === 'A' ||
el.tagName === 'BUTTON'
if (filled) el.setAttribute('data-tp-filled', '')
else if (cs.borderTopLeftRadius === '0px') el.style.borderRadius = '6px'
el.setAttribute('contenteditable', 'plaintext-only')
el.setAttribute('spellcheck', 'true')
el.__tpSent = el.textContent || ''
markEmpty(el)
}
var timers = {}
document.addEventListener('input', function (event) {
var el = event.target && event.target.closest && event.target.closest('[data-tp-editable]')
if (!el) return
markEmpty(el)
var key = el.getAttribute('data-tp-text-index')
clearTimeout(timers[key])
timers[key] = setTimeout(function () {
sendText(el)
}, TEXT_SEND_DELAY_MS)
})
document.addEventListener(
'blur',
function (event) {
var el = event.target
if (el && el.hasAttribute && el.hasAttribute('data-tp-editable')) {
clearTimeout(timers[el.getAttribute('data-tp-text-index')])
sendText(el)
}
},
true
)
document.addEventListener('keydown', function (event) {
var el = event.target
if (!el || !el.hasAttribute || !el.hasAttribute('data-tp-editable')) return
// One line of text: Enter and Escape end the edit.
if (event.key === 'Enter' || event.key === 'Escape') {
event.preventDefault()
el.blur()
}
})
// Plain text only, on one line, whatever the clipboard holds.
document.addEventListener('paste', function (event) {
var el = event.target && event.target.closest && event.target.closest('[data-tp-editable]')
if (!el) return
event.preventDefault()
var text = (event.clipboardData && event.clipboardData.getData('text/plain')) || ''
document.execCommand('insertText', false, text.replace(/\s*\n\s*/g, ' '))
})
// A button's label is edited, not followed.
document.addEventListener(
'click',
function (event) {
var el = event.target && event.target.closest && event.target.closest('[data-tp-editable]')
if (el && el.closest('a, button')) event.preventDefault()
},
true
)
}
function mount(html, skipTracking, editTexts) {
if (mounted) return
mounted = true
var container = document.createElement('div')
if (skipTracking) {
// An parsed into a loads at once, detached or not: tracking pixels
// are removed while the code still sits in an inert .
var template = document.createElement('template')
template.innerHTML = html || ''
removeTrackingPixels(template.content)
container.appendChild(template.content)
stubTrackingFunctions()
} else {
container.innerHTML = html || ''
}
// Patch creator