feat: Escape du menu slash retire le « / » + bouton « commenter » à droite du bloc (v7.66.0)
- 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:
@@ -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);},
|
||||
|
||||
Reference in New Issue
Block a user