Files
flowdeck/app/templates/_page_editor_scripts.html
T

1181 lines
90 KiB
HTML
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.
<script type="application/json" id="page-data">
{{ page_data | tojson }}
</script>
<script>
/* eslint-disable */
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:'lightbulb'},{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:'⊞'},
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'image'}]}];
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
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 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;');}
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='';
h+='<div class="block-wrapper"><div class="block-content ftable-editor" data-bid="'+bid+'">';
h+='<div class="ftable-flex"><div class="ftable-flex-main">';
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);var tag=isHead?'th':'td';
h+='<tr class="ftable-row">';
for(var ci=0;ci<w;ci++){
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'+(isHead?' head':'')+'" contenteditable="true" data-r="'+ri+'" data-c="'+ci+'"'+wpx+' spellcheck="false">'+esc(v);
if(isHead)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></div>';
h+='</div>';
h+='<div class="ftable-addcolwrap"><button type="button" class="ftable-addcol" data-tbl="coladdright" data-bid="'+bid+'" title="Add column on the right">+</button></div>';
h+='</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(!(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);}}
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();
});
}
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],
init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML='';
const inp=document.createElement('input');inp.type='text';inp.placeholder='Filter...';
inp.style.cssText='width:100%;padding:6px 10px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
inp.oninput=()=>SM.flt();inp.onkeydown=ev=>SM._kd(ev);
const hdr=document.createElement('div');hdr.style.cssText='padding:8px;border-bottom:1px solid var(--border)';hdr.appendChild(inp);
const list=document.createElement('div');list.style.cssText='overflow-y:auto;max-height:340px';
list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}});
e.appendChild(hdr);e.appendChild(list);
SM._e=e;SM._in=inp;SM._li=list;
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target))SM.close();});
},
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='block';
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
SM._e.style.left=Math.min(r.left,window.innerWidth-324)+'px';
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='');},
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 style="padding:6px 12px 4px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px">${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:8px 12px;font-size:14px;color:var(--text-primary);cursor:pointer">
<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
<span style="flex:1;font-weight:500">${c.name}</span></div>`;});}
SM._li.innerHTML=h;},
_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);});},
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
get isOpen(){return SM._o;}};
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"><hr class="block-divider"></div>`;
if(b.type==='image')return `<div class="block-wrapper"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'"></div></div>`;
if(b.type==='table_of_contents')return renderTOC();
if(b.type==='math')return `<div class="block-wrapper"><div class="block-content block-math">${renderMathBlock(b)}</div></div>`;
if(b.type==='columns')return renderColumnsBlock(b);
if(b.type==='embed'){
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper"><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"><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"><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"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;"><div style="font-size:48px;margin-bottom:12px;">{{ fd_icon("paperclip",14) }}</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>`;
}
}
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...':'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">${esc(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">${esc(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">${esc(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">${esc(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">${esc(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)'}"`:'';
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
return `<div class="block-wrapper">
<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} class="${cls}">${inner}</${tag}>
</div>`;
}
function renderTOC(){
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>`);
}
return `<div class="block-wrapper">${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 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"><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">
<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;
window.E=null;
function editorState(){
return {
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
updatedAt:'{{ page.get("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:'{{ page.get("created_at", "") }}',
shareTab:'share',favorited:false,
pagePublished:{{ page_published | tojson }},
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
pubAllowEdit:false,pubAllowComments:true,
pageUrl:window.location.href,publishedUrl:'',
inviteEmail:'',invitePermission:'editor',accessList:[],
toastVisible:false,toastMsg:'',
// ── v4.9.0: Collaboration — comments & mentions ──
commentsOpen:false,commentDraft:'',comments:[],commentCount:0,
currentUserId:{{ user.get('id', 0) if user else 0 }},
_commentSel:{range:null,bid:null,start:0,end:0},
_notifiedMentions:{},
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';const raw=data.content||'';
if(fmt==='file'){this.fileData=data;this.loadFileContent(data);}
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}}
else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
this.loadInlineDBs();
window.E=this;
this.insertTable=function(){this.sync();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();setTimeout(function(){_tblFocus(nb.id,0,0);},60);};
SM.init();_tblInitDelegate();this.render();
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
ct.addEventListener('keydown',e=>this.onKd(e));
ct.addEventListener('paste',e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();});
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=>{if(evt.oldIndex===evt.newIndex)return;const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);this.dirty=true;this.autoSave();this.render();}});
},100);
},
fileData:null,
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=[];return b;},
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)};},
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')){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')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=el.textContent||'';else if(el)b.content=el.textContent||'';
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=che.textContent||'';});
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=che.textContent||'';});
}},
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();})});
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
},
_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();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'){}this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
addAt(idx,type,content){this.sync();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();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();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();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
// ── 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();
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){
const lines=(text||'').split(/\r?\n/).map(function(s){return s.trim();}).filter(Boolean);
if(!lines.length) return;
this.sync();
const startEmpty=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
const nb=lines.map(l=>this.mkB('paragraph',l));
this.blocks = startEmpty ? nb : this.blocks.concat(nb);
this.dirty=true; this.autoSave(); this.render();
},
toggleFavorite(){
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
const m=this.favorited?'DELETE':'POST';
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}})
.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);}
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,
togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();},
saveShare(){
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).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 = document.cookie.match(/csrf_token=([^;]+)/);
fetch('/api/pages/' + this.pid + '/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }
}).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 = document.cookie.match(/csrf_token=([^;]+)/);
fetch('/api/pages/' + this.pid + '/publish', {
method: 'DELETE',
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
}).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.inviteEmail.trim()) return;
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
fetch('/api/pages/' + this.pid + '/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
body: JSON.stringify({ email: this.inviteEmail.trim(), permission: this.invitePermission })
}).then(function(r){ return r.json(); }).then(function(d){
if (d.status === 'shared') { self.inviteEmail = ''; self.showToast('Invitation sent'); self.loadShares(); }
else { self.showToast(d.detail || 'Share failed'); }
}).catch(function(){ self.showToast('Share failed'); });
},
loadShares() {
var self = this;
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
self.accessList = d.shares || [];
}).catch(function(){ self.accessList = []; });
},
removeShare(sid) {
var self = this;
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
fetch('/api/pages/' + this.pid + '/share/' + sid, {
method: 'DELETE',
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
}).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 = document.cookie.match(/csrf_token=([^;]+)/);
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=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages?title=${encodeURIComponent(this.pageTitle+' copy')}&section=Private&project=${encodeURIComponent('{{ workspace_key }}')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).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 = document.cookie.match(/csrf_token=([^;]+)/);
var self = this;
fetch('/api/pages/' + this.pid + '/move', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
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'); });
},
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
// ── v4.6.0: Get Started actions ──
async createDatabase(){
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
try{
const name='Database';
// Create collection via API
const r=await fetch('/db/api',{
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
body:JSON.stringify({name:name})
});
const d=await r.json();
if(d.id){
// Add inline database embed block to the page
this.blocks.push(this.mkB('embed','',{embed_type:'collection',collection_id:d.id}));
this.render(); this.dirty=true; this.autoSave();
this.showToast('Database created');
}else{
this.showToast('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=document.cookie.match(/csrf_token=([^;]+)/);
try{
const r=await fetch('/db/inline/api',{
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
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||[];
}
},
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();SM._in.dispatchEvent(new KeyboardEvent('keydown',{key:e.key,bubbles:true}));return;}
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
return;
}
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)SM.open(i2,e2);},15);}
if(e.key===' '){const i2=idx;setTimeout(()=>this.checkMd(i2),15);}
if(e.key==='Enter'){
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
e.preventDefault();
const text=el.textContent||'',pos=cp(el),before=text.substring(0,pos),after=text.substring(pos);
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.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();
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();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();
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'){if(cp(el)===0&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
if(e.key==='ArrowDown'){if(cp(el)>=(el.textContent||'').length&&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();block.type='paragraph';block.content='';this.render();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){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)){block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}}
if(text==='---'){block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
if(text==='```'){block.type='code';block.content='';block.language='Plain Text';this.render();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;
const block=this.blocks[idx];this.sync();block.content='';
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
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='';}}
this.render();this.dirty=true;this.autoSave();
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);}
},
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].checked=v;this.dirty=true;this.autoSave();}},
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].expanded=!this.blocks[idx].expanded;this.render();this.dirty=true;this.autoSave();}},
_addColumn(parentId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];b.children.push({id:genId(),type:'paragraph',content:''});this.render();this.dirty=true;this.autoSave();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];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();},
_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.blocks[idx].icon=icon;this.dirty=true;this.autoSave();this.render();}},
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 (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'';},
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 ──
mentionOpen:false,mentionQuery:'',mentionResults:[],mentionEl:null,
openMention(el){
const s=window.getSelection();if(!s.rangeCount)return;
const text=el.textContent||'';const pos=s.getRangeAt(0).startOffset;
const before=text.substring(0,pos);
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-280)?r.top-284:r.bottom+4)+'px';
menu.style.left=Math.min(r.left,window.innerWidth-260)+'px';
this.mentionOpen=true;this.loadMentionUsers();
},
async loadMentionUsers(){
const q=this.mentionQuery||'';
try{const r=await fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'});const d=await r.json();this.mentionResults=d.users||[];this.renderMentionMenu();}catch(e){this.mentionResults=[];}
},
renderMentionMenu(){
const menu=document.getElementById('_mentionMenu');if(!menu)return;
let h='';const self=this;
(this.mentionResults||[]).forEach(function(u,idx){
h+='<div class="mention-item" data-login="'+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;">'
+((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">'+u.login+'</div><div style="font-size:11px;color:var(--text-dim,#999)">'+(u.full_name||'')+'</div></div></div>';
});
if(!h)h='<div style="padding:10px;color:var(--text-dim,#999);font-size:13px;">No users found</div>';
menu.innerHTML=h;
menu.querySelectorAll('.mention-item').forEach(function(it){
it.addEventListener('click',function(){self.insertMention(it.getAttribute('data-login'));});
});
},
insertMention(login){
const el=this.mentionEl;if(!el)return;
const s=window.getSelection();if(s.rangeCount){s.deleteFromDocument();}
const text=el.textContent||'';const pos=s.rangeCount? (()=>{const r=s.getRangeAt(0);return r.startOffset;} )():text.length;
const before=text.substring(0,pos);
const at=before.lastIndexOf('@');
const full=before.substring(0,at)+'@'+login+' ';
const after=text.substring(pos);
el.textContent=full+after;
const ns=window.getSelection();const nr=document.createRange();nr.selectNodeContents(el);nr.collapse(false);ns.removeAllRanges();ns.addRange(nr);
this.closeMention();this.dirty=true;this.autoSave();
this.notifyNewMentions(full);
},
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();},
autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
save(cb){
if(this.fileData)return;
if(this.saving){ if(cb) cb(); return; }
this.sync();
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'New page';
this.saving=true;
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','colsW','meeting'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
if(cb)cb();
}).catch(()=>{this.saving=false;});
},
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',''));
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('- [ ] ')||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(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 out=rl(rows[0]); var seps=[];for(var k=0;k<w;k++)seps.push('---');out+='\n| '+seps.join(' | ')+' |';for(var ri=1;ri<rows.length;ri++)out+='\n'+rl(rows[ri]);return out; }
default: return c;
}
}
</script>