Files
flowdeck/static/js/page_editor_scripts.js
T
bruno 0218d8f5e5
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped
fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
2026-10-09 16:58:04 -04:00

3540 lines
253 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable */
if (!window.__fdEditorScriptsLoaded) {
window.__fdEditorScriptsLoaded = true;
const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.parse(el.textContent):{}}catch(e){return {}}})();
const CMDS=[{name:'BASIC',items:[
{id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
{id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'},
{id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'},
{id:'callout',name:'Callout',icon:'💡'},{id:'table_of_contents',name:'Table of contents',icon:'☰'},{id:'math',name:'Math equation',icon:'∑'},
{id:'columns',name:'Column list',icon:'▮'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'},
{id:'table',name:'Table',icon:'⊞'},{id:'button',name:'Button / Automation',icon:'⚡'},
]},{name:'AI',items:[
{id:'ai_write',name:'Write with AI',icon:'✨'},{id:'ai_summarize',name:'Summarize',icon:'📝'},
{id:'ai_translate',name:'Translate',icon:'🌐'},{id:'ai_continue',name:'Continue writing',icon:'➡️'},
{id:'meeting',name:'AI Meeting Notes',icon:'🎙️'},
]},
{name:'MEDIA',items:[
{id:'image',name:'Image',icon:'🖼️'},{id:'embed',name:'Embed',icon:'🔗'},
{id:'bookmark',name:'Bookmark',icon:'🔖'},{id:'video',name:'Video',icon:'🎬'},
{id:'audio',name:'Audio',icon:'🎵'},
]},
{name:'DATA',items:[{id:'database',name:'Database / Inline database',icon:'📊'}]},
{name:'SYNCED',items:[{id:'synced',name:'Synced block',icon:'🔗'}]},
{name:'ACTIONS',items:[{id:'duplicate',name:'Duplicate',icon:'▣'},{id:'turn_into',name:'Turn into…',icon:'↩'},{id:'copy_link',name:'Copy link to block',icon:'🔗'},{id:'delete',name:'Delete block',icon:'✕'}]}];
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
// v5.13.1: guarantee every block (and nested child) has a stable id.
// Template blocks may be persisted without ids; leaving them undefined
// makes `data-bid="undefined"` collide and corrupts editing.
function ensureBlockIds(list){if(!Array.isArray(list))return list;list.forEach(function(b){if(b&&typeof b==='object'){if(!b.id)b.id=genId();if(Array.isArray(b.children))ensureBlockIds(b.children);}});return list;}
window.ensureBlockIds=ensureBlockIds;
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
function gt(e){return (e&&e.innerText!==undefined)?e.innerText:(e?e.textContent:'');}
// v5.11.0: read a contenteditable block back to plain text, converting
// wiki chip nodes (contenteditable=false spans) back into their raw
// [[fdpage:ID]] / [[fddate:YYYY-MM-DD]] tokens.
function gtTok(e){
if(!e) return '';
if(!e.querySelector||!e.querySelector('.fd-wiki-chip,.fd-wiki-date,.fd-react-chip')) return gt(e); // le chemin rapide (innerText) inclurait les pastilles
let out='';
(function walk(node){
for(const n of node.childNodes){
if(n.nodeType===3){out+=n.nodeValue;continue;}
if(n.nodeType===1){
if(n.classList&&n.classList.contains('fd-wiki-chip')){out+=n.getAttribute('data-token')||'';continue;}
if(n.classList&&n.classList.contains('fd-wiki-date')){out+=n.getAttribute('data-token')||'';continue;}
if(n.classList&&n.classList.contains('fd-react-chip')){continue;} // annotation, pas du contenu
if(n.tagName==='BR'){out+='\n';continue;}
walk(n);
}
}
})(e);
return out;
}
window.gtTok=gtTok;
// v7.62.0 : lecture d'un bloc vers sa source markdown, en passant par le
// DOM formaté (bold/italic/underline/strike/code/mark/liens inline posés
// par le menu de sélection). Sans ça, sync() relisait du texte brut et
// perdait tout le formatage au prochain render(). Sans balise de mise en
// forme, on retombe sur gtTok (comportement identique à l'ancien).
function gtMd(e){
if(!e) return '';
if(!e.querySelector||!e.querySelector('strong,b,em,i,u,s,del,strike,code,mark,a[href]')) return gtTok(e);
function w(node){
var out='';
for(var i=0;i<node.childNodes.length;i++){
var n=node.childNodes[i];
if(n.nodeType===3){out+=n.nodeValue;continue;}
if(n.nodeType!==1)continue;
var cl=n.classList;
if(cl&&cl.contains('fd-react-chip')){continue;} // annotation, pas du contenu
if(cl&&(cl.contains('fd-wiki-chip')||cl.contains('fd-wiki-date'))){out+=n.getAttribute('data-token')||'';continue;}
if(n.tagName==='BR'){out+='\n';continue;}
if(cl&&cl.contains('fd-math-inline')){out+='$$'+(n.getAttribute('data-tex')||'')+'$$';continue;}
var t=n.tagName, inner=w(n);
if((t==='STRONG'||t==='B')&&inner){out+='**'+inner+'**';continue;}
if((t==='EM'||t==='I')&&inner){out+='*'+inner+'*';continue;}
if(t==='U'&&inner){out+='__'+inner+'__';continue;}
if((t==='S'||t==='DEL'||t==='STRIKE')&&inner){out+='~~'+inner+'~~';continue;}
if(t==='CODE'&&inner&&!(n.hasAttribute&&n.hasAttribute('data-bid'))){out+='`'+inner+'`';continue;}
if(t==='MARK'&&inner){out+='=='+inner+'==';continue;}
if(t==='A'){var h=n.getAttribute('href')||'';out+=/^https?:/i.test(h)?'['+(inner||h)+']('+h+')':inner;continue;}
out+=inner;
}
return out;
}
return w(e);
}
window.gtMd=gtMd;
// Enveloppe/désenveloppe une sélection dans une balise inline (code, mark)
function _selWrapHost(r,tag){
var a=r.startContainer.nodeType===1?r.startContainer:r.startContainer.parentElement;
var b=r.endContainer.nodeType===1?r.endContainer:r.endContainer.parentElement;
a=a&&a.closest(tag);b=b&&b.closest(tag);
if(!a||a!==b)return null;
return (a.hasAttribute&&a.hasAttribute('data-bid'))?null:a; // <code> racine d'un bloc code : ne pas dépiler
}
function _selWrapToggle(tag){
var s=window.getSelection();if(!s.rangeCount)return;
var r=s.getRangeAt(0);
var host=_selWrapHost(r,tag);
if(host){var p=host.parentNode;while(host.firstChild)p.insertBefore(host.firstChild,host);p.removeChild(host);p.normalize();return;}
var el=document.createElement(tag);
try{r.surroundContents(el);}catch(e){el.appendChild(r.extractContents());r.insertNode(el);}
var s2=window.getSelection(),nr=document.createRange();
nr.selectNodeContents(el);s2.removeAllRanges();s2.addRange(nr);
}
// v7.64 : marquage d'une plage commentée/réagie (jaune). Ne change pas le
// texte lu par sync() : les spans n'ajoutent que des nœuds d'annotation.
function _wrapAnchored(el,start,end,cls,attrs){
if(!(end>start))return null;
let pos=0,sN=null,sO=0,eN=null,eO=0;
const walker=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);
let n=walker.nextNode();
while(n){
const len=n.nodeValue.length;
if(!sN&&pos+len>start){sN=n;sO=start-pos;}
if(sN&&pos+len>=end){eN=n;eO=end-pos;break;}
pos+=len;n=walker.nextNode();
}
if(!sN||!eN)return null;
const r=document.createRange();
try{r.setStart(sN,sO);r.setEnd(eN,eO);}catch(e){return null;}
const span=document.createElement('span');span.className=cls;
span.setAttribute('contenteditable','false');
if(attrs)for(const k in attrs)span.setAttribute('data-'+k,attrs[k]);
try{r.surroundContents(span);}catch(e){try{span.appendChild(r.extractContents());r.insertNode(span);}catch(e2){return null;}}
return span;
}
// Offsets dans le texte SOURCE d'un bloc : les pastilles .fd-react-chip
// (annotations) ne comptent pas, comme dans gtTok/gtMd.
function _srcNodeLen(node){
if(!node)return 0;
if(node.nodeType===3){
const pe=node.parentElement;
if(pe&&pe.closest&&pe.closest('.fd-react-chip'))return 0;
return node.nodeValue.length;
}
if(node.nodeType!==1)return 0;
if(node.classList&&node.classList.contains('fd-react-chip'))return 0;
let t=0;const kids=node.childNodes;
for(let i=0;i<kids.length;i++)t+=_srcNodeLen(kids[i]);
return t;
}
function _srcOffset(root,container,offset){
if(!container)return 0;
if(container.nodeType===1){
let t=0;const kids=container.childNodes;
for(let i=0;i<offset&&i<kids.length;i++)t+=_srcNodeLen(kids[i]);
return t;
}
if(container.nodeType!==3)return 0;
let t=0;
const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);
let n;
while((n=walker.nextNode())){
if(n===container){
const pe=n.parentElement;
if(pe&&pe.closest&&pe.closest('.fd-react-chip'))return t;
return t+Math.max(0,Math.min(offset,n.nodeValue.length));
}
t+=_srcNodeLen(n);
}
return t;
}
// v7.69 : position de caret à des coordonnées — la sélection native ne
// franchit pas les blocs (chacun a son contenteditable), on la rebâtit.
function _caretAt(x,y){
if(document.caretPositionFromPoint){const q=document.caretPositionFromPoint(x,y);
if(q&&q.offsetNode){const r=document.createRange();r.setStart(q.offsetNode,q.offset||0);r.collapse(true);return r;}}
if(document.caretRangeFromPoint)return document.caretRangeFromPoint(x,y);
return null;
}
function _blkOf(n){let e=n&&(n.nodeType===1?n:n.parentElement);return e&&e.closest?e.closest('[data-bid]'):null;}
function _emojiHtml(v){
const s=String(v||'');
if(/^(https?:|\/)/.test(s))return '<img src="'+esc(s)+'" alt="">';
return esc(s);
}
function splitCaret(e){
const s=window.getSelection(); if(!s.rangeCount||!s.rangeCount)return null; const sel=s.getRangeAt(0);
const MK='\uF000FD'; const mark=document.createElement('span'); mark.textContent=MK;
sel.insertNode(mark);
const full=gtTok(e)||'';
const parts=full.split(MK);
mark.remove(); e.normalize();
return {before:parts[0]||'',after:(parts.length>1?parts[1]:'')||'',content:full.split(MK).join('')};
}
function ce(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(false);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
function cs(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(true);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
// Escape then render inline markdown (bold/italic/code/links/strikethrough)
// so agent-generated content displays without raw `**`/backtick characters.
function mdEsc(s){
if(!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/`([^`]+)`/g,'<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
.replace(/\*([^*]+)\*/g,'<em>$1</em>')
.replace(/~~([^~]+)~~/g,'<s>$1</s>')
.replace(/__([^_]+)__/g,'<u>$1</u>') // souligné (toolbar de sélection)
.replace(/==([^=]+)==/g,'<mark>$1</mark>') // surlignage
.replace(/\$\$([^$]+)\$\$/g,function(m,t){return '<span class="fd-math-inline" data-tex="'+t.replace(/"/g,'&quot;')+'"></span>';}) // équation inline
.replace(/\[\[fdpage:(\d+)\]\]/g,function(m,pid){
return '<a class="fd-wiki-chip" contenteditable="false" href="/pages/'+pid+'" data-token="'+m+'" data-pid="'+pid+'" title="Linked page"><span class="fd-wiki-label">…</span></a>';})
.replace(/\[\[fddate:([0-9]{4}-[0-9]{2}-[0-9]{2})(?:T[0-9:]{5,8})?\]\]/g,function(m,iso){
return '<span class="fd-wiki-date" contenteditable="false" data-token="'+m+'">'+fdDateLabel(iso)+'</span>';})
.replace(/\[([^\]]+)\]\((https?:[^\)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
.replace(/\n/g,'<br>');
}
// v5.11.0: labels of [[fdpage:ID]] chips are resolved from the live DB
// (rename propagation — tokens store only the id).
function fdDateLabel(iso){
try{
const p=iso.split('-'); const d=new Date(+p[0],+p[1]-1,+p[2]);
const DOW=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return DOW[d.getDay()]+' '+(+p[2])+' '+MON[+p[1]-1]+' '+p[0];
}catch(e){return iso;}
}
function _wikiResolve(root){
const ct=root||document.getElementById('_blocksCt'); if(!ct)return;
const pids={};
ct.querySelectorAll('.fd-wiki-chip[data-pid]').forEach(function(a){
const lab=a.querySelector('.fd-wiki-label');
if(!lab||lab.getAttribute('data-done'))return;
lab.setAttribute('data-done','1');
pids[a.dataset.pid]=true;
});
const ids=Object.keys(pids); if(!ids.length)return;
fetch('/board/api/wiki/titles?ids='+encodeURIComponent(ids.join(',')),{credentials:'same-origin'})
.then(function(r){return r.json();})
.then(function(d){
const titles=d.titles||{};
ct.querySelectorAll('.fd-wiki-chip[data-pid]').forEach(function(a){
const t=titles[a.dataset.pid];
const lab=a.querySelector('.fd-wiki-label');
if(lab) lab.textContent = t||'Deleted page';
});
}).catch(function(){});
}
window._wikiResolve=_wikiResolve;
const CALLOUT_ICONS=['💡','💡','📌','⚠️','ℹ️','✅','❌','🚀','🔔','📣','⭐','❤️','🔥','💬','👀','🧠','🎯','🔑','📅','💰','💡','📚','🧪','🌍'];
/* ═══════════ Simple Table block (v4.8.0) ═══════════ */
function _tblSplitLine(s){s=(s||'').trim();if(s.charAt(0)==='|')s=s.slice(1);if(s.charAt(s.length-1)==='|')s=s.slice(0,-1);return s.split('|').map(function(x){return x.trim();});}
// v7.68 : le pipe de tête est OPTIONNEL en GFM — l'IA rend souvent
// « A | B » / « --- | --- ». Une ligne de séparation doit garder ≥2 cellules,
// sinon « texte » suivi d'un « --- » deviendrait un tableau.
function _tblIsSep(s){s=(s||'').trim();if(!s)return false;var cells=_tblSplitLine(s);if(cells.length<2)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});}
function _tblIsData(s){s=(s||'').trim();return s.indexOf('|')>=0&&_tblSplitLine(s).length>=2;}
function _tblNew(rows,cols){var out=[];for(var i=0;i<rows;i++){var r=new Array(cols);for(var j=0;j<cols;j++)r[j]='';out.push(r);}return out;}
function _tblClone(b){b.rows=(b.rows||[]).map(function(r){return (r||[]).slice();});return b;}
function _tblAddCol(b,index){_tblClone(b);var w=b.rows.length?b.rows[0].length:1;if(index<0)index=0;if(index>w)index=w;b.rows.forEach(function(r){r.splice(index,0,'');});}
function _tblAddRow(b,index){_tblClone(b);var w=b.rows.length&&b.rows[0]?b.rows[0].length:1;if(index<0)index=0;if(index>b.rows.length)index=b.rows.length;b.rows.splice(index,0,_tblNew(1,w)[0]);}
function _tblDupCol(b,index){_tblClone(b);b.rows.forEach(function(r){var v=(index<r.length)?r[index]:'';var at=index+1>r.length?r.length:index+1;r.splice(at,0,v);});}
function _tblDupRow(b,index){_tblClone(b);if(index<0||index>b.rows.length)return;b.rows.splice(index+1,0,(b.rows[index]||[]).slice());}
function _tblClearCol(b,index){_tblClone(b);b.rows.forEach(function(r){if(r&&index<r.length)r[index]='';});}
function _tblClearRow(b,index){_tblClone(b);var w=b.rows[0]?b.rows[0].length:1;if(index>=0&&index<b.rows.length)b.rows[index]=_tblNew(1,w)[0];}
function _tblDelCol(b,index){_tblClone(b);if(!b.rows.length)return;if(b.rows[0].length<=1)return;b.rows.forEach(function(r){if(index>=0&&index<r.length)r.splice(index,1);});}
function _tblDelRow(b,index){_tblClone(b);if(b.rows.length<=1)return;if(index>=0&&index<b.rows.length)b.rows.splice(index,1);}
function _tblGrip(){return '<svg viewBox="0 0 16 16" width="12" height="12" style="display:block"><circle cx="4" cy="3" r="1.4"/><circle cx="11" cy="3" r="1.4"/><circle cx="4" cy="8" r="1.4"/><circle cx="11" cy="8" r="1.4"/><circle cx="4" cy="13" r="1.4"/><circle cx="11" cy="13" r="1.4"/></svg>';}
function _tblDefault(){return {rows:_tblNew(3,3)};}
function _tblFocus(bid,r,c){var sel=document.querySelector('.ftable-editor[data-bid="'+bid+'"]');if(!sel)return;var q='[data-c="'+c+'"]';var cell=sel.querySelector(q);if(!cell){var first=sel.querySelector('.ftable-cell[data-r]');if(first){first.focus();ce(first);}return;}cell.focus();if(r===undefined)cs(cell);else ce(cell);}
function renderTableBlock(b,idx){
var rows=(b&&Array.isArray(b.rows)&&b.rows.length)?b.rows:_tblNew(2,2);
var w=0;rows.forEach(function(r){if(r&&r.length>w)w=r.length;});if(w<1)w=2;
var widths=(b&&Array.isArray(b.colsW))?b.colsW:[];
var bid=b.id;var h='';
var hasHeader=(b.has_header!==false);var firstCol=(!!b.first_col_header);
h+='<div class="block-wrapper" data-id="'+bid+'"><div class="block-content ftable-editor" data-bid="'+bid+'">';
h+='<div class="ftable-colbar">';
for(var c=0;c<w;c++){
h+='<div class="ftable-colcell"><button type="button" class="ftable-colgrip" data-tbl="colmenu" data-bid="'+bid+'" data-col="'+c+'" title="Column options">'+_tblGrip()+'</button></div>';
}
h+='</div>';
h+='<table class="ftable ftable-edit"><tbody>';
for(var ri=0;ri<rows.length;ri++){
var isHead=(ri===0&&hasHeader)||(ri>0&&firstCol);var tag='td';
if(isHead){tag='th';}
h+='<tr class="ftable-row">';
for(var ci=0;ci<w;ci++){
var rcH=((ri===0&&hasHeader)||(ci===0&&firstCol));
var v=rows[ri]&&ci<rows[ri].length?rows[ri][ci]:'';
var wpx=(widths[ci]&&widths[ci]>0)?(' style="width:'+widths[ci]+'px"'):'';
h+='<'+tag+' class="ftable-cell'+(rcH?' head':'')+'" contenteditable="true" data-r="'+ri+'" data-c="'+ci+'"'+wpx+' spellcheck="false">'+esc(v);
if(ri===0&&hasHeader)h+='<span class="ftable-resize" data-col="'+ci+'" title="Drag to resize column"></span>';
h+='</'+tag+'>';
}
h+='</tr>';
}
h+='</tbody></table>';
h+='<div class="ftable-rowbar"><button type="button" class="ftable-addrow" data-tbl="rowadd" data-bid="'+bid+'" title="Click to add a row">+ Row</button><button type="button" class="ftable-addrow" data-tbl="coladdright" data-bid="'+bid+'" title="Add column on the right">+ Col</button><span class="ftable-hdr-toggle"><label><input type="checkbox" data-tbl="header" data-bid="'+bid+'" '+(hasHeader?'checked':'')+'> Header row</label></span><span class="ftable-hdr-toggle"><label><input type="checkbox" data-tbl="firstcol" data-bid="'+bid+'" '+(firstCol?'checked':'')+'> First col</label></span></div>';
h+='</div></div>';
return h;
}
function _tblBlockById(bid){var e=window.E;if(!e||!e.blocks)return null;for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return e.blocks[i];}return null;}
function _tblIdxByBid(bid){var e=window.E;if(!e||!e.blocks)return -1;for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return i;}return -1;}
function _tblMenuClose(){var m=document.getElementById('_tblMenu');if(m){m.parentNode&&m.parentNode.removeChild(m);}if(window._tblCur)window._tblCur=null;}
function _tblMenuOpen(x,y,bid,kind,index){
_tblMenuClose();
var items=[];
if(kind==='col'){
items=[['col-insert-left','Insert left'],['col-insert-right','Insert right'],['col-dup','Duplicate'],null,['col-clear','Clear contents'],['col-del','Delete column',1],null,['tbl-del','Delete table',1]];
}else{
items=[['row-insert-above','Insert above'],['row-insert-below','Insert below'],['row-dup','Duplicate'],null,['row-clear','Clear row'],['row-del','Delete row',1],null,['tbl-del','Delete table',1]];
}
var d=document.createElement('div');d.className='ftable-menu';d.id='_tblMenu';d.style.cssText='position:fixed;z-index:3000;';
var html='';
items.forEach(function(it){
if(!it){html+='<div class="ftable-menu-sep"></div>';return;}
html+='<div class="ftable-menu-item'+(it[2]?' danger':'')+'" data-tbl-act="'+it[0]+'">'+it[1]+'</div>';
});
d.innerHTML=html;
document.body.appendChild(d);
var r=d.getBoundingClientRect();
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
d.style.left=x+'px';d.style.top=y+'px';
window._tblCur={bid:bid,kind:kind,index:index};
}
function _tblDo(action,cur){
if(!cur)return;
var bid=cur.bid,index=cur.index,b=_tblBlockById(bid),e=window.E;
if(!b||!e)return;
e.sync();
if(action!=='tbl-del')e.pushHistory();
if(!(b.rows&&b.rows.length))b.rows=_tblNew(2,2);
var focusR=1,focusC=0;
if(action==='col-insert-left'){_tblAddCol(b,index);focusC=index;}
else if(action==='col-insert-right'){_tblAddCol(b,index+1);focusC=index+1;}
else if(action==='col-dup'){_tblDupCol(b,index);focusC=index+1;}
else if(action==='col-clear'){_tblClearCol(b,index);focusC=index;}
else if(action==='col-del'){_tblDelCol(b,index);focusC=Math.min(index,b.rows[0].length-1);}
else if(action==='row-insert-above'){_tblAddRow(b,index);focusR=index;}
else if(action==='row-insert-below'){_tblAddRow(b,index+1);focusR=index+1;}
else if(action==='row-dup'){_tblDupRow(b,index);focusR=index+1;}
else if(action==='row-clear'){_tblClearRow(b,index);focusR=index;}
else if(action==='row-del'){_tblDelRow(b,index);focusR=Math.min(index,b.rows.length-1);if(focusR<1)focusR=1;}
else if(action==='tbl-del'){_tblMenuClose();var bi=_tblIdxByBid(bid);if(bi>=0)e.removeBlock(bi);return;}
e.render();e.dirty=true;e.autoSave();
_tblFocus(bid,focusR,focusC);
}
function _tblResizeStart(ev){
var sp=ev.target&&ev.target.closest?ev.target.closest('.ftable-resize'):null;if(!sp)return;
var th=sp.parentElement;if(!th)return;
var wrap=th.closest('.ftable-editor');if(!wrap)return;
var bid=wrap.getAttribute('data-bid');var col=parseInt(sp.getAttribute('data-col')||'0',10);
var b=_tblBlockById(bid);if(!b)return;
ev.preventDefault();ev.stopPropagation();
var E=window.E;if(E&&E.sync)E.sync();
var widths=(b.colsW&&Array.isArray(b.colsW))?b.colsW.slice():[];
while(widths.length<=col)widths.push(null);
var startX=ev.clientX;var base=widths[col]||th.getBoundingClientRect().width;
document.body.style.cursor='col-resize';document.body.style.userSelect='none';
function move(me){var dx=me.clientX-startX;var nw=Math.max(48,Math.min(760,base+dx));th.style.width=Math.round(nw)+'px';widths[col]=Math.round(nw);}
function up(me){
document.removeEventListener('mousemove',move);document.removeEventListener('mouseup',up);
document.body.style.cursor='';document.body.style.userSelect='';
move(me);
b.colsW=widths;
if(E){E.dirty=true;E.autoSave();}
}
document.addEventListener('mousemove',move);
document.addEventListener('mouseup',up);
}
function _tblInitDelegate(){
if(window._tblDelegate)return;window._tblDelegate=true;
document.addEventListener('click',function(ev){
if(ev.target.closest&&ev.target.closest('#_tblMenu')){var it=ev.target.closest('.ftable-menu-item');if(it){var act=it.getAttribute('data-tbl-act');_tblDo(act,window._tblCur);}return;}
var g=ev.target.closest?ev.target.closest('[data-tbl]'):null;
if(g){var act=g.getAttribute('data-tbl'),bid=g.getAttribute('data-bid'),col=parseInt(g.getAttribute('data-col')||'0',10);
var gr=g.getBoundingClientRect?g.getBoundingClientRect():null;
if(act==='colmenu'){_tblMenuOpen(gr?gr.left:ev.clientX,(gr?gr.bottom:ev.clientY)+4,bid,'col',col);ev.preventDefault();}
else if(act==='colplus'){var b=_tblBlockById(bid);if(b){window.E.sync();_tblAddCol(b,col+1);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,col+1);}}
else if(act==='rowadd'){var b2=_tblBlockById(bid);if(b2){window.E.sync();var n=b2.rows.length;_tblAddRow(b2,n);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,n,0);}}
else if(act==='coladdright'){var b4=_tblBlockById(bid);if(b4){window.E.sync();var cw=(b4.rows&&b4.rows[0])?b4.rows[0].length:1;_tblAddCol(b4,cw);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,cw);}}
else if(act==='header'||act==='firstcol'){var b5=_tblBlockById(bid);if(b5){window.E.sync();window.E.pushHistory();if(act==='header')b5.has_header=g.checked?true:false;else b5.first_col_header=g.checked?true:false;window.E.render();window.E.dirty=true;window.E.autoSave();_rtSync();}}
return;
}
if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('[data-tbl]')))_tblMenuClose();
});
document.addEventListener('contextmenu',function(ev){
var cell=ev.target.closest?ev.target.closest('.ftable-cell'):null;
if(!cell)return;
var w=cell.closest('.ftable-editor');if(!w)return;
var bid=w.getAttribute('data-bid'),r=parseInt(cell.getAttribute('data-r')||'0',10),c=parseInt(cell.getAttribute('data-c')||'0',10);
ev.preventDefault();
if(r===0)_tblMenuOpen(ev.clientX,ev.clientY,bid,'col',c);
else _tblMenuOpen(ev.clientX,ev.clientY,bid,'row',r);
});
document.addEventListener('mousedown',function(ev){
if(ev.target&&ev.target.closest&&ev.target.closest('.ftable-resize')){_tblResizeStart(ev);return;}
if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('#_tblMenu')))_tblMenuClose();
});
}
// ── Block type shortcuts (shown on the right of each slash item) ──
// 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',meeting:'/meet',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>';};
switch(blockId){
case 'heading_1': return p('<div style="font-size:24px;font-weight:700">Big section heading</div>');
case 'heading_2': return p('<div style="font-size:19px;font-weight:700;margin-bottom:8px">Our Values</div><div style="font-size:13px;line-height:1.6">• Ownership<br>• Altruism<br>• Integrity</div><div style="font-size:11px;color:var(--text-dim);margin-top:8px">Medium section heading</div>');
case 'heading_3': return p('<div style="font-size:16px;font-weight:700;margin-bottom:6px">Small section heading</div><div style="font-size:13px;color:var(--text-dim)">Body text goes here…</div >');
case 'heading_4': return p('<div style="font-size:14px;font-weight:700">Tiny section heading</div>');
case 'bulleted_list': return p('<div style="font-size:13px;line-height:1.9">• List item<br>• Another item<br>• Third item</div>');
case 'numbered_list': return p('<div style="font-size:13px;line-height:1.9">1. First<br>2. Second<br>3. Third</div>');
case 'to_do': return p('<div style="font-size:13px;line-height:1.9">☐ Todo item<br>☑ Checked todo<br>☐ Another</div>');
case 'toggle': return p('<div style="font-size:13px">▸ Toggle content</div>');
case 'quote': return p('<div style="border-left:3px solid var(--accent);padding-left:10px;font-size:13px;color:var(--text-dim)">A blockquote to highlight a quotation.</div>');
case 'callout': return p('<div style="background:var(--blue-bg);padding:12px;border-radius:6px;font-size:13px">💡 A callout draws attention.</div>');
case 'table_of_contents': return p('<div style="font-size:13px;color:var(--text-dim)">On this page</div><div style="font-size:12px;line-height:1.8">· Heading 1<br>· Heading 2<br>· Heading 3</div>');
case 'math': return p('<div style="font-size:15px;text-align:center">E = mc²</div>');
case 'columns': return p('<div style="display:flex;gap:8px"><div style="flex:1;border:1px solid var(--border);border-radius:4px;padding:8px;font-size:12px">col 1</div><div style="flex:1;border:1px solid var(--border);border-radius:4px;padding:8px;font-size:12px">col 2</div></div>');
case 'divider': return p('<hr style="border-top:1px solid var(--border);margin:6px 0">');
case 'code': return p('<pre style="background:#111;padding:10px;border-radius:6px;font-size:12px"><code>const x = 1;</code></pre>');
case 'table': return p('<div style="font-size:12px"><table style="width:100%;border-collapse:collapse"><tr><td style="border:1px solid var(--border);padding:4px">A</td><td style="border:1px solid var(--border);padding:4px">B</td></tr><tr><td style="border:1px solid var(--border);padding:4px">1</td><td style="border:1px solid var(--border);padding:4px">2</td></tr></table></div>');
case 'image': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">[Image]</div>');
case 'embed': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🔗 Embed (YouTube, Figma, Maps…)</div>');
case 'bookmark': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🔖 Bookmark card with preview</div>');
case 'video': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🎬 Inline video player</div>');
case 'audio': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🎵 Inline audio player</div>');
case 'database': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">📊 Inline database table</div>');
case 'meeting': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">🎙️ <b>AI Meeting Notes</b><br><span style="color:var(--text-dim)">Record, transcribe and summarize this meeting.</span></div>');
default: return p('<div style="font-size:13px;color:var(--text-dim)">Text block</div>');
}
}
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],_pv:null,
init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML='';
const list=document.createElement('div');list.className='sm-list';list.style.cssText='overflow-y:auto;flex:1;min-height:120px;max-height:360px;padding:6px';
list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}});
list.addEventListener('mouseover',ev=>{const it=ev.target.closest('.slash-item');if(it)SM._hover(it);});
const footer=document.createElement('div');footer.className='sm-footer';footer.style.cssText='display:flex;align-items:center;justify-content:space-between;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--text-dim);cursor:pointer';
footer.innerHTML='<span style="display:flex;align-items:center;gap:8px"><span style="opacity:.6">⌘</span> Close menu</span><span style="color:var(--text-dim);opacity:.7">esc</span>';
footer.onclick=()=>SM.close();
const inp=document.createElement('input');inp.type='text';inp.placeholder='Type to search';
inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
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)&&!MTB.contains(ev.target))SM.close();});
if(!SM._doc){
SM._doc=true;
document.addEventListener('keydown',function(ev){
if(!SM._o)return;
if(ev.metaKey||ev.ctrlKey||ev.altKey)return;
if(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'){
ev.preventDefault();ev.stopPropagation();SM._docKd(ev);
}
});
}
SM._docKd=function(ev){
if(SM._o&&(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'))SM._kd(ev);
};
},
_hover(it){ if(!it) return; const id=it.dataset.sid; if(!id) return;
if(!SM._pv){ const d=document.createElement('div'); d.className='sm-preview'; d.style.cssText='position:fixed;z-index:1000;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);padding:12px;width:220px;pointer-events:none;'; document.body.appendChild(d); SM._pv=d; }
SM._pv.innerHTML=_previewHtml(id);
const r=SM._e.getBoundingClientRect(); const ir=it.getBoundingClientRect();
let left = r.right + 8; let top = ir.top - 12;
if(left+220 > window.innerWidth-8) left = r.left - 228;
top = Math.max(8, Math.min(top, window.innerHeight - SM._pv.offsetHeight - 8));
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';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>
<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+=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();}
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.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;}};
// ═══════════ 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;
const d=document.createElement('div'); d.id='_wikiMenu';
d.style.cssText='position:fixed;z-index:1250;display:none;width:340px;max-height:320px;flex-direction:column;background:var(--bg-modal,#1f1f1f);border:1px solid var(--border,#333);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden';
const list=document.createElement('div'); list.style.cssText='overflow-y:auto;flex:1;min-height:60px;max-height:250px;padding:6px';
const inp=document.createElement('input'); inp.type='text'; inp.placeholder='Search pages…';
inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
d.appendChild(list); d.appendChild(inp); document.body.appendChild(d);
this._root=d; this._li=list; this._in=inp;
inp.oninput=()=>{clearTimeout(this._t);this._t=setTimeout(()=>this.load(),180);};
list.addEventListener('click',ev=>{const it=ev.target.closest('.wm-item');if(it){ev.preventDefault();WM.sel(parseInt(it.dataset.wpid,10));}});
document.addEventListener('mousedown',ev=>{if(!d.contains(ev.target))WM.close();});
document.addEventListener('keydown',function(ev){
if(!WM._o)return;
if(ev.metaKey||ev.ctrlKey||ev.altKey)return;
if(ev.key==='Escape'){ev.preventDefault();ev.stopPropagation();WM.close();}
else if(ev.key==='ArrowDown'){ev.preventDefault();ev.stopPropagation();WM._mv(1);}
else if(ev.key==='ArrowUp'){ev.preventDefault();ev.stopPropagation();WM._mv(-1);}
else if(ev.key==='Enter'){ev.preventDefault();ev.stopPropagation();const c=WM._it[WM._s];if(c)WM.sel(c.id);}
},true);
return d; },
open(i,el){ this.mk(); const r=el.getBoundingClientRect();
this._root.style.display='flex';
this._root.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
this._root.style.left=Math.min(r.left,window.innerWidth-350)+'px';
this._idx=i; this._s=0; this._in.value=''; this._o=true;
this.load(); setTimeout(()=>this._in.focus(),10); },
close(){ if(this._root)this._root.style.display='none'; this._o=false; },
load(){ const self=this; const q=(this._in.value||'').toLowerCase();
fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'})
.then(r=>r.json()).then(d=>{
if(!self._o)return;
self._it=d.pages||[]; self._s=0;
let h='<div style="padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase">Pages</div>';
self._it.forEach(function(p,idx){
h+='<div class="wm-item" data-wpid="'+p.id+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;'+(idx===0?'background:rgba(35,131,226,.15)':'')+'">'
+'<span style="width:18px;text-align:center;flex-shrink:0">'+esc(p.icon||'📄')+'</span>'
+'<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(p.title)+'</span>'
+'<span style="font-size:11px;color:var(--text-dim)">'+esc(p.workspace||'')+'</span></div>';
});
if(!self._it.length)h+='<div style="padding:12px;color:var(--text-dim);font-size:13px">No page found</div>';
self._li.innerHTML=h;
}).catch(()=>{}); },
_mv(dir){ if(!this._it.length)return;
this._s=(this._s+dir+this._it.length)%this._it.length;
this._li.querySelectorAll('.wm-item').forEach((el,i)=>{el.style.background=i===this._s?'rgba(35,131,226,.15)':'';});
const cur=this._li.querySelectorAll('.wm-item')[this._s]; if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'}); },
sel(pid){ const E=window.E; if(!E||this._idx<0)return;
const b=E.blocks[this._idx]; if(!b)return;
// Drop the typed "[[" from the live DOM text, append the token chip.
const el=E.getEl(b.id); let text=(el?gtTok(el):b.content)||'';
if(text.endsWith('[[')) text=text.slice(0,-2);
else text=text.replace(/\[\[[^\]]*$/,'');
E._commitBlockText(b, text+'[[fdpage:'+pid+']] ');
this.close(); },
get isOpen(){return this._o;}};
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
// → framed result → "Insert below" → applyAIBlocks.
const AIC={_open:false,_idx:-1,_stage:'',_html:'',_md:'',
ROOT:null,
mk(){ if(this.ROOT) return this.ROOT;
const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none';
document.body.appendChild(d); this.ROOT=d; return d; },
open(idx){ const self=this; this._idx=idx; this._stage='input'; this.mk();
const r=this._anchorRect(idx);
const w=Math.max(r.width||0,280); this.ROOT.style.width=Math.min(w,1200)+'px';
this.ROOT.style.left=Math.max(8,Math.min(r.left,window.innerWidth-r.width-16))+ 'px';
this.ROOT.style.top=Math.max(8,(r.bottom+6>window.innerHeight-260)?r.top-180:r.bottom+6)+'px';
this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest();
document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } });
},
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; this._md=''; if(window.E)window.E.dirty=true; this._stage=''; },
_anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420};
const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180,width:420}; },
_focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} },
_escTag(tag,s){return '<'+tag+'>'+s+'</'+tag+'>';},
_mdToHtml(md){ // compact block render (headings, lists, bold, paragraphs)
const lines=String(md||'').split(/\r?\n/); let out=[]; let i=0;
const inline=function(t){return esc(t).replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank">$1</a>');};
while(i<lines.length){ const t=lines[i].replace(/\r$/,''); const s=t.trim();
if(!s){i++;continue;}
if(/^#{1,4}\s/.test(s)){ const lvl=s.match(/^(#+)\s/)[1].length; out.push('<h'+lvl+'>'+inline(s.replace(/^#+\s+/,''))+'</h'+lvl+'>'); i++; continue; }
if(/^[-*+] /.test(s)){ const items=[]; while(i<lines.length&&/^[-*+] /.test(lines[i].trim())){items.push('<li>'+inline(lines[i].trim().replace(/^[-*+] /,''))+'</li>'); i++;} out.push('<ul>'+items.join('')+'</ul>'); continue; }
if(/^\d+\. /.test(s)){ const items=[]; while(i<lines.length&&/^\d+\. /.test(lines[i].trim())){items.push('<li>'+inline(lines[i].trim().replace(/^\d+\. /,''))+'</li>'); i++;} out.push('<ol>'+items.join('')+'</ol>'); continue; }
if(s.startsWith('> ')){ out.push('<blockquote>'+inline(s.replace(/^> /,''))+'</blockquote>'); i++; continue; }
out.push('<p>'+inline(s)+'</p>'); i++;
}
return out.join(''); },
paint(){ const self=this; const h=this.ROOT;
if(this._stage==='input'){
h.innerHTML='<div style="display:flex;align-items:center;gap:10px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:999px;padding:8px 14px;box-shadow:0 8px 30px rgba(0,0,0,.45)">'
+'<span style="font-size:18px;color:var(--text-primary)">✨</span>'
+'<input class="aici-input" type="text" placeholder="Edit with AI" autocomplete="off" spellcheck="false" style="flex:1;background:none;border:none;outline:none;color:var(--text);font-size:14px">'
+'<button class="aici-send" title="Generate" style="background:var(--bg-secondary);border:none;border-radius:50%;width:26px;height:26px;color:var(--text);cursor:pointer;font-size:14px">↑</button>'
+'</div>';
const inp=h.querySelector('.aici-input'); if(inp){inp.focus(); const go=function(){const v=inp.value.trim(); if(v){self._idx=self._idx>=0?self._idx:0; self._run(v);} }; inp.onkeydown=function(ev){ if(ev.key==='Enter'){ev.preventDefault();go();} else if(ev.key==='Escape'){ev.preventDefault();self.close();} }; h.querySelector('.aici-send').onclick=go; }
} else if(this._stage==='brewing'){
h.innerHTML='<div style="display:flex;align-items:center;gap:12px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:14px;padding:12px 16px;box-shadow:0 8px 30px rgba(0,0,0,.45)">'
+'<span style="font-size:20px">✨</span><span style="color:var(--text-dim);font-size:14px;animation:ai-pulse 1.2s ease-in-out infinite">Brewing…</span>'
+'<button class="aici-stop" title="Stop" style="margin-left:auto;background:var(--bg-secondary);border:none;border-radius:6px;width:26px;height:26px;color:var(--text);cursor:pointer">■</button></div>';
h.querySelector('.aici-stop').onclick=function(){ self.close(); if(window.fdAgent&&window.fdAgent.cancel)window.fdAgent.cancel(); };
} else if(this._stage==='result'){
h.innerHTML='<div style="background:var(--bg-modal);border:1px solid var(--border-light);border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.5);overflow:hidden">'
+'<div class="aici-result" style="padding:14px 16px;font-size:14px;line-height:1.6;color:var(--text);max-height:300px;overflow-y:auto">'+this._html+'</div>'
+'<div style="display:flex;align-items:center;gap:6px;padding:8px 12px;border-top:1px solid var(--border)">'
+'<button class="aici-fb" data-fb="up" title="Good" style="background:none;border:none;color:var(--text-dim);cursor:pointer">👍</button>'
+'<button class="aici-fb" data-fb="down" title="Bad" style="background:none;border:none;color:var(--text-dim);cursor:pointer">👎</button>'
+'<span style="flex:1"></span>'
+'<button class="aici-insert" style="background:var(--accent,#2383E2);color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:13px;cursor:pointer">Insert below</button>'
+'</div></div>';
const self2=this; h.querySelector('.aici-insert').onclick=function(){ self2.insert(); }; h.querySelectorAll('.aici-fb').forEach(function(b){ b.onclick=function(){ self2.toast(b.dataset.fb==='up'?'👍 Merci':'👎 Enregistré'); }; });
}
},
_run(prompt){ const self=this; this._stage='brewing'; this.paint();
const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; }
window.fdAgent.generate(prompt, ctx).then(function(text){
// On conserve le MARKDOWN source : c'est lui qui est inséré (md2b),
// pas le texte extrait du HTML rendu qui perd titres/listes/gras.
self._md=String(text||'');
self._html=self._mdToHtml(self._md); self._stage='result'; self.paint();
}).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); });
},
_err(msg){ this._md=''; this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); },
insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync();
// Markdown source d'abord ; repli sur le texte visible (erreurs).
const result=String(this._md||'').trim()||this._resultText(); if(!result)return;
E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); },
_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); },
toast(m){ if(window.showToast)window.showToast(m,'info'); }
};
window.AIC=AIC;
// ═══════════ AI Autocomplete (v5.9.0) ═══════════
// After a short idle while typing in a text block, ask the AI for a short
// continuation and surface it as a "Tab to accept" pill anchored to the
// block. Tab inserts the suggestion; Escape dismisses it.
const AIAC={_timer:null,_sug:'',_bid:null,_busy:false,_enabled:true,_req:0,
ROOT:null,
_allowed:function(b){ if(!b)return false; return ['paragraph','heading_1','heading_2','heading_3','heading_4','bulleted_list','numbered_list','quote','callout','to_do'].indexOf(b.type)>=0; },
mk(){ if(this.ROOT)return this.ROOT; const d=document.createElement('div'); d.id='_aiAC'; d.style.cssText='position:fixed;z-index:1150;display:none;max-width:min(520px,90vw)'; document.body.appendChild(d); this.ROOT=d;
document.addEventListener('mousedown',function(ev){ if(AIAC.ROOT&&!AIAC.ROOT.contains(ev.target))AIAC.clear(); });
return d; },
schedule(bid){ if(!this._enabled)return; const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear();
clearTimeout(this._timer); const self=this; this._timer=setTimeout(function(){ self.fetch(bid); },900); },
fetch(bid){ const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear();
const el=E.getEl(bid); const prefix=((el&&el.textContent)||b.content||'').trim(); if(prefix.length<8)return this.clear();
const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
const req=++this._req; this._busy=true; const self=this; const csrf=(E.csrfTok?E.csrfTok():'');
fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({action:'autocomplete',prefix:prefix,context:ctx})})
.then(function(r){return r.json();})
.then(function(d){ self._busy=false; if(req!==self._req)return; if(d&&d.ok&&d.text){ self.show(bid,d.text); } else { self.clear(); } })
.catch(function(){ self._busy=false; if(req===self._req)self.clear(); });
},
show(bid,text){ text=(text||'').trim(); if(!text){return this.clear();} const E=window.E; const el=E&&E.getEl(bid); if(!el)return;
this._sug=text; this._bid=bid; this.mk(); const r=el.getBoundingClientRect();
this.ROOT.innerHTML='<div style="display:flex;align-items:center;gap:8px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;padding:6px 10px;box-shadow:0 6px 24px rgba(0,0,0,.4);font-size:13px;color:var(--text-dim)">'
+'<span style="opacity:.7">✨</span><span style="color:var(--text);font-style:italic">'+esc(text)+'</span>'
+'<span style="margin-left:6px;font-family:var(--font-mono);font-size:11px;background:var(--bg-secondary);border-radius:4px;padding:1px 6px">Tab</span>'
+'<button class="aiac-x" title="Dismiss" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:14px">✕</button></div>';
const btn=this.ROOT.querySelector('.aiac-x'); if(btn)btn.onclick=function(){ AIAC.clear(); };
let left=Math.min(Math.max(8,r.left+24),window.innerWidth-360); let top=r.bottom+4;
if(top+40>window.innerHeight)top=r.top-36;
this.ROOT.style.left=left+'px'; this.ROOT.style.top=Math.max(8,top)+'px'; this.ROOT.style.display='block';
},
accept(){ if(!this._sug)return false; const E=window.E; const idx=E&&E.getIdx(this._bid); if(idx<0){ this.clear(); return false; }
const b=E.blocks[idx]; const el=E.getEl(this._bid); E.sync(); E.pushHistory();
const cur=((el&&el.textContent)||b.content||''); const next=(cur+this._sug).replace(/\s+$/,' ');
b.content=next; if(el){ el.textContent=next; } this.clear(); E.dirty=true; E.autoSave(); if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow();
if(el){ el.focus(); ce(el); } return true;
},
clear(){ this._sug=''; this._bid=null; this._req++; if(this.ROOT){ this.ROOT.style.display='none'; this.ROOT.innerHTML=''; } },
get active(){ return !!this._sug; }
};
window.AIAC=AIAC;
// ── Code block renderer (hljs: highlight.js syntax highlighting) — literal
// text (esc, real newlines — NOT mdEsc, whose <br> would break
// highlight.js textContent reads) plus a language picker driving
// FDHighlight (~230 file types). Highlight spans are transparent to
// sync(): gt()/gtTok() re-read text only.
function renderCodeBlock(b){
var rawLang=b.language||'Plain Text';
var norm=(window.FDHighlight?FDHighlight.normalizeLang(rawLang):String(rawLang||'plaintext').toLowerCase().replace(/[^a-z0-9+#-]+/g,''))||'plaintext';
var ro=!!(window.E&&E.canEdit===false);
var head;
if(!ro&&window.FDHighlight){
var opts=FDHighlight.PICKER_LANGS.map(function(id){
return '<option value="'+id+'"'+(id===norm?' selected':'')+'>'+esc(FDHighlight.labelFor(id))+'</option>';
}).join('');
head='<div class="code-lang-label code-lang-bar"><select class="code-lang-select" onchange="E.setCodeLang(\''+b.id+'\',this.value)" title="Code language — syntax highlighting">'+opts+'</select></div>';
}else{
var label=(window.FDHighlight?FDHighlight.labelFor(norm):rawLang);
head='<div class="code-lang-label">'+esc(label)+'</div>';
}
return head+'<pre><code data-bid="'+b.id+'" data-hljs data-hljs-lang="'+norm+'" class="language-'+norm+'" contenteditable="'+(ro?'false':'true')+'" spellcheck="false">'+esc(b.content||'')+'</code></pre>';
}
function renderBlock(b,idx){
if(b.type==='meeting')return renderMeetingBlock(b,idx);
if(b.type==='table')return renderTableBlock(b,idx);
if(b.type==='divider')return `<div class="block-wrapper" data-id="${b.id}"><hr class="block-divider"></div>`;
if(b.type==='button')return renderAutomationButton(b,idx);
// v7.68 : ces trois renderers existaient mais n'étaient JAMAIS appelés —
// un bloc `columns`/`math`/`table_of_contents` tombait dans le rendu générique
// (un paragraphe vide). Colonnes (slash + Turn into) = bug rapporté.
if(b.type==='columns')return renderColumnsBlock(b);
if(b.type==='math')return renderMathBlock(b);
if(b.type==='table_of_contents')return renderTOC(b);
if(b.type==='image')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" class="fd-img" data-full="${b.src||''}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;cursor:zoom-in;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'" onclick="E._openLightbox(this)"></div></div>`;
if(b.type==='video'){
const src=b.src||'';return src?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-video"><video controls preload="metadata" style="max-width:100%;border-radius:8px;display:block;margin:0 auto;"><source src="${src}"></video></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-video" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setVideoSrc('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🎬</div><div style="font-size:14px;color:var(--text-dim);">Click to add video URL</div></div></div>`;
}
if(b.type==='audio'){
const src=b.src||'';return src?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-audio"><audio controls preload="metadata" style="width:100%;"><source src="${src}"></audio></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-audio" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setAudioSrc('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🎵</div><div style="font-size:14px;color:var(--text-dim);">Click to add audio URL</div></div></div>`;
}
if(b.type==='bookmark'){
var url=b.url||b.src||'';var title=esc(b.title||url);var desc=esc(b.description||'');var img=b.image||'';var site=esc(b.site_name||'');
var imgHtml=img?'<img src="'+img.replace(/"/g,'&quot;')+'" alt="" style="width:120px;height:90px;object-fit:cover;border-radius:8px;flex-shrink:0;">':'';
var descHtml=desc?'<div style="font-size:13px;color:var(--text-dim);margin-top:4px;">'+desc+'</div>':'';
var siteHtml=site?'<div style="font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.5px;margin-top:6px;">'+site+'</div>':'';
var safeUrl=url.replace(/"/g,'&quot;');
return url?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark"><a href="${safeUrl}" target="_blank" rel="noopener noreferrer" style="text-decoration:none;color:inherit;"><div style="display:flex;gap:14px;align-items:center;border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin:0;background:var(--bg-secondary);"><div style="flex:1;min-width:0;"><div style="font-weight:600;font-size:15px;">${title}</div>${descHtml}${siteHtml}</div>${imgHtml}</div></a></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setBookmarkUrl('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🔖</div><div style="font-size:14px;color:var(--text-dim);">Click to add bookmark URL</div></div></div>`;
}
if(b.type==='embed'){
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
if(b.embed_type==='collection')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed-collection" id="inline-db-${b.collection_id}" style="padding:0;min-height:200px"><div style="padding:40px;text-align:center;color:var(--text-dim)">Loading database...</div></div><`+`script>setTimeout(function(){var el=document.getElementById('inline-db-${b.collection_id}');if(el&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,${b.collection_id},null);},50);<`+`/script></div>`;
if(b.embed_type==='pdf')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="padding:0;"><iframe src="${b.src||''}" style="width:100%;height:80vh;border:none;border-radius:8px;" onerror="this.style.display='none';this.nextElementSibling.style.display='block';"></iframe><div style="display:none;text-align:center;padding:32px;color:var(--text-secondary);"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p style="margin-top:8px;">Failed to load PDF</p><a href="${b.src||''}" target="_blank" style="color:var(--accent);">Open in new tab →</a></div></div></div>`;
if(b.embed_type==='download'){
var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB';
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;"><div style="font-size:48px;margin-bottom:12px;">${PD.clip_icon||''}</div><div style="font-size:16px;font-weight:500;color:var(--text-primary);margin-bottom:4px;">${esc(b.file_name||'File')}</div><div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">${szStr} · ${esc(b.file_mime||'')}</div><a href="${b.src||''}" download style="display:inline-block;padding:8px 20px;background:var(--accent);color:#fff;text-decoration:none;border-radius:8px;font-size:13px;font-weight:500;">⬇ Download</a></div></div>`;
}
var url=b.src||'';if(url){
var src=b.embed_src||url;
var height=b.height||520;
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="position:relative;width:100%;height:${height}px;border-radius:8px;overflow:hidden;background:#0a0a0a;"><iframe src="${src}" loading="lazy" frameborder="0" style="position:absolute;inset:0;width:100%;height:100%;" allowfullscreen allow="autoplay; encrypted-media; picture-in-picture"></iframe></div></div>`;
}
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setEmbedUrl('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🔗</div><div style="font-size:14px;color:var(--text-dim);">Click to add embed URL</div></div></div>`;
}
if(b.type==='synced'){
if(b._synced_deleted){
// v6.5.0: source deleted → clear state instead of stale cache.
return `<div class="block-wrapper synced-block-wrapper" data-id="${b.id}" style="border-left:3px solid #e05e5e;background:var(--bg-secondary);opacity:.8">
<div class="synced-badge" style="font-size:11px;color:#e05e5e;padding:4px 8px;font-weight:600;">🔗 Deleted synced block</div>
<div style="color:var(--text-dim);padding:0 8px 8px;font-size:12px;">The original synced block was deleted — this copy no longer updates.</div>
</div>`;
}
var syncedTitle=b._synced_title||'Synced block';
var syncedContent=b._synced_content||[];
var sid=b.synced_id||'';
var syncedInner=syncedContent.map(function(ch){return'<div class="block-content" style="padding-left:16px;border-left:2px solid var(--accent);margin:4px 0;">'+mdEsc(typeof ch==='object'?ch.content||'':ch)+'</div>';}).join('');
return `<div class="block-wrapper synced-block-wrapper" data-id="${b.id}" style="border-left:3px solid var(--accent);background:var(--synced-bg,var(--bg-secondary))">
<div class="synced-badge" style="font-size:11px;color:var(--accent);padding:4px 8px;font-weight:600;">🔗 Synced — ${esc(syncedTitle)}</div>
<div class="synced-content">${syncedInner||'<div style="color:var(--text-dim);padding:8px;">Loading synced content…</div>'}</div>
</div>`;
}
const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':(b.type==='paragraph'&&!b.content?'Press \'space\' for AI or \'/\' for commands':'Press \'/\' for commands...');
let inner='';
if(b.type==='to_do')inner=`<input type="checkbox" class="todo-checkbox" ${b.checked?'checked':''} onchange="E._doToggle('${b.id}',this.checked)"><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
else if(b.type==='toggle')inner=`<div class="toggle-title-row"><button class="toggle-chevron ${b.expanded?'open':''}" onclick="E._doToggleExpand('${b.id}')">▶</button><div class="toggle-title" data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div></div><div class="toggle-children" ${(b.expanded===false)?'style="display:none"':''}>${(b.children||[]).map((ch)=>renderNestedBlock(ch)).join('')}</div>`;
else if(b.type==='code')inner=renderCodeBlock(b);
else if(b.type==='callout')inner=`<span class="callout-icon" onclick="E._toggleCalloutIcon('${b.id}')" title="Change icon">${b.icon||'💡'}</span><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div><div class="callout-icon-picker" id="callout-picker-${b.id}" style="display:none">${CALLOUT_ICONS.map(function(i){return '<button type="button" class="callout-icon-opt" onclick="E._setCalloutIcon(\''+b.id+'\',\''+i+'\')">'+i+'</button>';}).join('')}</div>`;
else inner=`<div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div';
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:'');
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
// Numéro d'ordre : rang des blocs `numbered_list` contigus (1,2,3…) ;
// passé en attribut data-num → le CSS le rend hors du contenu éditable.
let numAttr='';
if(b.type==='numbered_list'&&idx>=0){
const bs=(window.E&&window.E.blocks)||[];
let s=idx; while(s>0&&bs[s-1]&&bs[s-1].type==='numbered_list')s--;
numAttr=` data-num="${idx-s+1}"`;
}
// 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 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 8v5M9.5 10.5h5"/></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>
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
${cmtBtn}
</div>`;
}
function renderTOC(b){
var out=[];
var blocks=window.E?window.E.blocks:[];
var headings=blocks.filter(function(b){return /^heading_[1-4]$/.test(b.type)&&(b.content||'').trim();});
if(headings.length===0){
out.push(`<div class="block-content block-toc block-toc-empty"><span style="color:var(--text-dim);font-size:13px;">Add headings to generate a table of contents</span></div>`);
}else{
out.push(`<div class="block-content block-toc"><div class="block-toc-title">On this page</div>`);
headings.forEach(function(h){
var lvl=parseInt(h.type.slice(-1),10);
var id='h-'+ (h.id||'').replace(/[^a-zA-Z0-9]/g,'');
out.push(`<div class="block-toc-item" style="margin-left:${(lvl-1)*12}px" onclick="var el=document.getElementById('${id}');if(el){el.scrollIntoView({behavior:'smooth',block:'start'});}">${esc(h.content)}</div>`);
});
out.push(`</div>`);
}
if(blocks.length===0){}
return `<div class="block-wrapper" data-id="${b.id}">${out.join('')}</div>`;
}
function renderMathBlock(b){
var html=`<div class="math-display" data-bid="${b.id}"></div>`;
html+=`<textarea class="math-editor" data-math-bid="${b.id}" data-placeholder="Type a LaTeX math expression (e.g. E = mc^2)..." spellcheck="false">${esc(b.content)}</textarea>`;
return html;
}
function renderAutomationButton(b,idx){
var label=(b.content&&b.content.trim())?b.content:((b.automation_name||'Run automation'));
var bound=b.automation_id>0;
var badge=bound?'<span style="opacity:.65;font-weight:400;margin-left:8px;font-size:11px;">⚡ '+esc(b.automation_name||'')+'</span>':'<span style="opacity:.65;font-weight:400;margin-left:8px;font-size:11px;">Choose…</span>';
var actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
return `<div class="block-wrapper" data-id="${b.id}">
<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}
<div class="block-content"><button type="button" class="fd-automation-btn" onclick="E.runAutomationButton('${b.id}')">⚡ ${esc(label)}${badge}</button></div>
</div>`;
}
function renderColumnsBlock(b){
var cols=(b.children&&b.children.length)?b.children:[{id:genId(),type:'paragraph',content:''}];
var cells=cols.map(function(c){
return `<div class="column-cell">${renderChildColumn(c,b.id)}</div>`;
}).join('');
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-columns"><div class="columns-add-bar"><button class="columns-add-btn" onclick="E._addColumn('${b.id}')">+ Add column</button></div><div class="columns-row">${cells}</div></div></div>`;
}
function renderChildColumn(c,parentId){
return `<div class="block-column-wrapper" data-parent="${parentId}">
<div class="block-actions"><button class="block-add-btn" onclick="E._removeColumn('${parentId}','${c.id}')" title="Remove column">×</button></div>
<div class="block-content block-column" data-bid="${c.id}" contenteditable="true" data-placeholder="${c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':'Type...'}" spellcheck="false">${esc(c.content)}</div>
</div>`;
}
function renderNestedBlock(c){
var ph=c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':c.type==='heading_4'?'Heading 4':c.type==='bulleted_list'?'List':c.type==='numbered_list'?'List':c.type==='to_do'?'To-do':'Type...';
return `<div class="block-wrapper nested-block"><div class="block-content block-nested" data-bid="${c.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(c.content)}</div></div>`;
}
// ═══════════ AI Meeting Note — widget de réunion (v4.12.0) ═══════════
// Bloc interactif « Notion-like » : consentement, transcription live (Web
// Speech API quand disponible), transcript horodaté + notes, puis résumé
// généré par le FlowDeck Agent.
function renderMeetingBlock(b, idx){
var actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
return `<div class="block-wrapper" data-id="${b.id}">
<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}
<div class="block-content block-meeting" data-meeting-bid="${b.id}"><div class="fm-loading">Préparation de la réunion…</div></div>
</div>`;
}
function _fmFind(bid){var e=window.E;if(e&&e.blocks){for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return e.blocks[i];}}return null;}
function _fmDef(){return {title:'Meeting',date:new Date().toISOString().slice(0,10),consent:'self',started:false,status:'idle',lines:[],interim:'',notes:'',manualTranscript:'',summary:'',summaryError:'',summaryLoading:false,lastTab:'transcript'};}
function _fmEnsure(bid){var b=_fmFind(bid);if(!b)return null;if(!b.meeting)b.meeting=_fmDef();if(!b.meeting.lines)b.meeting.lines=[];if(b.meeting.summaryLoading===undefined)b.meeting.summaryLoading=false;if(b.meeting.manualTranscript===undefined)b.meeting.manualTranscript='';return b;}
function _fmTime(){var d=new Date();var p=function(n){return (n<10?'0':'')+n;};return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds());}
function _fmEsc(v){return esc(v==null?'':String(v));}
function _fmAttr(v){return _fmEsc(v).replace(/"/g,'&quot;');}
function _fmSave(){var e=window.E;if(e){e.dirty=true;if(e.autoSave)e.autoSave();}}
function _fmTranscriptText(b){var out=[];(b.meeting.lines||[]).forEach(function(l){out.push('['+l.t+'] '+(l.who||'Vous')+' : '+l.text);});if((b.meeting.manualTranscript||'').trim())out.push('(transcription saisie/collée) '+(b.meeting.manualTranscript||'').trim());if((b.meeting.notes||'').trim())out.push('(notes) '+(b.meeting.notes||'').trim());return out.join('\n');}
var FDMeeting = {
_rt:{},
_audio(){ return !!(window.SpeechRecognition||window.webkitSpeechRecognition); },
mount(bid,root){
var s=this._rt[bid]||(this._rt[bid]={});s.root=root;
if(!s.wired){
s.wired=true;
var self=this;
root.addEventListener('click',function(ev){var t=ev.target&&ev.target.closest?ev.target.closest('[data-act]'):null;if(t)self.act(bid,t.getAttribute('data-act'),t);});
root.addEventListener('input',function(ev){self.onInput(bid,ev.target);});
root.addEventListener('change',function(ev){self.onInput(bid,ev.target);});
}
this.paint(bid);
},
focus(bid){},
paint(bid){
var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;
s.root.innerHTML=this.html(bid);
},
_statusLabel(m){
if(m.status==='running')return '● Enregistrement';
if(m.status==='paused')return '⏸ En pause';
if(m.status==='done')return '✓ Terminée';
if(m.status==='manual')return '📝 Sans audio';
return 'Prête';
},
html(bid){
var b=_fmEnsure(bid);if(!b)return '';
var m=b.meeting;var s=this._rt[bid]||{};
var audioOk=this._audio();
var st=this._statusLabel(m);
var pill=m.status==='running'||m.status==='paused'||m.status==='done'||m.status==='manual'
? `<span class="fm-pill ${m.status==='done'?'fm-pill-ok':''}">${_fmEsc(st)}</span>`:'';
var head=`<div class="fm-head">
<span class="fm-ico">🎙️</span>
<input class="fm-title" data-fm="title" value="${_fmAttr(m.title)}" placeholder="Titre de la réunion" spellcheck="false">
<span class="fm-date">📅 ${_fmEsc(m.date)}</span>
${pill}
</div>`;
var body='';
if(!m.started){
body+=`<div class="fm-step">
<div class="fm-step-title">1 · Consentement des participants</div>
<label class="fm-radio"><input type="radio" name="fm-consent-${bid}" data-fm="consent" value="self" ${m.consent==='auto'?'':'checked'}> <span>🔘 Obtenir le consentement moi-même</span></label>
<label class="fm-radio"><input type="radio" name="fm-consent-${bid}" data-fm="consent" value="auto" ${m.consent==='auto'?'checked':''}> <span>🔘 Lire automatiquement un message audio</span></label>
<div class="fm-step-desc">${audioOk?'Notion AI est prêt : la réunion sera transcrite en direct. Autorisez le micro lorsque le navigateur le demande.':'Ce navigateur ne supporte pas la transcription audio : vous pourrez saisir ou coller la transcription manuellement.'}</div>
<button class="fm-btn fm-btn-primary" data-act="start" type="button">${audioOk?'🎙 Start transcribing':'Ouvrir la boîte de réunion (sans audio)'}</button>
</div>`;
}else{
var tabs=`<div class="fm-tabs">
<button class="fm-tab ${m.lastTab==='notes'?'active':''}" data-act="tab" data-tab="notes" type="button">Notes</button>
<button class="fm-tab ${m.lastTab==='transcript'?'active':''}" data-act="tab" data-tab="transcript" type="button">Transcript</button>
<button class="fm-tab ${m.lastTab==='summary'?'active':''}" data-act="tab" data-tab="summary" type="button">Summary</button>
</div>`;
var ctrl='';
if(m.status==='running'){
ctrl=`<div class="fm-wave">•••|ı|ı|ı|ı•••</div>
<div class="fm-actions">
<button class="fm-btn" data-act="pause" type="button">⏸ Pause</button>
<button class="fm-btn fm-btn-primary" data-act="stop" type="button">⏹ Stop</button>
</div>`;
}else if(m.status==='paused'){
ctrl=`<div class="fm-actions">
<button class="fm-btn fm-btn-primary" data-act="start" type="button">▶ Reprendre</button>
<button class="fm-btn" data-act="stop" type="button">⏹ Stop</button>
</div>`;
}else if(m.status==='manual'){
ctrl=`<div class="fm-actions"><button class="fm-btn" data-act="stop" type="button">⏹ Terminer la réunion</button></div>`;
}else if(m.status==='done'){
ctrl=`<div class="fm-actions"><button class="fm-btn" data-act="new" type="button">🔄 Nouvelle transcription</button></div>`;
}
var panel='';
if(m.lastTab==='notes'){
panel=`<textarea class="fm-area" data-fm="notes" rows="6" placeholder="Prenez des notes manuelles pendant la réunion…">${_fmEsc(m.notes)}</textarea>`;
}else if(m.lastTab==='transcript'){
var lines=(m.lines||[]).map(function(l){return `<div class="fm-line"><span class="fm-ts">${_fmEsc(l.t||'')}</span><span class="fm-who">${_fmEsc(l.who||'Vous')}</span><span class="fm-txt">${_fmEsc(l.text)}</span></div>`;}).join('');
if(m.status==='running'){lines+=`<div class="fm-line fm-interim"><span class="fm-ts">·</span><span class="fm-who">Vous</span><span class="fm-txt">${_fmEsc(m.interim||'')}${(m.interim||'')?'':'…'}</span></div>`;}
var manual=m.status!=='running'?`<div class="fm-manual"><div class="fm-manual-label">Transcript saisi ou collé (si aucune capture audio)</div><textarea class="fm-area" data-fm="manual" rows="4" placeholder="Collez ici la transcription de la réunion (optionnel)…">${_fmEsc(m.manualTranscript)}</textarea></div>`:'';
panel=`<div class="fm-transcript"><div data-fm-transcript-body>${lines||'<div class="fm-empty">La transcription apparaîtra ici en direct. Enregistrez la réunion pour générer un transcript.</div>'}</div>${manual}</div>`;
}else{
var summaryHtml='';
if(m.summaryLoading){
summaryHtml=`<div class="fm-loading">Génération du résumé…</div>`;
}else if(m.summary){
summaryHtml=`<textarea class="fm-area" data-fm="summary" rows="14">${_fmEsc(m.summary)}</textarea>
<div class="fm-actions"><button class="fm-btn" data-act="copy" type="button">📋 Copier le résumé</button></div>`;
}else if(m.summaryError){
summaryHtml=`<div class="fm-error">${_fmEsc(m.summaryError)}</div>
<button class="fm-btn fm-btn-primary" data-act="generate" type="button">Réessayer</button>`;
}else{
var has=!!(_fmTranscriptText(b).trim());
if(m.status==='done'&&has){
summaryHtml=`<button class="fm-btn fm-btn-primary" data-act="generate" type="button">✨ Générer le résumé</button>
<div class="fm-step-desc">L'agent analyse le transcript et produit un aperçu structuré (sujets, décisions, actions).</div>`;
}else{
summaryHtml=`<div class="fm-empty">Arrêtez l'enregistrement (ou ajoutez un transcript) puis générez le résumé ici.</div>`;
}
}
panel=summaryHtml;
}
body=`<div class="fm-step">${ctrl}${tabs}${panel}</div>`;
}
return `<div class="fm">${head}${body}</div>`;
},
onInput(bid,el){
var b=_fmEnsure(bid);if(!b||!el)return;var m=b.meeting;var k=el.getAttribute&&el.getAttribute('data-fm');
if(!k)return;
if(k==='title'){m.title=el.value||'';var tEl=document.getElementById('_titleEl');if(tEl&&!((tEl.textContent||'').trim())){tEl.textContent=m.title;if(window.E)window.E.pageTitle=m.title;}}
else if(k==='notes'){m.notes=el.value;}
else if(k==='manual'){m.manualTranscript=el.value;}
else if(k==='summary'){m.summary=el.value;}
else if(k==='consent'){m.consent=el.value;}
else return;
_fmSave();
},
act(bid,act,el){
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var self=this;
if(act==='start'){
if(m.status==='done'||m.status==='idle'||m.status==='paused'){
if(m.status==='done'){m.lines=[];m.summary='';m.summaryError='';m.manualTranscript='';}
m.started=true;m.lastTab='transcript';
if(!self._audio()){m.status='manual';m.interim='';_fmSave();self.paint(bid);return;}
if(m.consent==='auto'&&window.speechSynthesis){
var u=new SpeechSynthesisUtterance('Cette réunion est enregistrée et transcrite par FlowDeck. Merci de votre attention.');
try{window.speechSynthesis.speak(u);}catch(e){}
}
setTimeout(function(){self._startRec(bid);},m.consent==='auto'?1300:0);
}
}else if(act==='pause'){
this._stopRec(bid,false);m.status='paused';m.interim='';_fmSave();this.paint(bid);
}else if(act==='stop'){
this._stopRec(bid,true);m.status='done';m.lastTab='transcript';m.interim='';_fmSave();this.paint(bid);
}else if(act==='new'){
m.lines=[];m.summary='';m.summaryError='';m.status='idle';m.started=false;m.lastTab='transcript';_fmSave();this.paint(bid);
}else if(act==='tab'){
m.lastTab=(el&&el.getAttribute&&el.getAttribute('data-tab'))||'transcript';_fmSave();this.paint(bid);
}else if(act==='generate'){
this._generate(bid);
}else if(act==='copy'){
var txt=(m.summary||'').trim();
if(!txt)return;
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).catch(function(){});}
}
},
_startRec(bid){
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid]||(this._rt[bid]={});
var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
if(!SR){m.status='manual';_fmSave();this.paint(bid);return;}
if(s.rec){try{s.rec.stop();}catch(e){}}
var rec=new SR();s.rec=rec;s.offset=0;s.stopped=false;
rec.lang='fr-FR';rec.continuous=true;rec.interimResults=true;rec.maxAlternatives=1;
var self=this;
rec.onresult=function(ev){
var final='',interim='';
for(var i=(s.offset||0);i<ev.results.length;i++){
var r=ev.results[i];
if(r.isFinal)final+=(r[0]&&r[0].transcript||'');
else interim+=(r[0]&&r[0].transcript||'');
}
s.offset=ev.results.length;
if(final&&final.trim()){
m.lines.push({t:_fmTime(),who:'Vous',text:final.trim()});
_fmSave();
self._updateTranscriptOnly(bid);
}
m.interim=interim.trim();
self._updateInterim(bid);
};
rec.onend=function(){
if(!s.stopped&&m.status==='running'&&self._audio()){try{rec.start();}catch(e){}}
};
rec.onerror=function(ev){
if(ev&&ev.error==='not-allowed'){m.summaryError='Microphone refusé : autorisez le micro ou utilisez la saisie manuelle du transcript.';m.status='manual';s.stopped=true;_fmSave();self.paint(bid);}
};
m.status='running';m.interim='';_fmSave();
this.paint(bid);
try{rec.start();}catch(e){m.status='manual';s.stopped=true;_fmSave();this.paint(bid);}
},
_stopRec(bid,stop){
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid];
if(s&&s.rec){s.stopped=true;try{s.rec.stop();}catch(e){}}
if(stop&&m.interim&&m.interim.trim()){m.lines.push({t:_fmTime(),who:'Vous',text:m.interim.trim()});}
m.interim='';
},
_updateTranscriptOnly(bid){var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;var el=s.root.querySelector('[data-fm-transcript-body]');if(el)el.innerHTML=this._transcriptLines(b);},
_updateInterim(bid){var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;var el=s.root.querySelector('.fm-interim .fm-txt');if(el&&el.textContent!==(b.meeting.interim||'')){el.textContent=(b.meeting.interim||'')+'…';}},
_transcriptLines(b){var m=b.meeting;var lines=(m.lines||[]).map(function(l){return `<div class="fm-line"><span class="fm-ts">${_fmEsc(l.t||'')}</span><span class="fm-who">${_fmEsc(l.who||'Vous')}</span><span class="fm-txt">${_fmEsc(l.text)}</span></div>`;}).join('');return lines||'<div class="fm-empty">La transcription apparaîtra ici en direct…</div>';},
_generate(bid){
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;
var content=_fmTranscriptText(b);
if(!content.trim()){m.summaryError='Aucun contenu de réunion à résumer.';_fmSave();this.paint(bid);return;}
m.summaryLoading=true;m.summaryError='';_fmSave();this.paint(bid);
var self=this;
var prompt='Rédige le résumé structuré de cette réunion (format AI Meeting Note) : une ligne d\'en-tête avec la date du '+_fmEsc(m.date)+', puis les sections « Résumé » (Overview), « Décisions » et « Action items » avec des puces concrètes. Si le contenu est trop court ou fragmentaire, indique « No meaningful content could be extracted ». Ne précède pas la réponse de commentaires.';
if(!(window.fdAgent&&window.fdAgent.generate)){
m.summaryLoading=false;m.summaryError='Agent IA indisponible — configurez-le dans Paramètres → Agent & IA.';_fmSave();this.paint(bid);return;
}
window.fdAgent.generate(prompt, content).then(function(text){
m.summary=(text||'').trim();m.summaryLoading=false;m.summaryError='';_fmSave();self.paint(bid);
}).catch(function(err){
m.summaryLoading=false;m.summaryError=(err&&err.message)?err.message:'Échec de génération du résumé';_fmSave();self.paint(bid);
});
}
};
window.FDMeeting=FDMeeting;
// ═══════════ v5.10.0 — Interactions de bloc : sélection multiple, menu ⋮⋮, undo/redo ═══════════
let _selSet=new Set();
var _mdSelDown=null; // v7.69 : point de départ du glisser de sélection
var _mdSelRange=null; // v7.69 : plage reconstruite, à restaurer après le collapse du focus
function _selHas(bid){return _selSet.has(bid);}
function _selToggle(bid){if(!bid)return;if(_selSet.has(bid))_selSet.delete(bid);else _selSet.add(bid);
document.querySelectorAll('.block-wrapper[data-id="'+bid+'"]').forEach(function(w){w.classList.toggle('selected',_selSet.has(bid));});}
function _selClear(){_selSet.clear();document.querySelectorAll('.block-wrapper.selected').forEach(function(w){w.classList.remove('selected');});}
function _selApply(){document.querySelectorAll('.block-wrapper[data-id]').forEach(function(w){var id=w.getAttribute('data-id');w.classList.toggle('selected',!!(_selSet.has(id)));});}
function _selIndicesSorted(){var E=window.E;var out=[];if(!E)return out;E.blocks.forEach(function(b,i){if(_selSet.has(b.id))out.push(i);});return out;}
function _rtSync(){try{var rt=window.__fdRT;if(rt&&rt.syncNow)rt.syncNow();}catch(e){}}
const _BLOCK_TURN_TYPES=[
{id:'paragraph',name:'Text'},{id:'heading_1',name:'Heading 1'},{id:'heading_2',name:'Heading 2'},
{id:'heading_3',name:'Heading 3'},{id:'heading_4',name:'Heading 4'},{id:'bulleted_list',name:'Bulleted list'},
{id:'numbered_list',name:'Numbered list'},{id:'to_do',name:'To-do'},{id:'toggle',name:'Toggle'},
{id:'quote',name:'Quote'},{id:'callout',name:'Callout'},{id:'table',name:'Table'},
{id:'math',name:'Math equation'},{id:'columns',name:'Column list'},{id:'button',name:'Button / Automation'},
{id:'code',name:'Code block'},{id:'synced',name:'Synced block'}
];
// Sous-menu « Turn into » — ordre Notion ; id ∈ _BLOCK_TURN_TYPES, cols = nb de colonnes
const _BLOCK_TURN_MENU=[
{id:'paragraph',name:'Text',icon:'T'},{id:'heading_1',name:'Heading 1',icon:'H1'},
{id:'heading_2',name:'Heading 2',icon:'H2'},{id:'heading_3',name:'Heading 3',icon:'H3'},
{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},
{id:'numbered_list',name:'Numbered list',icon:'1.'},{id:'to_do',name:'To-do list',icon:'☑'},
{id:'toggle',name:'Toggle list',icon:'▸'},{id:'code',name:'Code',icon:'&lt;/&gt;'},
{id:'quote',name:'Quote',icon:'”'},{id:'callout',name:'Callout',icon:'ⓘ'},
{id:'math',name:'Block equation',icon:'∑'},{id:'synced',name:'Synced block',icon:'⛓'},
{id:'columns',name:'2 columns',icon:'▦',cols:2},{id:'columns',name:'3 columns',icon:'▦',cols:3},
{id:'columns',name:'4 columns',icon:'▦',cols:4},{id:'columns',name:'5 columns',icon:'▦',cols:5}
];
// Couleurs texte/fond alignées par index — noms Notion (Dernière utilisée ailleurs)
const _BLOCK_COLOR_NAMES=['Default','Gray','Brown','Orange','Yellow','Green','Blue','Purple','Pink','Red'];
const _BLOCK_COLORS_TEXT=['default','#94A3B8','#8A6642','#F76B15','#FFC53D','#46A758','#3E63DD','#8E4EC6','#E5539B','#E5484D'];
const _BLOCK_COLORS_BG=['default','rgba(148,163,184,.15)','rgba(138,102,66,.15)','rgba(247,107,21,.15)','rgba(255,197,61,.15)','rgba(70,154,88,.15)','rgba(62,99,221,.15)','rgba(142,78,198,.15)','rgba(229,83,155,.15)','rgba(229,72,77,.15)'];
// Raccourcis affichés — implémentés par le handler clavier en bas de fichier
const _BM_SHORTCUTS={link:'Alt+⇧+L',duplicate:'Ctrl+D',move:'Ctrl+⇧+P',delete:'Del',comment:'Ctrl+⇧+M',suggest:'Ctrl+⇧+Alt+X',ai:'Ctrl+J',color:'Ctrl+⇧+H'};
var _bmSubKind='',_bmSubTimer=null;
function _blockMenuClose(){var m=document.getElementById('blockMenu');if(m)m.remove();m=document.getElementById('blockMenuSub');if(m)m.remove();clearTimeout(_bmSubTimer);_bmSubKind='';}
function _blockAttr(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
function _blockMenuBid(){var m=document.getElementById('blockMenu');return m?(m.getAttribute('data-bid')||''):'';}
// Sous-menu ouvert au survol de l'entrée correspondante (panneau de droite)
function _blockSubOpen(kind,bid,anchor){
if(!kind||!bid)return;
var m=document.getElementById('blockMenu');if(!m)return;
if(_bmSubKind===kind&&document.getElementById('blockMenuSub'))return;
clearTimeout(_bmSubTimer);_bmSubKind=kind;
var r=m.getBoundingClientRect();
var a=anchor&&anchor.getBoundingClientRect?anchor.getBoundingClientRect():r;
var top=Math.max(8,Math.min(a.top,window.innerHeight-60));
if(kind==='turn')_blockTurnMenuOpen(r.right+4,top,bid);
else if(kind==='color')_blockColorMenuOpen(r.right+4,top,bid);
else if(kind==='skills')_blockSkillsMenuOpen(r.right+4,top,bid);
var s=document.getElementById('blockMenuSub');
if(s){s.addEventListener('mouseenter',function(){clearTimeout(_bmSubTimer);});
s.addEventListener('mouseleave',function(){_blockSubCloseLater();});}
}
function _blockSubCloseLater(){clearTimeout(_bmSubTimer);_bmSubTimer=setTimeout(function(){var s=document.getElementById('blockMenuSub');if(s&&!s.matches(':hover')){s.remove();_bmSubKind='';}},260);}
// Sous-menu ouvert depuis la toolbar de sélection (pas de #blockMenu parent)
function _subHoverClose(){var s=document.getElementById('blockMenuSub');if(s)s.addEventListener('mouseleave',function(){_blockSubCloseLater();});}
function _blockMenuClick(ev){
var t=ev.target.closest&&ev.target.closest('[data-bm-act]');if(!t)return;
var act=t.getAttribute('data-bm-act');var bid=t.getAttribute('data-bid')||_blockMenuBid();
if(!act)return;
ev.preventDefault();ev.stopPropagation();
var E=window.E;if(!E)return;
var idx=bid?E.getIdx(bid):-1;
if(act==='submenu'){_blockSubOpen(t.getAttribute('data-sub'),bid,t);return;}
if(act==='duplicate'){E.duplicateBlock(idx);}
else if(act==='copy-link'){E.copyBlockLink(bid||'');}
else if(act==='move-to'){_blockMoveToOpen(idx);}
else if(act==='delete'){E.deleteSelected(idx);}
else if(act==='comment'){E.commentBlock(idx);}
else if(act==='suggest'){E.suggestEdit(idx);}
else if(act==='ai'){E.askAIBlock(idx);}
else if(act==='skill'){E.runSkill(idx,t.getAttribute('data-prompt')||'');}
// v7.68 : Clipboard du menu (sélection multi-blocs gérée par copySelectedBlocks)
else if(act==='clip-cut'){
if(_selSet.size>1)E.cutSelectedBlocks();
else{var mst=document.getElementById('blockMenu');const md=E.clipCut?E.clipCut(mst&&mst._clipSt):null;if(md!=null&&navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md);E.showToast('Sélection coupée');}}
}
else if(act==='clip-copy'){
if(_selSet.size>1)E.copySelectedBlocks();
else{var mst2=document.getElementById('blockMenu');const md=E.clipCopy?E.clipCopy(mst2&&mst2._clipSt):null;if(md!=null&&navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md);E.showToast('Sélection copiée');}}
}
else if(act==='clip-paste'){E.pasteBlocks();}
else if(act==='color'){var key=t.getAttribute('data-color-key'),val=t.getAttribute('data-color-val');E.setBlockColor(idx,key,val);}
else if(act==='turn-type'){E.turnInto(idx,t.getAttribute('data-type'),parseInt(t.getAttribute('data-cols')||'0',10)||0);_blockMenuClose();return;}
_blockMenuClose();
}
function _blockMenuItem(act,icon,label,opts){
opts=opts||{};
var attr=' data-bm-act="'+act+'"';
if(opts.bid)attr+=' data-bid="'+_blockAttr(opts.bid)+'"';
if(opts.sub)attr+=' data-sub="'+opts.sub+'"';
if(opts.colorKey)attr+=' data-color-key="'+opts.colorKey+'" data-color-val="'+_blockAttr(opts.colorVal||'')+'"';
if(opts.prompt!=null)attr+=' data-prompt="'+_blockAttr(opts.prompt)+'"';
if(opts.type)attr+=' data-type="'+opts.type+'"';
if(opts.cols)attr+=' data-cols="'+opts.cols+'"';
if(opts.title)attr+=' title="'+_blockAttr(opts.title)+'"';
var right=opts.check?'<span class="bm-check">✓</span>'
:(opts.sub?'<span class="bm-arrow">▸</span>'
:(opts.shortcut?'<span class="bm-hint">'+opts.shortcut+'</span>':''));
return '<div class="bm-item'+(opts.danger?' bm-danger':'')+(opts.current?' bm-current':'')+'"'+attr+'>'
+(icon!=null?'<span class="bm-ico">'+icon+'</span>':'')+label+right+'</div>';
}
// Métadonnées du pied de page : auteur / heure de dernière édition / taille du bloc
function _blockMenuMeta(b){
var E=window.E||{};
var who='';
try{var n=document.querySelector('.um-name')||document.querySelector('.workspace-name');who=n?(n.textContent||'').trim():'';}catch(e){}
var when='';
if(E.updatedAt){var d=new Date(E.updatedAt);if(!isNaN(d)){
var same=d.toDateString()===new Date().toDateString();
var hm=d.toLocaleTimeString([],{hour:'numeric',minute:'2-digit'});
when=(same?'Today at ':(d.toLocaleDateString([])+' at '))+hm;}}
var content=(b&&b.content)||'';
var trimmed=content.trim();
var words=trimmed?trimmed.split(/\s+/).length:0;
return '<div class="bm-foot"><div>Last edited by '+esc(who||'you')+'</div>'
+(when?'<div>'+esc(when)+'</div>':'')
+'<div>'+words+' word'+(words===1?'':'s')+', '+content.length+' character'+(content.length===1?'':'s')+'</div></div>';
}
function blockMenuOpen(x,y,bid){
_blockMenuClose();
var E=window.E;if(!E)return;
var idx=E.getIdx(bid);if(idx<0)return;
var b=E.blocks[idx];if(!b)return;
var d=document.createElement('div');d.id='blockMenu';d.className='block-menu';d.setAttribute('data-bid',b.id);d.style.cssText='position:fixed;z-index:4000;';
var S=_BM_SHORTCUTS;
// v7.68 : Clipboard — une sélection de texte multi-blocs OU des blocs
// sélectionnés (handle + Maj-clic) donnent Cut/Copy ; Paste colle après.
// la sélection est détruite quand la recherche prend le focus : on la
// fige ici et les actions Clipboard la reprennent telle quelle.
var clipSt=(E._clipState?E._clipState():null);
d._clipSt=clipSt;
var clip=(_selSet.size>1||clipSt)
?'<div class="bm-group">Clipboard</div>'
+_blockMenuItem('clip-cut','✂','Cut',{shortcut:'Ctrl+X'})
+_blockMenuItem('clip-copy','⧉','Copy',{shortcut:'Ctrl+C'})
+_blockMenuItem('clip-paste','📋','Paste',{shortcut:'Ctrl+V'})
:'';
d.innerHTML='<input class="bm-search" type="text" placeholder="Search actions..." autocomplete="off" spellcheck="false">'
+clip
+'<div class="bm-group">Text</div>'
+_blockMenuItem('submenu','↩','Turn into',{sub:'turn'})
+_blockMenuItem('submenu','🎨','Color',{sub:'color'})
+_blockMenuItem('copy-link','🔗','Copy link to block',{shortcut:S.link})
+_blockMenuItem('duplicate','▣','Duplicate',{shortcut:S.duplicate})
+_blockMenuItem('move-to','↪','Move to…',{shortcut:S.move})
+_blockMenuItem('delete','✕','Delete',{shortcut:S.delete,danger:true})
+_blockMenuItem('comment','💬','Comment',{shortcut:S.comment})
+_blockMenuItem('suggest','✎','Suggest edits',{shortcut:S.suggest})
+_blockMenuItem('ai','✨','Ask AI',{shortcut:S.ai})
+_blockMenuItem('submenu','📚','Skills',{sub:'skills'})
+_blockMenuMeta(b);
d.addEventListener('click',_blockMenuClick);
d.addEventListener('input',function(ev){
if(!ev.target.classList||!ev.target.classList.contains('bm-search'))return;
var q=(ev.target.value||'').toLowerCase();
d.querySelectorAll('.bm-item').forEach(function(it){
it.style.display=(q&&it.textContent.toLowerCase().indexOf(q)<0)?'none':'';
});
d.querySelectorAll('.bm-group').forEach(function(g){g.style.display=q?'none':'';});
});
d.addEventListener('mouseover',function(ev){
var it=ev.target&&ev.target.closest?ev.target.closest('[data-sub]'):null;
if(it)_blockSubOpen(it.getAttribute('data-sub'),b.id,it);
});
d.addEventListener('mouseleave',function(){_blockSubCloseLater();});
d.addEventListener('contextmenu',function(ev){ev.preventDefault();_blockMenuClose();});
document.body.appendChild(d);
var r=d.getBoundingClientRect();
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
d.style.left=x+'px';d.style.top=y+'px';
// v7.69 : on ne vole PAS le focus quand un texte est sélectionné — sinon
// le navigateur efface le surlignage dès que le menu apparaît (les
// actions Clipboard gardent de toute façon la sélection figée ci-dessus).
var si=d.querySelector('.bm-search');
var sLive=window.getSelection();
var hasSel=sLive&&sLive.rangeCount&&!sLive.isCollapsed;
if(si&&!hasSel)setTimeout(function(){si.focus();},10);
}
// Panneau de droite : coquille commune des 3 sous-menus
function _blockSubShell(x,y){
var m=document.getElementById('blockMenuSub');if(m)m.remove();
var d=document.createElement('div');d.id='blockMenuSub';d.className='block-menu block-menu-sub';d.style.cssText='position:fixed;z-index:4001;';
d.addEventListener('click',_blockMenuClick);
document.body.appendChild(d);
var r=d.getBoundingClientRect();
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
d.style.left=x+'px';d.style.top=y+'px';
return d;
}
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='';
_BLOCK_TURN_MENU.forEach(function(t){
var on=(t.id===cur&&!t.cols);
html+=_blockMenuItem('turn-type',t.icon,t.name,{bid:bid,type:t.id,cols:t.cols,
title:t.cols?('Create '+t.cols+' columns of blocks'):'',
check:on,current:on});
});
return html;
}
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';
var last='';try{last=localStorage.getItem('fdLastBlockColor')||'';}catch(e){}
var html='';
if(last){
var i=last.indexOf('|');var lk=i<0?'color':last.slice(0,i);var lv=last.slice(i+1);
var isBg=(lk==='bgColor');
var arr=isBg?_BLOCK_COLORS_BG:_BLOCK_COLORS_TEXT;
var ni=arr.indexOf(lv);
var nm=(ni>=0?_BLOCK_COLOR_NAMES[ni]:'Custom')+(isBg?' background':' text');
html+='<div class="bm-group">Last used</div>'
+_blockMenuItem('color','<span style="color:'+(lv==='default'?'var(--text-secondary)':lv)+'">A</span>',
nm,{bid:bid,colorKey:lk,colorVal:lv,shortcut:_BM_SHORTCUTS.color});
}
html+='<div class="bm-group">Text color</div>';
_BLOCK_COLORS_TEXT.forEach(function(v,n){
html+=_blockMenuItem('color','<span style="color:'+(v==='default'?'var(--text-secondary)':v)+'">A</span>',
_BLOCK_COLOR_NAMES[n]+' text',{bid:bid,colorKey:'color',colorVal:v,
check:v===curT,current:v===curT});
});
html+='<div class="bm-group">Background color</div>';
_BLOCK_COLORS_BG.forEach(function(v,n){
html+=_blockMenuItem('color','<span class="bm-dot" style="background:'+(v==='default'?'var(--text-tertiary)':v)+'"></span>',
_BLOCK_COLOR_NAMES[n]+' background',{bid:bid,colorKey:'bgColor',colorVal:v,
check:v===curB,current:v===curB});
});
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);
d.innerHTML='<div class="bm-empty">Chargement…</div>';
fetch('/api/agent/skills',{credentials:'same-origin'})
.then(function(r){return r.json();})
.then(function(res){
if(!document.body.contains(d))return;
var list=(res&&res.skills)||[];
if(!list.length){d.innerHTML='<div class="bm-empty">Aucune compétence enregistrée — créez-en une dans le panneau Agent.</div>';return;}
var html='';
list.forEach(function(s){
html+=_blockMenuItem('skill','📚',esc(String(s.name||'Skill')),{bid:bid,prompt:s.prompt_template||''});
});
d.innerHTML=html;
})
.catch(function(){if(document.body.contains(d))d.innerHTML='<div class="bm-empty">Compétences indisponibles.</div>';});
}
function _rtMac(){return /Mac|iPhone|iPad/.test(navigator.userAgent);}
function _blockMoveToOpen(idx){
var E=window.E;if(!E)return;
var pages=[];var seen={};
document.querySelectorAll('.sidebar-item.ws-tree-item a.page-name[href^="/pages/"]').forEach(function(a){
var m=(a.getAttribute('href')||'').match(/\/pages\/(\d+)/);if(!m)return;
var id=parseInt(m[1],10);var name=(a.textContent||'').trim()||('Page '+id);
if(id===E.pid||seen[id])return;seen[id]=1;pages.push({id:id,name:name});
});
var ov=document.createElement('div');ov.id='fdMoveOverlay';ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;';
var box=document.createElement('div');box.style.cssText='background:var(--bg-secondary,#1f1f1f);border:1px solid var(--border,#333);border-radius:12px;padding:16px;width:420px;max-width:92vw;max-height:80vh;overflow:auto;';
var listHtml=pages.length?pages.map(function(p){return '<button type="button" data-pid="'+p.id+'" class="bm-page">'+esc(p.name)+'</button>';}).join(''):'<div class="bm-empty">Aucune autre page dans l\u2019arbre du workspace.<br>Ouvrez la page cible puis réessayez.</div>';
box.innerHTML='<div style="font-weight:600;font-size:14px;margin-bottom:10px;">Move block to…</div>'
+'<input type="text" class="bm-search" placeholder="Filter pages…" autofocus>'
+'<div class="bm-page-list">'+listHtml+'</div>'
+'<button type="button" data-cancel="1" class="bm-cancel">Annuler</button>';
box.addEventListener('click',function(ev){
var p=ev.target.closest('[data-pid]');
if(p){var pid=parseInt(p.getAttribute('data-pid'),10);ov.remove();E.moveBlockToPage(idx,pid);return;}
if(ev.target.closest('[data-cancel]')){ov.remove();}
});
box.addEventListener('input',function(ev){
if(ev.target.classList&&ev.target.classList.contains('bm-search')){
var q=(ev.target.value||'').toLowerCase();
box.querySelectorAll('.bm-page').forEach(function(b2){b2.style.display=(q&&b2.textContent.toLowerCase().indexOf(q)<0)?'none':'';});
}
});
ov.appendChild(box);
document.body.appendChild(ov);
}
function _blockDocDown(ev){
if(!(ev.target&&ev.target.closest))return;
if(ev.target.closest('#blockMenu')||ev.target.closest('#blockMenuSub')||ev.target.closest('#fdMoveOverlay'))return;
if(ev.target.closest('.block-handle'))return;
if(ev.button===2){const w0=ev.target.closest('.block-wrapper');if(w0&&_selSet.has(w0.getAttribute('data-id')))return;}
if(_selSet.size)_selClear();
_blockMenuClose();
}
window.E=null;
function editorState(){
return {
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
updatedAt:PD.updated_at||'',
fmt:{open:false,top:0,left:0,idx:-1},
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,exportOpen:false,
aiBusy:false,
moveOpen:false,moveWorkspaces:[],moveLoading:false,
activityOpen:false,pageCreated:PD.created_at||'',
shareTab:'share',favorited:false,
pagePublished:!!PD.published,
pageIsShared:!!PD.is_shared,
generalAccess:PD.share_mode||'private',publicPerm:'viewer',
pubAllowEdit:false,pubAllowComments:true,
pageUrl:window.location.href,publishedUrl:'',
inviteEmail:'',invitePermission:'edit',inviteUserId:null,inviteGroupId:null,inviteGroups:[],inviteUsers:[],inviteSuggestOpen:false,inviteSel:-1,accessList:[],
toastVisible:false,toastMsg:'',
// ── v4.9.0: Collaboration — comments & mentions ──
commentsOpen:false,commentDraft:'',comments:[],commentCount:0,reactions:[],commentMenu:null,
currentUserId:PD.user_id||0,
_commentSel:{range:null,bid:null,start:0,end:0},
_notifiedMentions:{},
undoStack:[],redoStack:[],_histCap:100,_histLock:false,
// ── Cover & icon (v5.10+) ──
coverUrl:'',
coverOpen:false,
coverLoading:false,
iconOpen:false,
iconValue:'',
// ── Import ──
importOpen:false,importText:'',importFile:null,
// ── v5.11.0 wiki-links & v5.12.0 page options ──
isLocked:false,lockedBy:null,canEdit:true,fullWidth:false,fontSmall:false,
tplSaving:false,
// ── DB Templates ──
dbTplOpen:false,dbTplLoading:false,dbTemplates:[],
// ── Version history ──
versionsOpen:false,versionsLoading:false,versionsList:[],
// ── Backlinks ──
backlinksOpen:false,backlinksLoading:false,backlinksList:[],
get timeAgo() {
if (!this.updatedAt) return 'just now';
var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000);
if (diff < 60) return 'just now';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
return Math.floor(diff / 86400) + 'd ago';
},
formatDate(d) {
if (!d) return '';
var dt = new Date(d + 'Z');
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
' at ' + dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
},
init(){
_bid=Math.floor(Math.random()*10000);
const dataEl=document.getElementById('page-data');
if(dataEl){try{const data=JSON.parse(dataEl.textContent);this.pid=data.id;this.pageTitle=data.title||'';this.favorited=data.favorited||false;const fmt=data.content_format||'blocks';this.contentFormat=fmt;const raw=data.content||'';
if(fmt==='file'){this.fileData=data;this.fileUrl=data.file_url||'';this.loadFileContent(data);}
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);ensureBlockIds(this.blocks);}catch(e){this.blocks=[];}}
else if(raw&&raw.trim())this.blocks=this.md2b(raw);
// v5.12.0: page lock + layout options
this.isLocked=!!data.is_locked;this.lockedBy=data.locked_by||null;
this.canEdit=data.can_edit!==false;this.fullWidth=!!data.full_width;this.fontSmall=!!data.font_small;}catch(e){}}
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
this.loadInlineDBs();
window.E=this;
this.insertTable=function(){this.sync();this.pushHistory();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(function(){_tblFocus(nb.id,0,0);},60);};
SM.init();_tblInitDelegate();this.render();
this._applyLockMode();this._applyLayout();_wikiResolve();
this.loadComments();this.loadReactions(); // ancres jaunes + pastilles (v7.64)
const tEl=document.getElementById('_titleEl');
if(tEl){const syncT=()=>{tEl.classList.toggle('empty',!(tEl.textContent||'').trim());};syncT();tEl.addEventListener('input',syncT);tEl.addEventListener('blur',syncT);}
if(this.contentFormat==='blocks'&&window.__fdRT&&window.__fdRT.start){setTimeout(()=>{try{window.__fdRT.start(this);}catch(e){}},150);}
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
ct.addEventListener('keydown',e=>this.onKd(e));
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=>{this._dragEnd(evt);}});
ct.addEventListener('click',e=>{const h=e.target&&e.target.closest?e.target.closest('.block-handle'):null;if(!h)return;const w=h.closest('.block-wrapper');if(!w)return;const wid=w.getAttribute('data-id');if(!wid)return;
if(e.shiftKey){_selToggle(wid);return;}
_selClear();blockMenuOpen(e.clientX,e.clientY,wid);});
ct.addEventListener('contextmenu',e=>{if(!(e.target&&e.target.closest))return;if(e.target.closest('.ftable-cell')||e.target.closest('#_tblMenu'))return;const c=e.target.closest('.block-content, .block-wrapper, .block-column-wrapper');const el0=e.target.closest('[data-bid]');if(!c)return;const w=e.target.closest('.block-wrapper');const wid=w?w.getAttribute('data-id'):null;const bid=wid||(el0?el0.getAttribute('data-bid'):null);if(!bid)return;e.preventDefault();
// v7.68 : clic droit SUR un bloc sélectionné → on garde la sélection
// multi-blocs (le menu Clipboard coupe/copie ces blocs-là)
if(!(_selSet.size>1&&_selSet.has(wid)))_selClear();
blockMenuOpen(e.clientX,e.clientY,bid);});
// v7.68 : Ctrl+C / Ctrl+X sur une sélection multi-blocs → markdown.
// Sous un bloc unique, on laisse le navigateur (copie native).
ct.addEventListener('copy',function(ev){const E=window.E;if(!E||!E._clipState)return;const st=E._clipState();if(!st||st.si>=st.ei)return;const md=E.clipCopy(st);if(md==null)return;ev.preventDefault();ev.clipboardData.setData('text/plain',md);E.showToast('Sélection copiée');});
ct.addEventListener('cut',function(ev){const E=window.E;if(!E||!E._clipState)return;const st=E._clipState();if(!st||st.si>=st.ei)return;const md=E.clipCut(st);if(md==null)return;ev.preventDefault();ev.clipboardData.setData('text/plain',md);E.showToast('Sélection coupée');});
// v7.69 : Ctrl+C/X sur une sélection MULTI-BLOCS. Le navigateur ne
// sait pas sérialiser une sélection qui traverse deux contenteditable
// : il n'émet AUCUN événement copy et le raccourci ne fait rien. On
// prend donc le raccourci à la main. (Un menu contextuel ne peut pas
// faire confiance à getSelection(), d'où d._clipSt figé à l'ouverture.)
document.addEventListener('keydown',function(ev){
if(!(ev.ctrlKey||ev.metaKey))return;
const k=(ev.key||'').toLowerCase();
if(k!=='c'&&k!=='x')return;
if(!ev.target||!ev.target.closest||!ev.target.closest('#_blocksCt'))return; // pas dans la recherche d'un menu
const E=window.E;if(!E||!E._clipState)return;
const st=E._clipState();if(!st||st.si>=st.ei)return; // sélection mono-bloc → natif
const md=(k==='x'?E.clipCut(st):E.clipCopy(st));
if(md==null)return;
ev.preventDefault();ev.stopPropagation();
const done=()=>E.showToast(k==='x'?'Sélection coupée':'Sélection copiée');
if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(md).then(done).catch(()=>{});else done();
},true);
// v7.69 : glisser de sélection entre blocs + clic droit préservant la sélection
ct.addEventListener('mousedown',function(ev){
_mdSelRange=null; // un nouveau geste remet à zéro la sélection en attente
if(ev.button===2){const s=window.getSelection();if(s&&s.rangeCount&&!s.isCollapsed)ev.preventDefault();return;}
if(ev.button===0)_mdSelDown={x:ev.clientX,y:ev.clientY};
},true);
// en CAPTURE : SortableJS (initié sur ce même conteneur) peut
// stopPropagation en fin de glisser → le bubble n'atteint plus document
document.addEventListener('mouseup',function(ev){
const d=_mdSelDown;_mdSelDown=null;
if(!d||ev.button!==0)return;
if(Math.abs(ev.clientX-d.x)<4&&Math.abs(ev.clientY-d.y)<4)return;
const E=window.E;if(E&&E._buildSelAt)E._buildSelAt(d.x,d.y,ev.clientX,ev.clientY);
},true);
// v7.69 : le « click » d'un contenteditable efface toute sélection
// existante (focusin → collapse). On la remet juste après.
document.addEventListener('focusin',function(){
if(!_mdSelRange)return;
var r=_mdSelRange;_mdSelRange=null;
setTimeout(function(){
var s=window.getSelection();
if(s&&s.rangeCount&&!s.isCollapsed)return; // survécue : rien à faire
try{s.removeAllRanges();s.addRange(r);}catch(e){}
},0);
},true);
document.addEventListener('mousedown',_blockDocDown);
if(window.location.hash&&window.location.hash.indexOf('#fdblk-')===0){const hb=window.location.hash.slice(7);setTimeout(()=>{const selEl=document.querySelector(`[data-bid="${hb}"]`);if(selEl&&selEl.scrollIntoView)selEl.scrollIntoView({behavior:'smooth',block:'center'});if(selEl){selEl.focus();ce(selEl);}},500);}
},100);
},
// ── v5.12.0: lock + layout helpers ──
_applyLockMode(){
document.body.classList.toggle('page-locked',!!this.isLocked);
let banner=document.getElementById('_lockBanner');
if(this.isLocked){
if(!banner){
banner=document.createElement('div');banner.id='_lockBanner';
banner.className='fd-lock-banner';
banner.innerHTML='<span>🔒</span><span>This page is locked (read-only)</span>'
+'<button type="button" class="btn-sm" id="_lockUnlockBtn" style="margin-left:auto;font-size:12px;cursor:pointer;">Unlock</button>';
const wrap=document.querySelector('.page-editor-wrapper');
if(wrap)wrap.insertBefore(banner,wrap.firstChild);
}
const ub=banner.querySelector('#_lockUnlockBtn');
if(ub)ub.style.display=this.canEdit?'':'none';
}else if(banner){banner.remove();}
const ct=document.getElementById('_blocksCt');
if(ct){ct.querySelectorAll('[data-bid]').forEach(el=>el.setAttribute('contenteditable',this.canEdit?'true':'false'));}
const t=document.getElementById('_titleEl');
if(t)t.setAttribute('contenteditable',this.canEdit?'true':'false');
},
_applyLayout(){
const wrap=document.querySelector('.page-editor-wrapper');
if(!wrap)return;
wrap.classList.toggle('full-width',!!this.fullWidth);
wrap.classList.toggle('small-text',!!this.fontSmall);
// Page dont le premier bloc est une base : layout « page de base »
// Notion — titre et tableau calés à gauche, pas centrés.
const b0=this.blocks&&this.blocks[0];
wrap.classList.toggle('db-first',!!(b0&&b0.type==='embed'&&b0.embed_type==='collection'));
},
closeInviteSuggest(){ this.inviteSuggestOpen = false; this.inviteUsers = []; },
setSharePerm(a, perm){ this.updateShare(a.id, perm); a.permOpen = false; },
removeShareAndClose(a){ this.removeShare(a.id); a.permOpen = false; },
pickGeneralAccess(v){ this.generalAccess = v; this.accessMenuOpen = false; },
applyIcon(){ this.setIcon(this.iconValue); this.iconOpen = false; },
moreToggleLock(){ this.moreOpen = false; this.toggleLock(); },
moreFullWidth(){ this.moreOpen = false; this.setPageOption('full_width', !this.fullWidth); },
moreFontSmall(){ this.moreOpen = false; this.setPageOption('font_small', !this.fontSmall); },
moreSaveTemplate(){ this.moreOpen = false; this.saveAsTemplate(); },
markAndSave(){ this.dirty = true; this.save(); },
async toggleLock(){
if(!this.canEdit){this.showToast&&this.showToast('Only the person who locked this page (or an admin) can unlock it','error');return;}
const next=!this.isLocked;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/lock',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
body:JSON.stringify({locked:next})});
if(!r.ok){const d=await r.json().catch(()=>({}));this.showToast((d&&d.detail)||'Lock failed','error');return;}
this.isLocked=next;this._applyLockMode();
this.showToast(next?'🔒 Page locked':'🔓 Page unlocked');
}catch(e){this.showToast('Lock failed','error');}
},
async setPageOption(key,val){
try{
const body={};body[key]=val;
const r=await fetch('/board/api/pages/'+this.pid+'/options',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify(body)});
if(r.ok){this[key]=val;this._applyLayout();}
}catch(e){}
},
async saveAsTemplate(){
if(!this.pageTitle||!(this.pageTitle||'').trim()){this.showToast('Give the page a title first','error');return;}
const name=(this.pageTitle||'').trim()+' template';
this.tplSaving=true;this.sync();
const blocks=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','children','rows'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;});
try{
const r=await fetch('/board/api/page-templates',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
body:JSON.stringify({name:name,blocks:blocks})});
if(r.ok){this.moreOpen=false;this.showToast('📑 Saved as template: '+name);}
else{const d=await r.json().catch(()=>({}));this.showToast((d&&d.detail)||'Save failed','error');}
}catch(e){this.showToast('Save failed','error');}
this.tplSaving=false;
},
fileData:null,
fileUrl:'',
async loadFileContent(data){
const url=data.file_url||'';const mime=data.file_mime||'';const name=data.file_name||'';const self=this;
if(!url){this.blocks=[this.mkB('paragraph','File not available')];this.render();return;}
const ext=name.toLowerCase().split('.').pop();
if(mime.startsWith('image/')||['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext)){
this.blocks=[this.mkB('image','',{src:url,alt:name})];this.render();return;
}
if(mime==='application/pdf'||ext==='pdf'){
this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return;
}
const _fh=(typeof window!=='undefined'&&window.FDHighlight)?window.FDHighlight:null;
const codeExts=_fh?Object.keys(_fh.EXT_TO_LANG):['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'};
if(mime.startsWith('text/')||codeExts.includes(ext)){
try{const r=await fetch(url);const text=await r.text();
if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);}
else{const lang=_fh?_fh.langForFile(name):(langMap[ext]||'Plain Text');this.blocks=[this.mkB('code',text,{language:lang})];}
this.render();
}catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();}
return;
}
this.blocks=[this.mkB('embed','',{src:url,embed_type:'download',file_name:name,file_size:data.file_size||0,file_mime:mime})];this.render();
},
mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='toggle')b.children=[];if(type==='button'){b.automation_id=0;b.automation_name='';}return b;},
// v5.11.0: rewrite one block's text (e.g. after inserting a wiki token)
// and re-render in place, caret at end.
_commitBlockText(b,text){
this.sync();this.pushHistory();
b.content=text;
this.dirty=true;this.autoSave();this.render();_rtSync();
const el=this.getEl(b.id);
if(el){el.focus();const r=document.createRange();r.selectNodeContents(el);r.collapse(false);const s=window.getSelection();s.removeAllRanges();s.addRange(r);}
if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow();
},
getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;},
getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);},
getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};},
_dragEnd(evt){
const draggedId=evt.item&&evt.item.getAttribute?evt.item.getAttribute('data-id'):null;
const selected=_selIndicesSorted();
const group=_selSet.has(draggedId)&&selected.length>1;
if(evt.oldIndex===evt.newIndex&&!group)return;
this.sync();this.pushHistory();
const byId={};this.blocks.forEach(b=>{byId[b.id]=b;});
if(group){
const groupIds={};selected.forEach(i=>{const bb=this.blocks[i];if(bb)groupIds[bb.id]=1;});
const groupBlocks=selected.map(i=>this.blocks[i]).filter(Boolean);
const ct=document.getElementById('_blocksCt');
const domIds=[];ct.querySelectorAll('.block-wrapper[data-id]').forEach(w=>{const id=w.getAttribute('data-id');if(byId[id])domIds.push(id);});
let inserted=false;const out=[];
for(let i=0;i<domIds.length;i++){
const id=domIds[i];
if(groupIds[id]){if(!inserted){groupBlocks.forEach(gb=>out.push(gb));inserted=true;}continue;}
out.push(byId[id]);
}
if(!inserted)groupBlocks.forEach(gb=>out.push(gb));
if(out.length)this.blocks=out;
}else{
const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);
}
this.dirty=true;this.autoSave();this.render();_rtSync();
},
focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}},
onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];if(b&&(b.type==='table'||b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='button')){this.addAt(this.blocks.length,'paragraph');}else{const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}}},
sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='table'){const wrap=ct.querySelector(`[data-bid="${b.id}"].ftable-editor`);if(wrap){const rows=[];wrap.querySelectorAll('tr.ftable-row').forEach(function(tr){const cells=tr.querySelectorAll('th.ftable-cell,td.ftable-cell');if(!cells.length)return;const rowarr=[];cells.forEach(function(cell){rowarr.push(cell.textContent||'');});rows.push(rowarr);});if(rows.length)b.rows=rows;}continue;}
if(b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='table_of_contents'||b.type==='button')continue;
if(b.type==='math'){const te=ct.querySelector(`[data-math-bid="${b.id}"]`);if(te)b.content=te.value||'';continue;}
const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el&&!el.classList.contains('toggle-title'))b.content=gtMd(el)||'';else if(el)b.content=gtMd(el)||'';
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtMd(che)||'';});
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtMd(che)||'';});
}},
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
// hljs: syntax-highlight code blocks (FDHighlight skips unchanged
// blocks via content fingerprint; spans are sync-transparent).
try{if(window.FDHighlight)FDHighlight.highlightAllIn(ct);}catch(e){}
// Render math blocks with KaTeX
var self=this;
ct.querySelectorAll('.math-display').forEach(function(el){var bid=el.dataset.bid;var b=self.blocks.find(function(x){return x.id===bid;});self._renderKatex(el,b?b.content:'');});
ct.querySelectorAll('.fd-math-inline').forEach(function(el){var tex=el.getAttribute('data-tex')||'';if(window.katex&&tex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:false,throwOnError:false});}catch(e){el.textContent=tex;}}else if(tex&&!el.textContent){el.textContent=tex;}});
ct.querySelectorAll('.math-editor').forEach(function(ta){var disp=ct.querySelector(`.math-display[data-bid="${ta.dataset.mathBid}"]`);ta.addEventListener('input',function(){self._renderKatex(disp,ta.value);self.dirty=true;self.autoSave();});});
// Initialize inline database blocks (scripts in innerHTML don't execute)
setTimeout(()=>{ct.querySelectorAll('.block-embed-collection').forEach(el=>{const cid=parseInt(el.id.replace('inline-db-',''));if(cid&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,cid,null);});},50);
// Initialize AI Meeting Note widgets (block type 'meeting')
setTimeout(()=>{ct.querySelectorAll('[data-meeting-bid]').forEach(function(el){if(window.FDMeeting)window.FDMeeting.mount(el.getAttribute('data-meeting-bid'),el);});},50);
ct.querySelectorAll('[data-bid]').forEach(el=>{el.addEventListener('input',()=>{self.dirty=true;self.autoSave();if(el.hasAttribute&&el.hasAttribute('data-placeholder'))el.classList.toggle('empty',!(el.textContent||'').trim());if(window.AIAC)window.AIAC.schedule(el.getAttribute('data-bid'));})});
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
_selApply();
try{this._paintAnchors();}catch(e){} // ancres jaunes + pastilles (v7.64)
var self2=this;
setTimeout(function(){
if(window._wikiResolve)_wikiResolve();
if(!self2.canEdit)self2._applyLockMode();
const ub=document.getElementById('_lockUnlockBtn');
if(ub&&!ub._wired){ub._wired=true;ub.onclick=function(){window.E.toggleLock();};}
},30);
},
_renderKatex(el,tex){
if(!el)return;if(!tex||!tex.trim()){el.innerHTML='<span style="color:var(--text-dim);font-style:italic;">Empty math block</span>';return;}
if(window.katex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:true,throwOnError:false});}catch(e){el.innerHTML='<span style="color:#e5484d;">Invalid LaTeX</span>';}}
else{el.innerHTML='<span>'+esc(tex)+'</span>';}
},
replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pushHistory();b.type=type;b.content='';if(type==='toggle'){b.expanded=true;b.children=[];}if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='math'){}if(type==='button'){b.automation_id=0;b.automation_name='';}if(type==='embed'){b.embed_type='';b.src='';b.height=520;}if(type==='bookmark'){b.url='';b.title='';b.description='';b.image='';b.site_name='';}if(type==='video'){b.src='';b.title='';}if(type==='audio'){b.src='';b.title='';}this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
addAt(idx,type,content){this.sync();this.pushHistory();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;},
addAfter(idx,type){return this.addAt(idx+1,type);},
removeBlock(idx){this.sync();this.pushHistory();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();this._rtSync();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
// ── v5.4.0/v5.5.0: Media block URL setters (click empty block → prompt) ──
_mediaPrompt(bid, field, label){
const idx=this.getIdx(bid); if(idx<0)return;
const b=this.blocks[idx]; if(!b)return;
const cur=b[field]||''; const u=prompt(label+' (URL):', cur);
if(u===null)return;
if(u.trim()===cur.trim())return;
this.sync();this.pushHistory();b[field]=u.trim();this.dirty=true;this.autoSave();this.render();_rtSync();
},
async setEmbedUrl(bid){
const idx=this.getIdx(bid); if(idx<0)return;
const b=this.blocks[idx]; if(!b)return;
const cur=b.src||''; const u=prompt('Embed URL (YouTube, Vimeo, Figma, Maps…):', cur);
if(u===null)return;
if(u.trim()===cur.trim())return;
this.sync();this.pushHistory();b.src=u.trim();b.embed_type='';this.dirty=true;
try{
const r=await fetch('/board/api/embed/resolve',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({url:u.trim()})});
const d=await r.json();
if(d&&d.ok&&d.src){b.embed_src=d.src;b.embed_provider=d.provider||'';}
}catch(e){}
this.render();this.autoSave();_rtSync();
},
setBookmarkUrl(bid){
const idx=this.getIdx(bid); if(idx<0)return;
const b=this.blocks[idx]; if(!b)return;
const cur=b.url||''; const u=prompt('Bookmark URL:', cur);
if(u===null)return;
if(u.trim()===cur.trim())return;
this.sync();this.pushHistory();b.url=u.trim();
this._fetchBookmarkPreview(bid, u.trim());
},
async _fetchBookmarkPreview(bid, url){
try{
const r=await fetch('/board/api/og/metadata',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({url})});
const d=await r.json();
if(d&&d.ok){
const idx=this.getIdx(bid); if(idx<0)return;
const b=this.blocks[idx];
if(d.title)b.title=d.title;
if(d.description)b.description=d.description;
if(d.image)b.image=d.image;
if(d.site_name)b.site_name=d.site_name;
this.render();this.dirty=true;this.autoSave();_rtSync();
}
}catch(e){}
},
setVideoSrc(bid){this._mediaPrompt(bid,'src','Video');},
setAudioSrc(bid){this._mediaPrompt(bid,'src','Audio');},
// ── v5.5.0: Image lightbox (fullscreen + keyboard navigation) ──
_openLightbox(imgEl){
const full=imgEl.dataset.full||imgEl.src;
if(!full)return;
const imgs=Array.from(document.querySelectorAll('.block-image img, .fd-img')).filter(i=>i.dataset.full||i.src);
let idx=imgs.indexOf(imgEl);
if(idx<0)idx=0;
const overlay=document.createElement('div');overlay.className='fd-lightbox';overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:5000;display:flex;align-items:center;justify-content:center;flex-direction:column;';
overlay.innerHTML='<button class="fd-lightbox-nav" data-nav="prev" aria-label="Previous">‹</button>'
+'<img class="fd-lightbox-img" style="max-width:95vw;max-height:88vh;border-radius:4px;box-shadow:0 0 40px rgba(0,0,0,.8);">'
+'<button class="fd-lightbox-nav" data-nav="next" aria-label="Next">›</button>'
+'<div class="fd-lightbox-hint" style="margin-top:14px;color:#999;font-size:13px;"></div>';
document.body.appendChild(overlay);
const img=overlay.querySelector('.fd-lightbox-img');
const hint=overlay.querySelector('.fd-lightbox-hint');
const show=()=>{const cur=imgs[idx];img.src=cur.dataset.full||cur.src;hint.textContent=(imgs.length>1?(idx+1)+' / '+imgs.length+' — ':'')+'← → to navigate · Esc to close';};
const step=(d)=>{if(imgs.length<2)return;idx=(idx+d+imgs.length)%imgs.length;show();};
show();
const close=()=>{overlay.remove();document.removeEventListener('keydown',kd);};
const kd=(e)=>{if(e.key==='Escape')close();else if(e.key==='ArrowRight')step(1);else if(e.key==='ArrowLeft')step(-1);};
document.addEventListener('keydown',kd);
overlay.querySelectorAll('[data-nav]').forEach(btn=>btn.addEventListener('click',e=>{e.stopPropagation();step(btn.dataset.nav==='next'?1:-1);}));
overlay.addEventListener('click',e=>{if(e.target===overlay)close();});
},
// ── v5.4.0: Version history UI ──
versionsOpen:false,versionsLoading:false,versionsList:[],
async toggleVersions(){
this.versionsOpen=!this.versionsOpen;
if(this.versionsOpen){await this.loadVersions();}
},
async loadVersions(){
this.versionsLoading=true;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/versions');
const d=await r.json();
this.versionsList=d.versions||[];
}catch(e){this.versionsList=[];}
this.versionsLoading=false;
},
async restoreVersion(vid){
if(!confirm('Restore this version? Current content will be replaced.'))return;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/versions/'+vid+'/restore',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()}});
const d=await r.json();
if(d.status==='ok'){
this.showToast('Version restored');this.versionsOpen=false;
setTimeout(()=>window.location.reload(),300);
}
}catch(e){this.showToast('Restore failed','error');}
},
// ── v5.4.0: Backlinks ──
backlinksOpen:false,backlinksLoading:false,backlinksList:[],
async toggleBacklinks(){
this.backlinksOpen=!this.backlinksOpen;
if(this.backlinksOpen){await this.loadBacklinks();}
},
async loadBacklinks(){
this.backlinksLoading=true;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/backlinks');
const d=await r.json();
this.backlinksList=d.backlinks||[];
}catch(e){this.backlinksList=[];}
this.backlinksLoading=false;
},
// ── v5.5.0: Cover & Icon ──
async loadCoverIcon(){
try{
const r=await fetch('/board/api/pages/'+this.pid);
const d=await r.json();
this.coverUrl=d.cover_url||'';
this.iconValue=d.page_icon||'';
}catch(e){}
},
toggleCover(){this.coverOpen=!this.coverOpen;if(this.coverOpen&&!this.coverUrl){this.loadCoverIcon();}},
async setCoverUrl(){
const u=prompt('Cover image URL:', this.coverUrl);
if(u===null)return;
this.coverLoading=true;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({cover_url:u})});
const d=await r.json();
if(d.status==='ok'){this.coverUrl=d.cover_url;this.coverOpen=false;this.showToast('Cover updated');}
}catch(e){this.showToast('Cover update failed','error');}
this.coverLoading=false;
},
async uploadCover(e){
const f=e.target.files?.[0];if(!f)return;
this.coverLoading=true;
const fd=new FormData();fd.append('file',f);
try{
const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()},body:fd});
const d=await r.json();
if(d.status==='ok'){this.coverUrl=d.cover_url;this.coverOpen=false;this.showToast('Cover uploaded');}
}catch(e){this.showToast('Cover upload failed','error');}
this.coverLoading=false;
e.target.value='';
},
async removeCover(){
if(!confirm('Remove cover?'))return;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'DELETE',headers:{'X-CSRF-Token':this.csrfTok()}});
const d=await r.json();
if(d.status==='ok'){this.coverUrl='';this.showToast('Cover removed');}
}catch(e){this.showToast('Remove failed','error');}
},
toggleIcon(){this.iconOpen=!this.iconOpen;},
openBacklink(b){
this.backlinksOpen = false;
window.location.href = '/pages/' + b.id;
},
fmtImportSize(f){ return Math.round(f.size / 1024) + ' KB'; },
// A20 ph3 : x-html interdit sous build CSP → effet réactif
bindIconHtml(el){
Alpine.effect(() => { el.innerHTML = this.iconHtml(); });
},
iconHtml(){
var v=this.iconValue;
if(!v)v=(this.contentFormat==='file')?'paperclip':'file';
return window.fdIconHtml?window.fdIconHtml(v,36):v;
},
openIconPicker(){
var self=this;
if(window.FDIconPicker)window.FDIconPicker.openFor({
x:120,y:160,
onPick:function(v){self.setIcon(v);},
onRemove:function(){self.setIcon('');}
});
},
async setIcon(icon){
this.iconValue=icon;
try{
const r=await fetch('/board/api/pages/'+this.pid+'/icon',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({icon})});
const d=await r.json();
if(d.status==='ok'){this.iconOpen=false;this.showToast(icon?'Icon updated':'Icon removed');}
}catch(e){this.showToast('Icon update failed','error');}
},
// ── v5.4.0: Import ──
importOpen:false,importFile:null,importText:'',
toggleImport(){this.importOpen=!this.importOpen;},
handleImportFile(e){
const f=e.target.files?.[0];if(!f)return;
this.importFile=f;
const reader=new FileReader();
reader.onload=()=>{this.importText=reader.result;};
reader.readAsText(f);
},
async doImport(){
if(!this.importText.trim()&&!this.importFile){this.showToast('Nothing to import');return;}
if(this.importFile){
const fd=new FormData();fd.append('file',this.importFile);
try{
const r=await fetch('/board/api/pages/import/file',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()},body:fd});
const d=await r.json();
if(d.status==='ok'){this.showToast(d.count+' page(s) imported');this.importOpen=false;this.importFile=null;this.importText='';}
}catch(e){this.showToast('Import failed','error');}
}else{
try{
const r=await fetch('/board/api/pages/import',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({markdown:this.importText,title:this.pageTitle||'Import'})});
const d=await r.json();
if(d.status==='ok'){this.showToast('Page imported');this.importOpen=false;this.importText='';setTimeout(()=>{window.location.href='/pages/'+d.id;},400);}
}catch(e){this.showToast('Import failed','error');}
}
},
// ── v5.10.0: Undo / Redo ──
pushHistory(){if(this._histLock)return;this.undoStack.push(JSON.parse(JSON.stringify(this.blocks)));if(this.undoStack.length>this._histCap)this.undoStack.shift();this.redoStack.length=0;},
undo(){if(!this.undoStack.length){this.showToast('Rien à annuler');return;}const fid=this._focusedBid();this.redoStack.push(JSON.parse(JSON.stringify(this.blocks)));this.applySnapshot(this.undoStack.pop());this._focusBid(fid);},
redo(){if(!this.redoStack.length){this.showToast('Rien à rétablir');return;}const fid=this._focusedBid();this.undoStack.push(JSON.parse(JSON.stringify(this.blocks)));this.applySnapshot(this.redoStack.pop());this._focusBid(fid);},
applySnapshot(snap){this._histLock=true;this.blocks=snap||[];if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();this._histLock=false;_rtSync();},
_focusedBid(){const a=this.getActiveBlock();return a?a.bid:null;},
_focusBid(bid){if(bid)setTimeout(()=>{const el=this.getEl(bid);if(el){el.focus();ce(el);}},60);},
_rtSync(){_rtSync();},
// ── v5.10.0: Duplicate ──
_deepCloneBlock(b){const c=JSON.parse(JSON.stringify(b));c.id=genId();if(Array.isArray(c.children)){c.children=c.children.map(ch=>{const n=JSON.parse(JSON.stringify(ch));n.id=genId();return n;});}return c;},
duplicateBlock(idx){if(idx<0||idx>=this.blocks.length)return;const sel=_selIndicesSorted();if(sel.length>1&&sel.includes(idx)){this.duplicateSelected();return;}this.sync();this.pushHistory();const copy=this._deepCloneBlock(this.blocks[idx]);this.blocks.splice(idx+1,0,copy);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(copy.id);_selClear();return copy;},
duplicateSelected(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const copies=indices.map(i=>this._deepCloneBlock(this.blocks[i]));const pos=indices[indices.length-1]+1;this.blocks.splice(pos,0,...copies);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(copies[0].id);_selClear();},
// ── v5.10.0: Turn into / couleurs / liens / déplacement ──
_blockTargets(idx){const sel=_selIndicesSorted();return(sel.includes(idx))?sel:[idx];},
// hljs: code-block language picker (drives FDHighlight re-render).
setCodeLang(bid,lang){const idx=this.getIdx(bid);if(idx<0)return;const b=this.blocks[idx];if(!b||b.type!=='code')return;this.sync();this.pushHistory();b.language=(window.FDHighlight?FDHighlight.normalizeLang(lang):lang)||'Plain Text';this.dirty=true;this.autoSave();this.render();_rtSync();},
turnInto(idx,type,cols){if(idx<0||idx>=this.blocks.length)return;if(!_BLOCK_TURN_TYPES.some(t=>t.id===type))return;const targets=this._blockTargets(idx);const focusId=this.blocks[idx].id;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(b.type===type)return;const keep=(b.content||'').trim();b.type=type;if(keep&&['image','embed','meeting','divider','table_of_contents','database','button','columns'].indexOf(b.type)<0){b.content=keep;}else{b.content='';}if(type==='toggle'){b.expanded=true;if(!Array.isArray(b.children))b.children=[];}if(type==='to_do')b.checked=false;if(type==='table'&&!Array.isArray(b.rows))b.rows=_tblNew(3,3);if(type==='code'&&!b.language)b.language='Plain Text';if(type==='columns'&&(!Array.isArray(b.children)||!b.children.length))b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='columns'&&cols>=2){b.children=Array.from({length:cols},()=>({id:genId(),type:'paragraph',content:''}));}if(type==='synced'){b.synced_id=0;b._synced_content=[];b._synced_title='';}if(type==='button'){b.automation_id=0;b.automation_name='';}});
if(type==='synced')setTimeout(()=>this._pickSyncedBlock(idx),80);
this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();
setTimeout(()=>{const bid=this.blocks[idx]?this.blocks[idx].id:focusId;if(type==='table'){_tblFocus(bid,0,0);}else{const el=this.getEl(bid);if(el){el.focus();ce(el);}}},60);},
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 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){r=document.createRange();r.selectNodeContents(el);}
const start=_srcOffset(el,r.startContainer,r.startOffset);
const end=_srcOffset(el,r.endContainer,r.endOffset);
if(!(end>start)){this.showToast('Sélectionnez du texte pour commenter','error');return;}
this._commentSel={range:r,bid:el.dataset.bid,start:start,end:end};
this.commentOnSelection();
},
// v7.62 : renommé — « askAI() » (sans argument, plus bas) l'écrasait,
// le menu de bloc et Ctrl+J ouvraient l'IA de page au lieu du bloc.
askAIBlock(idx){const b=this.blocks[idx];if(!b)return;if(window.AIC)window.AIC.open(idx);},
suggestEdit(idx){
const b=this.blocks[idx];if(!b||!(b.content||'').trim()){this.showToast('Bloc vide','error');return;}
this.showToast('FlowDeck AI : suggestion en cours…');
this._aiWritingRun('write',idx,{prompt:'Améliore ce bloc (grammaire, clarté, style) en gardant le sens, la langue et le format. Réponds UNIQUEMENT avec le contenu amélioré, sans préambule.\n\nBloc :\n'+b.content},'insert');
},
runSkill(idx,tpl){
const b=this.blocks[idx];if(!b||!(b.content||'').trim()){this.showToast('Bloc vide','error');return;}
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('|');
if(i<0){this.showToast('Aucune couleur récente','error');return;}
this.setBlockColor(idx,v.slice(0,i),v.slice(i+1));
},
copyBlockLink(bid){const url=window.location.origin+window.location.pathname+'#fdblk-'+bid;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(url).then(()=>this.showToast('Lien du bloc copié')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(url,'error');}},
deleteSelected(idx){const indices=_selIndicesSorted();if(indices.length<=1)return this.removeBlock(idx);this.sync();this.pushHistory();const keep=this.blocks.filter((b,i)=>indices.indexOf(i)<0);this.blocks=keep.length?keep:[this.mkB('paragraph','')];const fi=Math.min(idx,this.blocks.length-1);this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
// ── v5.11: Copy / Cut / Paste selected blocks ──
_blocksToMarkdown(blocks){return blocks.map(blocksToMarkdown).join('\n\n');},
copySelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>this.showToast('Blocs copiés')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(md,'error');}},
cutSelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>{});}this.blocks=this.blocks.filter((b,i)=>indices.indexOf(i)<0);if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Blocs coupés');},
async pasteBlocks(){let idx=this._focusedIdx();if(idx<0)idx=this.blocks.length-1;if(idx<0)return;try{const text=await navigator.clipboard.readText();if(!text||!text.trim())return;const parsed=this.md2b(text);if(!parsed.length)return;this.sync();this.pushHistory();this.blocks.splice(idx+1,0,...parsed);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(parsed[parsed.length-1].id);_selClear();}catch(e){if(e.name==='NotAllowedError'){this.showToast('Permission presse-papier refusée');}else{this.showToast('Coller impossible','error');}}},
// v7.69 : glisser la souris de bloc A vers bloc B — le navigateur ne
// sélectionne jamais à travers deux contenteditable, on reconstruit la
// plage au relâchement avec les carets sous le pointeur.
_buildSelAt(x0,y0,x1,y1){
const r0=_caretAt(x0,y0),r1=_caretAt(x1,y1);
if(!r0||!r1)return;
const b0=_blkOf(r0.startContainer),b1=_blkOf(r1.startContainer);
if(!b0||!b1||b0===b1)return; // même bloc → la sélection native suffit
const a=document.createRange();
if(r0.compareBoundaryPoints(Range.START_TO_START,r1)<=0){
a.setStart(r0.startContainer,r0.startOffset);a.setEnd(r1.startContainer,r1.startOffset);
}else{
a.setStart(r1.startContainer,r1.startOffset);a.setEnd(r0.startContainer,r0.startOffset);
}
if(a.collapsed)return;
const s=window.getSelection();s.removeAllRanges();s.addRange(a);
_mdSelRange=a; // le « click » suivant va effacer ça : on garde la copie
},
// v7.69 : Maj+↓ / Maj+↑ au début/fin d'un bloc → la sélection saute
// dans le bloc voisin (extension native impossible : blocs séparés).
_shiftBlockExtend(dir){
// le bloc de départ est celui du BOUT MOBILE de la sélection (le
// focus), pas l'élément focalisé : après une première extension le
// caret vit déjà dans le bloc voisin.
const s=window.getSelection();if(!s||!s.rangeCount)return false;
const fn=s.focusNode;if(!fn)return false;
const sbl=_blkOf(fn);if(!sbl)return false;
const si=this.getIdx(sbl.dataset.bid);if(si<0)return false;
const off=_srcOffset(sbl,fn,s.focusOffset),len=_srcNodeLen(sbl);
if(dir>0&&off<len)return false; // le navigateur étend encore dans le bloc
if(dir<0&&off>0)return false;
const nb=this.blocks[si+dir];if(!nb)return false;
const nel=this.getEl(nb.id);if(!nel)return false;
const r=s.getRangeAt(0).cloneRange();
if(dir>0)r.setEnd(nel,nel.childNodes.length);else r.setStart(nel,0);
if(r.collapsed)return false;
s.removeAllRanges();s.addRange(r);
return true;
},
// ── v7.68 : copier/couper une SÉLECTION DE TEXTE MULTI-BLOCS ──
// Sélection vivante qui traverse ≥2 blocs → plage ; sinon null (le
// navigateur gère tout seul la sélection à l'intérieur d'un bloc).
_clipState(){
const s=window.getSelection();
if(!s||!s.rangeCount||s.isCollapsed)return null;
const r=s.getRangeAt(0);
const up=n=>n?(n.nodeType===1?n:n.parentElement):null;
const se=up(r.startContainer), ee=up(r.endContainer);
const sblk=se&&se.closest?se.closest('[data-bid]'):null;
const eblk=ee&&ee.closest?ee.closest('[data-bid]'):null;
if(!sblk||!eblk)return null;
const si=this.getIdx(sblk.dataset.bid), ei=this.getIdx(eblk.dataset.bid);
if(si<0||ei<0||si>ei)return null;
return {si:si,ei:ei,
startOff:_srcOffset(sblk,r.startContainer,r.startOffset),
endOff:_srcOffset(eblk,r.endContainer,r.endOffset)};
},
// blocs dont la sélection ne couvre qu'une PARTIE (les autres = entiers)
_clipHeavy(b){return ['table','code','columns','math','image','embed','divider','meeting','button','table_of_contents'].indexOf(b.type)>=0;},
_clipMarkdown(st){
const parts=[];
for(let i=st.si;i<=st.ei;i++){
const b=this.blocks[i];if(!b)continue;
if(i===st.si&&i===st.ei)return this._anchorText(b.id,st.startOff,st.endOff);
if(i===st.si||i===st.ei){
if(this._clipHeavy(b)){parts.push(blocksToMarkdown(b));continue;}
const el=this.getEl(b.id);
parts.push(i===st.si
? this._anchorText(b.id,st.startOff,el?_srcNodeLen(el):0)
: this._anchorText(b.id,0,st.endOff));
}else parts.push(blocksToMarkdown(b));
}
return parts.join('\n\n');
},
// renvoie le markdown copié, ou null si la sélection n'est pas multi-blocs
clipCopy(st){st=st||this._clipState();if(!st)return null;this.sync();return this._clipMarkdown(st);},
clipCut(st){
st=st||this._clipState();if(!st)return null;
this.sync();this.pushHistory();
const md=this._clipMarkdown(st);
if(st.si===st.ei){
const b=this.blocks[st.si],bel=this.getEl(b.id);
b.content=this._anchorText(b.id,0,st.startOff)+this._anchorText(b.id,st.endOff,bel?_srcNodeLen(bel):0);
this.dirty=true;this.autoSave();this.render();_rtSync();
this._focusBid(b.id);
return md;
}
const first=this.blocks[st.si],last=this.blocks[st.ei];
const lastEl=this.getEl(last.id);
if(!this._clipHeavy(first))first.content=this._anchorText(first.id,0,st.startOff);
if(!this._clipHeavy(last))last.content=this._anchorText(last.id,st.endOff,lastEl?_srcNodeLen(lastEl):0);
this.blocks=this.blocks.slice(0,st.si+1).concat([last],this.blocks.slice(st.ei+1));
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
this.dirty=true;this.autoSave();this.render();_rtSync();
this._focusBid(first.id);
return md;
},
_focusedIdx(){const a=this.getActiveBlock();return a?a.idx:-1;},
async moveBlockToPage(idx,pageId){if(idx<0||idx>=this.blocks.length||pageId===this.pid)return;this.sync();this.pushHistory();const block=this.blocks[idx];const self=this;const csrf = getCsrf();;this.showToast('Déplacement du bloc…');try{const r=await fetch(`/board/api/pages/${pageId}`,{credentials:'same-origin'});const d=await r.json();let tblocks=[];if(d.content_format==='blocks'&&d.content){try{tblocks=JSON.parse(d.content);}catch(e){tblocks=[];}}tblocks.push(block);await fetch(`/board/api/pages/${pageId}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({title:d.title||'',blocks:tblocks})});
if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Bloc déplacé');
}catch(e){this.showToast('Échec du déplacement','error');}},
// ── v4.11.1: Inline AI — "Ask AI" & "AI meeting note" (Get started) ──
// Wire the contextual get-started actions into the FlowDeck Agent
// (Notion-style inline generation). The reply is streamed in the agent
// panel and inserted into this page via window.__fdInsertAI.
askAI(){
const self=this;
const title=(this.pageTitle||'Sans titre').trim();
if(!(window.fdAgent && window.fdAgent.generate)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
if(self.aiBusy) return;
self.aiBusy=true;
self.showToast('FlowDeck Agent : génération en cours…');
let ctx='';
try{ if(window.fdGetPageContext) ctx=String(window.fdGetPageContext()||''); }catch(e){}
const prompt='Rédige le contenu de la page FlowDeck intitulée « '+title+' ». Fournis un texte structuré et directement utilisable : paragraphes et, si utile, des listes à puces. Sépare chaque paragraphe par une ligne vide. Ne précède pas le texte de commentaires.';
window.fdAgent.generate(prompt, ctx).then(function(text){
self.aiBusy=false;
if(text && text.trim()){ self.applyAIBlocks(text); }
else { self.showToast('Aucun retour généré par l\u2019agent'); }
}).catch(function(err){
self.aiBusy=false;
self.showToast((err && err.message) ? err.message : 'Erreur de génération');
});
},
meetingNote(){
// v4.12.0: crée une vraie « boîte de réunion » (bloc 'meeting') au lieu
// d'insérer du texte. La page est renommée « Meeting @date » si elle
// est vide, puis le widget est ajouté (ou remplace le bloc vide initial).
var self=this;
this.gsMoreOpen=false;
var today=new Date();
var iso=today.toISOString().slice(0,10);
var title='Meeting @'+iso;
var tEl=document.getElementById('_titleEl');
var curTitle=tEl?(tEl.textContent||'').trim():'';
var blank=!curTitle||curTitle==='Untitled'||curTitle==='New page';
if(blank){
this.pageTitle=title;
if(tEl)tEl.textContent=title;
}
var mb=this.mkB('meeting','');
mb.meeting={
title:title,
date:iso,
consent:'self',
started:false,
status:'idle',
lines:[],
interim:'',
notes:'',
summary:'',
summaryError:'',
lastTab:'transcript'
};
this.sync();this.pushHistory();
var emptyFirst=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
if(emptyFirst){ this.blocks=[mb]; } else { this.blocks.push(mb); }
this.dirty=true; this.autoSave(); this.render();
this.showToast('AI Meeting Note : bloc de réunion ajouté. Cliquez sur « Start transcribing ».');
var bid=mb.id;
setTimeout(function(){ var el=document.querySelector('[data-meeting-bid="'+bid+'"]'); if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'}); if(el&&window.FDMeeting)window.FDMeeting.focus(bid); },120);
},
aiRun(prompt){
const self=this;
if(self.aiBusy) return;
if(!(window.fdAgent && window.fdAgent.ask)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
self.aiBusy=true;
window.__fdInsertAI = function(content){ window.__fdInsertAI=null; self.aiBusy=false; self.applyAIBlocks(content); };
self.showToast('FlowDeck Agent : génération en cours…');
window.fdAgent.ask(prompt);
},
applyAIBlocks(text){
// Convert agent markdown → real blocks (headings, lists, tables, code…)
// so it renders naturally (Notion-style) instead of showing raw `## …`.
const nb=this.md2b(text||'');
if(!nb.length) return;
this.sync();
const startEmpty=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
this.blocks = startEmpty ? nb : this.blocks.concat(nb);
this.dirty=true; this.autoSave(); this.render();
},
toggleFavorite(){
const csrf = getCsrf();;
const m=this.favorited?'DELETE':'POST';
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf}})
.then(r=>r.json()).then(()=>{this.favorited=!this.favorited;this.showToast(this.favorited?'Added to favorites':'Removed from favorites');
if(window.appState&&window.appState.refreshFavorites)window.appState.refreshFavorites();
}).catch(()=>{this.showToast('Failed to toggle favorite');});
},
toggleActivityOpen(){
if(!this.activityOpen){this.activityOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
else{this.activityOpen=false;}
},
toggleShareOpen(){
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);this.loadShares();}
else{this.shareOpen=false;}
},
toggleMoreOpen(){
if(!this.moreOpen){this.moreOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
else{this.moreOpen=false;}
},
_justOpened:false,
_syncIsShared(){this.pageIsShared=!!(this.pagePublished||this.generalAccess==='anyone'||(this.accessList||[]).length>0);},
togglePublish(){this.pagePublished=!this.pagePublished;this._syncIsShared();this.saveShare();},
saveShare(){
const csrf = getCsrf();;
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).then(()=>this._syncIsShared()).catch(()=>{});
},
async copyPageLink(){
console.log('copyPageLink invoked');
const url = this.pageUrl;
const fallback = () => {
const ta = document.createElement('textarea');
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select();
document.execCommand('copy'); document.body.removeChild(ta);
};
try {
await navigator.clipboard.writeText(url);
this.showToast('Link copied to clipboard');
} catch(e) { fallback(); this.showToast('Link copied to clipboard'); }
},
showToast(msg){
this.toastMsg = msg; this.toastVisible = true;
clearTimeout(this._toastTimer);
this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500);
},
publishPage() {
var self = this;
var csrf = getCsrf();;
fetch('/api/pages/' + this.pid + '/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }
}).then(function(r){ return r.json(); }).then(function(d){
if (d.is_published) {
self.pagePublished = true;
self.publishedUrl = window.location.origin + '/p/' + d.publish_slug;
self.showToast('Published to web — ' + self.publishedUrl);
} else {
self.showToast(d.detail || 'Publish failed');
}
}).catch(function(){ self.showToast('Publish failed'); });
},
unpublishPage() {
var self = this;
var csrf = getCsrf();;
fetch('/api/pages/' + this.pid + '/publish', {
method: 'DELETE',
headers: { 'X-CSRF-Token': csrf }
}).then(function(r){ return r.json(); }).then(function(d){
if (!d.is_published) {
self.pagePublished = false;
self.publishedUrl = '';
self.showToast('Unpublished');
} else {
self.showToast(d.detail || 'Unpublish failed');
}
}).catch(function(){ self.showToast('Unpublish failed'); });
},
shareInvite() {
var self = this;
if (this.inviteGroupId) {
var csrf = getCsrf();;
fetch('/api/pages/' + this.pid + '/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
body: JSON.stringify({ group_id: this.inviteGroupId, permission: this.invitePermission })
}).then(function(r){ return r.json(); }).then(function(d){
if (d.status === 'shared') {
self.inviteEmail = ''; self.inviteUserId = null; self.inviteGroupId = null;
self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false;
self.showToast('Group shared'); self.loadShares();
} else { self.showToast(d.detail || 'Share failed'); }
}).catch(function(){ self.showToast('Share failed'); });
return;
}
if (!this.inviteEmail.trim()) return;
var csrf2 = getCsrf();;
fetch('/api/pages/' + this.pid + '/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf2 },
body: JSON.stringify({ user_id: this.inviteUserId, email: this.inviteEmail.trim(), permission: this.invitePermission })
}).then(function(r){ return r.json(); }).then(function(d){
if (d.status === 'shared') {
self.inviteEmail = ''; self.inviteUserId = null; self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false;
self.showToast('Invitation sent'); self.loadShares();
} else { self.showToast(d.detail || 'Share failed'); }
}).catch(function(){ self.showToast('Share failed'); });
},
inviteInput(){
var self = this;
var q = (this.inviteEmail || '').trim();
if (!q) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteUserId = null; this.inviteGroupId = null; this.inviteSel = -1; return; }
clearTimeout(this._inviteTimer);
this._inviteTimer = setTimeout(function(){ self._loadInviteUsers(q); }, 150);
},
async _loadInviteUsers(q){
try {
const r = await fetch('/api/notifications/users/search?q=' + encodeURIComponent(q), { credentials: 'same-origin' });
const d = await r.json();
var me = this.currentUserId;
var shared = {};
(this.accessList || []).forEach(function(a){ if (a.user_login) shared[a.user_login.toLowerCase()] = true; });
this.inviteUsers = (d.users || []).filter(function(u){
return u.id !== me && !shared[(u.login || '').toLowerCase()];
});
// Groups: charger les groupes du workspace et filtrer
try {
const gr = await fetch('/api/v2/groups', { credentials: 'same-origin' });
const gd = await gr.json();
var sharedGroups = {};
(this.accessList || []).forEach(function(a){ if (a.shared_with_group_id) sharedGroups[a.shared_with_group_id] = true; });
var ql = (q || '').toLowerCase();
this.inviteGroups = (gd.groups || []).filter(function(g){
return !sharedGroups[g.id] && (!ql || (g.name || '').toLowerCase().indexOf(ql) >= 0);
}).slice(0, 8);
} catch(e2) { this.inviteGroups = []; }
this.inviteSel = this.inviteUsers.length ? 0 : -1;
this.inviteSuggestOpen = true;
} catch(e) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; }
},
inviteMove(dir){
var el = this.$refs.inviteSuggest;
var n = this.inviteUsers.length;
if (!n) return;
this.inviteSel = (this.inviteSel + dir + n) % n;
if (el) {
var item = el.querySelectorAll('.sd-user-item')[this.inviteSel];
if (item) item.scrollIntoView({ block: 'nearest' });
}
},
inviteEnter(){
if (this.inviteSuggestOpen && this.inviteSel >= 0 && this.inviteUsers[this.inviteSel]) {
this.pickInviteUser(this.inviteUsers[this.inviteSel]);
} else {
this.inviteSuggestOpen = false;
this.shareInvite();
}
},
pickInviteUser(u){
this.inviteUserId = u.id;
this.inviteGroupId = null;
this.inviteEmail = u.login || u.full_name || '';
this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
var el = this.$refs.inviteInput;
if (el) el.focus();
},
pickInviteGroup(g){
this.inviteGroupId = g.id;
this.inviteUserId = null;
this.inviteEmail = g.name || '';
this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
var el = this.$refs.inviteInput;
if (el) el.focus();
},
updateShare(sid, perm) {
var self = this;
var csrf = getCsrf();;
fetch('/api/pages/' + this.pid + '/share/' + sid, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
body: JSON.stringify({ permission: perm })
}).then(function(r){ return r.json(); }).then(function(d){
self.showToast(d.status === 'updated' ? 'Permission updated' : (d.detail || 'Update failed'));
self.loadShares();
}).catch(function(){ self.showToast('Update failed'); self.loadShares(); });
},
loadShares() {
var self = this;
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
self.accessList = d.shares || [];
self._syncIsShared();
}).catch(function(){ self.accessList = []; self._syncIsShared(); });
},
removeShare(sid) {
var self = this;
var csrf = getCsrf();;
fetch('/api/pages/' + this.pid + '/share/' + sid, {
method: 'DELETE',
headers: { 'X-CSRF-Token': csrf }
}).then(function(r){ return r.json(); }).then(function(d){
if (d.status === 'removed') { self.showToast('Share removed'); self.loadShares(); }
else { self.showToast(d.detail || 'Remove failed'); }
}).catch(function(){ self.showToast('Remove failed'); });
},
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
exportPage(fmt){
this.moreOpen=false;
this.exportOpen=false;
// Ensure latest blocks are persisted before exporting any format
var csrf = getCsrf();;
var self = this;
var doDownload = function(){
var title = encodeURIComponent((self.pageTitle || 'Untitled').trim() || 'Untitled');
var a = document.createElement('a');
a.href = '/api/export/' + (fmt==='markdown'?'markdown':fmt==='html'?'html':fmt==='pdf'?'pdf':'site') + '/' + self.pid;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
var label = {markdown:'Markdown',html:'HTML',pdf:'PDF',site:'Site'}[fmt]||'';
self.showToast('Exported as ' + label);
};
if (this.dirty) {
this.save(function(){ doDownload(); });
} else {
doDownload();
}
},
duplicatePage(){this.moreOpen=false;const csrf = getCsrf();;const t=(this.pageTitle||'').trim()||'New Page';fetch(`/board/api/pages?title=${encodeURIComponent(t+' copy')}&section=Private&project=${encodeURIComponent(PD.workspace_key||'')}`,{method:'POST',headers:{'X-CSRF-Token':csrf}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{window.showToast('Duplicate failed','error');});},
movePage(){
this.moreOpen=false;
this.moveOpen = true;
this.moveLoading = true;
var self = this;
fetch('/api/workspaces')
.then(function(r){ return r.json(); })
.then(function(d){
self.moveWorkspaces = d.workspaces || [];
self.moveLoading = false;
})
.catch(function(){ self.moveLoading = false; });
},
doMove(wsId) {
this.moveOpen = false;
var csrf = getCsrf();;
var self = this;
fetch('/board/api/pages/' + this.pid + '/move', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
body: JSON.stringify({ workspace_id: wsId })
}).then(function(r){ return r.json(); })
.then(function(d){
if (d.status === 'ok') {
self.showToast('Moved to workspace');
setTimeout(function(){ window.location.href = '/local-workspace'; }, 800);
} else {
self.showToast(d.detail || 'Move failed');
}
})
.catch(function(){ self.showToast('Move failed'); });
},
// Le route était /board/api/pages/{id}/trash → 404 permanent (le .then
// naviguait quand même : la page n'allait JAMAIS à la poubelle depuis
// l'éditeur). Route réelle : /api/pages/{id}/trash, et on ne navigue
// plus si la réponse n'est pas ok.
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf = getCsrf();fetch(`/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf}}).then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);window.location.href='/';}).catch(()=>{window.showToast('Move to Trash failed','error');});},
downloadFile(){this.moreOpen=false;if(!this.fileUrl)return;var a=document.createElement('a');a.href=this.fileUrl;a.download='';document.body.appendChild(a);a.click();document.body.removeChild(a);this.showToast('Download started','success');},
async copyFileContent(){this.moreOpen=false;if(!this.fileUrl)return;try{var r=await fetch('/api/pages/'+this.pid+'/file-content');var d=await r.json();if(d.ok&&d.content!==undefined){await navigator.clipboard.writeText(d.content);this.showToast('Content copied to clipboard','success');}else{this.showToast('Not a text file','error');}}catch(e){this.showToast('Copy failed','error');}},
// ── v4.6.0 / v5.3.0: Get Started actions — Database (template picker) ──
dbTplOpen:false, dbTplLoading:false, dbTemplates:[], dbTplTargetIdx:-1,
async createDatabase(){
this.openDbTemplatePicker(-1);
},
async openDbTemplatePicker(targetIdx){
this.gsMoreOpen=false;
this.dbTplOpen=true; this.dbTplTargetIdx=targetIdx; this.dbTplLoading=true;
var self=this;
try{
const r=await fetch('/workspace/templates/database');
const d=await r.json();
self.dbTemplates=d.templates||[];
}catch(e){ self.dbTemplates=[]; }
self.dbTplLoading=false;
self.$nextTick(()=>{ const f=document.getElementById('fd-dbtpl'); if(f) setTimeout(()=>{f.style.opacity='1';},10); });
},
closeDbTemplatePicker(){ this.dbTplOpen=false; },
async createDbFromTemplate(tplName){
const self=this;
var name = tplName ? (tplName+' — '+new Date().toLocaleDateString('fr-FR')) : 'Database';
const csrf = getCsrf();;
try{
const body={name:name, parent_page_id:this.pid};
if(tplName) body.template=tplName;
const r=await fetch('/db/inline/api',{
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},
body:JSON.stringify(body)
});
const d=await r.json();
if(d.id){
const nb=this.mkB('embed','',{embed_type:'collection',collection_id:d.id});
this.sync();
const idx=this.dbTplTargetIdx>=0?this.dbTplTargetIdx:(this.blocks.length);
this.blocks.splice(idx,0,nb);
this.dbTplOpen=false;
this.dirty=true; this.autoSave(); this.render();
this.showToast(tplName?('Database created from template — '+tplName):'Database created');
}else{
this.showToast(d.detail||'Failed to create database');
}
}catch(e){ this.showToast('Error: '+e.message); }
},
async createForm(){
const name=prompt('Form name:','New Form');
if(!name||!name.trim())return;
const csrf = getCsrf();;
try{
const r=await fetch('/db/inline/api',{
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},
body:JSON.stringify({name:name.trim(),parent_page_id:this.pid})
});
const d=await r.json();
if(d.id){
this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'});
this.showToast('Form created — '+d.name);
this.saveDBsToPage();
this.renderInlineDBs();
}else{
this.showToast('Failed to create form');
}
}catch(e){this.showToast('Error: '+e.message);}
},
saveDBsToPage(){
// Store inline DB refs in the page's blocks as a special embed block
if(this.inlineDBs.length===0)return;
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
if(existing.length===0){
this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs}));
}else{
existing.forEach(b=>{b.dbs=this.inlineDBs;});
}
this.render();
this.dirty=true;this.autoSave();
},
renderInlineDBs(){
// Re-render with inline DB blocks
this.render();
},
loadInlineDBs(){
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
if(existing.length>0){
this.inlineDBs=existing[0].dbs||[];
}
},
async onKd(e){
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.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
// focus n'a pas rejoint l'entrée du menu → on la répercute au filtre.
if(SM._in&&(e.key.length===1||e.key==='Backspace')){
const q=(el.textContent||'').replace(/^\s*\//,'');
if(SM._in.value!==q){SM._in.value=q;SM.flt();}
}
return;
}
// v7.69 : Maj+↓ / Maj+↑ franchit le bloc (les blocs sont des
// contenteditable distincts : le navigateur ne peut pas étendre)
if(e.shiftKey&&(e.key==='ArrowDown'||e.key==='ArrowUp')){
if(this._shiftBlockExtend(e.key==='ArrowDown'?1:-1)){e.preventDefault();return;}
}
if(window.AIAC&&window.AIAC.active){
if(e.key==='Tab'){e.preventDefault();e.stopPropagation();window.AIAC.accept();return;}
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);}
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){
e.preventDefault(); window.AIC.open(idx); return;
}
const i2=idx;setTimeout(()=>this.checkMd(i2),15);
}
if(e.key==='Enter'){
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
e.preventDefault();
const sp=splitCaret(el);
const before=sp?sp.before:gtTok(el)||'',after=sp?sp.after:'';
if(!before.trim()&&!after.trim()){
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
this.sync();this.pushHistory();this.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();_rtSync();
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();ce(ne);}},60);
this.dirty=true;this.autoSave();return;
}
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
this.sync();this.pushHistory();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();_rtSync();
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();cs(ne);}},60);
this.dirty=true;this.autoSave();return;
}
if(e.key==='ArrowUp'){const _sp=splitCaret(el);if(_sp&&!_sp.before.trim()&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
if(e.key==='ArrowDown'){const _sp=splitCaret(el);if(_sp&&!_sp.after.trim()&&idx<this.blocks.length-1){e.preventDefault();const next=this.getEl(this.blocks[idx+1]?.id);if(next){next.focus();cs(next);}}return;}
// Backspace / Delete sur un bloc vide → supprimer le bloc (même garde
// pour les deux touches ; le texte restant est lu comme à la sauvegarde
// pour ignorer les annotations .fd-react-chip)
if(e.key==='Backspace'||e.key==='Delete'){if(!(gtTok(el)||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();this.pushHistory();block.type='paragraph';block.content='';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
if(e.key===' '){const i3=idx;setTimeout(()=>this.checkMd(i3),15);}
if((e.ctrlKey||e.metaKey)&&!e.altKey){
if(e.key.toLowerCase()==='z'){e.preventDefault();if(e.shiftKey)this.redo();else this.undo();return;}
if(e.key.toLowerCase()==='y'){e.preventDefault();this.redo();return;}
if(e.key.toLowerCase()==='d'){e.preventDefault();const ab2=this.getActiveBlock();if(ab2)this.duplicateBlock(ab2.idx);return;}
if(e.key.toLowerCase()==='c'){
e.preventDefault();
const sel=_selIndicesSorted();
if(sel.length>0){this.copySelectedBlocks();return;}
}
if(e.key.toLowerCase()==='x'){
e.preventDefault();
const sel=_selIndicesSorted();
if(sel.length>0){this.cutSelectedBlocks();return;}
}
if(e.key.toLowerCase()==='v'){
e.preventDefault();
await this.pasteBlocks();return;
}
const m={b:'bold',i:'italic',u:'underline'};if(m[e.key]){e.preventDefault();document.execCommand(m[e.key]);this.dirty=true;this.autoSave();}}
if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');}
this.dirty=true;this.autoSave();
},
checkMd(idx){
if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||'';
const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'};
this.sync();
for(const[pfx,type] of Object.entries(sc)){if(text===pfx||text.startsWith(pfx)){this.pushHistory();block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}}
if(text==='---'){this.pushHistory();block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
if(text==='```'){this.pushHistory();block.type='code';block.content='';block.language='Plain Text';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
},
applySlash(id){
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
if(id==='database'){ this.openDbTemplatePicker(idx); return; }
if(id==='synced'){
const block=this.blocks[idx];this.sync();this.pushHistory();
block.type='synced';block.synced_id=0;block._synced_content=[];block._synced_title='';
this.render();this.dirty=true;this.autoSave();_rtSync();
// Open synced block picker
this._pickSyncedBlock(idx);
return;
}
if(id.indexOf('ai_')===0){ this.aiSlash(id,idx); return; }
if(id==='duplicate'){this.duplicateBlock(idx);return;}
if(id==='meeting'){
// v7.1.1 : bloc AI Meeting Notes (façon Notion) — même action que
// meetingNote(), mais en place dans le bloc courant (/meet).
const block=this.blocks[idx];this.sync();this.pushHistory();
const today=new Date();const iso=today.toISOString().slice(0,10);
block.type='meeting';block.content='';
block.meeting={v:2,title:'Meeting @'+iso,date:iso,state:'idle',
mode:'mic_only',instruction:'auto',lang:'fr',consent:null,
consentAudio:false,tab:'notes',channels:[],segments:[],segSeq:0,
notes:'',manual:'',interim:'',startedAt:0,elapsedBase:0,pausedMs:0,
steps:[],summaryMd:'',completeness:'',genTitle:'',titleEdited:false,
shareHidden:false,bannerHidden:false,transcriptId:null,error:'',
remoteSilentWarned:false,menuOpen:false,settingsOpen:false};
this.render();this.dirty=true;this.autoSave();_rtSync();
setTimeout(()=>{const el=document.querySelector('[data-meeting-bid="'+block.id+'"]');if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'});},60);
return;
}
if(id==='delete'){this.removeBlock(idx);return;}
if(id==='copy_link'){const blk=this.blocks[idx];if(blk)this.copyBlockLink(blk.id);return;}
if(id==='turn_into'){const blk=this.blocks[idx];if(!blk)return;const el=this.getEl(blk.id);const rk=el?el.getBoundingClientRect():null;blockMenuOpen(rk?rk.left:window.innerWidth/2,rk?rk.bottom:window.innerHeight/2,blk.id);return;}
const block=this.blocks[idx];this.sync();this.pushHistory();block.content='';
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
else if(id==='embed'){block.type='embed';block.embed_type='';block.src='';block.height=520;}
else if(id==='bookmark'){block.type='bookmark';block.url='';block.title='';block.description='';block.image='';block.site_name='';}
else if(id==='video'){block.type='video';block.src='';block.title='';}
else if(id==='audio'){block.type='audio';block.src='';block.title='';}
else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';if(id==='table'){block.rows=_tblNew(3,3);block.content='';}if(id==='button'){block.automation_id=0;block.automation_name='';}}
this.render();this.dirty=true;this.autoSave();_rtSync();
if(id==='table'){setTimeout(()=>_tblFocus(block.id,1,0),60);}
else if(id==='math'){setTimeout(()=>{const ne=document.querySelector(`[data-math-bid="${block.id}"]`);if(ne)ne.focus();},60);}
else if(id==='columns'){setTimeout(()=>{const ne=this.getEl(block.children[0].id);if(ne)ne.focus();},60);}
else{setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
},
// ── v5.9.0: AI writing slash commands ──
aiSlash(id,idx){
const action=id.slice(3); const block=this.blocks[idx];
if(block){ this.sync(); this.pushHistory(); block.content=''; }
if(action==='write'){ this.render(); this.dirty=true; this.autoSave(); if(window.AIC)window.AIC.open(idx); return; }
if(action==='translate'){ this.render(); this.dirty=true; this.autoSave();
const lang=window.prompt('Traduire en…','English'); if(!lang){return;}
this._aiWritingRun('translate',idx,{target_language:lang},'replace'); return; }
this.render(); this.dirty=true; this.autoSave();
this._aiWritingRun(action,idx,{},'insert');
},
_aiWritingRun(action,idx,extra,mode){
const self=this; this.sync();
const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
const payload={action:action,context:ctx};
if(action==='continue'){ let parts=[]; for(let i=0;i<idx;i++){ const b=this.blocks[i]; if(b&&b.content)parts.push(b.content); } payload.prefix=parts.join('\n').slice(-4000); }
if(extra&&extra.target_language)payload.target_language=extra.target_language;
if(extra&&extra.prompt)payload.prompt=extra.prompt;
if(extra&&extra.title)payload.title=extra.title;
this.showToast('FlowDeck AI : génération…');
fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify(payload)})
.then(function(r){return r.json();})
.then(function(d){ if(d&&d.ok&&d.text){ self.applyAIBlocksAt(d.text,idx,mode); } else { self.showToast((d&&d.error)||'Échec de la génération','error'); } })
.catch(function(err){ self.showToast((err&&err.message)||'Erreur réseau','error'); });
},
applyAIBlocksAt(text,idx,mode){
const nb=this.md2b(text||''); if(!nb.length)return;
this.sync(); this.pushHistory();
if(mode==='replace'){ this.blocks=nb; }
else{ const block=this.blocks[idx]; const empty=block&&block.type==='paragraph'&&!(block.content||'').trim();
if(empty){ this.blocks.splice(idx,1,...nb); } else { this.blocks.splice(idx+1,0,...nb); } }
this.dirty=true; this.autoSave(); this.render(); _rtSync();
setTimeout(()=>{ const el=this.getEl(this.blocks[Math.min(idx,this.blocks.length-1)]?.id); if(el){el.focus();ce(el);} },60);
},
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].checked=v;this.dirty=true;this.autoSave();_rtSync();}},
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].expanded=!this.blocks[idx].expanded;this.render();this.dirty=true;this.autoSave();_rtSync();}},
_addColumn(parentId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children.push({id:genId(),type:'paragraph',content:''});this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const ne=this.getEl(b.children[b.children.length-1].id);if(ne)ne.focus();},60);},
_removeColumn(parentId,childId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children=b.children.filter(function(c){return c.id!==childId;});if(b.children.length===0)b.children=[{id:genId(),type:'paragraph',content:''}];this.render();this.dirty=true;this.autoSave();_rtSync();},
_toggleCalloutIcon(bid){const picker=document.getElementById('callout-picker-'+bid);if(picker)picker.style.display=picker.style.display==='none'?'flex':'none';},
_setCalloutIcon(bid,icon){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].icon=icon;this.dirty=true;this.autoSave();this.render();_rtSync();}},
// ── v5.14.0: Synced blocks picker ──
async _pickSyncedBlock(idx){
const block=this.blocks[idx];if(!block)return;
try{
const r=await fetch('/board/api/synced-blocks');const d=await r.json();
const items=d.synced_blocks||[];
if(!items.length){this.showToast('No synced blocks yet — create one from a Board page first.');return;}
const names=items.map(function(s){return (s.title||'Untitled')+' (id:'+s.id+')';});
const choice=window.prompt('Choose a synced block:\n\n'+names.join('\n'));
if(!choice)return;
const match=choice.match(/id:(\d+)/);
if(!match)return;
const sid=parseInt(match[1]);
if(!sid){this.showToast('Invalid synced block','error');return;}
const sr=await fetch('/board/api/synced-blocks/'+sid);const sd=await sr.json();
if(!sd||!sd.content){this.showToast('Synced block not found','error');return;}
block.synced_id=sid;
block._synced_content=sd.content||[];
block._synced_title=sd.title||'';
this.render();this.dirty=true;this.autoSave();_rtSync();
}catch(e){this.showToast('Failed to load synced blocks','error');}
},
// ── v7.62.0 : menu contextuel au highlight de texte (façon Notion) ──
showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;this._sel=null;return;}const r=s.getRangeAt(0).getBoundingClientRect();
const H=300,W=230;let top=r.bottom+8;if(top+H>window.innerHeight-8)top=Math.max(8,r.top-H);
this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=top;
this.fmt.left=Math.max(8,Math.min(r.left+r.width/2-W/2,window.innerWidth-W-8));
this._sel={range:s.getRangeAt(0).cloneRange(),idx};},
_fmtRestore(){const s=window.getSelection();if(this._sel&&this._sel.range){s.removeAllRanges();s.addRange(this._sel.range);}},
_fmtBid(){const b=this.blocks[this.fmt.idx];return b?b.id:'';},
_fmtSaved(){this.sync();this.dirty=true;this.autoSave();},
// style de la 1ʳᵉ ligne : libellé du type de bloc courant
fmtStyleName(){const b=this.blocks[this.fmt.idx];const t=b?b.type:'paragraph';
const m={paragraph:'Normal Text',heading_1:'Heading 1',heading_2:'Heading 2',heading_3:'Heading 3',heading_4:'Heading 4',
bulleted_list:'Bulleted list',numbered_list:'Numbered list',to_do:'To-do list',toggle:'Toggle list',quote:'Quote',
callout:'Callout',code:'Code',math:'Block equation',divider:'Divider',table:'Table',image:'Image',embed:'Embed',button:'Button'};
return m[t]||'Normal Text';},
_fmtSubPos(ev){const r=ev.currentTarget.getBoundingClientRect();
return {x:Math.min(r.right+4,window.innerWidth-250),y:Math.min(Math.max(8,r.top),window.innerHeight-140)};},
// « Normal Text › » → sous-menu Turn into (menu de bloc existant)
fmtStyle(ev){const bid=this._fmtBid();if(!bid)return;const p=this._fmtSubPos(ev);_blockTurnMenuOpen(p.x,p.y,bid);_subHoverClose();},
// « A » → sous-menu Color
fmtColor(ev){const bid=this._fmtBid();if(!bid)return;const p=this._fmtSubPos(ev);_blockColorMenuOpen(p.x,p.y,bid);_subHoverClose();},
// mise en forme de base : le navigateur fait le toggle, sync() persiste (gtMd)
fmtApply(f){if(!this._sel)return;const idx=this.fmt.idx;this._fmtRestore();document.execCommand(f);this._fmtSaved();this.showFmt(idx);},
fmtCode(){if(!this._sel)return;this._fmtRestore();_selWrapToggle('code');this._fmtSaved();this.fmt.open=false;},
fmtHighlight(){if(!this._sel)return;this._fmtRestore();_selWrapToggle('mark');this._fmtSaved();this.fmt.open=false;},
fmtCodeBlock(){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.turnInto(i,'code');},
fmtLink(){if(!this._sel)return;this._fmtRestore();const u=prompt('URL:');if(!u)return;
document.execCommand('createLink',false,u);this._fmtSaved();this.fmt.open=false;},
fmtEquation(){if(!this._sel)return;this._fmtRestore();const s=window.getSelection();if(!s.rangeCount)return;
const tex=prompt('Equation (LaTeX):',s.toString()||'x^2');if(tex===null||!tex.trim())return;
const span=document.createElement('span');span.className='fd-math-inline';span.setAttribute('data-tex',tex.trim());
if(window.katex){try{span.innerHTML=window.katex.renderToString(tex.trim(),{displayMode:false,throwOnError:false});}catch(e){span.textContent=tex;}}
else span.textContent=tex;
const r=s.getRangeAt(0);r.deleteContents();r.insertNode(span);
this._fmtSaved();this.fmt.open=false;},
// « … » → 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)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);},
// offsets d'une Range dans le texte rendu d'un bloc (partagé bouton 💬 + réaction)
_selSpan(bel,range){return {start:_srcOffset(bel,range.startContainer,range.startOffset),
end:_srcOffset(bel,range.endContainer,range.endOffset)};},
// « React to selected text » (v7.64) : sélecteur d'émoji avec le texte
// sélectionné en en-tête → toggle de la réaction ancrée sur la plage
fmtReact(){
if(!this._sel)return;
const E=this;const bid=this._fmtBid();const bel=this.getEl(bid);
if(!bel||!window.FDIconPicker)return;
const span=this._selSpan(bel,this._sel.range);
if(span.end<=span.start)return;
const text=this._sel.range.toString();
const r=this._sel.range.getBoundingClientRect();
this.fmt.open=false;
window.FDIconPicker.openFor({
x:Math.min(Math.max(8,r.left-40),window.innerWidth-360),
y:Math.min(Math.max(8,r.bottom+8),window.innerHeight-260),
title:text.length>90?text.slice(0,90)+'…':text,
onPick:function(emoji){E.toggleReaction(bid,span.start,span.end,emoji);}
});
},
// ── v4.9.0: Collaboration — comments & mentions ──
toggleComments(){if(this.commentsOpen){this.commentsOpen=false;this._commentSel={range:null,bid:null,start:0,end:0};return;}this.commentsOpen=true;this.loadComments();},
async loadComments(){
try{const r=await fetch('/api/pages/'+this.pid+'/comments',{credentials:'same-origin'});const d=await r.json();
this.comments=d.comments||[];this.commentCount=this.comments.length;}
catch(e){this.comments=[];}
this._paintAnchors();
// #cmt-<id> (copié depuis le menu « … » du fil) → ouvre et centre le fil
if(!this._cmtHashDone&&location.hash.indexOf('#cmt-')===0){
this._cmtHashDone=true;
const cid=parseInt(location.hash.slice(5),10);
if(!isNaN(cid))setTimeout(()=>this.openThread(cid),300);
}
},
fmtTime(s){if(!s)return '';const t=new Date((String(s).includes('T')||String(s).includes('Z'))?s:(s+'Z'));if(isNaN(t.getTime()))t=new Date(s);const diff=Math.floor((Date.now()-t.getTime())/1000);if(diff<60)return 'just now';if(diff<3600)return Math.floor(diff/60)+'m ago';if(diff<86400)return Math.floor(diff/3600)+'h ago';return Math.floor(diff/86400)+'d ago';},
csrfTok(){return getCsrf();},
async addPageComment(){
const text=(this.commentDraft||'').trim();if(!text)return;
const self=this;const sel=this._commentSel;
try{
const r=await fetch('/api/pages/'+this.pid+'/comments',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
body:JSON.stringify({body:text,anchor_block_id:sel.bid||null,anchor_start:sel.bid?sel.start:null,anchor_end:sel.bid?sel.end:null})});
const d=await r.json();
if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};await this.loadComments();this.showToast('Comment added');}
else{this.showToast(d.detail||'Failed to add comment');}
}catch(e){this.showToast('Failed to add comment');}
},
async resolveComment(c){
// accepte l'objet (le fil → bascule resolve/reopen) ou un id brut
let id=c,resolved=true;
if(c&&typeof c==='object'){id=c.id;resolved=!c.resolved;}
if(!id)return;
try{await fetch('/api/comments/'+id,{method:'PUT',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
body:JSON.stringify({resolved:resolved})});
this.commentMenu=null;await this.loadComments();}catch(e){}
},
async deleteComment(id){if(!confirm('Delete this comment?'))return;try{await fetch('/api/comments/'+id,{method:'DELETE',credentials:'same-origin',headers:{'X-CSRF-Token':this.csrfTok()}});this.commentMenu=null;await this.loadComments();}catch(e){}},
// ── v7.64 : réactions emoji sur texte + ancres commentées ──
async loadReactions(){
try{const r=await fetch('/api/pages/'+this.pid+'/reactions',{credentials:'same-origin'});
const d=await r.json();this.reactions=(d&&d.reactions)||[];}catch(e){this.reactions=[];}
this._paintAnchors();
},
async toggleReaction(bid,start,end,emoji){
try{
const r=await fetch('/api/pages/'+this.pid+'/reactions',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
body:JSON.stringify({block_id:bid,anchor_start:start,anchor_end:end,emoji:emoji})});
const d=await r.json();
if(d.status==='added'||d.status==='removed'){await this.loadReactions();}
else this.showToast((d&&d.detail)||'Réaction impossible','error');
}catch(e){this.showToast('Réaction impossible','error');}
},
// ── v7.67 : les réactions vivent aussi dans le tiroir Comments ──
// (une ligne par plage × émoji, déjà groupées par l'API)
reactionList(){
return (this.reactions||[]).map(r=>{
const bid=r.block_id,start=parseInt(r.anchor_start,10),end=parseInt(r.anchor_end,10);
return {key:bid+':'+start+':'+end+':'+r.emoji,bid:bid,start:start,end:end,emoji:r.emoji,
count:parseInt(r.count,10)||1,mine:!!parseInt(r.mine,10),
text:this._anchorText(bid,start,end)};
});
},
// suppression : POST toggle (le backend retire MA ligne si elle existe)
removeReaction(r){if(!r)return;this.toggleReaction(r.bid,r.start,r.end,r.emoji);},
// texte source d'une plage ancrée (hors pastilles .fd-react-chip)
_anchorText(bid,start,end){
const el=this.getEl(bid);if(!el||!(end>start))return '';
const w=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);let n,pos=0,txt='';
while((n=w.nextNode())){
if(n.parentElement&&n.parentElement.closest('.fd-react-chip'))continue;
const len=n.nodeValue.length,a=Math.max(start,pos),b=Math.min(end,pos+len);
if(b>a)txt+=n.nodeValue.slice(a-pos,b-pos);
pos+=len;if(pos>=end)break;
}
return txt;
},
openThread(cid){
this.commentsOpen=true;this.commentMenu=null;
const self=this;
this.loadComments().then(function(){self.$nextTick(function(){
const el=document.querySelector('[data-thread="'+cid+'"]');
if(el&&el.scrollIntoView){el.scrollIntoView({block:'center',behavior:'smooth'});
el.classList.add('thread-flash');setTimeout(function(){el.classList.remove('thread-flash');},1500);}
});});
},
commentAt(){
this.commentDraft=(this.commentDraft||'')+'@';
this.$nextTick(function(){const ta=document.querySelector('.comments-drawer textarea');
if(ta){ta.focus();ta.selectionStart=ta.selectionEnd=ta.value.length;}});
},
copyCommentLink(id){
const url=location.origin+location.pathname+'#cmt-'+id;
this.commentMenu=null;
if(navigator.clipboard&&navigator.clipboard.writeText){
navigator.clipboard.writeText(url).then(()=>this.showToast('Lien du commentaire copié')).catch(()=>this.showToast(url,'error'));
} else this.showToast(url,'error');
},
// Recalcule les ancres jaunes (commentaires) + pastilles de réaction.
// Le texte des blocs est inchangé : sync() lit la même chaîne.
_paintAnchors(){
const ct=document.getElementById('_blocksCt');if(!ct)return;
ct.querySelectorAll('.fd-react-chip').forEach(function(el){el.remove();});
ct.querySelectorAll('.fd-anchor').forEach(function(el){
const par=el.parentNode;if(!par)return;
while(el.firstChild)par.insertBefore(el.firstChild,el);
par.removeChild(el);par.normalize();
});
const self=this;
(this.comments||[]).forEach(function(c){
if(!c.anchor_block_id||c.anchor_start==null||c.anchor_end==null)return;
const el=self.getEl(c.anchor_block_id);if(!el)return;
_wrapAnchored(el,parseInt(c.anchor_start,10),parseInt(c.anchor_end,10),
'fd-anchor'+(c.resolved?' fd-anchor-done':''),{cid:c.id});
});
const groups={};
(this.reactions||[]).forEach(function(r){
const k=r.block_id+':'+r.anchor_start+':'+r.anchor_end;
if(!groups[k])groups[k]={bid:r.block_id,start:parseInt(r.anchor_start,10),end:parseInt(r.anchor_end,10),items:[]};
groups[k].items.push(r);
});
Object.keys(groups).forEach(function(k){
const g=groups[k];
const el=self.getEl(g.bid);if(!el)return;
const sp=_wrapAnchored(el,g.start,g.end,'fd-anchor fd-anchor-react',null);
if(!sp||!sp.parentNode)return;
const n=g.items.reduce(function(a,i){return a+(parseInt(i.count,10)||1);},0);
const chip=document.createElement('span');
chip.className='fd-react-chip';
chip.setAttribute('contenteditable','false');
chip.setAttribute('data-react-bid',g.bid);
chip.setAttribute('data-react-start',String(g.start));
chip.setAttribute('data-react-end',String(g.end));
// chaque émoji porte son data-emoji ; `is-mine` = j'ai réagi →
// la croix de suppression (CSS ::after au survol) ne s'affiche
// que là, un clic sur les autres rouvre le sélecteur.
chip.innerHTML=g.items.map(function(i){
const q=String(i.emoji||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;');
return '<span class="fd-react-emo'+(parseInt(i.mine,10)?' is-mine':'')+'" data-emoji="'+q+'">'+_emojiHtml(i.emoji)+'</span>';
}).join('')
+(n>1?'<span class="fd-react-n">'+n+'</span>':'');
sp.parentNode.insertBefore(chip,sp.nextSibling);
});
},
commentOnSelection(){
const sel=this._commentSel;if(!sel.range)return;
this.commentsOpen=true;this.commentDraft='';
this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();});
},
// ── @mention autocomplete (v4.9.0 users + v5.11.0 pages & dates) ──
mentionOpen:false,mentionQuery:'',mentionResults:[],mentionPages:[],mentionEl:null,
openMention(el){
const s=window.getSelection();if(!s.rangeCount)return;
const range=s.getRangeAt(0).cloneRange();
range.selectNodeContents(el);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset);
const before=range.toString();
const m=before.match(/@([\w\-\.]*)$/);
if(!m){return;}
this.mentionQuery=m[1];this.mentionEl=el;
const r=s.getRangeAt(0).getBoundingClientRect();
const menu=document.getElementById('_mentionMenu');
menu.style.display='block';
menu.style.top=Math.max(4,(r.bottom+4>window.innerHeight-300)?r.top-304:r.bottom+4)+'px';
menu.style.left=Math.min(r.left,window.innerWidth-260)+'px';
this.mentionOpen=true;this.loadMentionResults();
},
async loadMentionResults(){
const q=this.mentionQuery||'';
const self=this;
const [u,p]=await Promise.all([
fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'}).then(r=>r.ok?r.json():{users:[]}).catch(()=>({users:[]})),
fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'}).then(r=>r.ok?r.json():{pages:[]}).catch(()=>({pages:[]}))
]);
if(!this.mentionOpen)return;
this.mentionResults=u.users||[];this.mentionPages=p.pages||[];
this.renderMentionMenu();
},
_dateSuggestions(q){
const out=[];const lq=(q||'').toLowerCase();
const iso=s=>s.toISOString().slice(0,10);
const now=new Date();
if(!lq||'today'.startsWith(lq))out.push({label:'Today',iso:iso(now)});
if(!lq||'tomorrow'.startsWith(lq))out.push({label:'Tomorrow',iso:iso(new Date(now.getTime()+864e5))});
if(!lq||'yesterday'.startsWith(lq))out.push({label:'Yesterday',iso:iso(new Date(now.getTime()-864e5))});
const m=lq.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if(m)out.push({label:'Set date',iso:m[1]+'-'+m[2].padStart(2,'0')+'-'+m[3].padStart(2,'0')});
return out;
},
renderMentionMenu(){
const menu=document.getElementById('_mentionMenu');if(!menu)return;
let h='';const self=this;
h+='<div class="mention-group-label">People</div>';
(this.mentionResults||[]).slice(0,6).forEach(function(u){
h+='<div class="mention-item" data-mkind="user" data-mval="'+esc(u.login)+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
+'<div style="width:24px;height:24px;border-radius:50%;background:'+(u.avatar_color||'#3A3A3A')+';color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;overflow:hidden;">'
+((u.avatar_url)?'<img src="'+u.avatar_url+'" style="width:24px;height:24px;border-radius:50%;object-fit:cover;">':((u.full_name||u.login||'?').charAt(0).toUpperCase()))
+'</div><div><div style="font-weight:500">'+esc(u.login)+'</div><div style="font-size:11px;color:var(--text-dim,#999)">'+esc(u.full_name||'')+'</div></div></div>';
});
if((this.mentionPages||[]).length){
h+='<div class="mention-group-label">Pages</div>';
this.mentionPages.slice(0,6).forEach(function(p){
h+='<div class="mention-item" data-mkind="page" data-mval="'+p.id+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
+'<span style="width:24px;text-align:center;flex-shrink:0">'+esc(p.icon||'📄')+'</span>'
+'<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(p.title)+'</span></div>';
});
}
const ds=this._dateSuggestions(this.mentionQuery);
if(ds.length){
h+='<div class="mention-group-label">Date</div>';
ds.forEach(function(d){
h+='<div class="mention-item" data-mkind="date" data-mval="'+esc(d.iso)+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
+'<span style="width:24px;text-align:center;flex-shrink:0">📅</span>'
+'<span style="flex:1">'+esc(d.label)+'</span><span style="font-size:11px;color:var(--text-dim)">'+esc(d.iso)+'</span></div>';
});
}
if(this.mentionResults.length===0&&this.mentionPages.length===0&&ds.length===0)h='<div style="padding:10px;color:var(--text-dim,#999);font-size:13px;">No matches</div>';
menu.innerHTML=h;
menu.querySelectorAll('.mention-item').forEach(function(it){
it.addEventListener('click',function(){
const kind=it.getAttribute('data-mkind');const val=it.getAttribute('data-mval');
if(kind==='user')self.insertMention(val);
else if(kind==='page')self.insertMentionToken('[[fdpage:'+val+']] ');
else if(kind==='date')self.insertMentionToken('[[fddate:'+val+']] ');
});
});
},
// Shared non-destructive insertion: replaces the typed "@query" right
// before the caret inside the token-reconstructed text (chips kept).
_replaceAtMention(replacement){
const el=this.mentionEl;if(!el)return null;
const idx=this.getIdx(el.dataset.bid);if(idx<0)return null;
const b=this.blocks[idx];
const text=gtTok(el)||'';
// robust: compute caret by walking with a marker
let caretPos=text.length;
try{
const MK='\uF000AT';const mark=document.createElement('span');mark.textContent=MK;
const live=s.getRangeAt(0).cloneRange();live.collapse(true);live.insertNode(mark);
const full=gtTok(el)||'';const at=full.indexOf(MK);
if(at>=0)caretPos=at;
mark.remove();el.normalize();
}catch(e){}
const before=text.slice(0,caretPos);
const m=before.match(/@([\w\-\.]*)$/);
if(!m)return null;
const start=caretPos-m[0].length;
const newText=text.slice(0,start)+replacement+text.slice(caretPos);
this._commitBlockText(b,newText);
return true;
},
insertMentionToken(token){
if(this._replaceAtMention(token)){this.closeMention();this.dirty=true;this.autoSave();}
},
insertMention(login){
if(this._replaceAtMention('@'+login+' ')){
this.closeMention();this.dirty=true;this.autoSave();
this.notifyNewMentions('@'+login);
}
},
closeMention(){const menu=document.getElementById('_mentionMenu');if(menu)menu.style.display='none';this.mentionOpen=false;this.mentionEl=null;},
notifyNewMentions(text){
const self=this;
const handles=[];const re=/@([\w\-\.]+)/g;let mm;
while((mm=re.exec(text)))handles.push(mm[1].toLowerCase());
const fresh=handles.filter(function(h){return !self._notifiedMentions[h];});
if(!fresh.length)return;
fetch('/api/pages/'+this.pid+'/mentions',{method:'POST',credentials:'same-origin',
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
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)return fallback();
// un seul bloc PARAGRAPHE = coller une ligne → texte inline (comme avant) ;
// un seul bloc structuré (table, code, titre…) = vraie insertion de bloc.
if(parsed.length===1&&parsed[0].type==='paragraph')return fallback();
const sp=splitCaret(bel);
let before='',after='';
if(sp){before=sp.before;after=sp.after;}else{before=gt(bel)||'';}
this.sync();this.pushHistory();
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);},
// Reflect the page title in the left navigation, the header breadcrumb
// and the browser tab as soon as it changes.
_syncTitleUI(){
const title=(this.pageTitle||'').trim()||'Untitled';
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=title;});
document.querySelectorAll(`.sidebar-item.ws-tree-item[data-id="${this.pid}"]`).forEach(item=>{item.setAttribute('data-name',title);const n=item.querySelector('.page-name');if(n)n.textContent=title;});
document.title='FlowDeck — '+title;
window.dispatchEvent(new CustomEvent('flowdeck:page-renamed',{detail:{id:this.pid,title:title}}));
if(window.parent&&window.parent!==window){try{window.parent.postMessage({type:'fd-page-renamed',id:this.pid,title:title},window.location.origin);}catch(e){}}
},
save(cb){
if(this.fileData){
// File page: only the title is editable here. Sync the sidebar,
// header breadcrumb and browser tab live, and persist the title
// without rewriting the file's blocks.
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
this._syncTitleUI();
if(this._fileTitleT)clearTimeout(this._fileTitleT);
this._fileTitleT=setTimeout(()=>{
this._fileTitleT=null;
const title=(this.pageTitle||'').trim();
if(!title)return;
const csrf = getCsrf();;
fetch(`/board/api/pages/${this.pid}?title=${encodeURIComponent(title)}`,{method:'PUT',headers:{'X-CSRF-Token':csrf}})
.then(()=>{this.dirty=false;})
.catch(()=>{});
},600);
if(cb)cb();
return;
}
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
this._syncTitleUI();
if(this.saving){ if(cb) cb(); return; }
this.sync();
this.saving=true;
const blocksArr=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','first_col_header','colsW','meeting','automation_id','automation_name','url','title','description','image','site_name'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;});
// v6.0.0 PWA: hors ligne (ou échec réseau) → file IndexedDB rejouée au retour du réseau
const queueOffline=()=>{
if(!window.FlowOffline){this.saving=false;return;}
window.FlowOffline.savePageOffline({id:this.pid,title:this.pageTitle,content:JSON.stringify(blocksArr),content_format:'blocks'})
.then(()=>{this.saving=false;this.dirty=false;this.lastSaved='offline';if(cb)cb();})
.catch(()=>{this.saving=false;});
};
if(!navigator.onLine){queueOffline();return;}
const csrf = getCsrf();;
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({title:this.pageTitle,blocks:blocksArr})})
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
this._syncTitleUI();
if(cb)cb();
}).catch(()=>{this.saving=false;queueOffline();});
},
runAutomationButton(bid){
const idx=this.getIdx(bid);if(idx<0)return;
const b=this.blocks[idx];
if(!b.automation_id){this.pickAutomation(bid);return;}
const csrf = getCsrf();;
const self=this;
this.showToast('⚡ '+ (b.automation_name||'Automation') + '…');
fetch('/api/automations/'+b.automation_id+'/run',{method:'POST',
headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},
body:JSON.stringify({page_id:this.pid})})
.then(r=>r.json())
.then(d=>{
if(d.status==='fired'){self.showToast('✅ Automation exécutée');}
else if(d.status==='skipped'){self.showToast('⏭️ Condition non remplie (skippée)');}
else{self.showToast((d.detail||'Erreur')+'','error');}
})
.catch(()=>{self.showToast('Échec de l\u2019exécution','error');});
},
pickAutomation(bid){
const self=this;
fetch('/workspace/automations',{credentials:'same-origin'})
.then(r=>r.json())
.then(d=>{
const list=d.automations||[];
if(!list.length){self.showToast('Aucune automation — créez-en dans Paramètres → Automations','error');return;}
const ov=document.createElement('div');ov.id='fdAutomationOverlay';
ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;';
const box=document.createElement('div');box.style.cssText='background:var(--bg-secondary,#1f1f1f);border:1px solid var(--border,#333);border-radius:12px;padding:20px;width:420px;max-width:92vw;max-height:80vh;overflow:auto;';
box.innerHTML='<div style="font-weight:600;font-size:14px;margin-bottom:12px;">Choisir une automation</div>'+
list.filter(a=>a.enabled).map(function(a){
return `<button type="button" data-id="${a.id}" style="display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:9px 10px;background:var(--bg-hover,transparent);border:1px solid var(--border,#333);color:var(--text-primary);border-radius:8px;margin-bottom:8px;cursor:pointer;"><span>⚡</span><span style="flex:1;font-size:13px;">${esc(a.name||'Automation')}</span><span style="font-size:11px;color:var(--text-tertiary);">${esc(a.trigger_type||'event')}</span></button>`;
}).join('')+
'<button type="button" data-cancel="1" style="width:100%;text-align:center;padding:8px;border:none;background:transparent;color:var(--text-secondary);cursor:pointer;font-size:12px;">Annuler</button>';
ov.appendChild(box);
document.body.appendChild(ov);
box.addEventListener('click',function(ev){
const btn=ev.target.closest('[data-id]');if(btn){
const idx=self.getIdx(bid);if(idx>=0){self.blocks[idx].automation_id=parseInt(btn.getAttribute('data-id'),10);const a=list.find(function(x){return x.id===parseInt(btn.getAttribute('data-id'),10);});self.blocks[idx].automation_name=a?a.name:'';self.dirty=true;self.autoSave();self.render();}
ov.remove();
}else if(ev.target.closest('[data-cancel]')){ov.remove();}
});
})
.catch(()=>{self.showToast('Erreur de chargement','error');});
},
md2b(md){const bl=[];const lines=String(md).split('\n');let i=0;for(;i<lines.length;i++){const raw=lines[i];const t=raw.trim();if(!t)continue;
if(t.startsWith('```')){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];}bl.push(this.mkB('code',code,{language:lang}));continue;}
if(_tblIsData(t)&&i+1<lines.length&&_tblIsSep(lines[i+1])){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(function(r){if(r.length>w)w=r.length;});if(w<1)w=1;const rows2=rows.map(function(r){const a=r.slice();while(a.length<w)a.push('');return a;});bl.push(this.mkB('table','',{rows:rows2}));i=j-1;continue;}
if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));
else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));
else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));
else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));
else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
// AVANT les puces : « - [ ] … » commence aussi par « - » →
// sinon la case à cocher de l'agent devenait une puce.
else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));
else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
else if(/^\d+[.)] /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+[.)] /,'')));
else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));
else bl.push(this.mkB('paragraph',t));
}return bl;},
};
}
// v7.62 : data Alpine du wrapper éditeur — Alpine v3 n'expose plus __x
// (l'ancien accès rendait muets showFmt/mentions).
function _xed(el){ if(!el) return null; if(el.__x&&el.__x.$data) return el.__x.$data;
return (el._x_dataStack&&el._x_dataStack[0])||null; }
// v7.64 : clic sur une ancre jaune → ouvre et centre le fil de discussion ;
// clic sur une pastille de réaction → réouvre le sélecteur pour cette plage.
document.addEventListener('click',function(ev){
const t=ev.target;if(!t||!t.closest)return;
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
const chip=t.closest('.fd-react-chip');
if(chip){
const bid=chip.getAttribute('data-react-bid');
const start=parseInt(chip.getAttribute('data-react-start'),10);
const end=parseInt(chip.getAttribute('data-react-end'),10);
// v7.67 : clic sur la croix d'un émoji à moi → retire MA réaction
const emo=t.closest('.fd-react-emo');
if(emo&&emo.classList.contains('is-mine')){
d.removeReaction({bid:bid,start:start,end:end,emoji:emo.getAttribute('data-emoji')});
return;
}
const el=d.getEl(bid);const rect=chip.getBoundingClientRect();
if(!window.FDIconPicker||!el||isNaN(start)||isNaN(end))return;
window.FDIconPicker.openFor({
x:Math.min(Math.max(8,rect.left-40),window.innerWidth-360),
y:Math.min(Math.max(8,rect.bottom+8),window.innerHeight-260),
title:(el.textContent||'').slice(start,end),
onPick:function(emoji){d.toggleReaction(bid,start,end,emoji);}
});
return;
}
const anch=t.closest('.fd-anchor[data-cid]');
if(anch&&d.openThread){
const cid=parseInt(anch.getAttribute('data-cid'),10);
if(!isNaN(cid))d.openThread(cid);
}
});
document.addEventListener('mouseup',(ev)=>{setTimeout(()=>{
// v7.62 : clic dans la toolbar → on laisse la sélection (et le menu) intacts ;
// sélection relâchée/cliquée ailleurs → le menu de sélection se ferme.
if(ev.target&&ev.target.closest&&ev.target.closest('.format-toolbar'))return;
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
const s=window.getSelection();
if(!s||s.isCollapsed){d.fmt.open=false;d._sel=null;return;}
const ct=document.getElementById('_blocksCt');
if(!ct?.contains(s.anchorNode)){d.fmt.open=false;d._sel=null;return;}
const bel0=s.anchorNode&&(s.anchorNode.nodeType===1?s.anchorNode:s.anchorNode.parentElement);
const bel=bel0?bel0.closest('[data-bid]'):null;
if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}
},80);});
document.addEventListener('keyup',(e)=>{
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
if(!d.mentionOpen){if(e.key!=='@')return;}
const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return;
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(el))return;
if(d.mentionOpen){
if(e.key==='Escape'){d.closeMention();return;}
if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const items=document.querySelectorAll('#_mentionMenu .mention-item');if(!items.length)return;let idx=[].indexOf.call(items,document.querySelector('#_mentionMenu .mention-item.hover'));idx+= (e.key==='ArrowDown'?1:-1);if(idx<0)idx=items.length-1;if(idx>=items.length)idx=0;items.forEach(function(x){x.classList.remove('hover');});items[idx].classList.add('hover');return;}
if(e.key==='Enter'){e.preventDefault();const hover=document.querySelector('#_mentionMenu .mention-item.hover')||document.querySelector('#_mentionMenu .mention-item');if(hover)hover.click();return;}
}
d.openMention(el);
},true);
document.addEventListener('click',(e)=>{
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
if(!d.mentionOpen)return;
const menu=document.getElementById('_mentionMenu');
if(menu&&!menu.contains(e.target))d.closeMention();
});
var fdStyle=document.createElement('style');
fdStyle.textContent='#_mentionMenu .mention-item:hover,#_mentionMenu .mention-item.hover{background:rgba(35,131,226,.15);}';
document.head.appendChild(fdStyle);
function blocksToMarkdown(b) {
var c = b.content || '';
switch(b.type) {
case 'heading_1': return '# ' + c;
case 'heading_2': return '## ' + c;
case 'heading_3': return '### ' + c;
case 'heading_4': return '#### ' + c;
case 'bulleted_list': return '- ' + c;
case 'numbered_list': return '1. ' + c;
case 'to_do': return (b.checked ? '- [x] ' : '- [ ] ') + c;
case 'quote': return '> ' + c;
case 'divider': return '---';
case 'code': return '```' + (b.language || '') + '\n' + c + '\n```';
case 'toggle': return (b.children && b.children.length ? '## ' : '## ') + c + (b.children && b.children.length ? '\n\n' + b.children.map(blocksToMarkdown).join('\n\n') : '');
case 'math': return '$$\n' + c + '\n$$';
case 'table_of_contents': return '[TOC]';
case 'columns': return (b.children || []).map(blocksToMarkdown).join('\n\n');
case 'table': { var rows=b.rows||[]; if(!rows.length)return ''; var w=0; rows.forEach(function(r){if(r&&r.length>w)w=r.length;}); var rl=function(r){var cells=[];for(var k=0;k<w;k++)cells.push((r&&k<r.length)?String(r[k]==null?'':r[k]):'');return '| '+cells.join(' | ')+' |';}; var hasH=(b.has_header!==false),fc=!!b.first_col_header; var startRows=[],sep=false; var out=''; var ri0=0; if(hasH){startRows.push(rows[0]);ri0=1;} for(var si=0;si<startRows.length;si++){out+=(out?'\n':'')+rl(startRows[si]);} if(hasH){var seps=[];for(var k2=0;k2<w;k2++)seps.push('---');out+=(out?'\n':'')+'| '+seps.join(' | ')+' |';} for(var ri2=ri0;ri2<rows.length;ri2++)out+='\n'+rl(rows[ri2]); return out; }
default: return c;
}
}
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
// globales window — probe « Undefined variable: editorState »).
// Navigation partielle (fdLoad) : Alpine démarre déjà → 'alpine:init'
// ne sera plus émis, on enregistre tout de suite (pattern _ctx_menu.js)
// et on remonte la racine montée avant l'enregistrement.
if (window.Alpine) { Alpine.data('editorState', editorState); fdRefreshXData('editorState'); }
else document.addEventListener('alpine:init', function () { Alpine.data('editorState', editorState); });
window.editorState = editorState;
// ── Ctrl/Cmd + S : sauvegarder le document ────────────────────────
// L'autosave (1,5 s) suffit pour la frappe au quotidien, mais on ne peut
// pas fermer l'onglet « au bout d'un moment » en etant sur que tout est
// parti. Le raccourci est attendu par tout le monde : il appelle le même
// `save()` que l'autosave (meme endpoint, meme file hors-ligne) et rend
// la main explicitement.
// `window.E` survit aux navigations partielles : on exige donc que
// l'editeur soit reellement monte (`#_blocksCt`) avant d'agir, sinon
// Ctrl+S sauvegarderait un editeur detache depuis une autre page.
// ── v7.61 : raccourcis du menu contextuel de bloc ──
document.addEventListener('keydown', function (e) {
var E = window.E;
if (!E || !document.getElementById('_blocksCt')) return;
var k = String(e.key || '').toLowerCase();
var ctrl = e.ctrlKey || e.metaKey, sh = e.shiftKey, alt = e.altKey;
var menu = document.getElementById('blockMenu');
var menuBid = menu ? (menu.getAttribute('data-bid') || '') : '';
function curIdx(){
if (menuBid) { var mi = E.getIdx(menuBid); if (mi >= 0) return mi; }
var ab = E.getActiveBlock();
return ab && ab.idx >= 0 ? ab.idx : -1;
}
if (ctrl && !sh && !alt && k === 'j') { e.preventDefault(); var i1 = curIdx(); if (i1 >= 0) E.askAIBlock(i1); return; }
if (ctrl && sh && !alt && k === 'h') { e.preventDefault(); var i2 = curIdx(); if (i2 >= 0) E.applyLastColor(i2); return; }
if (ctrl && sh && !alt && k === 'm') { e.preventDefault(); var i3 = curIdx(); if (i3 >= 0) E.commentBlock(i3); return; }
if (ctrl && sh && alt && k === 'x') { e.preventDefault(); var i4 = curIdx(); if (i4 >= 0) E.suggestEdit(i4); return; }
if (alt && sh && !ctrl && k === 'l') { e.preventDefault(); var i5 = curIdx(); if (i5 >= 0) E.copyBlockLink(E.blocks[i5].id); return; }
if (alt && sh && !ctrl && k === 'e') { e.preventDefault(); var i7 = curIdx(); if (i7 >= 0) E.askAIBlock(i7); return; }
if (ctrl && sh && !alt && k === 'p') { e.preventDefault(); var i6 = curIdx(); if (i6 >= 0) _blockMoveToOpen(i6); return; }
// Ctrl+D : déjà géré par la saisie quand le bloc a le focus (pas de doublon)
if (ctrl && !sh && !alt && k === 'd') {
var ab = E.getActiveBlock();
if (ab && document.activeElement === ab.el) return;
var idc = ab && ab.idx >= 0 ? ab.idx : (menuBid ? E.getIdx(menuBid) : -1);
if (idc >= 0) { e.preventDefault(); E.duplicateBlock(idc); }
return;
}
// Del : supprime le bloc SEULEMENT avec le menu ouvert (jamais pendant la saisie)
if (k === 'delete' && !ctrl && !sh && !alt && menu) {
var tgt = e.target;
if (tgt && (tgt.tagName === 'INPUT' || tgt.tagName === 'TEXTAREA' || tgt.isContentEditable)) return;
var i8 = curIdx();
if (i8 >= 0) { e.preventDefault(); E.deleteSelected(i8); }
return;
}
if (k === 'escape' && menu) { _blockMenuClose(); return; }
});
document.addEventListener('keydown', function (e) {
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey) return;
if (String(e.key).toLowerCase() !== 's') return;
var E = window.E;
if (!E || !document.getElementById('_blocksCt') || typeof E.save !== 'function') return;
e.preventDefault(); // sinon le navigateur ouvre « Enregistrer la page »
if (E.canEdit === false) {
if (E.showToast) E.showToast('This page is locked (read-only)', 'error');
return;
}
if (E.saving) { if (E.showToast) E.showToast('Saving…'); return; }
E.dirty = true;
var notify = function () {
if (E.showToast) E.showToast('Saved', 'success');
else if (window.showToast) window.showToast('Saved', 'success');
};
try { E.save(notify); } catch (err) {
if (E.showToast) E.showToast('Save failed', 'error');
}
});
}