Files
memind/src/utils/mindspaceVisualEditor.ts
T
John 79457230c1 Improve MindSpace visual editor UX and default RMB billing.
Add color-coded edit highlights with an in-preview legend, strip CSP meta
for srcdoc editing, and simplify fullscreen preview to immersive visual edit
only. Default billing to CNY token rates unless H5_USE_BACKEND_COST=1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 00:59:28 -07:00

667 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const MINDSPACE_PAGE_CONTENT_MESSAGE = 'mindspace:page-content';
const EDITOR_STYLE = `<style id="mindspace-visual-editor-style">
[data-mindspace-editing="true"] [data-mindspace-edit-kind] {
position: relative;
}
[data-mindspace-editing="true"][data-mindspace-highlights="off"] [data-mindspace-edit-kind] {
outline: none !important;
box-shadow: none !important;
}
[data-mindspace-editing="true"][data-mindspace-highlights="off"] [data-mindspace-edit-kind]::after,
[data-mindspace-editing="true"][data-mindspace-highlights="off"] [data-mindspace-edit-kind]::before {
display: none !important;
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="text"] {
outline: 1px dashed rgba(61, 139, 253, 0.72);
outline-offset: 2px;
box-shadow: inset 0 0 0 1px rgba(61, 139, 253, 0.12);
cursor: text;
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="text"]:focus {
outline: 2px solid rgba(61, 139, 253, 0.95);
background: rgba(61, 139, 253, 0.08);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"] {
outline: 2px dashed rgba(238, 176, 78, 0.82);
outline-offset: 3px;
cursor: pointer;
box-shadow: 0 0 0 1px rgba(238, 176, 78, 0.18);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"]:hover {
outline-color: rgba(238, 176, 78, 1);
box-shadow: 0 0 0 4px rgba(238, 176, 78, 0.16);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="link"] {
outline: 1px dotted rgba(124, 92, 255, 0.72);
outline-offset: 2px;
text-decoration: underline dotted rgba(124, 92, 255, 0.55);
cursor: text;
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="link"]:hover {
outline-color: rgba(124, 92, 255, 0.95);
background: rgba(124, 92, 255, 0.08);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"] {
outline: 1px dashed rgba(47, 111, 87, 0.55);
outline-offset: -2px;
cursor: copy;
box-shadow: inset 0 0 0 9999px rgba(47, 111, 87, 0.04);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"]:hover {
outline-color: rgba(47, 111, 87, 0.95);
box-shadow: inset 0 0 0 9999px rgba(47, 111, 87, 0.1);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind]::after {
position: absolute;
top: 0;
left: 0;
z-index: 2147483640;
max-width: calc(100% - 8px);
padding: 2px 7px;
border-radius: 0 0 8px 0;
color: #fffaf0;
font: 10px/1.35 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
letter-spacing: 0.02em;
white-space: nowrap;
pointer-events: none;
opacity: 0.92;
content: attr(data-mindspace-edit-label);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="text"]::after {
background: rgba(61, 139, 253, 0.92);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"]::after {
background: rgba(201, 132, 24, 0.94);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="link"]::after {
background: rgba(124, 92, 255, 0.92);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"]::after {
background: rgba(47, 111, 87, 0.92);
}
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"][data-mindspace-bg-large="true"]::before {
content: '';
position: absolute;
inset: 10px;
border: 1px dashed rgba(47, 111, 87, 0.35);
border-radius: 8px;
pointer-events: none;
}
.mindspace-editor-popover {
position: fixed;
z-index: 2147483646;
width: min(300px, calc(100vw - 24px));
padding: 12px;
border: 1px solid rgba(24, 33, 29, 0.14);
border-radius: 12px;
background: rgba(255, 252, 244, 0.98);
color: #18211d;
box-shadow: 0 18px 48px rgba(24, 33, 29, 0.18);
font: 12px/1.45 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.mindspace-editor-popover p {
margin: 0 0 8px;
font-size: 13px;
font-weight: 700;
}
.mindspace-editor-popover-hint {
margin: 0 0 10px;
color: #6d7771;
font-size: 11px;
}
.mindspace-editor-popover label {
display: grid;
gap: 4px;
margin-bottom: 8px;
}
.mindspace-editor-popover label[data-inline="true"] {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.mindspace-editor-popover input[type='url'],
.mindspace-editor-popover input[type='text'],
.mindspace-editor-popover input[type='color'],
.mindspace-editor-popover input[type='file'] {
width: 100%;
padding: 7px 8px;
border: 1px solid rgba(24, 33, 29, 0.14);
border-radius: 8px;
background: #fff;
color: inherit;
font: inherit;
}
.mindspace-editor-popover-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
}
.mindspace-editor-popover-actions button {
border: 0;
border-radius: 999px;
padding: 6px 12px;
cursor: pointer;
font: inherit;
}
.mindspace-editor-popover-actions button[data-action='apply'] {
color: #fffaf0;
background: #18211d;
}
.mindspace-editor-popover-actions button[data-action='cancel'],
.mindspace-editor-popover-actions button[data-action='clear'] {
color: #52605a;
background: rgba(24, 33, 29, 0.08);
}
.mindspace-editor-guide {
position: fixed;
z-index: 2147483645;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
max-width: min(720px, calc(100vw - 24px));
padding: 8px 14px;
border-radius: 999px;
color: #52605a;
font: 11px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: rgba(255, 252, 244, 0.94);
box-shadow: 0 8px 28px rgba(24, 33, 29, 0.12);
pointer-events: none;
}
.mindspace-editor-legend {
position: fixed;
z-index: 2147483645;
top: 12px;
left: 12px;
display: grid;
gap: 8px;
width: min(280px, calc(100vw - 24px));
padding: 10px 12px;
border: 1px solid rgba(24, 33, 29, 0.12);
border-radius: 12px;
background: rgba(255, 252, 244, 0.96);
color: #18211d;
box-shadow: 0 10px 28px rgba(24, 33, 29, 0.12);
font: 11px/1.45 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.mindspace-editor-legend strong {
font-size: 12px;
}
.mindspace-editor-legend-items {
display: grid;
gap: 6px;
}
.mindspace-editor-legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.mindspace-editor-legend-swatch {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.mindspace-editor-legend-swatch[data-kind="text"] { background: rgba(61, 139, 253, 0.92); }
.mindspace-editor-legend-swatch[data-kind="media"] { background: rgba(201, 132, 24, 0.94); }
.mindspace-editor-legend-swatch[data-kind="link"] { background: rgba(124, 92, 255, 0.92); }
.mindspace-editor-legend-swatch[data-kind="background"] { background: rgba(47, 111, 87, 0.92); }
.mindspace-editor-legend-toggle {
justify-self: start;
border: 0;
border-radius: 999px;
padding: 5px 10px;
color: #52605a;
font: inherit;
background: rgba(24, 33, 29, 0.08);
cursor: pointer;
}
</style>`;
const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
(function () {
var MESSAGE = ${JSON.stringify(MINDSPACE_PAGE_CONTENT_MESSAGE)};
var TEXT_SELECTOR = 'h1,h2,h3,h4,h5,h6,p,li,td,th,figcaption,blockquote,span,a,label,small,strong,em,b,i,u,sub,sup,cite,time,address,dt,dd,button,.sub,.tagline,[class*="title"],[class*="subtitle"],[class*="desc"],[class*="text"]';
var BLOCK_CHILD_SELECTOR = 'div,section,article,header,footer,main,p,h1,h2,h3,h4,h5,h6,ul,ol,table,form,video,audio,iframe,svg,canvas';
var MEDIA_BLOCK_SELECTOR = 'img,video,iframe,svg,canvas,table,ul,ol,form';
var popover = null;
var emitTimer = null;
function debounceEmit() {
if (emitTimer) window.clearTimeout(emitTimer);
emitTimer = window.setTimeout(emitChange, 350);
}
function escapeAttr(value) {
return String(value || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
}
function stripEditorArtifacts(root) {
root.querySelectorAll('#mindspace-visual-editor-style,#mindspace-visual-editor-script,.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend').forEach(function (node) {
node.remove();
});
root.querySelectorAll('[contenteditable]').forEach(function (node) {
node.removeAttribute('contenteditable');
node.removeAttribute('spellcheck');
});
root.querySelectorAll('[data-mindspace-bg-target],[data-mindspace-link-target],[data-mindspace-media-target],[data-mindspace-edit-kind],[data-mindspace-edit-label],[data-mindspace-bg-large]').forEach(function (node) {
node.removeAttribute('data-mindspace-bg-target');
node.removeAttribute('data-mindspace-link-target');
node.removeAttribute('data-mindspace-media-target');
node.removeAttribute('data-mindspace-edit-kind');
node.removeAttribute('data-mindspace-edit-label');
node.removeAttribute('data-mindspace-bg-large');
});
if (root.body) {
root.body.removeAttribute('data-mindspace-editing');
root.body.removeAttribute('data-mindspace-highlights');
}
}
function markEditable(el, kind, label) {
el.setAttribute('data-mindspace-edit-kind', kind);
el.setAttribute('data-mindspace-edit-label', label);
}
function serializeDocument() {
var clone = document.documentElement.cloneNode(true);
stripEditorArtifacts(clone);
var doctype = document.doctype;
var prefix = doctype
? '<!DOCTYPE ' + doctype.name + (doctype.publicId ? ' PUBLIC "' + doctype.publicId + '"' : '') + (doctype.systemId ? ' "' + doctype.systemId + '"' : '') + '>'
: '<!DOCTYPE html>';
return prefix + '\\n' + clone.outerHTML;
}
function emitChange() {
parent.postMessage({ type: MESSAGE, html: serializeDocument() }, '*');
}
function closePopover() {
if (popover && popover.parentNode) popover.parentNode.removeChild(popover);
popover = null;
}
function openPopover(title, fieldsHtml, onApply, anchor, hint) {
closePopover();
popover = document.createElement('div');
popover.className = 'mindspace-editor-popover';
popover.innerHTML =
'<p>' + title + '</p>' +
(hint ? '<p class="mindspace-editor-popover-hint">' + hint + '</p>' : '') +
fieldsHtml +
'<div class="mindspace-editor-popover-actions">' +
'<button type="button" data-action="cancel">取消</button>' +
'<button type="button" data-action="apply">应用</button>' +
'</div>';
document.body.appendChild(popover);
var rect = anchor.getBoundingClientRect();
var top = Math.min(rect.bottom + 8, window.innerHeight - popover.offsetHeight - 8);
var left = Math.min(rect.left, window.innerWidth - popover.offsetWidth - 8);
popover.style.top = Math.max(8, top) + 'px';
popover.style.left = Math.max(8, left) + 'px';
popover.querySelector('[data-action="cancel"]').addEventListener('click', closePopover);
popover.querySelector('[data-action="apply"]').addEventListener('click', function () {
var pending = onApply(popover);
closePopover();
if (pending !== 'async') emitChange();
});
var clearBtn = popover.querySelector('[data-action="clear"]');
if (clearBtn) {
clearBtn.addEventListener('click', function () {
var pending = onApply(popover, 'clear');
closePopover();
if (pending !== 'async') emitChange();
});
}
}
function readFileAsDataUrl(file, callback) {
var reader = new FileReader();
reader.onload = function () { callback(String(reader.result || '')); };
reader.readAsDataURL(file);
}
function openImageEditor(img) {
openPopover(
'替换图片',
'<label>图片链接<input type="url" data-role="url" value="' + (img.src.indexOf('data:') === 0 ? '' : escapeAttr(img.src)) + '" placeholder="https://..." /></label>' +
'<label>替代文字(Alt<input type="text" data-role="alt" value="' + escapeAttr(img.alt || '') + '" placeholder="图片描述" /></label>' +
'<label>本地图片<input type="file" accept="image/*" data-role="file" /></label>',
function (panel) {
var fileInput = panel.querySelector('[data-role="file"]');
var urlInput = panel.querySelector('[data-role="url"]');
var altInput = panel.querySelector('[data-role="alt"]');
img.alt = String(altInput.value || '');
if (fileInput.files && fileInput.files[0]) {
readFileAsDataUrl(fileInput.files[0], function (dataUrl) {
img.src = dataUrl;
emitChange();
});
return 'async';
}
var next = String(urlInput.value || '').trim();
if (next) img.src = next;
},
img,
);
}
function openMediaEditor(el, label) {
var src = el.getAttribute('src') || '';
var poster = el.getAttribute('poster') || '';
openPopover(
label,
'<label>资源链接<input type="url" data-role="url" value="' + escapeAttr(src) + '" placeholder="https://..." /></label>' +
(el.tagName === 'VIDEO'
? '<label>封面图(Poster<input type="url" data-role="poster" value="' + escapeAttr(poster) + '" placeholder="可选" /></label>'
: '') +
'<label>本地文件<input type="file" data-role="file" accept="' + (el.tagName === 'AUDIO' ? 'audio/*' : 'video/*,image/*') + '" /></label>',
function (panel) {
var fileInput = panel.querySelector('[data-role="file"]');
var urlInput = panel.querySelector('[data-role="url"]');
var posterInput = panel.querySelector('[data-role="poster"]');
if (fileInput.files && fileInput.files[0]) {
readFileAsDataUrl(fileInput.files[0], function (dataUrl) {
el.setAttribute('src', dataUrl);
if (el.tagName === 'VIDEO' && fileInput.files[0].type.startsWith('image/')) {
el.setAttribute('poster', dataUrl);
}
emitChange();
});
return 'async';
}
var next = String(urlInput.value || '').trim();
if (next) el.setAttribute('src', next);
if (posterInput) {
var posterNext = String(posterInput.value || '').trim();
if (posterNext) el.setAttribute('poster', posterNext);
}
},
el,
);
}
function openIframeEditor(iframe) {
var src = iframe.getAttribute('src') || '';
openPopover(
'替换嵌入内容',
'<label>嵌入链接<input type="url" data-role="url" value="' + escapeAttr(src) + '" placeholder="https://..." /></label>',
function (panel) {
var next = String(panel.querySelector('[data-role="url"]').value || '').trim();
if (next) iframe.setAttribute('src', next);
},
iframe,
'适用于地图、视频嵌入等 iframe 内容',
);
}
function openLinkEditor(anchor) {
openPopover(
'编辑链接',
'<label>显示文字<input type="text" data-role="text" value="' + escapeAttr(anchor.textContent || '') + '" /></label>' +
'<label>链接地址<input type="url" data-role="href" value="' + escapeAttr(anchor.getAttribute('href') || '') + '" placeholder="https://..." /></label>' +
'<label data-inline="true"><input type="checkbox" data-role="blank" ' + (anchor.target === '_blank' ? 'checked' : '') + ' /> 新窗口打开</label>',
function (panel) {
var text = String(panel.querySelector('[data-role="text"]').value || '');
var href = String(panel.querySelector('[data-role="href"]').value || '').trim();
var blank = panel.querySelector('[data-role="blank"]').checked;
anchor.textContent = text;
if (href) anchor.setAttribute('href', href);
else anchor.removeAttribute('href');
if (blank) anchor.setAttribute('target', '_blank');
else anchor.removeAttribute('target');
},
anchor,
'单击链接可改文字,双击打开此面板改地址',
);
}
function rgbToHex(color) {
var match = String(color || '').match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/i);
if (!match) return '#ffffff';
function hex(n) { return Number(n).toString(16).padStart(2, '0'); }
return '#' + hex(match[1]) + hex(match[2]) + hex(match[3]);
}
function extractBackgroundUrl(style) {
var bg = style.backgroundImage;
if (!bg || bg === 'none') return '';
var match = bg.match(/url\\(["']?([^"')]+)["']?\\)/);
return match ? match[1] : '';
}
function openBackgroundEditor(el) {
var style = window.getComputedStyle(el);
var currentUrl = extractBackgroundUrl(style);
openPopover(
'替换背景',
'<label>背景颜色<input type="color" data-role="color" value="' + rgbToHex(style.backgroundColor) + '" /></label>' +
'<label>背景图片链接<input type="url" data-role="bg-url" value="' + escapeAttr(currentUrl) + '" placeholder="留空则仅使用颜色" /></label>' +
'<label>本地背景图<input type="file" accept="image/*" data-role="bg-file" /></label>' +
'<button type="button" data-action="clear">清除背景图</button>',
function (panel, mode) {
var color = panel.querySelector('[data-role="color"]').value;
var bgUrl = String(panel.querySelector('[data-role="bg-url"]').value || '').trim();
var bgFile = panel.querySelector('[data-role="bg-file"]');
function applyBackground(url) {
if (mode === 'clear' || !url) {
el.style.backgroundImage = 'none';
} else {
el.style.backgroundImage = 'url("' + url.replace(/"/g, '\\"') + '")';
el.style.backgroundSize = el.style.backgroundSize || 'cover';
el.style.backgroundPosition = el.style.backgroundPosition || 'center';
el.style.backgroundRepeat = el.style.backgroundRepeat || 'no-repeat';
}
if (color) el.style.backgroundColor = color;
}
if (mode === 'clear') {
applyBackground('');
return;
}
if (bgFile.files && bgFile.files[0]) {
readFileAsDataUrl(bgFile.files[0], function (url) {
applyBackground(url);
emitChange();
});
return 'async';
}
applyBackground(bgUrl);
},
el,
'点击大面积空白或有色区块即可替换背景',
);
}
function markBackgroundTargets() {
var nodes = document.querySelectorAll('body, main, section, article, header, footer, div');
nodes.forEach(function (el) {
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
if (el.isContentEditable) return;
var style = window.getComputedStyle(el);
var cls = String(el.className || '');
var structural = /\\b(page|hero|cover|banner|bg|background|section|card|panel|wrap|container|shell|stage|canvas|frame|viewport)\\b/i.test(cls);
var painted = style.backgroundImage !== 'none' || (style.backgroundColor && style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent');
if (el.tagName === 'BODY' || structural || (painted && el.offsetWidth > 64 && el.offsetHeight > 64)) {
el.setAttribute('data-mindspace-bg-target', 'true');
markEditable(el, 'background', '点击换背景');
if (el.offsetWidth > 180 && el.offsetHeight > 120) {
el.setAttribute('data-mindspace-bg-large', 'true');
}
}
});
}
function markMediaTargets() {
document.querySelectorAll('img').forEach(function (img) {
if (img.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
markEditable(img, 'media', '点击换图片');
});
document.querySelectorAll('video').forEach(function (video) {
if (video.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
markEditable(video, 'media', '点击换视频');
});
document.querySelectorAll('audio').forEach(function (audio) {
if (audio.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
markEditable(audio, 'media', '点击换音频');
});
document.querySelectorAll('iframe').forEach(function (iframe) {
if (iframe.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
iframe.setAttribute('data-mindspace-media-target', 'true');
markEditable(iframe, 'media', '点击换嵌入');
});
}
function markLinkTargets() {
document.querySelectorAll('a[href]').forEach(function (anchor) {
if (anchor.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
anchor.setAttribute('data-mindspace-link-target', 'true');
markEditable(anchor, 'link', '双击改链接');
});
}
function enableTextEditing() {
document.querySelectorAll(TEXT_SELECTOR).forEach(function (el) {
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
if (el.querySelector && el.querySelector(MEDIA_BLOCK_SELECTOR)) return;
if (el.matches('a[data-mindspace-link-target="true"]')) return;
var textParent = el.closest('[data-mindspace-edit-kind="text"]');
if (textParent && textParent !== el) return;
el.setAttribute('contenteditable', 'true');
el.setAttribute('spellcheck', 'true');
markEditable(el, 'text', '点击改文字');
});
document.querySelectorAll('div').forEach(function (el) {
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
if (el.hasAttribute('contenteditable')) return;
if (el.querySelector(BLOCK_CHILD_SELECTOR)) return;
if (el.querySelector(MEDIA_BLOCK_SELECTOR)) return;
if (!String(el.textContent || '').trim()) return;
el.setAttribute('contenteditable', 'true');
el.setAttribute('spellcheck', 'true');
markEditable(el, 'text', '点击改文字');
});
}
function addEditLegend() {
var legend = document.createElement('div');
legend.className = 'mindspace-editor-legend';
legend.innerHTML =
'<strong>编辑模式 · 可修改区域</strong>' +
'<div class="mindspace-editor-legend-items">' +
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="text"></span><span>蓝色虚线 = 文字,直接点击修改</span></div>' +
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="media"></span><span>橙色虚线 = 图片/音视频,点击替换</span></div>' +
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="link"></span><span>紫色虚线 = 链接,双击改地址</span></div>' +
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="background"></span><span>绿色虚线 = 背景区域,点击换背景</span></div>' +
'</div>' +
'<button type="button" class="mindspace-editor-legend-toggle" data-action="toggle-highlights">隐藏标记</button>';
document.body.appendChild(legend);
legend.querySelector('[data-action="toggle-highlights"]').addEventListener('click', function () {
var hidden = document.body.getAttribute('data-mindspace-highlights') === 'off';
if (hidden) {
document.body.removeAttribute('data-mindspace-highlights');
this.textContent = '隐藏标记';
} else {
document.body.setAttribute('data-mindspace-highlights', 'off');
this.textContent = '显示标记';
}
});
}
function onClick(event) {
var target = event.target;
if (!(target instanceof Element)) return;
if (target.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
if (target.tagName === 'IMG') {
event.preventDefault();
openImageEditor(target);
return;
}
if (target.tagName === 'VIDEO' || target.tagName === 'AUDIO') {
event.preventDefault();
openMediaEditor(target, target.tagName === 'VIDEO' ? '替换视频' : '替换音频');
return;
}
if (target.tagName === 'IFRAME' && target.getAttribute('data-mindspace-media-target') === 'true') {
event.preventDefault();
openIframeEditor(target);
return;
}
if (target.isContentEditable) return;
if (target.matches('[data-mindspace-bg-target="true"]') || target.tagName === 'BODY') {
event.preventDefault();
openBackgroundEditor(target.tagName === 'BODY' ? document.body : target);
}
}
function onDblClick(event) {
var target = event.target;
if (!(target instanceof Element)) return;
var anchor = target.closest('a[data-mindspace-link-target="true"]');
if (!anchor) return;
event.preventDefault();
openLinkEditor(anchor);
}
function init() {
document.body.setAttribute('data-mindspace-editing', 'true');
enableTextEditing();
markBackgroundTargets();
markMediaTargets();
markLinkTargets();
addEditLegend();
document.addEventListener('input', debounceEmit);
document.addEventListener('blur', debounceEmit, true);
document.addEventListener('click', onClick, true);
document.addEventListener('dblclick', onDblClick, true);
document.addEventListener('click', function (event) {
var target = event.target;
if (target instanceof HTMLAnchorElement) event.preventDefault();
}, true);
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') closePopover();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>`;
/** Remove inline CSP meta tags so preview-edit bootstrap can run inside srcdoc iframes. */
export function stripPreviewEditCspMeta(html: string): string {
return String(html ?? '').replace(
/<meta\s+http-equiv\s*=\s*["']Content-Security-Policy["'][^>]*>/gi,
'',
);
}
export function buildEditablePreviewDocument(html: string): string {
const source = String(html ?? '').trim();
const cleaned = stripPreviewEditCspMeta(source)
.replace(/<style id="mindspace-visual-editor-style">[\s\S]*?<\/style>/gi, '')
.replace(/<script id="mindspace-visual-editor-script">[\s\S]*?<\/script>/gi, '');
const injection = `${EDITOR_STYLE}${EDITOR_SCRIPT}`;
if (!cleaned) {
return buildEditablePreviewDocument(
'<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body><p>空白页面</p></body></html>',
);
}
if (/<\/body>/i.test(cleaned)) {
return cleaned.replace(/<\/body>/i, `${injection}</body>`);
}
if (/<\/html>/i.test(cleaned)) {
return cleaned.replace(/<\/html>/i, `${injection}</html>`);
}
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body>${cleaned}${injection}</body></html>`;
}