From 8b0a0aea3a932085e64d7da7852cd64be42829f4 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 7 Sep 2026 20:17:08 -0400 Subject: [PATCH] =?UTF-8?q?feat(editor):=20collage=20intelligent=20?= =?UTF-8?q?=E2=80=94=20d=C3=A9coupe=20du=20texte=20coll=C3=A9=20en=20plusi?= =?UTF-8?q?eurs=20blocs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 23 +++++++ VERSION | 2 +- app/main.py | 2 +- app/templates/_page_editor_scripts.html | 88 ++++++++++++++++++++++++- 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9995ac0..93258c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index ed45e0a..831c6cd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.1.10 \ No newline at end of file +5.1.11 \ No newline at end of file diff --git a/app/main.py b/app/main.py index 7bdb010..0b42f5c 100644 --- a/app/main.py +++ b/app/main.py @@ -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, diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 8ec2367..54f8fb2 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -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{if(r.length>w)w=r.length;});if(w<1)w=1; + const rows2=rows.map(r=>{const a=r.slice();while(a.length '){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;