fix: colonnes rendues, tables IA sans pipe de tête, copier/couper/coller multi-blocs (v7.68.0)
Colonnes / équation / TOC - renderBlock() n'appelait JAMAIS renderColumnsBlock()/renderMathBlock()/renderTOC() (3 renderers morts) → un bloc colonnes (slash « Column list », Turn into 2/3/4/5 colonnes) tombait dans le rendu générique = paragraphe vide. Dispatch ajouté ; la × de suppression de colonne (opacity:0 sans survol) devient visible au survol. Tables rendues par l'IA - _tblIsData/_tblIsSep exigeaient un | de tête → le GFM sans pipe (A | B / --- | ---) devenait des paragraphes. Pipe de tête optionnel, séparateur gardé à ≥2 cellules (« texte » + « --- » reste un séparateur). md2b et paste2b partagent les helpers. Copier / couper / coller - Sélection de texte MULTI-BLOCS : _clipState()/_clipMarkdown() (début partiel → blocs entiers via blocksToMarkdown → fin partielle), clipCopy()/clipCut() (coupure = tête du 1er bloc + queue du dernier), listeners copy/cut (Ctrl+C/X via les événements natifs), groupe Clipboard (Cut/Copy/Paste) dans le menu du clic droit avec la sélection figée à l'ouverture (la recherche prend le focus), clic droit sur un bloc sélectionné = sélection multi-blocs conservée. - Coller un seul bloc structuré (table/code/titre) = vraie insertion (avant : texte brut) ; pasteBlocks colle après le dernier bloc sans focus. - « Copy link to block » réparé (data-bm-act link → copy-link). Tests: pytest tests/ -q -n auto → 1394 passed / 0 failed ; ruff OK ; e2e v768_columns_clipboard (4 : slash+Turn into colonnes, table IA sans pipe, copier/couper/coller, clic droit Clipboard) + v762/v764/v766/v7641/v763/ regression_editor_mount → verts sur :8081 (image flowdeck:v7.68.0).
This commit is contained in:
@@ -2865,6 +2865,11 @@ button.breadcrumb-current { max-width: 260px; }
|
||||
border-radius: 50%;
|
||||
line-height: 1;
|
||||
}
|
||||
/* v7.68 : la × de suppression de colonne était en opacity:0 SANS règle de
|
||||
survol → invisible et pourtant cliquable (même piège que .block-actions) */
|
||||
.block-column-wrapper:hover .block-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.block-column {
|
||||
outline: none;
|
||||
min-height: 24px;
|
||||
|
||||
@@ -236,8 +236,11 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
|
||||
/* ═══════════ Simple Table block (v4.8.0) ═══════════ */
|
||||
function _tblSplitLine(s){s=(s||'').trim();if(s.charAt(0)==='|')s=s.slice(1);if(s.charAt(s.length-1)==='|')s=s.slice(0,-1);return s.split('|').map(function(x){return x.trim();});}
|
||||
function _tblIsSep(s){s=(s||'').trim();if(!s||s.charAt(0)!=='|')return false;var cells=_tblSplitLine(s);if(!cells.length)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});}
|
||||
function _tblIsData(s){s=(s||'').trim();return /^\|/.test(s)&&s.indexOf('|',1)>=0;}
|
||||
// v7.68 : le pipe de tête est OPTIONNEL en GFM — l'IA rend souvent
|
||||
// « A | B » / « --- | --- ». Une ligne de séparation doit garder ≥2 cellules,
|
||||
// sinon « texte » suivi d'un « --- » deviendrait un tableau.
|
||||
function _tblIsSep(s){s=(s||'').trim();if(!s)return false;var cells=_tblSplitLine(s);if(cells.length<2)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});}
|
||||
function _tblIsData(s){s=(s||'').trim();return s.indexOf('|')>=0&&_tblSplitLine(s).length>=2;}
|
||||
function _tblNew(rows,cols){var out=[];for(var i=0;i<rows;i++){var r=new Array(cols);for(var j=0;j<cols;j++)r[j]='';out.push(r);}return out;}
|
||||
function _tblClone(b){b.rows=(b.rows||[]).map(function(r){return (r||[]).slice();});return b;}
|
||||
function _tblAddCol(b,index){_tblClone(b);var w=b.rows.length?b.rows[0].length:1;if(index<0)index=0;if(index>w)index=w;b.rows.forEach(function(r){r.splice(index,0,'');});}
|
||||
@@ -744,6 +747,12 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
if(b.type==='table')return renderTableBlock(b,idx);
|
||||
if(b.type==='divider')return `<div class="block-wrapper" data-id="${b.id}"><hr class="block-divider"></div>`;
|
||||
if(b.type==='button')return renderAutomationButton(b,idx);
|
||||
// v7.68 : ces trois renderers existaient mais n'étaient JAMAIS appelés —
|
||||
// un bloc `columns`/`math`/`table_of_contents` tombait dans le rendu générique
|
||||
// (un paragraphe vide). Colonnes (slash + Turn into) = bug rapporté.
|
||||
if(b.type==='columns')return renderColumnsBlock(b);
|
||||
if(b.type==='math')return renderMathBlock(b);
|
||||
if(b.type==='table_of_contents')return renderTOC(b);
|
||||
if(b.type==='image')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" class="fd-img" data-full="${b.src||''}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;cursor:zoom-in;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'" onclick="E._openLightbox(this)"></div></div>`;
|
||||
if(b.type==='video'){
|
||||
const src=b.src||'';return src?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-video"><video controls preload="metadata" style="max-width:100%;border-radius:8px;display:block;margin:0 auto;"><source src="${src}"></video></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-video" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setVideoSrc('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🎬</div><div style="font-size:14px;color:var(--text-dim);">Click to add video URL</div></div></div>`;
|
||||
@@ -1185,6 +1194,16 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
else if(act==='suggest'){E.suggestEdit(idx);}
|
||||
else if(act==='ai'){E.askAIBlock(idx);}
|
||||
else if(act==='skill'){E.runSkill(idx,t.getAttribute('data-prompt')||'');}
|
||||
// v7.68 : Clipboard du menu (sélection multi-blocs gérée par copySelectedBlocks)
|
||||
else if(act==='clip-cut'){
|
||||
if(_selSet.size>1)E.cutSelectedBlocks();
|
||||
else{var mst=document.getElementById('blockMenu');const md=E.clipCut?E.clipCut(mst&&mst._clipSt):null;if(md!=null&&navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md);E.showToast('Sélection coupée');}}
|
||||
}
|
||||
else if(act==='clip-copy'){
|
||||
if(_selSet.size>1)E.copySelectedBlocks();
|
||||
else{var mst2=document.getElementById('blockMenu');const md=E.clipCopy?E.clipCopy(mst2&&mst2._clipSt):null;if(md!=null&&navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md);E.showToast('Sélection copiée');}}
|
||||
}
|
||||
else if(act==='clip-paste'){E.pasteBlocks();}
|
||||
else if(act==='color'){var key=t.getAttribute('data-color-key'),val=t.getAttribute('data-color-val');E.setBlockColor(idx,key,val);}
|
||||
else if(act==='turn-type'){E.turnInto(idx,t.getAttribute('data-type'),parseInt(t.getAttribute('data-cols')||'0',10)||0);_blockMenuClose();return;}
|
||||
_blockMenuClose();
|
||||
@@ -1229,11 +1248,24 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
var b=E.blocks[idx];if(!b)return;
|
||||
var d=document.createElement('div');d.id='blockMenu';d.className='block-menu';d.setAttribute('data-bid',b.id);d.style.cssText='position:fixed;z-index:4000;';
|
||||
var S=_BM_SHORTCUTS;
|
||||
// v7.68 : Clipboard — une sélection de texte multi-blocs OU des blocs
|
||||
// sélectionnés (handle + Maj-clic) donnent Cut/Copy ; Paste colle après.
|
||||
// la sélection est détruite quand la recherche prend le focus : on la
|
||||
// fige ici et les actions Clipboard la reprennent telle quelle.
|
||||
var clipSt=(E._clipState?E._clipState():null);
|
||||
d._clipSt=clipSt;
|
||||
var clip=(_selSet.size>1||clipSt)
|
||||
?'<div class="bm-group">Clipboard</div>'
|
||||
+_blockMenuItem('clip-cut','✂','Cut',{shortcut:'Ctrl+X'})
|
||||
+_blockMenuItem('clip-copy','⧉','Copy',{shortcut:'Ctrl+C'})
|
||||
+_blockMenuItem('clip-paste','📋','Paste',{shortcut:'Ctrl+V'})
|
||||
:'';
|
||||
d.innerHTML='<input class="bm-search" type="text" placeholder="Search actions..." autocomplete="off" spellcheck="false">'
|
||||
+clip
|
||||
+'<div class="bm-group">Text</div>'
|
||||
+_blockMenuItem('submenu','↩','Turn into',{sub:'turn'})
|
||||
+_blockMenuItem('submenu','🎨','Color',{sub:'color'})
|
||||
+_blockMenuItem('link','🔗','Copy link to block',{shortcut:S.link})
|
||||
+_blockMenuItem('copy-link','🔗','Copy link to block',{shortcut:S.link})
|
||||
+_blockMenuItem('duplicate','▣','Duplicate',{shortcut:S.duplicate})
|
||||
+_blockMenuItem('move-to','↪','Move to…',{shortcut:S.move})
|
||||
+_blockMenuItem('delete','✕','Delete',{shortcut:S.delete,danger:true})
|
||||
@@ -1249,7 +1281,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
d.querySelectorAll('.bm-item').forEach(function(it){
|
||||
it.style.display=(q&&it.textContent.toLowerCase().indexOf(q)<0)?'none':'';
|
||||
});
|
||||
var g=d.querySelector('.bm-group');if(g)g.style.display=q?'none':'';
|
||||
d.querySelectorAll('.bm-group').forEach(function(g){g.style.display=q?'none':'';});
|
||||
});
|
||||
d.addEventListener('mouseover',function(ev){
|
||||
var it=ev.target&&ev.target.closest?ev.target.closest('[data-sub]'):null;
|
||||
@@ -1377,6 +1409,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
if(!(ev.target&&ev.target.closest))return;
|
||||
if(ev.target.closest('#blockMenu')||ev.target.closest('#blockMenuSub')||ev.target.closest('#fdMoveOverlay'))return;
|
||||
if(ev.target.closest('.block-handle'))return;
|
||||
if(ev.button===2){const w0=ev.target.closest('.block-wrapper');if(w0&&_selSet.has(w0.getAttribute('data-id')))return;}
|
||||
if(_selSet.size)_selClear();
|
||||
_blockMenuClose();
|
||||
}
|
||||
@@ -1463,7 +1496,15 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
ct.addEventListener('click',e=>{const h=e.target&&e.target.closest?e.target.closest('.block-handle'):null;if(!h)return;const w=h.closest('.block-wrapper');if(!w)return;const wid=w.getAttribute('data-id');if(!wid)return;
|
||||
if(e.shiftKey){_selToggle(wid);return;}
|
||||
_selClear();blockMenuOpen(e.clientX,e.clientY,wid);});
|
||||
ct.addEventListener('contextmenu',e=>{if(!(e.target&&e.target.closest))return;if(e.target.closest('.ftable-cell')||e.target.closest('#_tblMenu'))return;const c=e.target.closest('.block-content, .block-wrapper, .block-column-wrapper');const el0=e.target.closest('[data-bid]');if(!c)return;const w=e.target.closest('.block-wrapper');const wid=w?w.getAttribute('data-id'):null;const bid=wid||(el0?el0.getAttribute('data-bid'):null);if(!bid)return;e.preventDefault();_selClear();blockMenuOpen(e.clientX,e.clientY,bid);});
|
||||
ct.addEventListener('contextmenu',e=>{if(!(e.target&&e.target.closest))return;if(e.target.closest('.ftable-cell')||e.target.closest('#_tblMenu'))return;const c=e.target.closest('.block-content, .block-wrapper, .block-column-wrapper');const el0=e.target.closest('[data-bid]');if(!c)return;const w=e.target.closest('.block-wrapper');const wid=w?w.getAttribute('data-id'):null;const bid=wid||(el0?el0.getAttribute('data-bid'):null);if(!bid)return;e.preventDefault();
|
||||
// v7.68 : clic droit SUR un bloc sélectionné → on garde la sélection
|
||||
// multi-blocs (le menu Clipboard coupe/copie ces blocs-là)
|
||||
if(!(_selSet.size>1&&_selSet.has(wid)))_selClear();
|
||||
blockMenuOpen(e.clientX,e.clientY,bid);});
|
||||
// v7.68 : Ctrl+C / Ctrl+X sur une sélection multi-blocs → markdown.
|
||||
// Sous un bloc unique, on laisse le navigateur (copie native).
|
||||
ct.addEventListener('copy',function(ev){const E=window.E;if(!E||!E.clipCopy)return;const md=E.clipCopy();if(md==null)return;ev.preventDefault();ev.clipboardData.setData('text/plain',md);E.showToast('Sélection copiée');});
|
||||
ct.addEventListener('cut',function(ev){const E=window.E;if(!E||!E.clipCut)return;const md=E.clipCut();if(md==null)return;ev.preventDefault();ev.clipboardData.setData('text/plain',md);E.showToast('Sélection coupée');});
|
||||
document.addEventListener('mousedown',_blockDocDown);
|
||||
if(window.location.hash&&window.location.hash.indexOf('#fdblk-')===0){const hb=window.location.hash.slice(7);setTimeout(()=>{const selEl=document.querySelector(`[data-bid="${hb}"]`);if(selEl&&selEl.scrollIntoView)selEl.scrollIntoView({behavior:'smooth',block:'center'});if(selEl){selEl.focus();ce(selEl);}},500);}
|
||||
},100);
|
||||
@@ -1960,7 +2001,58 @@ replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pu
|
||||
_blocksToMarkdown(blocks){return blocks.map(blocksToMarkdown).join('\n\n');},
|
||||
copySelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>this.showToast('Blocs copiés')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(md,'error');}},
|
||||
cutSelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>{});}this.blocks=this.blocks.filter((b,i)=>indices.indexOf(i)<0);if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Blocs coupés');},
|
||||
async pasteBlocks(){const idx=this._focusedIdx();if(idx<0)return;try{const text=await navigator.clipboard.readText();if(!text||!text.trim())return;const parsed=this.md2b(text);if(!parsed.length)return;this.sync();this.pushHistory();this.blocks.splice(idx+1,0,...parsed);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(parsed[parsed.length-1].id);_selClear();}catch(e){if(e.name==='NotAllowedError'){this.showToast('Permission presse-papier refusée');}else{this.showToast('Coller impossible','error');}}},
|
||||
async pasteBlocks(){let idx=this._focusedIdx();if(idx<0)idx=this.blocks.length-1;if(idx<0)return;try{const text=await navigator.clipboard.readText();if(!text||!text.trim())return;const parsed=this.md2b(text);if(!parsed.length)return;this.sync();this.pushHistory();this.blocks.splice(idx+1,0,...parsed);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(parsed[parsed.length-1].id);_selClear();}catch(e){if(e.name==='NotAllowedError'){this.showToast('Permission presse-papier refusée');}else{this.showToast('Coller impossible','error');}}},
|
||||
// ── v7.68 : copier/couper une SÉLECTION DE TEXTE MULTI-BLOCS ──
|
||||
// Sélection vivante qui traverse ≥2 blocs → plage ; sinon null (le
|
||||
// navigateur gère tout seul la sélection à l'intérieur d'un bloc).
|
||||
_clipState(){
|
||||
const s=window.getSelection();
|
||||
if(!s||!s.rangeCount||s.isCollapsed)return null;
|
||||
const r=s.getRangeAt(0);
|
||||
const up=n=>n?(n.nodeType===1?n:n.parentElement):null;
|
||||
const se=up(r.startContainer), ee=up(r.endContainer);
|
||||
const sblk=se&&se.closest?se.closest('[data-bid]'):null;
|
||||
const eblk=ee&&ee.closest?ee.closest('[data-bid]'):null;
|
||||
if(!sblk||!eblk)return null;
|
||||
const si=this.getIdx(sblk.dataset.bid), ei=this.getIdx(eblk.dataset.bid);
|
||||
if(si<0||ei<0||si>=ei)return null;
|
||||
return {si:si,ei:ei,
|
||||
startOff:_srcOffset(sblk,r.startContainer,r.startOffset),
|
||||
endOff:_srcOffset(eblk,r.endContainer,r.endOffset)};
|
||||
},
|
||||
// blocs dont la sélection ne couvre qu'une PARTIE (les autres = entiers)
|
||||
_clipHeavy(b){return ['table','code','columns','math','image','embed','divider','meeting','button','table_of_contents'].indexOf(b.type)>=0;},
|
||||
_clipMarkdown(st){
|
||||
const parts=[];
|
||||
for(let i=st.si;i<=st.ei;i++){
|
||||
const b=this.blocks[i];if(!b)continue;
|
||||
if(i===st.si&&i===st.ei)return this._anchorText(b.id,st.startOff,st.endOff);
|
||||
if(i===st.si||i===st.ei){
|
||||
if(this._clipHeavy(b)){parts.push(blocksToMarkdown(b));continue;}
|
||||
const el=this.getEl(b.id);
|
||||
parts.push(i===st.si
|
||||
? this._anchorText(b.id,st.startOff,el?_srcNodeLen(el):0)
|
||||
: this._anchorText(b.id,0,st.endOff));
|
||||
}else parts.push(blocksToMarkdown(b));
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
},
|
||||
// renvoie le markdown copié, ou null si la sélection n'est pas multi-blocs
|
||||
clipCopy(st){st=st||this._clipState();if(!st)return null;this.sync();return this._clipMarkdown(st);},
|
||||
clipCut(st){
|
||||
st=st||this._clipState();if(!st)return null;
|
||||
this.sync();this.pushHistory();
|
||||
const md=this._clipMarkdown(st);
|
||||
const first=this.blocks[st.si],last=this.blocks[st.ei];
|
||||
const lastEl=this.getEl(last.id);
|
||||
if(!this._clipHeavy(first))first.content=this._anchorText(first.id,0,st.startOff);
|
||||
if(!this._clipHeavy(last))last.content=this._anchorText(last.id,st.endOff,lastEl?_srcNodeLen(lastEl):0);
|
||||
this.blocks=this.blocks.slice(0,st.si+1).concat([last],this.blocks.slice(st.ei+1));
|
||||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||||
this.dirty=true;this.autoSave();this.render();_rtSync();
|
||||
this._focusBid(first.id);
|
||||
return md;
|
||||
},
|
||||
_focusedIdx(){const a=this.getActiveBlock();return a?a.idx:-1;},
|
||||
async moveBlockToPage(idx,pageId){if(idx<0||idx>=this.blocks.length||pageId===this.pid)return;this.sync();this.pushHistory();const block=this.blocks[idx];const self=this;const csrf = getCsrf();;this.showToast('Déplacement du bloc…');try{const r=await fetch(`/board/api/pages/${pageId}`,{credentials:'same-origin'});const d=await r.json();let tblocks=[];if(d.content_format==='blocks'&&d.content){try{tblocks=JSON.parse(d.content);}catch(e){tblocks=[];}}tblocks.push(block);await fetch(`/board/api/pages/${pageId}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({title:d.title||'',blocks:tblocks})});
|
||||
if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Bloc déplacé');
|
||||
@@ -2972,7 +3064,10 @@ applyAIBlocks(text){
|
||||
const btype=block.type||'paragraph';
|
||||
if(['code','table','math','image','embed','meeting','divider','table_of_contents','columns'].indexOf(btype)>=0)return fallback();
|
||||
const parsed=this.paste2b(text);
|
||||
if(parsed.length<=1)return fallback();
|
||||
if(!parsed.length)return fallback();
|
||||
// un seul bloc PARAGRAPHE = coller une ligne → texte inline (comme avant) ;
|
||||
// un seul bloc structuré (table, code, titre…) = vraie insertion de bloc.
|
||||
if(parsed.length===1&&parsed[0].type==='paragraph')return fallback();
|
||||
const sp=splitCaret(bel);
|
||||
let before='',after='';
|
||||
if(sp){before=sp.before;after=sp.after;}else{before=gt(bel)||'';}
|
||||
|
||||
Reference in New Issue
Block a user