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
+23
View File
@@ -1,5 +1,28 @@
# Changelog — FlowDeck
## v5.1.11 (2026-09-07) — Éditeur : collage intelligent (découpe en blocs)
> Coller un texte dans un bloc le plaçait en entier dans ce bloc, même s'il
> contenait plusieurs paragraphes, des listes, des titres markdown, du code ou
> un tableau.
- **Découpe automatique au collage** : le contenu collé est analysé
(`paste2b`) et réparti en plusieurs blocs selon son format (markdown ou
texte simple).
- Pris en charge : titres `#`→`####`, listes à puces (`-`, `*`, `+`), listes
numérotées, cases à cocher `- [ ]` / `- [x]` (y compris `[X]`), citations
`>` , séparateurs `---`/`***`, blocs de code (fences ```), tableaux
markdown et paragraphes.
- Texte simple : les lignes courtes (≤ 55 caractères) deviennent chacune un
bloc ; les blocs de texte longs (paragraphes) restent groupés ; une ligne
vide crée une nouvelle section.
- Comportement au curseur : le texte avant le curseur reste dans le bloc
courant (qui est réutilisé comme premier bloc si vide), puis les blocs
analysés sont insérés, et le texte après le curseur conserve sa propre
place (refocus sur le dernier bloc inséré).
- Les blocs non textuels (code, tableau, image, formules…) reçoivent toujours
le collage en texte brut, sans découpe.
## v5.1.10 (2026-09-07) — Espace local : boîte de renommage à la taille du nom
> Renommer un fichier en cliquant sur son nom ouvrait une boîte d'édition qui
+1 -1
View File
@@ -1 +1 @@
5.1.10
5.1.11
+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;