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>
This commit is contained in:
@@ -1,41 +1,92 @@
|
||||
export const MINDSPACE_PAGE_CONTENT_MESSAGE = 'mindspace:page-content';
|
||||
|
||||
const EDITOR_STYLE = `<style id="mindspace-visual-editor-style">
|
||||
[data-mindspace-editing="true"] [contenteditable="true"] {
|
||||
outline: 1px dashed rgba(61, 139, 253, 0.45);
|
||||
[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"] [contenteditable="true"]:focus {
|
||||
[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.06);
|
||||
background: rgba(61, 139, 253, 0.08);
|
||||
}
|
||||
[data-mindspace-editing="true"] img,
|
||||
[data-mindspace-editing="true"] video,
|
||||
[data-mindspace-editing="true"] audio,
|
||||
[data-mindspace-editing="true"] iframe[data-mindspace-media-target="true"] {
|
||||
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"] {
|
||||
outline: 2px dashed rgba(238, 176, 78, 0.82);
|
||||
outline-offset: 3px;
|
||||
cursor: pointer;
|
||||
outline: 1px dashed transparent;
|
||||
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;
|
||||
transition: outline-color 0.15s ease;
|
||||
}
|
||||
[data-mindspace-editing="true"] img:hover,
|
||||
[data-mindspace-editing="true"] video:hover,
|
||||
[data-mindspace-editing="true"] audio:hover,
|
||||
[data-mindspace-editing="true"] iframe[data-mindspace-media-target="true"]:hover {
|
||||
outline-color: rgba(238, 176, 78, 0.95);
|
||||
}
|
||||
[data-mindspace-editing="true"] a[data-mindspace-link-target="true"] {
|
||||
text-decoration: underline dotted rgba(124, 92, 255, 0.55);
|
||||
cursor: text;
|
||||
}
|
||||
[data-mindspace-editing="true"] a[data-mindspace-link-target="true"]:hover {
|
||||
text-decoration: underline dotted rgba(238, 176, 78, 0.85);
|
||||
[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-bg-target="true"] {
|
||||
[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-bg-target="true"]:hover {
|
||||
box-shadow: inset 0 0 0 2px rgba(238, 176, 78, 0.45);
|
||||
[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;
|
||||
@@ -119,6 +170,54 @@ const EDITOR_STYLE = `<style id="mindspace-visual-editor-style">
|
||||
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">
|
||||
@@ -140,19 +239,30 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
||||
}
|
||||
|
||||
function stripEditorArtifacts(root) {
|
||||
root.querySelectorAll('#mindspace-visual-editor-style,#mindspace-visual-editor-script,.mindspace-editor-popover,.mindspace-editor-guide').forEach(function (node) {
|
||||
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]').forEach(function (node) {
|
||||
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');
|
||||
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() {
|
||||
@@ -368,61 +478,103 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
||||
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')) return;
|
||||
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')) return;
|
||||
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')) return;
|
||||
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')) return;
|
||||
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')) return;
|
||||
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 addEditGuide() {
|
||||
var bar = document.createElement('div');
|
||||
bar.className = 'mindspace-editor-guide';
|
||||
bar.textContent = '文字:直接点击 · 图片/音视频:点击替换 · 背景:点击空白区域 · 链接:双击改地址';
|
||||
document.body.appendChild(bar);
|
||||
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')) return;
|
||||
if (target.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||
|
||||
if (target.tagName === 'IMG') {
|
||||
event.preventDefault();
|
||||
@@ -461,7 +613,7 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
||||
markBackgroundTargets();
|
||||
markMediaTargets();
|
||||
markLinkTargets();
|
||||
addEditGuide();
|
||||
addEditLegend();
|
||||
document.addEventListener('input', debounceEmit);
|
||||
document.addEventListener('blur', debounceEmit, true);
|
||||
document.addEventListener('click', onClick, true);
|
||||
@@ -483,21 +635,32 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
||||
})();
|
||||
</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 (!source) {
|
||||
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(source)) {
|
||||
return source.replace(/<\/body>/i, `${injection}</body>`);
|
||||
if (/<\/body>/i.test(cleaned)) {
|
||||
return cleaned.replace(/<\/body>/i, `${injection}</body>`);
|
||||
}
|
||||
if (/<\/html>/i.test(source)) {
|
||||
return source.replace(/<\/html>/i, `${injection}</html>`);
|
||||
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>${source}${injection}</body></html>`;
|
||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body>${cleaned}${injection}</body></html>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user