feat(editor): collage intelligent — découpe du texte collé en plusieurs blocs
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:
+1
-1
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user