feat: menus mobiles façon Notion — barre horizontale au « / » + Insert block plein écran (v7.63.0)
- Barre flottante défilante sous le curseur (mobile ≤ 768 px) au lieu de la popup desktop, 15 boutons dans l'ordre de la description : +▾, Turn into▾, @, 💬, 🖼, 🗑, ←, →, ↑, ↓, ↩, ↪, Color▾, More▾, ⌨ - +▾ → feuille plein écran « Insert block » (titre centré, Cancel bleu, groupes Basic blocks / AI writing / Media / Data / Synced / Actions, raccourcis markdown à droite) ; items partagés avec la popup desktop (SM._slashItemHtml) - Turn into ▾ / Color ▾ → feuilles réutilisant les générateurs extraits _blockTurnItems / _blockColorItems (extraits de _blockTurnMenuOpen / _blockColorMenuOpen, comportement desktop inchangé) - More ▾ → menu contextuel de bloc existant ; @ insère + ouvre les mentions ; 💬 commentaire ; 🖼 bloc image ; 🗑 suppression ; ⌨ masque le clavier ; ↩ ↪ annuler/rétablir - moveBlock(bid, dir) : ↑↓ réordonne (racine + enfants toggle/columns), → indent dans le frère précédent, ← outdent, garde-fous avec toast - Fermeture auto de la barre quand le « / » n'est plus en tête de bloc ; taps dans la barre/les feuilles exclus du handler « clic hors-zone » ; Cancel ne referme que la feuille (la barre reste tant que le / est tapé) - Raccourcis slash alignés sur les préfixes markdown (-, [], >, ---), groupe SYNCED libellé « Synced » - Tests : tests/test_v763_mobile_menus.py (ordre, câblage, structure, styles + moveBlock exécuté sous node), e2e/v763_mobile_menus.spec.js (390×844, 1 passed) ; test_v761 adapté aux extractions ; suite complète 1375 passed / 0 failed, ruff OK, OpenAPI 7.63.0 (525 chemins)
This commit is contained in:
@@ -320,8 +320,9 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
}
|
||||
|
||||
// ── Block type shortcuts (shown on the right of each slash item) ──
|
||||
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'•',numbered_list:'1.',to_do:'[ ]',toggle:'▸',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'———',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
|
||||
const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions'};
|
||||
// Raccourcis = préfixes markdown (alignés sur la description mobile Notion : -, [], >, ---)
|
||||
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'-',numbered_list:'1.',to_do:'[]',toggle:'>',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'---',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
|
||||
const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions',SYNCED:'Synced'};
|
||||
// ── Hover preview card (matches Notion's visual hint) ──
|
||||
function _previewHtml(blockId){
|
||||
const p=function(inner,cls){return '<div class="slash-prev" style="padding:0">'+inner+'</div>';};
|
||||
@@ -365,7 +366,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
inp.oninput=()=>SM.flt();
|
||||
e.appendChild(list);e.appendChild(footer);e.appendChild(inp);
|
||||
SM._e=e;SM._in=inp;SM._li=list;SM._f=footer;
|
||||
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target))SM.close();});
|
||||
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target)&&!MTB.contains(ev.target))SM.close();});
|
||||
if(!SM._doc){
|
||||
SM._doc=true;
|
||||
document.addEventListener('keydown',function(ev){
|
||||
@@ -390,19 +391,22 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
SM._pv.style.left=left+'px'; SM._pv.style.top=top+'px'; SM._pv.style.display='block';
|
||||
},
|
||||
open(i,el){if(!SM._e)SM.init();SM.close();SM._i=i;SM._s=0;SM._in.value='';SM.flt();
|
||||
// v7.63 : mobile → barre horizontale façon Notion sous le curseur
|
||||
if(_fdMobile()){SM._o=true;MTB.open(i,el);return;}
|
||||
const r=el.getBoundingClientRect();SM._e.style.display='flex';
|
||||
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-360)?r.top-364:r.bottom+4)+'px';
|
||||
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';},
|
||||
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();},
|
||||
// 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>
|
||||
<span class="slash-item-name" style="flex:1;font-weight:500">${c.name}</span>
|
||||
<span class="slash-item-hint" style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);opacity:.7">${_SHORTCUT[c.id]||''}</span></div>`;},
|
||||
flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h='';
|
||||
for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue;
|
||||
h+=`<div class="sm-group" style="padding:6px 10px 3px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px">${_GROUP_LABEL[g.name]||g.name}</div>`;
|
||||
items.forEach(c=>{SM._it.push(c);
|
||||
h+=`<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>
|
||||
<span class="slash-item-name" style="flex:1;font-weight:500">${c.name}</span>
|
||||
<span class="slash-item-hint" style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);opacity:.7">${_SHORTCUT[c.id]||''}</span></div>`;});}
|
||||
items.forEach(c=>{SM._it.push(c);h+=SM._slashItemHtml(c);});}
|
||||
SM._li.innerHTML=h;SM._rs();},
|
||||
_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();}
|
||||
@@ -412,6 +416,64 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
|
||||
get isOpen(){return SM._o;}};
|
||||
|
||||
// ═══════════ v7.63.0 — Menus mobiles façon Notion : barre horizontale sous le curseur
|
||||
// + feuille plein écran « Insert block » / « Turn into » / « Color ». Tout est
|
||||
// branché sur les actions existantes (applySlash, turnInto, blockMenu, moveBlock…).
|
||||
function _fdMobile(){try{return window.matchMedia('(max-width: 768px)').matches;}catch(e){return false;}}
|
||||
const MTB={
|
||||
_b:null,_sheet:null,
|
||||
contains(t){return !!t&&((this._b&&this._b.contains(t))||(this._sheet&&this._sheet.contains(t)));},
|
||||
el(){ if(this._b)return this._b;
|
||||
const wide=['plus','turn','color','more'];
|
||||
const items=[['plus','+<span class="fd-mtb-caret">▾</span>'],['turn','Turn into<span class="fd-mtb-caret">▾</span>'],
|
||||
['at','@'],['comment','💬'],['image','🖼️'],['trash','🗑️'],
|
||||
['left','←'],['right','→'],['up','↑'],['down','↓'],['undo','↩'],['redo','↪'],
|
||||
['color','Color<span class="fd-mtb-caret">▾</span>'],['more','More<span class="fd-mtb-caret">▾</span>'],['kbd','⌨️']];
|
||||
const d=document.createElement('div');d.id='fdMobileBar';d.className='fd-mtb';
|
||||
d.innerHTML=items.map(it=>'<button type="button" class="fd-mtb-item'+(wide.indexOf(it[0])>=0?' fd-mtb-wide':'')+'" data-mtb="'+it[0]+'">'+it[1]+'</button>').join('');
|
||||
d.addEventListener('click',ev=>{const bt=ev.target.closest('[data-mtb]');if(bt)MTB.act(bt.getAttribute('data-mtb'),bt);});
|
||||
document.body.appendChild(d);this._b=d;return d;},
|
||||
open(i,el){const d=this.el();d.style.display='flex';
|
||||
const r=el.getBoundingClientRect();
|
||||
const w=Math.min(d.scrollWidth||280,window.innerWidth-16);
|
||||
d.style.left=Math.max(8,Math.min(r.left,window.innerWidth-w-8))+'px';
|
||||
let top=r.bottom+6;if(top+54>window.innerHeight-8)top=Math.max(8,r.top-54);
|
||||
d.style.top=top+'px';d.scrollLeft=0;},
|
||||
close(){if(this._b)this._b.style.display='none';this.closeSheet();},
|
||||
closeSheet(){if(this._sheet){this._sheet.remove();this._sheet=null;}},
|
||||
sheet(title,html,handler){
|
||||
this.closeSheet();
|
||||
const s=document.createElement('div');s.className='fd-msheet';
|
||||
s.innerHTML='<div class="fd-msheet-head"><span class="fd-msheet-title">'+title+'</span><button type="button" class="fd-msheet-cancel">Cancel</button></div><div class="fd-msheet-list block-menu">'+html+'</div>';
|
||||
s.querySelector('.fd-msheet-cancel').addEventListener('click',()=>{const E=window.E;this.closeSheet();if(E&&SM._i>=0&&E.blocks[SM._i])E._focusBid(E.blocks[SM._i].id);});
|
||||
s.addEventListener('click',handler);
|
||||
document.body.appendChild(s);this._sheet=s;},
|
||||
_insertHtml(){let h='';for(const g of CMDS){if(!g.items||!g.items.length)continue;
|
||||
h+='<div class="sm-group">'+(_GROUP_LABEL[g.name]||g.name)+'</div>';
|
||||
g.items.forEach(c=>{h+=SM._slashItemHtml(c);});}return h;},
|
||||
_bmAct(ev){if(!ev.target||!ev.target.closest||!ev.target.closest('[data-bm-act]'))return;_blockMenuClick(ev);this.closeSheet();},
|
||||
act(k,btn){
|
||||
const E=window.E;if(!E)return;
|
||||
const idx=SM._i;const b=(idx>=0&&idx<E.blocks.length)?E.blocks[idx]:null;
|
||||
const bid=b?b.id:'';
|
||||
if(k==='plus'){this.sheet('Insert block',this._insertHtml(),ev=>{const it=ev.target.closest('.slash-item');if(it)SM.sel(it.dataset.sid);});return;}
|
||||
if(k==='turn'){this.sheet('Turn into',_blockTurnItems(bid),ev=>this._bmAct(ev));return;}
|
||||
if(k==='color'){this.sheet('Color',_blockColorItems(bid),ev=>this._bmAct(ev));return;}
|
||||
if(k==='more'){const el=E.getEl(bid);const r=el?el.getBoundingClientRect():btn.getBoundingClientRect();
|
||||
this.close();blockMenuOpen(Math.max(8,Math.min(r.left,window.innerWidth-250)),Math.min(r.bottom+6,window.innerHeight-120),bid);return;}
|
||||
if(k==='kbd'){const a=document.activeElement;if(a&&a.blur)a.blur();return;}
|
||||
if(!b)return;
|
||||
if(k==='at'){const el=E.getEl(bid);if(!el)return;el.focus();document.execCommand('insertText',false,'@');
|
||||
E.dirty=true;E.autoSave();E.openMention(el);return;}
|
||||
if(k==='comment'){E.commentBlock(idx);return;}
|
||||
if(k==='image'){E.applySlash('image');return;}
|
||||
if(k==='trash'){E.deleteSelected(idx);return;}
|
||||
if(k==='up'||k==='down'||k==='left'||k==='right'){E.moveBlock(bid,k);return;}
|
||||
if(k==='undo'){E.undo();return;}
|
||||
if(k==='redo'){E.redo();return;}
|
||||
}
|
||||
};
|
||||
|
||||
// ═══════════ v5.11.0 Wiki-link picker ([[ → page search) ═══════════
|
||||
const WM={_o:false,_idx:-1,_s:0,_it:[],_root:null,_in:null,_li:null,_pv:null,
|
||||
mk(){ if(this._root)return this._root;
|
||||
@@ -1125,8 +1187,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
d.style.left=x+'px';d.style.top=y+'px';
|
||||
return d;
|
||||
}
|
||||
function _blockTurnMenuOpen(x,y,bid){
|
||||
var d=_blockSubShell(x,y);
|
||||
function _blockTurnItems(bid){
|
||||
var E=window.E;var cur='';
|
||||
if(E){var i=E.getIdx(bid);if(i>=0&&E.blocks[i])cur=E.blocks[i].type||'';}
|
||||
var html='';
|
||||
@@ -1136,10 +1197,13 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
title:t.cols?('Create '+t.cols+' columns of blocks'):'',
|
||||
check:on,current:on});
|
||||
});
|
||||
d.innerHTML=html;
|
||||
return html;
|
||||
}
|
||||
function _blockColorMenuOpen(x,y,bid){
|
||||
function _blockTurnMenuOpen(x,y,bid){
|
||||
var d=_blockSubShell(x,y);
|
||||
d.innerHTML=_blockTurnItems(bid);
|
||||
}
|
||||
function _blockColorItems(bid){
|
||||
var E=window.E;var idx=E?E.getIdx(bid):-1;var b=idx>=0?E.blocks[idx]:null;
|
||||
var curT=(b&&b.style&&b.style.color)||'default';
|
||||
var curB=(b&&b.style&&b.style.bgColor)||'default';
|
||||
@@ -1167,7 +1231,11 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
_BLOCK_COLOR_NAMES[n]+' background',{bid:bid,colorKey:'bgColor',colorVal:v,
|
||||
check:v===curB,current:v===curB});
|
||||
});
|
||||
d.innerHTML=html;
|
||||
return html;
|
||||
}
|
||||
function _blockColorMenuOpen(x,y,bid){
|
||||
var d=_blockSubShell(x,y);
|
||||
d.innerHTML=_blockColorItems(bid);
|
||||
}
|
||||
function _blockSkillsMenuOpen(x,y,bid){
|
||||
var d=_blockSubShell(x,y);
|
||||
@@ -1750,6 +1818,35 @@ replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pu
|
||||
this.showToast('FlowDeck AI : compétence en cours…');
|
||||
this._aiWritingRun('write',idx,{prompt:String(tpl||'')+'\n\nContenu du bloc :\n'+b.content},'insert');
|
||||
},
|
||||
// v7.63 : déplacement de bloc depuis la barre mobile
|
||||
// (↑↓ = ordre, → = indent dans le frère précédent, ← = sortir du parent)
|
||||
moveBlock(bid,dir){
|
||||
const top=this.blocks.findIndex(b=>b.id===bid);
|
||||
const pi=this.blocks.findIndex(b=>Array.isArray(b.children)&&b.children.some(c=>c.id===bid));
|
||||
const parent=pi>=0?this.blocks[pi]:null;
|
||||
const list=parent?parent.children:this.blocks;
|
||||
const k=list.findIndex(b=>b.id===bid);
|
||||
if(k<0){this.showToast('Bloc introuvable','error');return;}
|
||||
if(dir==='up'||dir==='down'){
|
||||
const j=dir==='up'?k-1:k+1;
|
||||
if(j<0||j>=list.length){this.showToast(dir==='up'?'Déjà en haut':'Déjà en bas','error');return;}
|
||||
this.sync();this.pushHistory();
|
||||
const mv=list.splice(k,1)[0];list.splice(j,0,mv);
|
||||
}else if(dir==='right'){
|
||||
if(parent){this.showToast('Déjà indenté','error');return;}
|
||||
const prev=this.blocks[top-1];
|
||||
if(!prev||!Array.isArray(prev.children)||(prev.type!=='toggle'&&prev.type!=='columns')){this.showToast('Pas de bloc cible à droite','error');return;}
|
||||
this.sync();this.pushHistory();
|
||||
if(prev.type==='toggle')prev.expanded=true;
|
||||
const mv=this.blocks.splice(top,1)[0];prev.children.push(mv);
|
||||
}else{
|
||||
if(!parent){this.showToast('Déjà à la racine','error');return;}
|
||||
this.sync();this.pushHistory();
|
||||
const mv=parent.children.splice(k,1)[0];
|
||||
this.blocks.splice(pi+1,0,mv);
|
||||
}
|
||||
this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(bid);
|
||||
},
|
||||
applyLastColor(idx){
|
||||
let v='';try{v=localStorage.getItem('fdLastBlockColor')||'';}catch(e){}
|
||||
const i=v.indexOf('|');
|
||||
@@ -2225,7 +2322,9 @@ applyAIBlocks(text){
|
||||
if(e.key==='Escape'){e.preventDefault();window.AIAC.clear();return;}
|
||||
if(e.key==='Enter'||e.key==='Backspace'){window.AIAC.clear();}
|
||||
}
|
||||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen&&!WM.isOpen)SM.open(i2,e2);
|
||||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);
|
||||
if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen&&!WM.isOpen){SM.open(i2,e2);}
|
||||
else if(SM.isOpen){const t=(e2&&e2.textContent)||'';if(t.charAt(0)!=='/')SM.close();}
|
||||
if(e2&&!SM.isOpen&&!WM.isOpen){const t=gtTok(e2)||'';const mm=t.match(/\[\[$/);if(mm){WM.open(i2,e2);}}},15);}
|
||||
if(e.key===' '){
|
||||
if(block.type==='paragraph'&&!(block.content||'').trim()&&!(el.textContent||'').trim()&&!SM.isOpen&&window.AIC){
|
||||
|
||||
Reference in New Issue
Block a user