Files
mkt.ose.tw/frontend/public/editor-bridge-snippet.js
2026-03-23 20:23:58 +08:00

2829 lines
92 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;(function () {
var READY = "mkt-editor:ready"
var HOVER = "mkt-editor:hover"
var HOVER_CLEAR = "mkt-editor:hover-clear"
var SELECTION = "mkt-editor:selection"
var HOTSPOT_ACTION = "mkt-editor:hotspot-action"
var FOCUS_SELECTOR = "mkt-editor:focus-selector"
var INSPECT_SELECTOR = "mkt-editor:inspect-selector"
var START_INLINE_EDIT = "mkt-editor:start-inline-edit"
var MOVE_BLOCK = "mkt-editor:move-block"
var TOGGLE_BLOCK_VISIBILITY = "mkt-editor:toggle-block-visibility"
var DUPLICATE_BLOCK = "mkt-editor:duplicate-block"
var DELETE_BLOCK = "mkt-editor:delete-block"
var INSERT_BLOCK = "mkt-editor:insert-block"
var RESTORE_CANVAS_STATE = "mkt-editor:restore-canvas-state"
var INLINE_TEXT_CHANGE = "mkt-editor:inline-text-change"
var INLINE_ATTRIBUTE_CHANGE = "mkt-editor:inline-attribute-change"
var INLINE_STYLE_CHANGE = "mkt-editor:inline-style-change"
var BLOCK_VISIBILITY_CHANGE = "mkt-editor:block-visibility-change"
var BLOCK_STRUCTURE_CHANGE = "mkt-editor:block-structure-change"
var STATUS_SUMMARY = "mkt-editor:status-summary"
var HIGHLIGHT_ID = "__mkt_editor_highlight__"
var HOVER_HIGHLIGHT_ID = "__mkt_editor_hover_highlight__"
var DROP_INDICATOR_ID = "__mkt_editor_drop_indicator__"
var DROP_BADGE_ID = "__mkt_editor_drop_badge__"
var FEEDBACK_BADGE_ID = "__mkt_editor_feedback_badge__"
var TOOLTIP_ID = "__mkt_editor_tooltip__"
var PANEL_ID = "__mkt_editor_panel__"
var HOTSPOT_ID = "__mkt_editor_hotspot__"
var INLINE_BADGE_ID = "__mkt_editor_inline_badge__"
var SHARED_INLINE_PANEL_POSITION_ID = "__mkt_editor_shared_inline_panel__"
var INLINE_TEXT_PANEL_ID = "__mkt_editor_inline_text_panel__"
var INLINE_ATTRIBUTE_PANEL_ID = "__mkt_editor_inline_attribute_panel__"
var previewOriginalState = new Map()
var latestPreviewOperations = []
var currentFocusedElement = null
var currentHoveredElement = null
var currentInlineEditableElement = null
var currentInlineAttributeElement = null
var currentInlineTextOriginalValue = null
var currentInlineAttributeOriginalValue = null
var currentDragMoveState = null
var floatingPanelPositions = {}
var currentFloatingPanelDragState = null
var runtimeSelectorSeed = Date.now()
var lastHoveredSelector = ""
var latestStatusSummary = null
var PANEL_THEME = {
shellBackground: "#f8fafc",
shellBorder: "1px solid rgba(148,163,184,0.24)",
shellShadow: "0 18px 36px rgba(15, 23, 42, 0.12)",
headerBackground: "rgba(248,250,252,0.96)",
headerBorder: "1px solid rgba(148,163,184,0.14)",
dragHandleBackground: "#e8f0ff",
dragHandleBorder: "1px solid rgba(96,165,250,0.24)",
dragHandleText: "#35507a",
closeButtonText: "#475569",
sectionTitleText: "#334155",
iconText: "#64748b",
inputBackground: "#ffffff",
inputBorder: "1px solid rgba(100,116,139,0.30)",
inputText: "#0f172a",
divider: "1px solid rgba(148,163,184,0.14)",
pickerBackground: "#ffffff",
pickerBorder: "1px solid rgba(100,116,139,0.30)",
}
function refreshVisualState() {
updateHighlight(currentFocusedElement, "focus")
updateHoverHighlight(currentHoveredElement)
}
function rememberOriginalState(element) {
if (!element || previewOriginalState.has(element)) {
return
}
previewOriginalState.set(element, {
textContent: element.textContent,
innerHTML: element.innerHTML,
attributes: Array.prototype.slice.call(element.attributes || []).reduce(function (summary, attr) {
summary[attr.name] = attr.value
return summary
}, {}),
style: element.getAttribute("style") || "",
})
}
function resetPreviewState() {
previewOriginalState.forEach(function (originalState, element) {
if (!originalState || !element) {
return
}
element.innerHTML = originalState.innerHTML
element.textContent = originalState.textContent
Array.prototype.slice.call(element.attributes || []).forEach(function (attr) {
if (!(attr.name in originalState.attributes)) {
element.removeAttribute(attr.name)
}
})
Object.keys(originalState.attributes).forEach(function (name) {
element.setAttribute(name, originalState.attributes[name])
})
if (originalState.style) {
element.setAttribute("style", originalState.style)
} else {
element.removeAttribute("style")
}
})
}
function applyPreviewOperations(operations) {
resetPreviewState()
latestPreviewOperations = Array.isArray(operations) ? operations : []
if (!Array.isArray(operations) || operations.length === 0) {
refreshVisualState()
return
}
operations.forEach(function (operation) {
var selector = operation.selector_value || operation.selector || ""
if (!selector) {
return
}
var element = document.querySelector(selector)
if (!element) {
return
}
rememberOriginalState(element)
var changeType = operation.change_type || operation.action || ""
var payload = operation.payload || {}
if (changeType === "replace_text" && typeof payload.text === "string") {
element.textContent = payload.text
return
}
if (changeType === "set_html" && typeof payload.html === "string") {
element.innerHTML = payload.html
return
}
if (changeType === "set_attribute" && payload.name) {
element.setAttribute(payload.name, payload.value || "")
return
}
if (changeType === "set_style" && payload.property) {
element.style[payload.property] = payload.value || ""
}
})
refreshVisualState()
}
function getPreviewSummaryForSelector(selector) {
if (!selector) {
return {
total: latestPreviewOperations.length,
matched: 0,
types: [],
}
}
var matchedOperations = latestPreviewOperations.filter(function (operation) {
return (operation.selector_value || operation.selector || "") === selector
})
return {
total: latestPreviewOperations.length,
matched: matchedOperations.length,
types: matchedOperations
.map(function (operation) {
return operation.change_type || operation.action || ""
})
.filter(Boolean),
}
}
function getPreviewTypeLabel(type) {
if (type === "replace_text") {
return "文字"
}
if (type === "set_html") {
return "內容"
}
if (type === "set_attribute") {
return "屬性"
}
if (type === "set_style") {
return "樣式"
}
return "修改"
}
function getModeTone(mode, status) {
if (status === "success" || mode === "preview-ready") {
return {
badge: "rgba(14,116,144,0.10);color:#0f766e;",
panel: "rgba(20,184,166,0.06)",
panelBorder: "rgba(94,234,212,0.24)",
border: "#0f766e",
shadow: "0 0 0 9999px rgba(15, 118, 110, 0.06)",
}
}
if (mode === "inspect") {
return {
badge: "rgba(180,83,9,0.10);color:#9a3412;",
panel: "rgba(245,158,11,0.06)",
panelBorder: "rgba(251,191,36,0.24)",
border: "#b45309",
shadow: "0 0 0 9999px rgba(180, 83, 9, 0.06)",
}
}
if (mode === "hover" || mode === "compare" || status === "warning") {
return {
badge: "rgba(30,64,175,0.10);color:#1d4ed8;",
panel: "rgba(59,130,246,0.06)",
panelBorder: "rgba(96,165,250,0.24)",
border: "#2563eb",
shadow: "0 0 0 9999px rgba(37, 99, 235, 0.05)",
}
}
return {
badge: "rgba(30,64,175,0.10);color:#1d4ed8;",
panel: "rgba(59,130,246,0.06)",
panelBorder: "rgba(96,165,250,0.24)",
border: "#2563eb",
shadow: "0 0 0 9999px rgba(37, 99, 235, 0.06)",
}
}
function postToParent(payload) {
if (window.parent && window.parent !== window) {
window.parent.postMessage(payload, "*")
}
}
function stripEditorArtifacts(root) {
if (!root || typeof root.querySelectorAll !== "function") {
return
}
;[
HIGHLIGHT_ID,
HOVER_HIGHLIGHT_ID,
DROP_INDICATOR_ID,
DROP_BADGE_ID,
FEEDBACK_BADGE_ID,
TOOLTIP_ID,
PANEL_ID,
HOTSPOT_ID,
INLINE_BADGE_ID,
INLINE_TEXT_PANEL_ID,
INLINE_ATTRIBUTE_PANEL_ID,
].forEach(function (id) {
var node = root.querySelector("#" + id)
if (node && node.parentNode) {
node.parentNode.removeChild(node)
}
})
Array.prototype.forEach.call(root.querySelectorAll("[data-mkt-inline-edit-original]"), function (node) {
node.removeAttribute("data-mkt-inline-edit-original")
node.removeAttribute("contenteditable")
})
Array.prototype.forEach.call(root.querySelectorAll("[style]"), function (node) {
if (!(node instanceof Element)) {
return
}
node.style.outline = ""
node.style.outlineOffset = ""
if (!node.getAttribute("style")) {
node.removeAttribute("style")
}
})
}
function ensureEditorTooltip() {
var tooltip = document.getElementById(TOOLTIP_ID)
if (tooltip) {
return tooltip
}
tooltip = document.createElement("div")
tooltip.id = TOOLTIP_ID
tooltip.style.position = "fixed"
tooltip.style.zIndex = "2147483647"
tooltip.style.display = "none"
tooltip.style.pointerEvents = "none"
tooltip.style.maxWidth = "180px"
tooltip.style.padding = "6px 8px"
tooltip.style.borderRadius = "8px"
tooltip.style.background = "rgba(15,23,42,0.94)"
tooltip.style.color = "#fff"
tooltip.style.fontSize = "12px"
tooltip.style.lineHeight = "1.4"
tooltip.style.boxShadow = "0 8px 20px rgba(15,23,42,0.22)"
tooltip.style.fontFamily =
'"Noto Sans TC","PingFang TC","Microsoft JhengHei",system-ui,sans-serif'
document.body.appendChild(tooltip)
return tooltip
}
function hideEditorTooltip() {
var tooltip = document.getElementById(TOOLTIP_ID)
if (!tooltip) {
return
}
tooltip.style.display = "none"
}
function showEditorTooltip(label, event) {
if (!label || !event) {
return
}
var tooltip = ensureEditorTooltip()
tooltip.textContent = label
tooltip.style.display = "block"
var offsetX = 12
var offsetY = 14
var left = event.clientX + offsetX
var top = event.clientY + offsetY
var maxLeft = window.innerWidth - tooltip.offsetWidth - 12
var maxTop = window.innerHeight - tooltip.offsetHeight - 12
tooltip.style.left = Math.max(8, Math.min(left, maxLeft)) + "px"
tooltip.style.top = Math.max(8, Math.min(top, maxTop)) + "px"
}
function decoratePanelIconTooltips(panel) {
if (!panel || typeof panel.querySelectorAll !== "function") {
return
}
Array.prototype.forEach.call(panel.querySelectorAll("[title]"), function (node) {
if (!(node instanceof Element)) {
return
}
var label = node.getAttribute("title") || ""
if (!label) {
return
}
node.removeAttribute("title")
node.style.cursor = node.getAttribute("data-mkt-panel-drag-handle")
? "grab"
: node.style.cursor || "help"
node.onmouseenter = function (event) {
showEditorTooltip(label, event)
}
node.onmousemove = function (event) {
showEditorTooltip(label, event)
}
node.onmouseleave = function () {
hideEditorTooltip()
}
Array.prototype.forEach.call(node.querySelectorAll("*"), function (child) {
if (!(child instanceof Element)) {
return
}
child.removeAttribute("title")
child.style.pointerEvents = "none"
})
})
}
function createPageHtmlSnapshot() {
if (!document.body) {
return ""
}
var clone = document.body.cloneNode(true)
stripEditorArtifacts(clone)
return clone.innerHTML
}
function resolveActionTargetElement() {
if (
latestStatusSummary &&
(latestStatusSummary.mode === "hover" || latestStatusSummary.mode === "compare")
) {
return currentHoveredElement
}
return currentFocusedElement || currentHoveredElement
}
function dispatchActionFromTarget(action, targetElement) {
if (!targetElement) {
return
}
var meta = extractElementMeta(targetElement)
var selector = meta.selector
var actionIntent = resolveActionIntent(action, meta)
if (action === "select-current") {
handleSelection(targetElement, "canvas-hotspot")
clearHoverState()
}
if (action === "inspect-current") {
handleInspectSelector(selector)
}
if (action === "edit-current") {
handleSelection(targetElement, "canvas-hotspot")
}
if (action === "confirm-current") {
handleFocusSelector(selector)
}
postToParent({
type: HOTSPOT_ACTION,
action: action,
selector: selector,
tag: meta.tag,
text: meta.text,
href: meta.href,
src: meta.src,
intent_change_type: actionIntent.changeType,
intent_field: actionIntent.field,
intent_attribute_name: actionIntent.attributeName,
intent_attribute_value: actionIntent.attributeValue,
source: "canvas-hotspot",
})
}
function resolveEditAction(targetElement) {
if (!targetElement) {
return "edit-current"
}
var tag = targetElement.tagName ? targetElement.tagName.toLowerCase() : ""
var href = targetElement.getAttribute && targetElement.getAttribute("href")
var src = targetElement.getAttribute && targetElement.getAttribute("src")
var text = ((targetElement.innerText || targetElement.textContent) || "").trim()
var className = typeof targetElement.className === "string" ? targetElement.className : ""
if (tag === "img" || src) {
return "edit-image-current"
}
if (
(tag === "a" || tag === "button") &&
text &&
(tag === "button" ||
targetElement.getAttribute("role") === "button" ||
/button|btn|cta/i.test(className))
) {
return "edit-text-current"
}
if (tag === "a" || href) {
return "edit-link-current"
}
if (["h1", "h2", "h3", "h4", "h5", "h6", "p", "span", "button", "li"].indexOf(tag) >= 0) {
return "edit-text-current"
}
return "edit-style-current"
}
function resolveActionIntent(action, meta) {
if (action === "edit-text-current") {
return {
changeType: "replace_text",
field: "content",
attributeName: "",
attributeValue: "",
}
}
if (action === "edit-link-current") {
return {
changeType: "set_attribute",
field: "attribute-value",
attributeName: "href",
attributeValue: meta.href || "",
}
}
if (action === "edit-image-current") {
return {
changeType: "set_attribute",
field: "attribute-value",
attributeName: "src",
attributeValue: meta.src || "",
}
}
if (action === "edit-style-current") {
return {
changeType: "set_style",
field: "style-property",
attributeName: "",
attributeValue: "",
}
}
return {
changeType: "",
field: "",
attributeName: "",
attributeValue: "",
}
}
function getActionLabel(action) {
if (action === "edit-text-current") {
return "改文字"
}
if (action === "edit-link-current") {
return "改連結"
}
if (action === "edit-image-current") {
return "換圖片"
}
if (action === "edit-style-current") {
return "改樣式"
}
if (action === "select-current") {
return "設為檢查目標"
}
if (action === "inspect-current") {
return "先檢查"
}
if (action === "confirm-current") {
return "回畫面確認"
}
return "開始修改"
}
function resolveQuickEditActions(targetElement) {
if (!targetElement) {
return []
}
var primaryEditAction = resolveEditAction(targetElement)
var quickActions = [primaryEditAction]
if (primaryEditAction !== "edit-style-current") {
quickActions.push("edit-style-current")
}
return quickActions.slice(0, 2)
}
function ensureHighlightBox() {
var box = document.getElementById(HIGHLIGHT_ID)
if (box) {
return box
}
box = document.createElement("div")
box.id = HIGHLIGHT_ID
box.style.position = "fixed"
box.style.zIndex = "2147483647"
box.style.pointerEvents = "none"
box.style.border = "2px solid #1d4ed8"
box.style.borderRadius = "10px"
box.style.boxShadow = "none"
box.style.transition = "all 120ms ease"
box.style.display = "none"
document.body.appendChild(box)
return box
}
function ensureHoverHighlightBox() {
var box = document.getElementById(HOVER_HIGHLIGHT_ID)
if (box) {
return box
}
box = document.createElement("div")
box.id = HOVER_HIGHLIGHT_ID
box.style.position = "fixed"
box.style.zIndex = "2147483646"
box.style.pointerEvents = "none"
box.style.border = "2px dashed #2563eb"
box.style.borderRadius = "10px"
box.style.boxShadow = "none"
box.style.transition = "all 120ms ease"
box.style.display = "none"
document.body.appendChild(box)
return box
}
function ensureDropIndicator() {
var indicator = document.getElementById(DROP_INDICATOR_ID)
if (indicator) {
return indicator
}
indicator = document.createElement("div")
indicator.id = DROP_INDICATOR_ID
indicator.style.position = "fixed"
indicator.style.zIndex = "2147483647"
indicator.style.pointerEvents = "none"
indicator.style.height = "4px"
indicator.style.borderRadius = "999px"
indicator.style.background = "#1d4ed8"
indicator.style.boxShadow = "0 0 0 2px rgba(255,255,255,0.85)"
indicator.style.display = "none"
document.body.appendChild(indicator)
return indicator
}
function ensureDropBadge() {
var badge = document.getElementById(DROP_BADGE_ID)
if (badge) {
return badge
}
badge = document.createElement("div")
badge.id = DROP_BADGE_ID
badge.style.position = "fixed"
badge.style.zIndex = "2147483647"
badge.style.pointerEvents = "none"
badge.style.display = "none"
badge.style.padding = "6px 10px"
badge.style.borderRadius = "999px"
badge.style.background = "rgba(30,41,59,0.92)"
badge.style.color = "#fff"
badge.style.fontSize = "12px"
badge.style.fontWeight = "700"
badge.style.fontFamily =
'"Noto Sans TC","PingFang TC","Microsoft JhengHei",system-ui,sans-serif'
badge.style.boxShadow = "0 8px 18px rgba(15, 23, 42, 0.18)"
badge.textContent = "放到這裡"
document.body.appendChild(badge)
return badge
}
function ensureFeedbackBadge() {
var badge = document.getElementById(FEEDBACK_BADGE_ID)
if (badge) {
return badge
}
badge = document.createElement("div")
badge.id = FEEDBACK_BADGE_ID
badge.style.position = "fixed"
badge.style.zIndex = "2147483647"
badge.style.pointerEvents = "none"
badge.style.opacity = "0"
badge.style.padding = "7px 14px"
badge.style.borderRadius = "999px"
badge.style.background = "rgba(15,23,42,0.88)"
badge.style.color = "#f1f5f9"
badge.style.fontSize = "12px"
badge.style.fontWeight = "600"
badge.style.letterSpacing = "0.01em"
badge.style.fontFamily =
'"Noto Sans TC","PingFang TC","Microsoft JhengHei",system-ui,sans-serif'
badge.style.boxShadow = "0 8px 20px rgba(15, 23, 42, 0.22), 0 0 0 1px rgba(255,255,255,0.06) inset"
badge.style.backdropFilter = "blur(8px)"
badge.style.transition = "opacity 0.18s ease"
document.body.appendChild(badge)
return badge
}
function showFeedbackBadge(message, element) {
if (!message) {
return
}
var badge = ensureFeedbackBadge()
badge.textContent = message
if (element) {
var rect = element.getBoundingClientRect()
var approxWidth = message.length * 8 + 28
badge.style.left = Math.max(12, Math.min(window.innerWidth - approxWidth - 12, rect.left + rect.width / 2 - approxWidth / 2)) + "px"
badge.style.top = Math.max(12, rect.top - 38) + "px"
badge.style.transform = ""
} else {
badge.style.left = "50%"
badge.style.top = "16px"
badge.style.transform = "translateX(-50%)"
}
badge.style.opacity = "1"
window.clearTimeout(showFeedbackBadge._timer)
showFeedbackBadge._timer = window.setTimeout(function () {
badge.style.opacity = "0"
}, 1400)
}
function ensureInspectPanel() {
return null
}
function ensureActionHotspot() {
var hotspot = document.getElementById(HOTSPOT_ID)
if (hotspot) {
return hotspot
}
hotspot = document.createElement("div")
hotspot.id = HOTSPOT_ID
hotspot.style.position = "fixed"
hotspot.style.zIndex = "2147483647"
hotspot.style.display = "none"
hotspot.style.pointerEvents = "auto"
hotspot.style.padding = "4px"
hotspot.style.borderRadius = "999px"
hotspot.style.background = "rgba(30,41,59,0.84)"
hotspot.style.backdropFilter = "blur(10px)"
hotspot.style.boxShadow = "0 10px 24px rgba(15, 23, 42, 0.16)"
hotspot.style.color = "#fff"
hotspot.style.fontFamily =
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
hotspot.innerHTML =
'<div style="display:flex;align-items:center;gap:4px;">' +
'<button id="__mkt_editor_hotspot_move_up__" type="button" title="上移" style="width:32px;height:32px;border:0;border-radius:999px;background:rgba(255,255,255,0.10);color:#fff;font-size:14px;font-weight:700;cursor:pointer;">↑</button>' +
'<button id="__mkt_editor_hotspot_drag__" type="button" title="拖移" style="width:32px;height:32px;border:0;border-radius:999px;background:rgba(255,255,255,0.10);color:#fff;font-size:13px;font-weight:700;cursor:grab;">⋮⋮</button>' +
'<button id="__mkt_editor_hotspot_move_down__" type="button" title="下移" style="width:32px;height:32px;border:0;border-radius:999px;background:rgba(255,255,255,0.10);color:#fff;font-size:14px;font-weight:700;cursor:pointer;">↓</button>' +
"</div>"
document.body.appendChild(hotspot)
hotspot
.querySelector("#__mkt_editor_hotspot_move_up__")
.addEventListener("click", function (event) {
event.preventDefault()
event.stopPropagation()
var targetElement = resolveActionTargetElement()
if (!targetElement) {
return
}
handleMoveBlock({
selector: buildSelector(targetElement),
direction: "up",
})
})
hotspot
.querySelector("#__mkt_editor_hotspot_move_down__")
.addEventListener("click", function (event) {
event.preventDefault()
event.stopPropagation()
var targetElement = resolveActionTargetElement()
if (!targetElement) {
return
}
handleMoveBlock({
selector: buildSelector(targetElement),
direction: "down",
})
})
hotspot
.querySelector("#__mkt_editor_hotspot_drag__")
.addEventListener("pointerdown", function (event) {
event.preventDefault()
event.stopPropagation()
var targetElement = resolveActionTargetElement()
if (!targetElement) {
return
}
startDragMove(event, targetElement)
})
return hotspot
}
function ensureInlineEditBadge() {
return null
}
function buildPanelInputStyle(options) {
var minHeight = (options && options.minHeight) || "42px"
var fontSize = (options && options.fontSize) || "13px"
var padding = (options && options.padding) || "10px 12px"
var extra = (options && options.extra) || ""
return (
"width:100%;" +
"min-height:" +
minHeight +
";" +
"border:" +
PANEL_THEME.inputBorder +
";" +
"border-radius:12px;" +
"padding:" +
padding +
";" +
"font-size:" +
fontSize +
";" +
"color:" +
PANEL_THEME.inputText +
";" +
"background:" +
PANEL_THEME.inputBackground +
";" +
"box-shadow:inset 0 1px 0 rgba(255,255,255,0.7);" +
extra
)
}
function applyInlineFloatingPanelStyles(panel, options) {
if (!panel) {
return
}
var width = (options && options.width) || "324px"
var left = (options && options.left) || "calc(100vw - 356px)"
var top = (options && options.top) || "88px"
var maxHeight = (options && options.maxHeight) || "min(520px, calc(100vh - 96px))"
var padding = (options && options.padding) || "10px 12px 12px"
var radius = (options && options.radius) || "16px"
panel.style.position = "fixed"
panel.style.left = left
panel.style.top = top
panel.style.zIndex = "2147483647"
panel.style.display = "none"
panel.style.width = width
panel.style.maxHeight = maxHeight
panel.style.overflowY = "auto"
panel.style.padding = padding
panel.style.borderRadius = radius
panel.style.background = PANEL_THEME.shellBackground
panel.style.border = PANEL_THEME.shellBorder
panel.style.boxShadow = PANEL_THEME.shellShadow
panel.style.fontFamily =
'"Noto Sans TC","PingFang TC","Microsoft JhengHei",system-ui,sans-serif'
}
function buildInlineFloatingPanelHeader(closeButtonId, titleId) {
var gripIcon =
'<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><circle cx="4" cy="4" r="1.2" fill="currentColor"/><circle cx="4" cy="7" r="1.2" fill="currentColor"/><circle cx="4" cy="10" r="1.2" fill="currentColor"/><circle cx="10" cy="4" r="1.2" fill="currentColor"/><circle cx="10" cy="7" r="1.2" fill="currentColor"/><circle cx="10" cy="10" r="1.2" fill="currentColor"/></svg>'
return (
'<div data-mkt-panel-drag-handle="true" style="position:sticky;top:0;z-index:4;margin:0 0 10px;padding:9px 8px 10px;display:flex;align-items:center;gap:8px;cursor:grab;user-select:none;background:' +
PANEL_THEME.headerBackground +
';backdrop-filter:blur(10px);border-bottom:' +
PANEL_THEME.headerBorder +
';">' +
'<div title="拖曳編輯框" style="flex-shrink:0;width:26px;height:26px;border-radius:8px;background:' +
PANEL_THEME.dragHandleBackground +
';display:flex;align-items:center;justify-content:center;color:' +
PANEL_THEME.dragHandleText +
';cursor:grab;border:' +
PANEL_THEME.dragHandleBorder +
';">' +
gripIcon +
"</div>" +
'<span id="' +
(titleId || "__mkt_panel_title__") +
'" style="flex:1;font-size:12px;font-weight:700;color:#0f172a;letter-spacing:0.02em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">編輯</span>' +
'<button id="' +
closeButtonId +
'" type="button" title="關閉" style="flex-shrink:0;width:26px;height:26px;border:0;border-radius:8px;background:rgba(148,163,184,0.1);color:' +
PANEL_THEME.closeButtonText +
';font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;">×</button>' +
"</div>"
)
}
function buildPanelSelectOptions(options) {
return (options || [])
.map(function (option) {
return '<option value="' + option.value + '">' + option.label + "</option>"
})
.join("")
}
function buildPanelControlRow(config) {
var colorControl =
config.controlType === "color"
? '<input id="' +
config.controlId +
'" type="color" style="width:44px;height:42px;border:' +
PANEL_THEME.pickerBorder +
';border-radius:10px;padding:4px;background:' +
PANEL_THEME.pickerBackground +
';" />'
: '<select id="' +
config.controlId +
'" style="' +
config.selectStyle +
'"><option value=""></option>' +
buildPanelSelectOptions(config.options) +
"</select>"
return (
'<div style="' +
config.controlShellStyle +
'">' +
'<div title="' +
config.iconLabel +
'" style="' +
config.iconStyle +
'">' +
config.iconSvg +
"</div>" +
'<input id="' +
config.inputId +
'" type="text" placeholder="' +
config.placeholder +
'" style="' +
config.inputStyle +
'" />' +
colorControl +
"</div>"
)
}
function buildPanelSectionTitle(label, marginTop) {
return (
'<div style="margin-top:' +
(marginTop || "14px") +
';font-size:12px;font-weight:700;color:' +
PANEL_THEME.sectionTitleText +
';letter-spacing:0.02em;">' +
label +
"</div>"
)
}
function buildPanelRowStyles() {
return {
controlShellStyle:
"display:grid;grid-template-columns:34px minmax(0,1fr) 52px;gap:10px;align-items:center;",
iconStyle:
"width:34px;height:34px;display:flex;align-items:center;justify-content:center;color:" +
PANEL_THEME.iconText +
";opacity:0.92;",
inputStyle: buildPanelInputStyle(),
selectStyle: buildPanelInputStyle({ padding: "10px 8px", fontSize: "12px" }),
}
}
function buildBlockStyleRows(prefix, styles) {
return (
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "寬度",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 10 6 7v2h8V7l3 3-3 3v-2H6v2l-3-3Z" fill="currentColor"/></svg>',
inputId: prefix + "_width__",
placeholder: "寬度,例如 400px",
controlId: prefix + "_width_preset__",
options: [
{ value: "100%", label: "滿" },
{ value: "400px", label: "4" },
{ value: "600px", label: "6" },
{ value: "800px", label: "8" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "圓角",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 6a1 1 0 0 1 1-1h4a5 5 0 0 1 5 5v4a1 1 0 1 1-2 0v-4a3 3 0 0 0-3-3H6a1 1 0 0 1-1-1Z" fill="currentColor"/></svg>',
inputId: prefix + "_radius__",
placeholder: "圓角,例如 16px",
controlId: prefix + "_radius_preset__",
options: [
{ value: "0px", label: "0" },
{ value: "8px", label: "8" },
{ value: "12px", label: "12" },
{ value: "16px", label: "16" },
{ value: "24px", label: "24" },
{ value: "999px", label: "∞" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "陰影",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="4" y="4" width="8" height="8" rx="2" fill="currentColor" opacity=".35"/><rect x="8" y="8" width="8" height="8" rx="2" stroke="currentColor" stroke-width="1.5"/></svg>',
inputId: prefix + "_shadow__",
placeholder: "陰影",
controlId: prefix + "_shadow_preset__",
options: [
{ value: "none", label: "無" },
{ value: "0 6px 16px rgba(15,23,42,0.08)", label: "輕" },
{ value: "0 12px 28px rgba(15,23,42,0.14)", label: "中" },
{ value: "0 18px 40px rgba(15,23,42,0.18)", label: "深" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "外距",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="6" y="6" width="8" height="8" stroke="currentColor" stroke-width="1.5" stroke-dasharray="2 2"/><path d="M10 2v3M10 15v3M2 10h3M15 10h3" stroke="currentColor" stroke-width="1.5"/></svg>',
inputId: prefix + "_margin__",
placeholder: "外距",
controlId: prefix + "_margin_preset__",
options: [
{ value: "0px", label: "0" },
{ value: "0px 0px 16px", label: "16" },
{ value: "0px 0px 24px", label: "24" },
{ value: "24px auto", label: "中" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "內距",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="5" width="10" height="10" stroke="currentColor" stroke-width="1.5"/><rect x="8" y="8" width="4" height="4" fill="currentColor"/></svg>',
inputId: prefix + "_padding__",
placeholder: "內距",
controlId: prefix + "_padding_preset__",
options: [
{ value: "0px", label: "0" },
{ value: "8px", label: "8" },
{ value: "12px", label: "12" },
{ value: "16px", label: "16" },
{ value: "24px", label: "24" },
{ value: "32px", label: "32" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "背景色",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6 13.5 11.5 8 15 11.5 9.5 17H6v-3.5Z" fill="currentColor"/><path d="M4 17h12" stroke="currentColor" stroke-width="1.5"/></svg>',
inputId: prefix + "_background__",
placeholder: "背景色",
controlId: prefix + "_background_picker__",
controlType: "color",
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "邊框寬度",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="4" y="4" width="12" height="12" stroke="currentColor" stroke-width="2"/></svg>',
inputId: prefix + "_border_width__",
placeholder: "框線寬",
controlId: prefix + "_border_width_preset__",
options: [
{ value: "0px", label: "0" },
{ value: "1px", label: "1" },
{ value: "2px", label: "2" },
{ value: "4px", label: "4" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "邊框樣式",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="4" y="4" width="12" height="12" stroke="currentColor" stroke-width="1.5" stroke-dasharray="2 2"/></svg>',
inputId: prefix + "_border_style__",
placeholder: "框線樣式",
controlId: prefix + "_border_style_preset__",
options: [
{ value: "none", label: "無" },
{ value: "solid", label: "實" },
{ value: "dashed", label: "虛" },
{ value: "dotted", label: "點" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "邊框顏色",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="4" y="4" width="12" height="12" stroke="currentColor" stroke-width="1.5"/><path d="M4 15h12" stroke="#2563eb" stroke-width="2"/></svg>',
inputId: prefix + "_border_color__",
placeholder: "框線色",
controlId: prefix + "_border_color_picker__",
controlType: "color",
})
)
}
function buildTextStyleRows(prefix, styles) {
return (
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "字級",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 15L8 5h1.6l4 10h-1.9l-.9-2.4H6.6L5.7 15H4Zm3.1-3.9h3.1L8.65 7 7.1 11.1Z" fill="currentColor"/></svg>',
inputId: prefix + "_font_size__",
placeholder: "字級,例如 16px",
controlId: prefix + "_font_size_preset__",
options: [
{ value: "12px", label: "12" },
{ value: "14px", label: "14" },
{ value: "16px", label: "16" },
{ value: "18px", label: "18" },
{ value: "20px", label: "20" },
{ value: "24px", label: "24" },
{ value: "32px", label: "32" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "粗細",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6 4h4.8c2.2 0 3.7 1.2 3.7 3.1 0 1.2-.6 2.1-1.7 2.6 1.4.4 2.2 1.5 2.2 3 0 2.1-1.6 3.3-4.1 3.3H6V4Zm1.8 4.9h2.7c1.3 0 2.1-.6 2.1-1.6s-.8-1.6-2.1-1.6H7.8v3.2Zm0 5.5h3c1.5 0 2.4-.6 2.4-1.8s-.9-1.8-2.4-1.8h-3v3.6Z" fill="currentColor"/></svg>',
inputId: prefix + "_font_weight__",
placeholder: "粗細,例如 400",
controlId: prefix + "_font_weight_preset__",
options: [
{ value: "300", label: "300" },
{ value: "400", label: "400" },
{ value: "500", label: "500" },
{ value: "600", label: "600" },
{ value: "700", label: "700" },
{ value: "800", label: "800" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "字型",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 5h12v1.8h-5V16H9V6.8H4V5Z" fill="currentColor"/></svg>',
inputId: prefix + "_font_family__",
placeholder: "字型,例如 Noto Sans TC",
controlId: prefix + "_font_family_preset__",
options: [
{ value: '"Noto Sans TC", sans-serif', label: "黑" },
{ value: '"PingFang TC", sans-serif', label: "蘋" },
{ value: '"Microsoft JhengHei", sans-serif', label: "微" },
{ value: "Georgia, serif", label: "G" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "對齊",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 5h12v2H4V5Zm2 4h8v2H6V9Zm-2 4h12v2H4v-2Z" fill="currentColor"/></svg>',
inputId: prefix + "_align__",
placeholder: "對齊,例如 center",
controlId: prefix + "_align_preset__",
options: [
{ value: "left", label: "左" },
{ value: "center", label: "中" },
{ value: "right", label: "右" },
{ value: "justify", label: "齊" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "文字顏色",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10.9 4 15 15h-2.1l-.8-2.3H7.8L7 15H5L9.1 4h1.8Zm.5 6.9L10 6.8 8.6 10.9h2.8ZM5 16h10v1.5H5V16Z" fill="currentColor"/></svg>',
inputId: prefix + "_color__",
placeholder: "顏色,例如 #111827",
controlId: prefix + "_color_picker__",
controlType: "color",
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "文字裝飾",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M6 4v4a4 4 0 1 0 8 0V4h-2v4a2 2 0 1 1-4 0V4H6Zm-1 11h10v1.5H5V15Z" fill="currentColor"/></svg>',
inputId: prefix + "_decoration__",
placeholder: "裝飾,例如 underline",
controlId: prefix + "_decoration_preset__",
options: [
{ value: "none", label: "無" },
{ value: "underline", label: "底" },
{ value: "line-through", label: "刪" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "行高",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3 7 6h2v8H7l3 3 3-3h-2V6h2l-3-3ZM4 7h2v6H4V7Zm10 0h2v6h-2V7Z" fill="currentColor"/></svg>',
inputId: prefix + "_line_height__",
placeholder: "行高,例如 1.6",
controlId: prefix + "_line_height_preset__",
options: [
{ value: "1.2", label: "1.2" },
{ value: "1.4", label: "1.4" },
{ value: "1.6", label: "1.6" },
{ value: "1.8", label: "1.8" },
{ value: "2", label: "2" },
],
}) +
buildPanelControlRow({
controlShellStyle: styles.controlShellStyle,
iconStyle: styles.iconStyle,
inputStyle: styles.inputStyle,
selectStyle: styles.selectStyle,
iconLabel: "字距",
iconSvg:
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M4 10 7 7v2h6V7l3 3-3 3v-2H7v2l-3-3ZM2 5h1.5v10H2V5Zm14.5 0H18v10h-1.5V5Z" fill="currentColor"/></svg>',
inputId: prefix + "_letter_spacing__",
placeholder: "字距,例如 1px",
controlId: prefix + "_letter_spacing_preset__",
options: [
{ value: "normal", label: "N" },
{ value: "0px", label: "0" },
{ value: "0.5px", label: "0.5" },
{ value: "1px", label: "1" },
{ value: "2px", label: "2" },
],
})
)
}
function ensureInlineTextPanel() {
var panel = document.getElementById(INLINE_TEXT_PANEL_ID)
if (panel) {
return panel
}
panel = document.createElement("div")
panel.id = INLINE_TEXT_PANEL_ID
applyInlineFloatingPanelStyles(panel, {
width: "324px",
left: "calc(100vw - 356px)",
})
var panelRowStyles = buildPanelRowStyles()
var textStyleRows = buildTextStyleRows("__mkt_editor_inline_text", panelRowStyles)
var blockStyleRows = buildBlockStyleRows("__mkt_editor_inline_text_block", panelRowStyles)
panel.innerHTML =
buildInlineFloatingPanelHeader("__mkt_editor_inline_text_close__", "__mkt_editor_inline_text_title__") +
'<textarea id="__mkt_editor_inline_text_area__" style="margin-top:2px;' +
buildPanelInputStyle({
minHeight: "88px",
fontSize: "14px",
padding: "12px 14px",
extra: "line-height:1.7;resize:vertical;",
}) +
'"></textarea>' +
'<div id="__mkt_editor_inline_text_link_group__" style="display:none;margin-top:8px;">' +
'<div style="font-size:12px;font-weight:700;color:#0f172a;margin-bottom:6px;">按鈕 / 連結網址</div>' +
'<input id="__mkt_editor_inline_text_link_input__" type="text" placeholder="https://example.com" style="' +
buildPanelInputStyle({}) +
'" />' +
"</div>" +
'<div style="margin-top:14px;padding-top:12px;border-top:' +
PANEL_THEME.divider +
';">' +
buildPanelSectionTitle("文字樣式", "0px") +
'<div style="margin-top:8px;display:grid;gap:10px;">' +
textStyleRows +
buildPanelSectionTitle("區塊樣式") +
'<div style="margin-top:8px;display:grid;gap:10px;">' +
blockStyleRows +
"</div>"
document.body.appendChild(panel)
bindFloatingPanelDrag(panel, INLINE_TEXT_PANEL_ID)
decoratePanelIconTooltips(panel)
var closeButton = panel.querySelector("#__mkt_editor_inline_text_close__")
if (closeButton) {
closeButton.onclick = function (event) {
event.preventDefault()
event.stopPropagation()
clearInlineEdit()
}
}
return panel
}
function ensureInlineAttributePanel() {
var panel = document.getElementById(INLINE_ATTRIBUTE_PANEL_ID)
if (panel) {
return panel
}
panel = document.createElement("div")
panel.id = INLINE_ATTRIBUTE_PANEL_ID
applyInlineFloatingPanelStyles(panel, {
width: "324px",
left: "calc(100vw - 356px)",
})
var panelRowStyles = buildPanelRowStyles()
panel.innerHTML =
buildInlineFloatingPanelHeader("__mkt_editor_inline_attr_close__", "__mkt_editor_inline_attr_title__") +
'<div id="__mkt_editor_inline_attr_label__" style="font-size:11px;font-weight:700;color:#64748b;letter-spacing:0.05em;text-transform:uppercase;margin:2px 0 5px;"></div>' +
'<input id="__mkt_editor_inline_attr_input__" type="text" style="' +
buildPanelInputStyle({ fontSize: "14px", padding: "12px 14px" }) +
'" />' +
'<div style="margin-top:14px;padding-top:12px;border-top:' +
PANEL_THEME.divider +
';">' +
buildPanelSectionTitle("區塊樣式", "0px") +
'<div style="margin-top:8px;display:grid;gap:10px;">' +
buildBlockStyleRows("__mkt_editor_inline_attr", panelRowStyles) +
"</div>"
document.body.appendChild(panel)
bindFloatingPanelDrag(panel, INLINE_ATTRIBUTE_PANEL_ID)
decoratePanelIconTooltips(panel)
var closeButton = panel.querySelector("#__mkt_editor_inline_attr_close__")
if (closeButton) {
closeButton.onclick = function (event) {
event.preventDefault()
event.stopPropagation()
clearInlineEdit()
}
}
return panel
}
function updateInlineEditBadge(element) {
var badge = ensureInlineEditBadge()
if (!badge || !element) {
return
}
}
function applyInlineStyleValue(element, property, value, source) {
if (!element || !property) {
return
}
element.style[property] = value || ""
postToParent({
type: INLINE_STYLE_CHANGE,
selector: buildSelector(element),
style_property: property,
style_value: value || "",
page_html: createPageHtmlSnapshot(),
source: source || "canvas-inline-edit",
})
}
function bindImmediateStyleControl(element, inputNode, property, source) {
if (!element || !inputNode || !property) {
return
}
inputNode.oninput = function () {
applyInlineStyleValue(element, property, inputNode.value || "", source || "canvas-inline-edit-live")
}
}
function rgbStringToHex(value) {
if (!value || typeof value !== "string") {
return ""
}
if (value[0] === "#") {
return value
}
var match = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
if (!match) {
return ""
}
var hex = [match[1], match[2], match[3]]
.map(function (part) {
var number = Number(part)
var normalized = Number.isNaN(number) ? 0 : Math.max(0, Math.min(255, number))
return normalized.toString(16).padStart(2, "0")
})
.join("")
return "#" + hex
}
function bindPresetSelectToInput(selectNode, inputNode) {
if (!selectNode || !inputNode) {
return
}
selectNode.onchange = function () {
if (!selectNode.value) {
return
}
inputNode.value = selectNode.value
if (typeof inputNode.oninput === "function") {
inputNode.oninput()
}
selectNode.value = ""
}
}
function bindColorPickerToInput(colorPickerNode, inputNode, fallbackHex) {
if (!colorPickerNode || !inputNode) {
return
}
colorPickerNode.value = rgbStringToHex(inputNode.value) || fallbackHex || "#000000"
colorPickerNode.oninput = function () {
inputNode.value = colorPickerNode.value || ""
if (typeof inputNode.oninput === "function") {
inputNode.oninput()
}
}
}
function initializeBlockStyleControls(targetElement, controls) {
if (!targetElement || !controls) {
return
}
if (controls.widthNode) {
controls.widthNode.value = getComputedStyleValue(targetElement, "width") || ""
}
bindImmediateStyleControl(targetElement, controls.widthNode, "width")
bindPresetSelectToInput(controls.widthPresetNode, controls.widthNode)
if (controls.radiusNode) {
controls.radiusNode.value = getComputedStyleValue(targetElement, "borderRadius") || "0px"
}
bindImmediateStyleControl(targetElement, controls.radiusNode, "borderRadius")
bindPresetSelectToInput(controls.radiusPresetNode, controls.radiusNode)
if (controls.shadowNode) {
controls.shadowNode.value = getComputedStyleValue(targetElement, "boxShadow") || "none"
}
bindImmediateStyleControl(targetElement, controls.shadowNode, "boxShadow")
bindPresetSelectToInput(controls.shadowPresetNode, controls.shadowNode)
if (controls.marginNode) {
controls.marginNode.value = getComputedStyleValue(targetElement, "margin") || "0px"
}
bindImmediateStyleControl(targetElement, controls.marginNode, "margin")
bindPresetSelectToInput(controls.marginPresetNode, controls.marginNode)
if (controls.paddingNode) {
controls.paddingNode.value = getComputedStyleValue(targetElement, "padding") || "0px"
}
bindImmediateStyleControl(targetElement, controls.paddingNode, "padding")
bindPresetSelectToInput(controls.paddingPresetNode, controls.paddingNode)
if (controls.backgroundNode) {
var backgroundValue = getComputedStyleValue(targetElement, "backgroundColor")
controls.backgroundNode.value =
rgbStringToHex(backgroundValue) || backgroundValue || "#ffffff"
}
bindImmediateStyleControl(targetElement, controls.backgroundNode, "backgroundColor")
bindColorPickerToInput(controls.backgroundPickerNode, controls.backgroundNode, "#ffffff")
if (controls.borderWidthNode) {
controls.borderWidthNode.value = getComputedStyleValue(targetElement, "borderWidth") || "0px"
}
bindImmediateStyleControl(targetElement, controls.borderWidthNode, "borderWidth")
bindPresetSelectToInput(controls.borderWidthPresetNode, controls.borderWidthNode)
if (controls.borderStyleNode) {
controls.borderStyleNode.value = getComputedStyleValue(targetElement, "borderStyle") || "none"
}
bindImmediateStyleControl(targetElement, controls.borderStyleNode, "borderStyle")
bindPresetSelectToInput(controls.borderStylePresetNode, controls.borderStyleNode)
if (controls.borderColorNode) {
var borderColorValue = getComputedStyleValue(targetElement, "borderColor")
controls.borderColorNode.value =
rgbStringToHex(borderColorValue) || borderColorValue || "#cbd5e1"
}
bindImmediateStyleControl(targetElement, controls.borderColorNode, "borderColor")
bindColorPickerToInput(controls.borderColorPickerNode, controls.borderColorNode, "#cbd5e1")
}
function getComputedStyleValue(element, property) {
if (!element || !property || typeof window.getComputedStyle !== "function") {
return ""
}
try {
return window.getComputedStyle(element)[property] || ""
} catch (error) {
return ""
}
}
function queryBlockStyleControlNodes(panel, prefix) {
return {
widthNode: panel.querySelector("#" + prefix + "_width__"),
widthPresetNode: panel.querySelector("#" + prefix + "_width_preset__"),
radiusNode: panel.querySelector("#" + prefix + "_radius__"),
radiusPresetNode: panel.querySelector("#" + prefix + "_radius_preset__"),
shadowNode: panel.querySelector("#" + prefix + "_shadow__"),
shadowPresetNode: panel.querySelector("#" + prefix + "_shadow_preset__"),
marginNode: panel.querySelector("#" + prefix + "_margin__"),
marginPresetNode: panel.querySelector("#" + prefix + "_margin_preset__"),
paddingNode: panel.querySelector("#" + prefix + "_padding__"),
paddingPresetNode: panel.querySelector("#" + prefix + "_padding_preset__"),
backgroundNode: panel.querySelector("#" + prefix + "_background__"),
backgroundPickerNode: panel.querySelector("#" + prefix + "_background_picker__"),
borderWidthNode: panel.querySelector("#" + prefix + "_border_width__"),
borderWidthPresetNode: panel.querySelector("#" + prefix + "_border_width_preset__"),
borderStyleNode: panel.querySelector("#" + prefix + "_border_style__"),
borderStylePresetNode: panel.querySelector("#" + prefix + "_border_style_preset__"),
borderColorNode: panel.querySelector("#" + prefix + "_border_color__"),
borderColorPickerNode: panel.querySelector("#" + prefix + "_border_color_picker__"),
}
}
function queryTextStyleControlNodes(panel, prefix) {
return {
fontSizeNode: panel.querySelector("#" + prefix + "_font_size__"),
fontSizePresetNode: panel.querySelector("#" + prefix + "_font_size_preset__"),
fontWeightNode: panel.querySelector("#" + prefix + "_font_weight__"),
fontWeightPresetNode: panel.querySelector("#" + prefix + "_font_weight_preset__"),
fontFamilyNode: panel.querySelector("#" + prefix + "_font_family__"),
fontFamilyPresetNode: panel.querySelector("#" + prefix + "_font_family_preset__"),
textAlignNode: panel.querySelector("#" + prefix + "_align__"),
textAlignPresetNode: panel.querySelector("#" + prefix + "_align_preset__"),
textColorNode: panel.querySelector("#" + prefix + "_color__"),
textColorPickerNode: panel.querySelector("#" + prefix + "_color_picker__"),
textDecorationNode: panel.querySelector("#" + prefix + "_decoration__"),
textDecorationPresetNode: panel.querySelector("#" + prefix + "_decoration_preset__"),
textLineHeightNode: panel.querySelector("#" + prefix + "_line_height__"),
textLineHeightPresetNode: panel.querySelector("#" + prefix + "_line_height_preset__"),
textLetterSpacingNode: panel.querySelector("#" + prefix + "_letter_spacing__"),
textLetterSpacingPresetNode: panel.querySelector("#" + prefix + "_letter_spacing_preset__"),
}
}
function applyFloatingPanelPosition(panel, panelId) {
if (!panel) {
return
}
var savedPosition = floatingPanelPositions[panelId]
if (savedPosition) {
panel.style.left = savedPosition.left + "px"
panel.style.top = savedPosition.top + "px"
panel.style.right = "auto"
panel.style.bottom = "auto"
return
}
var panelWidth = panel.offsetWidth || 300
var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0
var left = Math.max(16, viewportWidth - panelWidth - 24)
var top = 88
panel.style.left = left + "px"
panel.style.top = top + "px"
panel.style.right = "auto"
panel.style.bottom = "auto"
}
function showFloatingPanel(panel, panelId) {
if (!panel) {
return
}
panel.style.display = "block"
applyFloatingPanelPosition(panel, panelId)
}
function bindFloatingPanelDrag(panel, panelId) {
var handle = panel && panel.querySelector("[data-mkt-panel-drag-handle='true']")
if (!handle) {
return
}
handle.addEventListener("pointerdown", function (event) {
event.preventDefault()
event.stopPropagation()
var rect = panel.getBoundingClientRect()
currentFloatingPanelDragState = {
panel: panel,
panelId: SHARED_INLINE_PANEL_POSITION_ID,
offsetX: event.clientX - rect.left,
offsetY: event.clientY - rect.top,
}
handle.style.cursor = "grabbing"
})
}
function updateFloatingPanelDrag(event) {
if (!currentFloatingPanelDragState) {
return
}
var panel = currentFloatingPanelDragState.panel
var panelWidth = panel.offsetWidth || 300
var panelHeight = panel.offsetHeight || 240
var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0
var left = event.clientX - currentFloatingPanelDragState.offsetX
var top = event.clientY - currentFloatingPanelDragState.offsetY
left = Math.min(Math.max(12, left), Math.max(12, viewportWidth - panelWidth - 12))
top = Math.min(Math.max(72, top), Math.max(12, viewportHeight - panelHeight - 12))
panel.style.left = left + "px"
panel.style.top = top + "px"
panel.style.right = "auto"
panel.style.bottom = "auto"
floatingPanelPositions[currentFloatingPanelDragState.panelId] = { left: left, top: top }
}
function endFloatingPanelDrag() {
if (!currentFloatingPanelDragState) {
return
}
var handle = currentFloatingPanelDragState.panel.querySelector("[data-mkt-panel-drag-handle='true']")
if (handle) {
handle.style.cursor = "grab"
}
currentFloatingPanelDragState = null
}
function bindLiveAttributeInput(inputNode, targetElement, attributeName) {
if (!inputNode || !targetElement || !attributeName) {
return
}
inputNode.oninput = function () {
targetElement.setAttribute(attributeName, inputNode.value || "")
postToParent({
type: INLINE_ATTRIBUTE_CHANGE,
selector: buildSelector(targetElement),
attribute_name: attributeName,
attribute_value: inputNode.value || "",
page_html: createPageHtmlSnapshot(),
source: "canvas-inline-edit-live",
})
}
inputNode.onkeydown = function (event) {
if (event.key === "Escape") {
event.preventDefault()
clearInlineEdit()
}
}
}
function focusAndSelectInput(inputNode) {
if (!inputNode) {
return
}
window.setTimeout(function () {
inputNode.focus()
if (typeof inputNode.select === "function") {
inputNode.select()
}
}, 32)
}
function bindLiveTextInput(textAreaNode, targetElement) {
if (!textAreaNode || !targetElement) {
return
}
textAreaNode.value = ((targetElement.innerText || targetElement.textContent) || "").trim()
currentInlineTextOriginalValue = textAreaNode.value
focusAndSelectInput(textAreaNode)
textAreaNode.oninput = function () {
targetElement.textContent = textAreaNode.value || ""
updateInlineEditBadge(targetElement)
postToParent({
type: INLINE_TEXT_CHANGE,
selector: buildSelector(targetElement),
text: ((targetElement.innerText || targetElement.textContent) || "")
.trim()
.slice(0, 500),
page_html: createPageHtmlSnapshot(),
source: "canvas-inline-edit-live",
})
}
textAreaNode.onkeydown = function (event) {
if (event.key === "Escape") {
event.preventDefault()
clearInlineEdit()
}
}
}
function bindLinkedAttributeInput(groupNode, inputNode, targetElement, attributeName) {
if (!groupNode || !inputNode) {
return
}
var editableLinkElement =
targetElement && targetElement.tagName && targetElement.tagName.toLowerCase() === "a"
? targetElement
: targetElement && targetElement.closest
? targetElement.closest("a")
: null
if (!editableLinkElement || !attributeName) {
groupNode.style.display = "none"
inputNode.value = ""
inputNode.oninput = null
inputNode.onkeydown = null
return
}
groupNode.style.display = "block"
inputNode.value = editableLinkElement.getAttribute(attributeName) || ""
bindLiveAttributeInput(inputNode, editableLinkElement, attributeName)
}
function initializeTextStyleControls(targetElement, controls) {
if (!targetElement || !controls) {
return
}
if (controls.fontSizeNode) {
controls.fontSizeNode.value = getComputedStyleValue(targetElement, "fontSize") || "16px"
}
bindImmediateStyleControl(targetElement, controls.fontSizeNode, "fontSize")
bindPresetSelectToInput(controls.fontSizePresetNode, controls.fontSizeNode)
if (controls.fontWeightNode) {
controls.fontWeightNode.value = getComputedStyleValue(targetElement, "fontWeight") || "400"
}
bindImmediateStyleControl(targetElement, controls.fontWeightNode, "fontWeight")
bindPresetSelectToInput(controls.fontWeightPresetNode, controls.fontWeightNode)
if (controls.fontFamilyNode) {
controls.fontFamilyNode.value = getComputedStyleValue(targetElement, "fontFamily") || ""
}
bindImmediateStyleControl(targetElement, controls.fontFamilyNode, "fontFamily")
bindPresetSelectToInput(controls.fontFamilyPresetNode, controls.fontFamilyNode)
if (controls.textAlignNode) {
controls.textAlignNode.value = getComputedStyleValue(targetElement, "textAlign") || "left"
}
bindImmediateStyleControl(targetElement, controls.textAlignNode, "textAlign")
bindPresetSelectToInput(controls.textAlignPresetNode, controls.textAlignNode)
if (controls.textColorNode) {
var textColorValue = getComputedStyleValue(targetElement, "color")
controls.textColorNode.value = rgbStringToHex(textColorValue) || textColorValue || "#000000"
}
bindImmediateStyleControl(targetElement, controls.textColorNode, "color")
bindColorPickerToInput(controls.textColorPickerNode, controls.textColorNode, "#000000")
if (controls.textDecorationNode) {
controls.textDecorationNode.value = getComputedStyleValue(targetElement, "textDecoration") || "none"
}
bindImmediateStyleControl(targetElement, controls.textDecorationNode, "textDecoration")
bindPresetSelectToInput(controls.textDecorationPresetNode, controls.textDecorationNode)
if (controls.textLineHeightNode) {
var textLineHeightValue = getComputedStyleValue(targetElement, "lineHeight")
controls.textLineHeightNode.value =
textLineHeightValue && textLineHeightValue !== "normal" ? textLineHeightValue : "1.6"
}
bindImmediateStyleControl(targetElement, controls.textLineHeightNode, "lineHeight")
bindPresetSelectToInput(controls.textLineHeightPresetNode, controls.textLineHeightNode)
if (controls.textLetterSpacingNode) {
controls.textLetterSpacingNode.value =
getComputedStyleValue(targetElement, "letterSpacing") || "normal"
}
bindImmediateStyleControl(targetElement, controls.textLetterSpacingNode, "letterSpacing")
bindPresetSelectToInput(controls.textLetterSpacingPresetNode, controls.textLetterSpacingNode)
}
function updateInspectPanel(mode, selector, element) {
var panel = ensureInspectPanel()
if (!panel) {
return
}
}
function updateHighlight(element, mode) {
var box = ensureHighlightBox()
var hotspot = ensureActionHotspot()
if (!element) {
box.style.display = "none"
hotspot.style.display = "none"
return
}
var rect = element.getBoundingClientRect()
var tone = getModeTone(mode, latestStatusSummary && latestStatusSummary.status)
var computedRadius = window.getComputedStyle(element).borderRadius
box.style.display = "block"
box.style.top = rect.top + "px"
box.style.left = rect.left + "px"
box.style.width = rect.width + "px"
box.style.height = rect.height + "px"
box.style.borderRadius = computedRadius || "10px"
box.style.border = mode === "hover" ? "2px dashed " + tone.border : "2px solid " + tone.border
box.style.boxShadow = "none"
if (mode === "focus" || mode === "inspect") {
hotspot.style.display = "block"
hotspot.style.top = Math.max(12, rect.top - 42) + "px"
hotspot.style.left = Math.max(12, rect.left + rect.width / 2 - 36) + "px"
} else {
hotspot.style.display = "none"
}
}
function updateHoverHighlight(element) {
var box = ensureHoverHighlightBox()
if (!element) {
box.style.display = "none"
return
}
var rect = element.getBoundingClientRect()
var hoverRadius = window.getComputedStyle(element).borderRadius
box.style.display = "block"
box.style.top = rect.top + "px"
box.style.left = rect.left + "px"
box.style.width = rect.width + "px"
box.style.height = rect.height + "px"
box.style.borderRadius = hoverRadius || "10px"
}
function hideDropIndicator() {
var indicator = ensureDropIndicator()
indicator.style.display = "none"
ensureDropBadge().style.display = "none"
}
function clearDragTargetMarker() {
if (!currentDragMoveState || !currentDragMoveState.target) {
return
}
currentDragMoveState.target.style.outline = ""
currentDragMoveState.target.style.outlineOffset = ""
currentDragMoveState.target.style.backgroundImage = currentDragMoveState.targetOriginalBackground || ""
}
function updateDropIndicator(targetElement, position) {
var indicator = ensureDropIndicator()
var badge = ensureDropBadge()
if (!targetElement) {
indicator.style.display = "none"
badge.style.display = "none"
return
}
var rect = targetElement.getBoundingClientRect()
indicator.style.display = "block"
indicator.style.left = rect.left + "px"
indicator.style.width = rect.width + "px"
indicator.style.top =
(position === "before" ? rect.top : rect.bottom) - 2 + "px"
badge.style.display = "block"
badge.textContent = position === "before" ? "插入到這塊前面" : "插入到這塊後面"
badge.style.left = Math.max(12, rect.left + rect.width / 2 - 58) + "px"
badge.style.top = Math.max(12, (position === "before" ? rect.top : rect.bottom) - 34) + "px"
}
function maybeAutoScrollDuringDrag(event) {
if (!currentDragMoveState) {
return
}
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0
var threshold = 96
var delta = 0
if (event.clientY < threshold) {
delta = -18
} else if (event.clientY > viewportHeight - threshold) {
delta = 18
}
if (!delta) {
return
}
window.scrollBy({
top: delta,
left: 0,
behavior: "auto",
})
}
function buildSelector(element) {
if (!element || !(element instanceof Element)) {
return ""
}
if (element.id) {
var escapedId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(element.id) : element.id
return "#" + escapedId
}
if (element.dataset && element.dataset.mktSelector) {
return '[data-mkt-selector="' + element.dataset.mktSelector.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]'
}
return assignRuntimeSelector(element)
}
function assignRuntimeSelector(element, force) {
if (!element || !(element instanceof Element)) {
return ""
}
if (!force && element.dataset && element.dataset.mktSelector) {
return '[data-mkt-selector="' + element.dataset.mktSelector.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"]'
}
runtimeSelectorSeed += 1
var token = "mkt-" + runtimeSelectorSeed
element.setAttribute("data-mkt-selector", token)
return '[data-mkt-selector="' + token + '"]'
}
function seedRuntimeSelectors(root, force) {
if (!root || !(root instanceof Element)) {
return
}
Array.prototype.forEach.call(
root.querySelectorAll(
"a,button,img,h1,h2,h3,h4,h5,h6,p,span,li,[role='button'],section,article,aside,nav,header,footer,main,div"
),
function (node) {
assignRuntimeSelector(node, force)
}
)
}
function assignRuntimeSelectorsInSubtree(root, force) {
if (!root || !(root instanceof Element)) {
return ""
}
var rootSelector = assignRuntimeSelector(root, force)
seedRuntimeSelectors(root, force)
return rootSelector
}
function isBridgeUiElement(element) {
return Boolean(
element &&
typeof element.closest === "function" &&
element.closest(
"#" +
PANEL_ID +
",#" +
HOTSPOT_ID +
",#" +
HIGHLIGHT_ID +
",#" +
HOVER_HIGHLIGHT_ID +
",#" +
INLINE_TEXT_PANEL_ID +
",#" +
INLINE_ATTRIBUTE_PANEL_ID
)
)
}
function resolveInspectableElement(element) {
if (!element || !(element instanceof Element) || isBridgeUiElement(element)) {
return null
}
var candidate = element.closest(
"[data-mkt-selector],a,button,img,h1,h2,h3,h4,h5,h6,p,span,li,[role='button'],section,article,aside,nav,header,footer,main,div"
)
if (!candidate || candidate === document.body || candidate === document.documentElement) {
return element
}
return candidate
}
function resolveStructureTargetElement(element) {
if (!element || !(element instanceof Element) || isBridgeUiElement(element)) {
return null
}
var preferred = element.closest(
"[data-mkt-block],section,article,aside,header,footer,main,nav,.card,.feature-card,.hero,.offer-section,.testimonial-section"
)
if (!preferred || preferred === document.body || preferred === document.documentElement) {
return element
}
return preferred
}
function extractElementMeta(element) {
return {
selector: buildSelector(element),
tag: element && element.tagName ? element.tagName.toLowerCase() : "",
text: ((element && (element.innerText || element.textContent)) || "").trim().slice(0, 120),
href: (element && element.getAttribute && element.getAttribute("href")) || "",
src: (element && element.getAttribute && element.getAttribute("src")) || "",
hidden: Boolean(element && element.getAttribute && element.getAttribute("data-mkt-hidden") === "true"),
}
}
function handleSelection(element, source) {
var inspectableElement = resolveInspectableElement(element)
if (!inspectableElement) {
return
}
var meta = extractElementMeta(inspectableElement)
currentFocusedElement = inspectableElement
refreshVisualState()
postToParent({
type: SELECTION,
selector: meta.selector,
tag: meta.tag,
text: meta.text,
href: meta.href,
src: meta.src,
source: source || "canvas-bridge",
})
}
function handleHover(element) {
var inspectableElement = resolveInspectableElement(element)
if (!inspectableElement) {
return
}
var meta = extractElementMeta(inspectableElement)
var selector = meta.selector
if (!selector || selector === lastHoveredSelector) {
return
}
lastHoveredSelector = selector
currentHoveredElement = inspectableElement
refreshVisualState()
}
function clearHoverState() {
if (!lastHoveredSelector) {
return
}
lastHoveredSelector = ""
currentHoveredElement = null
refreshVisualState()
}
function handleFocusSelector(selector) {
if (!selector) {
return
}
var element = document.querySelector(selector)
if (!element) {
return
}
currentFocusedElement = element
refreshVisualState()
}
function handleInspectSelector(selector) {
handleFocusSelector(selector)
}
function postStructureChange(action, selector, nextSelector, extra) {
postToParent(Object.assign({
type: BLOCK_STRUCTURE_CHANGE,
action: action,
selector: selector,
next_selector: nextSelector,
page_html: createPageHtmlSnapshot(),
source: "canvas-bridge",
}, extra || {}))
}
function handleMoveBlock(payload) {
var selector = typeof payload === "string" ? payload : payload && payload.selector
var direction = (payload && payload.direction) || "down"
if (!selector) {
return
}
clearInlineEdit()
var element = resolveInspectableElement(document.querySelector(selector))
if (!element || !element.parentElement) {
return
}
if (direction === "up") {
var previous = element.previousElementSibling
if (previous) {
element.parentElement.insertBefore(element, previous)
}
} else {
var next = element.nextElementSibling
if (next) {
element.parentElement.insertBefore(next, element)
}
}
currentFocusedElement = element
refreshVisualState()
var meta = extractElementMeta(element)
postStructureChange("move", selector, meta.selector, { tag: meta.tag, text: meta.text, href: meta.href, src: meta.src, hidden: meta.hidden })
}
function handleToggleBlockVisibility(payload) {
var selector = typeof payload === "string" ? payload : payload && payload.selector
if (!selector) {
return
}
clearInlineEdit()
var element = resolveInspectableElement(document.querySelector(selector))
if (!element) {
return
}
var isHidden = element.getAttribute("data-mkt-hidden") === "true"
if (isHidden) {
var originalDisplay = element.getAttribute("data-mkt-hidden-display") || ""
element.style.display = originalDisplay
element.removeAttribute("data-mkt-hidden")
element.removeAttribute("data-mkt-hidden-display")
showFeedbackBadge("已顯示區塊", element)
} else {
element.setAttribute("data-mkt-hidden-display", element.style.display || "")
element.setAttribute("data-mkt-hidden", "true")
element.style.display = "none"
showFeedbackBadge("已隱藏區塊")
}
postToParent({
type: BLOCK_VISIBILITY_CHANGE,
selector: selector,
hidden: !isHidden,
page_html: createPageHtmlSnapshot(),
source: "canvas-bridge",
})
refreshVisualState()
}
function handleDuplicateBlock(payload) {
var selector = typeof payload === "string" ? payload : payload && payload.selector
if (!selector) {
return
}
clearInlineEdit()
var element = resolveInspectableElement(document.querySelector(selector))
if (!element || !element.parentElement) {
return
}
var clone = element.cloneNode(true)
clone.removeAttribute("id")
Array.prototype.forEach.call(clone.querySelectorAll("[id]"), function (node) {
node.removeAttribute("id")
})
var nextSelector = assignRuntimeSelectorsInSubtree(clone, true)
element.parentElement.insertBefore(clone, element.nextElementSibling)
currentFocusedElement = clone
refreshVisualState()
showFeedbackBadge("已複製區塊", clone)
var meta = extractElementMeta(clone)
postStructureChange("duplicate", selector, nextSelector || meta.selector, { tag: meta.tag, text: meta.text, href: meta.href, src: meta.src, hidden: false })
}
function handleDeleteBlock(payload) {
var selector = typeof payload === "string" ? payload : payload && payload.selector
if (!selector) {
return
}
clearInlineEdit()
var element = resolveInspectableElement(document.querySelector(selector))
if (!element || !element.parentElement) {
return
}
var nextElement = element.nextElementSibling || element.previousElementSibling
var nextSelector = nextElement ? buildSelector(nextElement) : ""
if (currentFocusedElement === element) {
currentFocusedElement = nextElement || null
}
element.remove()
refreshVisualState()
showFeedbackBadge("已刪除區塊", nextElement || null)
postStructureChange("delete", selector, nextSelector)
}
function buildInsertedBlock(type) {
var blockType = type || "content-card"
var element
if (blockType === "text-block") {
element = document.createElement("section")
element.className = "card feature-card"
element.style.padding = "24px"
element.style.marginTop = "18px"
element.innerHTML =
'<span class="hero-kicker" data-mkt-selector="inserted-kicker">新段落</span>' +
'<h3 data-mkt-selector="inserted-title">新的文字標題</h3>' +
'<p data-mkt-selector="inserted-copy">在這裡輸入新的段落內容,之後可以直接改文字與樣式。</p>'
} else if (blockType === "button-block") {
element = document.createElement("section")
element.className = "card feature-card"
element.style.padding = "24px"
element.style.marginTop = "18px"
element.innerHTML =
'<h3 data-mkt-selector="inserted-title">新的行動按鈕</h3>' +
'<p data-mkt-selector="inserted-copy">你可以直接替換按鈕文案與連結,快速建立新的 CTA 區塊。</p>' +
'<div class="hero-actions"><a href="https://ose.tw" class="cta-button" data-mkt-selector="inserted-button">立即了解</a></div>'
} else if (blockType === "image-block") {
element = document.createElement("section")
element.className = "card feature-card"
element.style.padding = "24px"
element.style.marginTop = "18px"
element.innerHTML =
'<img src="https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=1200&q=80" alt="新的圖片區塊" style="width: 100%; border-radius: 20px;" data-mkt-selector="inserted-image">' +
'<p data-mkt-selector="inserted-caption" style="margin: 12px 0 0; color: #475569;">新的圖片說明,可直接替換成活動主視覺描述。</p>'
} else {
element = document.createElement("section")
element.className = "card feature-card"
element.style.padding = "24px"
element.style.marginTop = "18px"
element.innerHTML =
'<h3 data-mkt-selector="inserted-title">新的內容卡片</h3>' +
'<p data-mkt-selector="inserted-copy">在這裡輸入新的區塊內容,之後可以直接改文字、連結、圖片或樣式。</p>' +
'<a href="https://ose.tw" class="ghost-button" data-mkt-selector="inserted-link">延伸了解</a>'
}
assignRuntimeSelectorsInSubtree(element, true)
return element
}
function handleInsertBlock(payload) {
var selector = typeof payload === "string" ? payload : payload && payload.selector
var position = (payload && payload.position) || "after"
if (!selector) {
return
}
clearInlineEdit()
var target = resolveInspectableElement(document.querySelector(selector))
if (!target || !target.parentElement) {
return
}
var newBlock = buildInsertedBlock(payload && payload.blockType)
if (position === "before") {
target.parentElement.insertBefore(newBlock, target)
} else if (target.nextElementSibling) {
target.parentElement.insertBefore(newBlock, target.nextElementSibling)
} else {
target.parentElement.appendChild(newBlock)
}
currentFocusedElement = newBlock
refreshVisualState()
showFeedbackBadge(position === "before" ? "已在前面插入區塊" : "已在後面插入區塊", newBlock)
var meta = extractElementMeta(newBlock)
postStructureChange("insert", selector, buildSelector(newBlock), {
block_type: payload && payload.blockType ? payload.blockType : "content-card",
tag: meta.tag, text: meta.text, href: meta.href, src: meta.src, hidden: false,
})
}
function startDragMove(event, element) {
if (!element || !element.parentElement) {
return
}
currentDragMoveState = {
element: element,
parent: element.parentElement,
pointerId: event.pointerId,
target: null,
position: "after",
}
element.style.opacity = "0.72"
element.style.cursor = "grabbing"
document.body.style.userSelect = "none"
}
function updateDragMove(event) {
if (!currentDragMoveState) {
return
}
maybeAutoScrollDuringDrag(event)
var hovered = document.elementFromPoint(event.clientX, event.clientY)
var target = resolveInspectableElement(hovered)
if (!target || target === currentDragMoveState.element) {
return
}
if (!target.parentElement || target.parentElement !== currentDragMoveState.parent) {
return
}
var rect = target.getBoundingClientRect()
var position = event.clientY < rect.top + rect.height / 2 ? "before" : "after"
if (
currentDragMoveState.target === target &&
currentDragMoveState.position === position
) {
updateDropIndicator(target, position)
return
}
clearDragTargetMarker()
currentDragMoveState.target = target
currentDragMoveState.targetOriginalBackground = target.style.backgroundImage || ""
currentDragMoveState.position = position
currentDragMoveState.target.style.outline = "2px dashed rgba(37, 99, 235, 0.45)"
currentDragMoveState.target.style.outlineOffset = "4px"
currentDragMoveState.target.style.backgroundImage =
position === "before"
? "linear-gradient(to bottom, rgba(37,99,235,0.08), rgba(37,99,235,0.08) 18px, transparent 18px)"
: "linear-gradient(to top, rgba(37,99,235,0.08), rgba(37,99,235,0.08) 18px, transparent 18px)"
updateDropIndicator(target, position)
}
function endDragMove() {
if (!currentDragMoveState) {
hideDropIndicator()
return
}
var source = currentDragMoveState.element
var target = currentDragMoveState.target
var parent = currentDragMoveState.parent
var position = currentDragMoveState.position
source.style.opacity = ""
source.style.cursor = ""
document.body.style.userSelect = ""
clearDragTargetMarker()
var moved = false
if (source && target && parent && target !== source) {
if (position === "before") {
parent.insertBefore(source, target)
moved = true
} else if (target.nextElementSibling) {
parent.insertBefore(source, target.nextElementSibling)
moved = true
} else {
parent.appendChild(source)
moved = true
}
}
currentFocusedElement = source || currentFocusedElement
currentDragMoveState = null
hideDropIndicator()
refreshVisualState()
if (moved && source) {
showFeedbackBadge("已移動區塊", source)
var movedMeta = extractElementMeta(source)
postToParent({
type: BLOCK_STRUCTURE_CHANGE,
action: "move",
selector: buildSelector(source),
next_selector: movedMeta.selector,
tag: movedMeta.tag,
text: movedMeta.text,
href: movedMeta.href,
src: movedMeta.src,
hidden: movedMeta.hidden,
page_html: createPageHtmlSnapshot(),
source: "canvas-bridge",
})
}
}
function clearInlineEdit() {
if (!currentInlineEditableElement) {
updateInlineEditBadge(null)
return
}
var originalEditableValue = currentInlineEditableElement.getAttribute(
"data-mkt-inline-edit-original"
)
if (originalEditableValue == null || originalEditableValue === "none") {
currentInlineEditableElement.removeAttribute("contenteditable")
} else {
currentInlineEditableElement.setAttribute(
"contenteditable",
originalEditableValue || "false"
)
}
currentInlineEditableElement.removeAttribute("data-mkt-inline-edit-original")
currentInlineEditableElement.oninput = null
currentInlineEditableElement = null
currentInlineTextOriginalValue = null
updateInlineEditBadge(null)
ensureInlineTextPanel().style.display = "none"
currentInlineAttributeElement = null
currentInlineAttributeOriginalValue = null
ensureInlineAttributePanel().style.display = "none"
}
function focusEditableText(element) {
if (!element) {
return
}
if (typeof element.focus === "function") {
element.focus({ preventScroll: true })
}
if (typeof window.getSelection !== "function" || typeof document.createRange !== "function") {
return
}
var range = document.createRange()
range.selectNodeContents(element)
range.collapse(false)
var selection = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
}
function handleStartInlineEdit(payload) {
var selector = typeof payload === "string" ? payload : payload && payload.selector
if (!selector) {
return
}
var element = document.querySelector(selector)
if (!element) {
return
}
var inspectableElement = resolveInspectableElement(element)
if (!inspectableElement) {
return
}
clearInlineEdit()
currentFocusedElement = inspectableElement
refreshVisualState()
currentInlineEditableElement = inspectableElement
updateInlineEditBadge(inspectableElement)
var elemTag = inspectableElement.tagName ? inspectableElement.tagName.toLowerCase() : ""
var textPanel = ensureInlineTextPanel()
if (payload && payload.editKind === "replace_text") {
var textTitle = textPanel.querySelector("#__mkt_editor_inline_text_title__")
if (textTitle) {
textTitle.textContent = "文字編輯" + (elemTag ? " · " + elemTag : "")
}
showFloatingPanel(textPanel, SHARED_INLINE_PANEL_POSITION_ID)
bindLiveTextInput(textPanel.querySelector("#__mkt_editor_inline_text_area__"), inspectableElement)
bindLinkedAttributeInput(
textPanel.querySelector("#__mkt_editor_inline_text_link_group__"),
textPanel.querySelector("#__mkt_editor_inline_text_link_input__"),
inspectableElement,
"href"
)
initializeTextStyleControls(inspectableElement, queryTextStyleControlNodes(textPanel, "__mkt_editor_inline_text"))
initializeBlockStyleControls(inspectableElement, queryBlockStyleControlNodes(textPanel, "__mkt_editor_inline_text_block"))
}
inspectableElement.oninput = null
var isAttr = payload && payload.editKind === "set_attribute" &&
(payload.attributeName === "href" || payload.attributeName === "src")
var isStyle = payload && payload.editKind === "set_style"
if (isAttr || isStyle) {
currentInlineAttributeElement = inspectableElement
var attrPanel = ensureInlineAttributePanel()
var attrTitle = attrPanel.querySelector("#__mkt_editor_inline_attr_title__")
var attrLabel = attrPanel.querySelector("#__mkt_editor_inline_attr_label__")
var inputNode = attrPanel.querySelector("#__mkt_editor_inline_attr_input__")
if (isAttr) {
var isHref = payload.attributeName === "href"
if (attrTitle) {
attrTitle.textContent = (isHref ? "連結網址" : "圖片路徑") + (elemTag ? " · " + elemTag : "")
}
if (attrLabel) {
attrLabel.textContent = isHref ? "連結 URL (href)" : "圖片來源 (src)"
attrLabel.style.display = ""
}
} else {
if (attrTitle) {
attrTitle.textContent = "樣式調整" + (elemTag ? " · " + elemTag : "")
}
if (attrLabel) {
attrLabel.style.display = "none"
}
}
showFloatingPanel(attrPanel, SHARED_INLINE_PANEL_POSITION_ID)
if (isAttr) {
if (inputNode) {
inputNode.style.display = ""
inputNode.value = payload.attributeValue || ""
currentInlineAttributeOriginalValue = payload.attributeValue || ""
focusAndSelectInput(inputNode)
}
bindLiveAttributeInput(inputNode, inspectableElement, payload.attributeName)
} else {
if (inputNode) {
inputNode.style.display = "none"
}
}
initializeBlockStyleControls(inspectableElement, queryBlockStyleControlNodes(attrPanel, "__mkt_editor_inline_attr"))
}
}
function handleClick(event) {
var element = resolveInspectableElement(event.target)
if (!element) {
return
}
event.preventDefault()
event.stopPropagation()
handleSelection(element, "canvas-click")
dispatchActionFromTarget(resolveEditAction(element), element)
}
function handleMouseOver(event) {
var element = resolveInspectableElement(event.target)
if (!element) {
return
}
handleHover(element)
}
function handleMouseLeave() {
clearHoverState()
}
function shouldIgnoreEditorKeydown(event) {
var target = event && event.target
if (!target || !(target instanceof Element)) {
return false
}
if (isBridgeUiElement(target)) {
return true
}
var tagName = target.tagName ? target.tagName.toLowerCase() : ""
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
return true
}
if (target.closest("[contenteditable='true']")) {
return true
}
return false
}
function handleKeyDown(event) {
if (!currentFocusedElement || shouldIgnoreEditorKeydown(event)) {
return
}
if (!event.altKey) {
return
}
if (event.key === "ArrowUp") {
event.preventDefault()
handleMoveBlock({
selector: buildSelector(currentFocusedElement),
direction: "up",
})
return
}
if (event.key === "ArrowDown") {
event.preventDefault()
handleMoveBlock({
selector: buildSelector(currentFocusedElement),
direction: "down",
})
}
}
function handlePointerMove(event) {
updateFloatingPanelDrag(event)
updateDragMove(event)
}
function handlePointerUp() {
endFloatingPanelDrag()
endDragMove()
}
function handleMessage(event) {
// Only accept messages from the direct parent frame
if (!event.source || event.source !== window.parent) {
return
}
var payload = event.data || {}
if (payload.type === FOCUS_SELECTOR) {
handleFocusSelector(payload.selector)
return
}
if (payload.type === INSPECT_SELECTOR) {
handleInspectSelector(payload.selector)
return
}
if (payload.type === START_INLINE_EDIT) {
handleStartInlineEdit(payload)
return
}
if (payload.type === MOVE_BLOCK) {
handleMoveBlock(payload)
return
}
if (payload.type === TOGGLE_BLOCK_VISIBILITY) {
handleToggleBlockVisibility(payload)
return
}
if (payload.type === DUPLICATE_BLOCK) {
handleDuplicateBlock(payload)
return
}
if (payload.type === DELETE_BLOCK) {
handleDeleteBlock(payload)
return
}
if (payload.type === INSERT_BLOCK) {
handleInsertBlock(payload)
return
}
if (payload.type === RESTORE_CANVAS_STATE) {
if (!payload.pageHtml || !document.body) {
return
}
currentInlineEditableElement = null
currentInlineAttributeElement = null
currentInlineTextOriginalValue = null
currentInlineAttributeOriginalValue = null
currentFloatingPanelDragState = null
if (currentDragMoveState) {
document.body.style.userSelect = ""
currentDragMoveState = null
}
document.body.innerHTML = payload.pageHtml
currentHoveredElement = null
currentFocusedElement = payload.selector ? document.querySelector(payload.selector) : null
lastHoveredSelector = ""
refreshVisualState()
return
}
if (payload.type === STATUS_SUMMARY) {
latestStatusSummary = {
mode: payload.mode || "idle",
modeLabel: payload.modeLabel || "",
title: payload.title || "",
selector: payload.selector || "",
detail: payload.detail || "",
helper: payload.helper || "",
primaryActionLabel: payload.primaryActionLabel || "",
secondaryActionLabel: payload.secondaryActionLabel || "",
status: payload.status || "info",
}
refreshVisualState()
return
}
if (payload.type === "mkt-editor:preview-operations") {
applyPreviewOperations(payload.operations || [])
}
}
function init() {
if (document.body) {
seedRuntimeSelectors(document.body)
}
document.addEventListener("click", handleClick, true)
document.addEventListener("mouseover", handleMouseOver, true)
document.addEventListener("mouseleave", handleMouseLeave, true)
document.addEventListener("keydown", handleKeyDown, true)
document.addEventListener("pointermove", handlePointerMove, true)
document.addEventListener("pointerup", handlePointerUp, true)
document.addEventListener("pointercancel", handlePointerUp, true)
window.addEventListener("message", handleMessage)
window.addEventListener("resize", function () {
refreshVisualState()
if (currentInlineEditableElement) {
applyFloatingPanelPosition(ensureInlineTextPanel(), SHARED_INLINE_PANEL_POSITION_ID)
}
if (currentInlineAttributeElement) {
applyFloatingPanelPosition(ensureInlineAttributePanel(), SHARED_INLINE_PANEL_POSITION_ID)
}
})
window.addEventListener("scroll", function () {
refreshVisualState()
}, true)
postToParent({
type: READY,
page_url: window.location.href,
page_html: createPageHtmlSnapshot(),
})
refreshVisualState()
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
})()