feat: Escape du menu slash retire le « / » + bouton « commenter » à droite du bloc (v7.66.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- SM.escClose() : un seul implément, appelé par les deux chemins de fermeture
  du menu slash (focus dans le bloc → onKd, focus dans l'entrée du menu →
  listener document). Retire le « / » d'ouverture seulement, remet le caret,
  sync() + autoSave().
- Bouton .block-comment-btn à droite du bloc (gouttière 64px) : icône bulle de
  conversation + , visible au survol/focus comme le handle, masqué sur mobile
  (2 breakpoints) et sur page verrouillée ; onclick = E.commentBlock(idx).
  Remplace l'ancien .block-actions de renderBlock (opacity:0 sans règle de
  survol → un + invisible qui capait les clics dans la gouttière).
- fmtComment() restaure la plage de la toolbar (_fmtRestore) avant d'ancrer :
  commentBlock() ne se replie plus sur this._sel (plage périmée éventuelle).

Tests: pytest tests/ -q -n auto → 1386 passed ; ruff check app tests → OK ;
e2e v766 (Esc → menu fermé + bloc vide ; bouton masqué → visible au survol,
à droite du wrapper, ancre = bloc visé) + v764/v762/v7641/v763/
regression_editor_mount → 7 passed sur :8081.
This commit is contained in:
2026-10-08 12:47:00 -04:00
parent 00c5e2a60d
commit ab6977733d
11 changed files with 280 additions and 16 deletions
+33 -12
View File
@@ -463,6 +463,27 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
SM._e.style.left=Math.min(r.left,window.innerWidth-340)+'px';
SM._rs();setTimeout(()=>SM._in.focus(),10);SM._o=true;},
close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');if(SM._pv)SM._pv.style.display='none';MTB.close();},
// v7.66 : Escape ferme le menu ET retire le « / » qui l'a ouvert.
escClose(){
const i=SM._i;SM.close();
const E=window.E;if(!E||i<0||i>=E.blocks.length)return;
const el=E.getEl(E.blocks[i].id);if(!el)return;
const w=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);let n;
while((n=w.nextNode())){
if(n.parentElement&&n.parentElement.closest('.fd-react-chip'))continue;
const v=n.nodeValue,at=v.indexOf('/');
if(at>=0){
if(v.slice(0,at).trim())return; // pas le / d'ouverture → on ne touche à rien
n.nodeValue=v.slice(0,at)+v.slice(at+1);
E.sync();E.dirty=true;E.autoSave();
el.focus();
const r=document.createRange();r.setStart(n,Math.min(at,n.nodeValue.length));r.collapse(true);
const s=window.getSelection();s.removeAllRanges();s.addRange(r);
return;
}
if(v.trim())return; // 1er texte non vide sans / → rien à retirer
}
},
// Ligne d'item du slash (partagée par la popup desktop et la feuille mobile)
_slashItemHtml(c){return `<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:7px 10px;font-size:14px;color:var(--text-primary);cursor:pointer;border-radius:5px">
<span class="slash-item-icon" style="width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:15px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
@@ -476,7 +497,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
_kd(e){if(e.key==='ArrowDown'){e.preventDefault();SM._s=Math.min(SM._s+1,SM._it.length-1);SM._rs();}
else if(e.key==='ArrowUp'){e.preventDefault();SM._s=Math.max(SM._s-1,0);SM._rs();}
else if(e.key==='Enter'){e.preventDefault();const c=SM._it[SM._s];if(c)SM.sel(c.id);}
else if(e.key==='Escape'){e.preventDefault();SM.close();}},
else if(e.key==='Escape'){e.preventDefault();SM.escClose();}},
_rs(){const its=SM._li.querySelectorAll('.slash-item');its.forEach((el,i)=>{el.classList.toggle('selected',i===SM._s);});const cur=its[SM._s];if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'});},
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
get isOpen(){return SM._o;}};
@@ -789,14 +810,17 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
let s=idx; while(s>0&&bs[s-1]&&bs[s-1].type==='numbered_list')s--;
numAttr=` data-num="${idx-s+1}"`;
}
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
// v7.66 : bouton « commenter ce bloc » à droite du bloc (gouttière 64px),
// icône bulle de conversation + . Remplace l'ancien .block-actions mort
// (opacity:0 sans règle de survol → un + invisible qui capait les clics).
const cmtBtn=idx>=0?`<button type="button" class="block-comment-btn" title="Comment on block" aria-label="Comment on block" onclick="E.commentBlock(${idx})"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13.6 7.3C13.6 10.2 11 12.5 7.9 12.5C7.3 12.5 6.7 12.4 6.2 12.2L3.5 13.3L4.4 10.9C3.4 10 2.4 8.7 2.4 7.3C2.4 4.4 5 2.2 8 2.2C11 2.2 13.6 4.4 13.6 7.3Z"/><path d="M8 5.3V9.3M6 7.3H10"/></svg></button>`:'';
// + « ajouter un bloc dessous », juste avant le handle (gouttière 64px)
const insertBtn=idx>=0?`<button type="button" class="block-insert-btn" title="Add block below" aria-label="Add block below" onclick="E.addAfter(${idx})">+</button>`:'';
return `<div class="block-wrapper" data-id="${b.id}">
${insertBtn}
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
${actions}
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
${cmtBtn}
</div>`;
}
@@ -1865,19 +1889,16 @@ replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pu
setBlockColor(idx,key,val){if(idx<0||idx>=this.blocks.length)return;const targets=this._blockTargets(idx);const v=(!val||val==='default')?null:val;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(!b.style)b.style={};if(v==null)delete b.style[key];else b.style[key]=v;});if(v!=null){try{localStorage.setItem('fdLastBlockColor',key+'|'+v);}catch(e){}}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();},
// ── v7.61 : actions du menu contextuel de bloc ──
commentBlock(idx){
// L'ancre est la SÉLECTION de texte si elle vit dans ce bloc (la
// sélection de la toolbar aussi : le clic sur le bouton peut l'avoir
// fait disparaître du DOM), sinon tout le bloc.
// L'ancre est la SÉLECTION de texte vivante dans ce bloc, sinon tout
// le bloc. La toolbar de sélection restaure sa plage AVANT (fmtComment)
// — pas de repli sur `this._sel` ici : sinon le bouton de droite / le
// menu de bloc reprendraient une sélection périmée.
const b=this.blocks[idx];if(!b)return;const el=this.getEl(b.id);if(!el)return;
let r=null;const s=window.getSelection();
if(s&&s.rangeCount&&!s.isCollapsed){
const rr=s.getRangeAt(0);
if(el.contains(rr.startContainer)&&el.contains(rr.endContainer))r=rr.cloneRange();
}
if(!r&&this._sel&&this._sel.range&&this._sel.idx===idx
&&el.contains(this._sel.range.startContainer)&&el.contains(this._sel.range.endContainer)){
r=this._sel.range.cloneRange();
}
if(!r){r=document.createRange();r.selectNodeContents(el);}
const start=_srcOffset(el,r.startContainer,r.startOffset);
const end=_srcOffset(el,r.endContainer,r.endOffset);
@@ -2386,7 +2407,7 @@ applyAIBlocks(text){
const ab=this.getActiveBlock();if(!ab)return;
const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return;
if(SM.isOpen){
if(e.key==='Escape'){e.preventDefault();SM.close();return;}
if(e.key==='Escape'){e.preventDefault();SM.escClose();return;}
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;}
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
// La requête peut être tapée DANS le bloc (« /numbered list ») si le
@@ -2594,7 +2615,7 @@ applyAIBlocks(text){
// « … » → le menu contextuel de bloc (recherche + Text + pied métadonnées)
fmtMore(ev){const bid=this._fmtBid();if(!bid)return;const r=ev.currentTarget.getBoundingClientRect();
this.fmt.open=false;blockMenuOpen(Math.max(8,Math.min(r.left-190,window.innerWidth-250)),r.bottom+6,bid);},
fmtComment(){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.commentBlock(i);},
fmtComment(){this.fmt.open=false;const i=this.fmt.idx;if(i<0)return;this._fmtRestore();this.commentBlock(i);},
fmtAskAI(){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.askAIBlock(i);},
fmtEditAI(){this.fmtAskAI();},
fmtSkill(tpl){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.runSkill(i,tpl);},