;(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 = '
在這裡輸入新的段落內容,之後可以直接改文字與樣式。
' } 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() } })()