8884e100f9
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>
148 lines
13 KiB
JavaScript
148 lines
13 KiB
JavaScript
/* 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);}
|
||
});
|
||
})();
|