Files
flowdeck/static/js/page_editor_scripts.js
T
bruno a79bdc1641
FlowDeck CI / lint (push) Successful in 2m20s
FlowDeck CI / test (push) Failing after 3h12m11s
FlowDeck CI / docker (push) Skipped
fix: « Turn into » du menu contextuel ne fonctionnait pas (v7.59.3)
- cause : la garde de turnInto() faisait _BLOCK_TURN_TYPES.indexOf(type) sur
  un tableau d'objets {id,name} → toujours -1 → retour immédiat après
  l'ouverture du sous-menu, aucun changement de format
- correction : validation par id (_BLOCK_TURN_TYPES.some(t=>t.id===type))
- même fonction : convertir un texte en To-do / Toggle vidait le contenu
  (exclusion par erreur) ; seuls les types sans champ texte (image, embed,
  meeting, divider, table_of_contents, database, button, columns) le vident
- tests : tests/test_v7593_turn_into.py (4 tests, dont 1 comportemental node
  qui exécute la vraie turnInto() extraite de la source)
- livraison : VERSION + app/main = 7.59.3, OpenAPI 525 chemins, CHANGELOG
2026-10-07 21:32:22 -04:00

2604 lines
196 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:'➡️'},
]},
{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')) return gt(e);
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.tagName==='BR'){out+='\n';continue;}
walk(n);
}
}
})(e);
return out;
}
window.gtTok=gtTok;
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(/\[\[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();});}
function _tblIsSep(s){s=(s||'').trim();if(!s||s.charAt(0)!=='|')return false;var cells=_tblSplitLine(s);if(!cells.length)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});}
function _tblIsData(s){s=(s||'').trim();return /^\|/.test(s)&&s.indexOf('|',1)>=0;}
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) ──
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'•',numbered_list:'1.',to_do:'[ ]',toggle:'▸',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'———',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions'};
// ── 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>');
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))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();
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';},
flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h='';
for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue;
h+=`<div class="sm-group" style="padding:6px 10px 3px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px">${_GROUP_LABEL[g.name]||g.name}</div>`;
items.forEach(c=>{SM._it.push(c);
h+=`<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:7px 10px;font-size:14px;color:var(--text-primary);cursor:pointer;border-radius:5px">
<span class="slash-item-icon" style="width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:15px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
<span class="slash-item-name" style="flex:1;font-weight:500">${c.name}</span>
<span class="slash-item-hint" style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);opacity:.7">${_SHORTCUT[c.id]||''}</span></div>`;});}
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.close();}},
_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;}};
// ═══════════ 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;
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);
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=`<div class="code-lang-label">${b.language||'Plain Text'}</div><pre><code data-bid="${b.id}" contenteditable="true" spellcheck="false">${mdEsc(b.content)}</code></pre>`;
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}"`;
}
const 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}
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
</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();
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'}
];
const _BLOCK_COLORS_TEXT=['default','#E5484D','#F76B15','#FFC53D','#46A758','#3E63DD','#8E4EC6','#12A594','#94A3B8'];
const _BLOCK_COLORS_BG=['default','rgba(229,72,77,.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(18,165,148,.15)','rgba(148,163,184,.2)'];
function _blockMenuClose(){var m=document.getElementById('blockMenu');if(m)m.remove();m=document.getElementById('blockMenuSub');if(m)m.remove();}
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');
if(!act)return;
ev.preventDefault();ev.stopPropagation();
var E=window.E;if(!E)return;
var idx=bid?E.getIdx(bid):-1;
if(act==='turn'){var m=document.getElementById('blockMenu');if(m){var r=m.getBoundingClientRect();_blockTurnMenuOpen(r.right+4,r.top,bid||'');}return;}
if(act==='duplicate'){E.duplicateBlock(idx);}
else if(act==='copy-link'){E.copyBlockLink(bid||'');}
else if(act==='copy-blocks'){E.copySelectedBlocks();}
else if(act==='cut-blocks'){E.cutSelectedBlocks();}
else if(act==='paste-blocks'){E.pasteBlocks();}
else if(act==='move-to'){_blockMoveToOpen(idx);}
else if(act==='delete'){E.deleteSelected(idx);}
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'));_blockMenuClose();}
_blockMenuClose();
}
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.style.cssText='position:fixed;z-index:4000;';
var colorsText='',colorsBg='';
_BLOCK_COLORS_TEXT.forEach(function(c){colorsText+='<button type="button" data-bm-act="color" data-color-key="color" data-color-val="'+c+'" title="Text color" style="background:'+(c==='default'?'transparent':c)+'" class="bm-swatch'+(c==='default'?' bm-default':'')+'">'+(c==='default'?'A':'')+'</button>';});
_BLOCK_COLORS_BG.forEach(function(c){colorsBg+='<button type="button" data-bm-act="color" data-color-key="bgColor" data-color-val="'+c+'" title="Background color" style="background:'+(c==='default'?'transparent':c)+'" class="bm-swatch'+(c==='default'?' bm-default':'')+'"></button>';});
d.innerHTML='<div class="bm-item" data-bm-act="turn" data-bid="'+b.id+'"><span class="bm-ico">↩</span>Turn into<span class="bm-arrow">▸</span></div>'
+'<div class="bm-sep"></div>'
+'<div class="bm-item" data-bm-act="duplicate" data-bid="'+b.id+'"><span class="bm-ico">▣</span>Duplicate <span class="bm-hint">'+(_rtMac()?'⌘D':'Ctrl+D')+'</span></div>'
+'<div class="bm-item" data-bm-act="copy-link" data-bid="'+b.id+'"><span class="bm-ico">🔗</span>Copy link to block</div>'
+'<div class="bm-sep"></div>'
+'<div class="bm-item" data-bm-act="copy-blocks" data-bid="'+b.id+'"><span class="bm-ico">⎘</span>Copy blocks <span class="bm-hint">'+(_rtMac()?'⌘C':'Ctrl+C')+'</span></div>'
+'<div class="bm-item" data-bm-act="cut-blocks" data-bid="'+b.id+'"><span class="bm-ico">✂</span>Cut blocks <span class="bm-hint">'+(_rtMac()?'⌘X':'Ctrl+X')+'</span></div>'
+'<div class="bm-item" data-bm-act="paste-blocks" data-bid="'+b.id+'"><span class="bm-ico">⎘</span>Paste blocks <span class="bm-hint">'+(_rtMac()?'⌘V':'Ctrl+V')+'</span></div>'
+'<div class="bm-sep"></div>'
+'<div class="bm-item" data-bm-act="move-to" data-bid="'+b.id+'"><span class="bm-ico">↪</span>Move to…</div>'
+'<div class="bm-sep"></div>'
+'<div class="bm-colors"><div class="bm-colors-label">Text</div><div class="bm-swatches">'+colorsText+'</div></div>'
+'<div class="bm-colors"><div class="bm-colors-label">Background</div><div class="bm-swatches">'+colorsBg+'</div></div>'
+'<div class="bm-sep"></div>'
+'<div class="bm-item bm-danger" data-bm-act="delete" data-bid="'+b.id+'"><span class="bm-ico">✕</span>Delete <span class="bm-hint">⌫</span></div>';
d.addEventListener('click',_blockMenuClick);
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';
}
function _blockTurnMenuOpen(x,y,bid){
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;';
var html='';
_BLOCK_TURN_TYPES.forEach(function(t){html+='<div class="bm-item" data-bm-act="turn-type" data-type="'+t.id+'" data-bid="'+bid+'">'+t.name+'</div>';});
d.innerHTML=html;
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';
}
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(_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,
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();
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();_selClear();blockMenuOpen(e.clientX,e.clientY,bid);});
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 codeExts=['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{this.blocks=[this.mkB('code',text,{language:langMap[ext]||'Plain Text'})];}
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=gtTok(el)||'';else if(el)b.content=gtTok(el)||'';
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtTok(che)||'';});
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtTok(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;
// 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('.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();
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];},
turnInto(idx,type){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==='button'){b.automation_id=0;b.automation_name='';}});
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;});this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();},
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(){const idx=this._focusedIdx();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');}}},
_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.close();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;
}
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);
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;}
if(e.key==='Backspace'){if(!(el.textContent||'').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 b2=bid;setTimeout(()=>this.checkMd(b2),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==='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');}
},
showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;return;}const r=s.getRangeAt(0).getBoundingClientRect();this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=Math.max(r.top-44,0);this.fmt.left=Math.min(r.left+r.width/2-120,window.innerWidth-260);this._sel={range:s.getRangeAt(0).cloneRange(),idx};},
fmtApply(f){this.fmt.open=false;if(!this._sel)return;const s=window.getSelection();s.removeAllRanges();s.addRange(this._sel.range);document.execCommand(f==='strikeThrough'?'strikeThrough':f);this._sel=null;this.dirty=true;this.autoSave();},
fmtLink(){this.fmt.open=false;const u=prompt('URL:');if(u){document.execCommand('createLink',false,u);this.dirty=true;this.autoSave();}},
// ── v4.9.0: Collaboration — comments & mentions ──
toggleComments(){this.commentsOpen=!this.commentsOpen;if(this.commentsOpen)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=[];}},
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};this.hideCommentBtn();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(id){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:true})});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()}});await this.loadComments();}catch(e){}},
commentOnSelection(){
const sel=this._commentSel;if(!sel.range)return;
const bid=sel.bid||'';this.commentsOpen=true;this.commentDraft='';
const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';
this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();});
},
showCommentBtn(){
const s=window.getSelection();if(!s||s.isCollapsed)return;
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(s.anchorNode))return;
const bel=s.anchorNode.parentElement? s.anchorNode.parentElement.closest('[data-bid]'):null;
if(!bel)return;
const bid=bel.dataset.bid;
const range=s.getRangeAt(0).cloneRange();range.selectNodeContents(bel);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset);
const start=range.toString().length;
const end=start+s.toString().length;
if(end<=start)return;
this._commentSel={range:s.getRangeAt(0).cloneRange(),bid:bid,start:start,end:end};
const r=s.getRangeAt(0).getBoundingClientRect();
const btn=document.getElementById('_commentSelBtn');if(!btn)return;
btn.style.display='block';
const bw=btn.offsetWidth||110,bh=btn.offsetHeight||34;
let x=r.left+r.width/2-bw/2,y=r.top-bh-8;
x=Math.min(Math.max(8,x),window.innerWidth-bw-8);if(y<8)y=r.bottom+8;
btn.style.left=x+'px';btn.style.top=y+'px';
},
hideCommentBtn(){const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';},
// ── @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<=1)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;},
};
}
document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);d.showCommentBtn();}else{d.hideCommentBtn();}},80);});
document.addEventListener('mousedown',()=>{setTimeout(()=>{const ed=document.querySelector('.page-editor-wrapper');if(ed?.__x)ed.__x.$data.hideCommentBtn();},10);});
document.addEventListener('keyup',(e)=>{
const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;
const d=ed.__x.$data;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 ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;
const d=ed.__x.$data;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.
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');
}
});
}