Files
cms-SiteForge/app/static/studio-canvas-bridge.js
T
浅念 8884e100f9 upgrade: adopt V8.31.1 (theme 1.3/1.4 redesigns, editor deep-canvas, guided launch, component library)
Overlay 190 upstream items (V8.22→8.31) preserving trade-pro theme,
registry tests, and design docs. Re-applied full patch set on identical
anchors (discovery/env_for/blocks.css/type-mapping/locale-override/
theme_color/frontend entries/test_theme_pro_v3 mods) plus compose pins.

Verified: theme suites 35/35; dual-baseline full run 415 passed vs
pristine 386+29 ours, FAILED-set diff EMPTY (same 10 Windows env fails).
Live: container 8.31.1-dev.1 healthy, hesupipe 192 entries intact,
trade-pro build 203 pages 0 errors.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-26 09:43:17 +08:00

148 lines
13 KiB
JavaScript
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.
/* V8.22 trusted, sandboxed studio canvas bridge. No arbitrary user code. */
(function(){
'use strict';
const source='siteforge-studio-canvas';
const list=()=>Array.from(document.querySelectorAll('.sf-section[data-block]')).filter(n=>!n.parentElement.closest('.sf-section[data-block]'));
const send=(action,id,extra={})=>parent.postMessage({source,action,blockId:id,...extra},'*');
let active='', dragging='',resize=null;
const all=()=>Array.from(document.querySelectorAll('.sf-section[data-block]'));
const style=document.createElement('style');
style.textContent=`
.sf-section[data-block]{position:relative}
.sf-section[data-block].sf-canvas-selected{outline:2px solid #ec7c35!important;outline-offset:-2px}
.sf-section[data-block].sf-canvas-drop{box-shadow:inset 0 4px 0 #ef8741!important}
.sf-section[data-block]:hover{outline:1px dashed #eb995a;outline-offset:-1px}
.sf-canvas-toolbar{position:absolute;right:7px;top:5px;z-index:2147480000;display:none;align-items:center;gap:3px;padding:5px;background:#263447;color:#fff;border-radius:9px;box-shadow:0 5px 20px #20304450;font:12px/1.4 system-ui,sans-serif;white-space:nowrap;pointer-events:auto}
.sf-canvas-selected>.sf-canvas-toolbar,.sf-section[data-block]:hover>.sf-canvas-toolbar,.sf-canvas-toolbar:focus-within{display:flex}
.sf-canvas-toolbar button{background:transparent;color:inherit;border:0;cursor:pointer;padding:5px 7px;border-radius:5px;font:inherit}
.sf-canvas-toolbar button:hover,.sf-canvas-toolbar button:focus-visible{background:#52687e;outline:1px solid #fff6}
.sf-canvas-toolbar .sf-canvas-drag{cursor:grab;background:#496174}
.sf-canvas-toolbar .sf-canvas-danger:hover{background:#ab4d46}
.sf-section.sf-canvas-child-selected{outline:2px solid #0e97ac!important;outline-offset:-2px}
.sf-nested-toolbar{position:absolute;top:6px;left:6px;z-index:2147480001;display:none;gap:3px;background:#126c80;color:#fff;padding:5px;border-radius:8px;font:12px system-ui;pointer-events:auto}
.sf-canvas-child-selected>.sf-nested-toolbar,.sf-section[data-block]:hover>.sf-nested-toolbar,.sf-nested-toolbar:focus-within{display:flex}
.sf-nested-toolbar button{color:#fff;background:transparent;border:0;border-radius:5px;cursor:pointer;padding:5px 7px}
.sf-nested-toolbar button:hover,.sf-nested-toolbar button:focus-visible{background:#248ea4;outline:1px solid #fff6}
.sf-canvas-hint{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);background:#1c293ee8;color:white;padding:9px 13px;border-radius:9px;z-index:2147480000;font:12px system-ui;pointer-events:none}
.sf-layout-grid{position:relative}
.sf-canvas-resize{position:absolute;z-index:2147480000;top:4px;bottom:4px;width:12px;transform:translateX(-50%);cursor:col-resize;touch-action:none;opacity:0;display:flex;align-items:center;justify-content:center}
.sf-canvas-resize:after{content:'⋮';background:#e77632;color:white;box-shadow:0 2px 8px #2225;border-radius:8px;font:20px/26px system-ui;text-align:center;width:19px;min-width:19px}
.sf-section:hover .sf-canvas-resize,.sf-canvas-resize:focus-visible,.sf-canvas-resize.sf-resizing{opacity:1}
[contenteditable].sf-canvas-editing{outline:2px dashed #df7435!important;border-radius:3px;cursor:text!important}
@media(max-width:820px){.sf-canvas-resize{display:none!important}}
`;
document.head.appendChild(style);
const types={title:'h1,h2',description:'.sf-hero-copy p,.sf-image-text p,.sf-cta-wrap p'};
function activate(id,notify=true){
active=id||'';for(const el of all())el.classList.toggle('sf-canvas-selected',el.dataset.block===id);
if(notify&&id)send('select',id);
}
const topSections=list();
for(const block of topSections){
const id=block.dataset.block;
const bar=document.createElement('div');bar.className='sf-canvas-toolbar';bar.setAttribute('role','toolbar');bar.setAttribute('aria-label','区块快捷操作');
const controls=[['↕','拖动区块','drag'],['✎','编辑区块','edit'],['↑','上移区块','up'],['↓','下移区块','down'],['+','在下方添加区块','add-after'],['⧉','复制区块','duplicate'],['×','删除区块','delete']];
for(const [txt,label,action] of controls){
const b=document.createElement('button');b.type='button';b.textContent=txt;b.title=label;b.setAttribute('aria-label',label);b.dataset.action=action;
if(action==='drag'){b.draggable=true;b.className='sf-canvas-drag';b.addEventListener('dragstart',e=>{dragging=id;e.dataTransfer.setData('text/plain',id);e.dataTransfer.effectAllowed='move';e.stopPropagation()});b.addEventListener('dragend',()=>{dragging='';list().forEach(el=>el.classList.remove('sf-canvas-drop'))})}
else {if(action==='delete')b.className='sf-canvas-danger';b.addEventListener('click',e=>{e.preventDefault();e.stopPropagation();activate(id,false);send(action,id)})}
bar.appendChild(b);
}
block.prepend(bar);
block.addEventListener('click',event=>{
if(event.target.closest('.sf-canvas-toolbar,.sf-canvas-resize'))return;
if(event.target.closest('a'))event.preventDefault();
activate(id);
});
block.addEventListener('dragover',event=>{if(!dragging)return;event.preventDefault();event.stopPropagation();block.classList.add('sf-canvas-drop')});
block.addEventListener('dragleave',event=>{if(!block.contains(event.relatedTarget))block.classList.remove('sf-canvas-drop')});
block.addEventListener('drop',event=>{if(!dragging)return;event.preventDefault();event.stopPropagation();block.classList.remove('sf-canvas-drop');if(dragging!==id)send('reorder',dragging,{targetId:id});dragging=''});
}
// Rich text is not edited as plain text: modifying sanitized HTML would lose formatting.
for(const section of topSections){
const id=section.dataset.block;
for(const [field,selector] of Object.entries(types)){
const target=section.querySelector(selector);if(!target||target.closest('.sf-section[data-block]')!==section)continue;
target.title='双击直接修改'+(field==='title'?'标题':'描述');
target.addEventListener('dblclick',event=>{
event.stopPropagation();activate(id);const before=target.textContent;
target.contentEditable='true';target.classList.add('sf-canvas-editing');target.focus();
let closed=false;
const finish=commit=>{
if(closed)return;closed=true;target.removeAttribute('contenteditable');target.classList.remove('sf-canvas-editing');
const value=target.textContent.replace(/\r/g,'').trim();
if(!commit)target.textContent=before;
else if(value!==before&&value.length<=500)send('inline-text',id,{field,value});
else if(value.length>500)target.textContent=before;
};
target.addEventListener('blur',()=>finish(true),{once:true});
target.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();finish(false);target.blur()}if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();target.blur()} });
});
}
}
// Children of every nested layout (including second-level content) are selectable. The parent
// checks IDs against the current schema and rejects forged/obsolete events.
const nested=Array.from(document.querySelectorAll('.sf-layout-column > .sf-section[data-block]'));
for(const child of nested){
const layout=child.closest('.sf-layout-grid[data-layout]');
if(!layout)continue;
const parentId=layout.dataset.layout,childId=child.dataset.block;
const push=(action,extra={})=>send(action,parentId,{childId,...extra});
const toolbar=document.createElement('div');toolbar.className='sf-nested-toolbar';toolbar.setAttribute('role','toolbar');toolbar.setAttribute('aria-label','列内组件操作');
for(const [symbol,label,action] of [['✎','编辑子组件','child-edit'],['↑','上移子组件','child-up'],['↓','下移子组件','child-down'],['⧉','复制子组件','child-duplicate'],['×','删除子组件','child-delete']]){
const button=document.createElement('button');button.type='button';button.textContent=symbol;button.setAttribute('aria-label',label);
button.addEventListener('click',event=>{event.preventDefault();event.stopPropagation();push(action)});
toolbar.appendChild(button);
}
child.prepend(toolbar);
child.addEventListener('click',event=>{if(event.target.closest('.sf-nested-toolbar'))return;if(event.target.closest('a'))event.preventDefault();event.stopPropagation();push('child-select')});
// Only plain text fields are inline editable; rich HTML remains in the form.
for(const [field,selector] of Object.entries(types)){
const node=child.querySelector(selector);
if(!node||node.closest('.sf-section[data-block]')!==child)continue;
node.title='双击编辑列内'+(field==='title'?'标题':'描述');
node.addEventListener('dblclick',event=>{
event.stopPropagation();push('child-select');const original=node.textContent;
node.contentEditable='true';node.classList.add('sf-canvas-editing');node.focus();let complete=false;
const finish=save=>{if(complete)return;complete=true;node.removeAttribute('contenteditable');node.classList.remove('sf-canvas-editing');const text=node.textContent.replace(/\r/g,'').trim();if(!save||text.length>500)node.textContent=original;else if(text!==original)push('child-inline-text',{field,value:text})};
node.addEventListener('blur',()=>finish(true),{once:true});
node.addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();finish(false);node.blur()}if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();node.blur()}});
});
}
}
// Desktop-only handles keep the total adjacent width stable and never free-position blocks.
for(const grid of document.querySelectorAll('.sf-layout-grid[data-layout]')){
const id=grid.dataset.layout;
const cols=Array.from(grid.children).filter(el=>el.classList.contains('sf-layout-column'));
if(cols.length<2)continue;
for(let i=0;i<cols.length-1;i++){
const handle=document.createElement('div');handle.className='sf-canvas-resize';handle.tabIndex=0;handle.setAttribute('role','separator');handle.setAttribute('aria-label',`拖动调整第 ${i+1} 列宽度`);
const place=()=>{const a=cols[i].getBoundingClientRect(),g=grid.getBoundingClientRect();handle.style.left=(a.right-g.left+parseFloat(getComputedStyle(grid).columnGap||'0')/2)+'px'};
grid.appendChild(handle);place();
handle.addEventListener('pointerdown',e=>{
if(e.button!==0||matchMedia('(max-width:820px)').matches)return;
e.stopPropagation();e.preventDefault();handle.setPointerCapture(e.pointerId);handle.classList.add('sf-resizing');
const raw=grid.style.getPropertyValue('--sf-layout-cols').trim().split(/\s+/).map(x=>parseFloat(x)||1);
const start=e.clientX,span=Math.max(50,cols[i].getBoundingClientRect().width+cols[i+1].getBoundingClientRect().width);
resize={id,i,raw,start,span};activate(id);
});
handle.addEventListener('pointermove',e=>{
if(!resize||resize.i!==i)return;
const v=resize.raw.slice(),total=v[i]+v[i+1];const delta=(e.clientX-resize.start)/resize.span*total;
v[i]=Math.round(Math.min(4,Math.max(.5,resize.raw[i]+delta))*100)/100;
v[i+1]=Math.round(Math.min(4,Math.max(.5,total-v[i]))*100)/100;
if(Math.abs(v[i]+v[i+1]-total)>0.011)return;
grid.style.setProperty('--sf-layout-cols',v.map(x=>x+'fr').join(' '));place();
});
const done=e=>{if(!resize||resize.i!==i)return;const raw=grid.style.getPropertyValue('--sf-layout-cols').trim().split(/\s+/).map(x=>parseFloat(x)||1);resize=null;handle.classList.remove('sf-resizing');send('resize-columns',id,{widths:raw});};
handle.addEventListener('pointerup',done);handle.addEventListener('pointercancel',()=>{resize=null;handle.classList.remove('sf-resizing')});
handle.addEventListener('keydown',e=>{if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return;e.preventDefault();const w=grid.style.getPropertyValue('--sf-layout-cols').trim().split(/\s+/).map(x=>parseFloat(x)||1);const delta=e.key==='ArrowRight'?.1:-.1;const total=w[i]+w[i+1];const first=Math.round(Math.min(4,Math.max(.5,w[i]+delta))*100)/100,second=Math.round((total-first)*100)/100;if(second<.5||second>4)return;w[i]=first;w[i+1]=second;grid.style.setProperty('--sf-layout-cols',w.map(x=>x+'fr').join(' '));place();send('resize-columns',id,{widths:w})});
new ResizeObserver(place).observe(grid);
}
}
window.addEventListener('message',event=>{
if(event.source!==parent||event.data?.source!=='siteforge-studio-host')return;
if(event.data.action==='highlight'&&typeof event.data.blockId==='string'){activate(event.data.blockId,false);for(const n of nested)n.classList.toggle('sf-canvas-child-selected',n.dataset.block===event.data.childId);}
});
})();