;(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 = '
' + '' + '' + '' + "
" 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 = '' return ( '
' + '
' + gripIcon + "
" + '編輯' + '' + "
" ) } function buildPanelSelectOptions(options) { return (options || []) .map(function (option) { return '" }) .join("") } function buildPanelControlRow(config) { var colorControl = config.controlType === "color" ? '' : '" return ( '
' + '
' + config.iconSvg + "
" + '' + colorControl + "
" ) } function buildPanelSectionTitle(label, marginTop) { return ( '
' + label + "
" ) } 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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: '', 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__") + '' + '" + '
' + buildPanelSectionTitle("文字樣式", "0px") + '
' + textStyleRows + buildPanelSectionTitle("區塊樣式") + '
' + blockStyleRows + "
" 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__") + '
' + '' + '
' + buildPanelSectionTitle("區塊樣式", "0px") + '
' + buildBlockStyleRows("__mkt_editor_inline_attr", panelRowStyles) + "
" 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 = '新段落' + '

新的文字標題

' + '

在這裡輸入新的段落內容,之後可以直接改文字與樣式。

' } else if (blockType === "button-block") { element = document.createElement("section") element.className = "card feature-card" element.style.padding = "24px" element.style.marginTop = "18px" element.innerHTML = '

新的行動按鈕

' + '

你可以直接替換按鈕文案與連結,快速建立新的 CTA 區塊。

' + '' } else if (blockType === "image-block") { element = document.createElement("section") element.className = "card feature-card" element.style.padding = "24px" element.style.marginTop = "18px" element.innerHTML = '新的圖片區塊' + '

新的圖片說明,可直接替換成活動主視覺描述。

' } else { element = document.createElement("section") element.className = "card feature-card" element.style.padding = "24px" element.style.marginTop = "18px" element.innerHTML = '

新的內容卡片

' + '

在這裡輸入新的區塊內容,之後可以直接改文字、連結、圖片或樣式。

' + '延伸了解' } 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() } })()