feat(editor): collage intelligent — découpe du texte collé en plusieurs blocs
FlowDeck CI / test (push) Failing after 16s
FlowDeck CI / docker (push) Skipped

Analyse le contenu collé dans un bloc (paste2b) et le répartit en blocs selon
son format : titres markdown, listes à puces et numérotées, cases à cocher,
citations, séparateurs, blocs de code, tableaux et paragraphes. Le texte avant
le curseur est conservé dans le bloc courant (réutilisé si vide), les nouveaux
blocs sont insérés après, le texte après le curseur garde sa place, refocus sur
le dernier bloc inséré. Les blocs non textuels (code, tableau, image...) gardent
un collage en texte brut.
This commit is contained in:
2026-09-07 20:17:08 -04:00
parent 52d7a0d006
commit 8b0a0aea3a
4 changed files with 112 additions and 3 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="5.1.10",
version="5.1.11",
docs_url="/docs" if settings.log_level == "DEBUG" else None,
redoc_url=None,
lifespan=lifespan,
+87 -1
View File
@@ -715,7 +715,7 @@
if(tEl){const syncT=()=>{tEl.classList.toggle('empty',!(tEl.textContent||'').trim());};syncT();tEl.addEventListener('input',syncT);tEl.addEventListener('blur',syncT);}
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
ct.addEventListener('keydown',e=>this.onKd(e));
ct.addEventListener('paste',e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();});
ct.addEventListener('paste',e=>this.handleSmartPaste(e));
if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper:not(.nested-block):not(.block-column-wrapper)',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{if(evt.oldIndex===evt.newIndex)return;const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);this.dirty=true;this.autoSave();this.render();}});
},100);
},
@@ -1285,6 +1285,92 @@ applyAIBlocks(text){
body:JSON.stringify({text:'@'+fresh.join(' @')})}).catch(function(){});
},
pastePlain(e){e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();},
// ── Smart paste: analyze pasted text and split it into multiple blocks ──
paste2b(md){
const out=[];let para='';
const lines=String(md||'').replace(/\r\n/g,'\n').split('\n');
const flush=()=>{
if(!para.trim()){para='';return;}
const pa=para.split('\n').map(s=>s.trim()).filter(Boolean);
if(pa.length>1&&pa.every(l=>l.length<=55)){pa.forEach(l=>out.push(this.mkB('paragraph',l)));}
else out.push(this.mkB('paragraph',para.trim()));
para='';
};
let i=0;
for(;i<lines.length;i++){
const line=lines[i];const t=line.trim();
if(!t){flush();continue;}
if(t.startsWith('```')){
flush();
const lang=t.substring(3).trim()||'Plain Text';
let code='';i++;
for(;i<lines.length;i++){if(lines[i].trim()==='```')break;code+=(code?'\n':'')+lines[i];}
const cb=this.mkB('code',code);cb.language=lang;out.push(cb);
continue;
}
if(_tblIsData(t)&&i+1<lines.length&&_tblIsSep(lines[i+1])){
flush();
const header=_tblSplitLine(t);const rows=[header];let j=i+2;
while(j<lines.length&&_tblIsData(lines[j])){rows.push(_tblSplitLine(lines[j]));j++;}
let w=0;rows.forEach(r=>{if(r.length>w)w=r.length;});if(w<1)w=1;
const rows2=rows.map(r=>{const a=r.slice();while(a.length<w)a.push('');return a;});
out.push(this.mkB('table','',{rows:rows2}));i=j-1;
continue;
}
const hmm=t.match(/^(#{1,6})\s+(.*)$/);
if(hmm){flush();out.push(this.mkB('heading_'+Math.min(hmm[1].length,4),hmm[2]));continue;}
if(/^[-*+]\s+\[[ xX]\]\s+/.test(t)){
flush();
const nb=this.mkB('to_do',t.replace(/^[-*+]\s+\[[ xX]\]\s+/,''));nb.checked=/^[-*+]\s+\[x\]/.test(t)||/^[-*+]\s+\[X\]/.test(t);
out.push(nb);continue;
}
if(/^[-*+]\s+/.test(t)){flush();out.push(this.mkB('bulleted_list',t.replace(/^[-*+]\s+/,'')));continue;}
if(/^\d+\.\s+/.test(t)){flush();out.push(this.mkB('numbered_list',t.replace(/^\d+\.\s+/,'')));continue;}
if(t.substring(0,2)==='> '){flush();out.push(this.mkB('quote',t.substring(2)));continue;}
if(t==='---'||t==='***'){flush();out.push(this.mkB('divider',''));continue;}
para+=(para?'\n':'')+line.trim();
}
flush();
return out;
},
handleSmartPaste(e){
e.preventDefault();
const text=(((e.clipboardData||window.clipboardData))||{}).getData('text/plain')||'';
const fallback=()=>{document.execCommand('insertText',false,text);this.dirty=true;this.autoSave();};
const ab=this.getActiveBlock();
if(!ab)return fallback();
const bel=ab.el,idx=ab.idx,block=this.blocks[idx];
if(!block)return fallback();
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();
const sp=splitCaret(bel);
let before='',after='';
if(sp){before=sp.before;after=sp.after;}else{before=gt(bel)||'';}
this.sync();
const beforeHasText=!!(before||'').trim();
block.content=before;
const ins=[];
parsed.forEach((pb,k)=>{
if(k===0&&!beforeHasText){
const nbf={id:block.id,type:pb.type,content:pb.content};
for(const key in pb){if(key==='id'||key==='type'||key==='content')continue;nbf[key]=pb[key];}
this.blocks[idx]=nbf;
}else{
const nb=this.mkB(pb.type,pb.content);
if(pb.language!==undefined)nb.language=pb.language;
if(pb.checked!==undefined)nb.checked=pb.checked;
if(pb.children)nb.children=pb.children;
ins.push(nb);
}
});
if((after||'').trim())ins.push(this.mkB('paragraph',after.trim()));
this.blocks.splice(idx+1,0,...ins);
const focusId=ins.length?ins[ins.length-1].id:block.id;
this.render();this.dirty=true;this.autoSave();
setTimeout(()=>{const fe=this.getEl(focusId);if(fe){fe.focus();ce(fe);}},60);
},
autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
save(cb){
if(this.fileData)return;