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