79457230c1
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>
667 lines
26 KiB
TypeScript
667 lines
26 KiB
TypeScript
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, '&').replace(/"/g, '"').replace(/</g, '<');
|
||
}
|
||
|
||
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>`;
|
||
}
|