- "Get started with" : suppression du bouton "⋯" et de son menu mort
(Table/Board/List/Timeline/Calendar/Gallery/Import — aucun handler).
- Pilule Form : createForm() plantait sur page vide ("Cannot read
properties of undefined (reading 'push')") car this.inlineDBs n'était
jamais initialisé dans editorState() (seulement via loadInlineDBs si un
bloc inline_dbs existait déjà). Corrigé : inlineDBs:[] initialisé.
Le nom du form est demandé via une modale Alpine personnalisée
(flowdeck-modal*) au lieu du prompt() natif.
- Sidebar tree après drag&drop : refreshSidebarTree() préservait le lien
"Manage Workspaces" via ul.querySelector('li:last-child') — non scopé,
il attrapait le dernier fichier d'une sous-liste de dossier imbriquée
(premier li:last-child du document) et le ré-insérait → doublon du
dernier fichier. Corrigé : ':scope > li:last-child' (enfant direct).
3688 lines
261 KiB
JavaScript
3688 lines
261 KiB
JavaScript
|
||
/* eslint-disable */
|
||
if (!window.__fdEditorScriptsLoaded) {
|
||
window.__fdEditorScriptsLoaded = true;
|
||
const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.parse(el.textContent):{}}catch(e){return {}}})();
|
||
const CMDS=[{name:'BASIC',items:[
|
||
{id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
|
||
{id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'},
|
||
{id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'},
|
||
{id:'callout',name:'Callout',icon:'💡'},{id:'table_of_contents',name:'Table of contents',icon:'☰'},{id:'math',name:'Math equation',icon:'∑'},
|
||
{id:'columns',name:'Column list',icon:'▮'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'},
|
||
{id:'table',name:'Table',icon:'⊞'},{id:'button',name:'Button / Automation',icon:'⚡'},
|
||
]},{name:'AI',items:[
|
||
{id:'ai_write',name:'Write with AI',icon:'✨'},{id:'ai_summarize',name:'Summarize',icon:'📝'},
|
||
{id:'ai_translate',name:'Translate',icon:'🌐'},{id:'ai_continue',name:'Continue writing',icon:'➡️'},
|
||
{id:'meeting',name:'AI Meeting Notes',icon:'🎙️'},
|
||
]},
|
||
{name:'MEDIA',items:[
|
||
{id:'image',name:'Image',icon:'🖼️'},{id:'embed',name:'Embed',icon:'🔗'},
|
||
{id:'bookmark',name:'Bookmark',icon:'🔖'},{id:'video',name:'Video',icon:'🎬'},
|
||
{id:'audio',name:'Audio',icon:'🎵'},
|
||
]},
|
||
{name:'DATA',items:[{id:'database',name:'Database / Inline database',icon:'📊'}]},
|
||
{name:'TEMPLATES',items:[{id:'template',name:'Templates (base, ligne, blocs)',icon:'🧩'}]},
|
||
{name:'SYNCED',items:[{id:'synced',name:'Synced block',icon:'🔗'}]},
|
||
{name:'ACTIONS',items:[{id:'duplicate',name:'Duplicate',icon:'▣'},{id:'turn_into',name:'Turn into…',icon:'↩'},{id:'copy_link',name:'Copy link to block',icon:'🔗'},{id:'delete',name:'Delete block',icon:'✕'}]}];
|
||
|
||
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
|
||
// v5.13.1: guarantee every block (and nested child) has a stable id.
|
||
// Template blocks may be persisted without ids; leaving them undefined
|
||
// makes `data-bid="undefined"` collide and corrupts editing.
|
||
function ensureBlockIds(list){if(!Array.isArray(list))return list;list.forEach(function(b){if(b&&typeof b==='object'){if(!b.id)b.id=genId();if(Array.isArray(b.children))ensureBlockIds(b.children);}});return list;}
|
||
window.ensureBlockIds=ensureBlockIds;
|
||
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
|
||
function gt(e){return (e&&e.innerText!==undefined)?e.innerText:(e?e.textContent:'');}
|
||
// v5.11.0: read a contenteditable block back to plain text, converting
|
||
// wiki chip nodes (contenteditable=false spans) back into their raw
|
||
// [[fdpage:ID]] / [[fddate:YYYY-MM-DD]] tokens.
|
||
function gtTok(e){
|
||
if(!e) return '';
|
||
if(!e.querySelector||!e.querySelector('.fd-wiki-chip,.fd-wiki-date,.fd-react-chip')) return gt(e); // le chemin rapide (innerText) inclurait les pastilles
|
||
let out='';
|
||
(function walk(node){
|
||
for(const n of node.childNodes){
|
||
if(n.nodeType===3){out+=n.nodeValue;continue;}
|
||
if(n.nodeType===1){
|
||
if(n.classList&&n.classList.contains('fd-wiki-chip')){out+=n.getAttribute('data-token')||'';continue;}
|
||
if(n.classList&&n.classList.contains('fd-wiki-date')){out+=n.getAttribute('data-token')||'';continue;}
|
||
if(n.classList&&n.classList.contains('fd-react-chip')){continue;} // annotation, pas du contenu
|
||
if(n.tagName==='BR'){out+='\n';continue;}
|
||
walk(n);
|
||
}
|
||
}
|
||
})(e);
|
||
return out;
|
||
}
|
||
window.gtTok=gtTok;
|
||
// v7.62.0 : lecture d'un bloc vers sa source markdown, en passant par le
|
||
// DOM formaté (bold/italic/underline/strike/code/mark/liens inline posés
|
||
// par le menu de sélection). Sans ça, sync() relisait du texte brut et
|
||
// perdait tout le formatage au prochain render(). Sans balise de mise en
|
||
// forme, on retombe sur gtTok (comportement identique à l'ancien).
|
||
function gtMd(e){
|
||
if(!e) return '';
|
||
if(!e.querySelector||!e.querySelector('strong,b,em,i,u,s,del,strike,code,mark,a[href]')) return gtTok(e);
|
||
function w(node){
|
||
var out='';
|
||
for(var i=0;i<node.childNodes.length;i++){
|
||
var n=node.childNodes[i];
|
||
if(n.nodeType===3){out+=n.nodeValue;continue;}
|
||
if(n.nodeType!==1)continue;
|
||
var cl=n.classList;
|
||
if(cl&&cl.contains('fd-react-chip')){continue;} // annotation, pas du contenu
|
||
if(cl&&(cl.contains('fd-wiki-chip')||cl.contains('fd-wiki-date'))){out+=n.getAttribute('data-token')||'';continue;}
|
||
if(n.tagName==='BR'){out+='\n';continue;}
|
||
if(cl&&cl.contains('fd-math-inline')){out+='$$'+(n.getAttribute('data-tex')||'')+'$$';continue;}
|
||
var t=n.tagName, inner=w(n);
|
||
if((t==='STRONG'||t==='B')&&inner){out+='**'+inner+'**';continue;}
|
||
if((t==='EM'||t==='I')&&inner){out+='*'+inner+'*';continue;}
|
||
if(t==='U'&&inner){out+='__'+inner+'__';continue;}
|
||
if((t==='S'||t==='DEL'||t==='STRIKE')&&inner){out+='~~'+inner+'~~';continue;}
|
||
if(t==='CODE'&&inner&&!(n.hasAttribute&&n.hasAttribute('data-bid'))){out+='`'+inner+'`';continue;}
|
||
if(t==='MARK'&&inner){out+='=='+inner+'==';continue;}
|
||
if(t==='A'){var h=n.getAttribute('href')||'';out+=/^https?:/i.test(h)?'['+(inner||h)+']('+h+')':inner;continue;}
|
||
out+=inner;
|
||
}
|
||
return out;
|
||
}
|
||
return w(e);
|
||
}
|
||
window.gtMd=gtMd;
|
||
// Enveloppe/désenveloppe une sélection dans une balise inline (code, mark)
|
||
function _selWrapHost(r,tag){
|
||
var a=r.startContainer.nodeType===1?r.startContainer:r.startContainer.parentElement;
|
||
var b=r.endContainer.nodeType===1?r.endContainer:r.endContainer.parentElement;
|
||
a=a&&a.closest(tag);b=b&&b.closest(tag);
|
||
if(!a||a!==b)return null;
|
||
return (a.hasAttribute&&a.hasAttribute('data-bid'))?null:a; // <code> racine d'un bloc code : ne pas dépiler
|
||
}
|
||
function _selWrapToggle(tag){
|
||
var s=window.getSelection();if(!s.rangeCount)return;
|
||
var r=s.getRangeAt(0);
|
||
var host=_selWrapHost(r,tag);
|
||
if(host){var p=host.parentNode;while(host.firstChild)p.insertBefore(host.firstChild,host);p.removeChild(host);p.normalize();return;}
|
||
var el=document.createElement(tag);
|
||
try{r.surroundContents(el);}catch(e){el.appendChild(r.extractContents());r.insertNode(el);}
|
||
var s2=window.getSelection(),nr=document.createRange();
|
||
nr.selectNodeContents(el);s2.removeAllRanges();s2.addRange(nr);
|
||
}
|
||
// v7.64 : marquage d'une plage commentée/réagie (jaune). Ne change pas le
|
||
// texte lu par sync() : les spans n'ajoutent que des nœuds d'annotation.
|
||
function _wrapAnchored(el,start,end,cls,attrs){
|
||
if(!(end>start))return null;
|
||
let pos=0,sN=null,sO=0,eN=null,eO=0;
|
||
const walker=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);
|
||
let n=walker.nextNode();
|
||
while(n){
|
||
const len=n.nodeValue.length;
|
||
if(!sN&&pos+len>start){sN=n;sO=start-pos;}
|
||
if(sN&&pos+len>=end){eN=n;eO=end-pos;break;}
|
||
pos+=len;n=walker.nextNode();
|
||
}
|
||
if(!sN||!eN)return null;
|
||
const r=document.createRange();
|
||
try{r.setStart(sN,sO);r.setEnd(eN,eO);}catch(e){return null;}
|
||
const span=document.createElement('span');span.className=cls;
|
||
span.setAttribute('contenteditable','false');
|
||
if(attrs)for(const k in attrs)span.setAttribute('data-'+k,attrs[k]);
|
||
try{r.surroundContents(span);}catch(e){try{span.appendChild(r.extractContents());r.insertNode(span);}catch(e2){return null;}}
|
||
return span;
|
||
}
|
||
// Offsets dans le texte SOURCE d'un bloc : les pastilles .fd-react-chip
|
||
// (annotations) ne comptent pas, comme dans gtTok/gtMd.
|
||
function _srcNodeLen(node){
|
||
if(!node)return 0;
|
||
if(node.nodeType===3){
|
||
const pe=node.parentElement;
|
||
if(pe&&pe.closest&&pe.closest('.fd-react-chip'))return 0;
|
||
return node.nodeValue.length;
|
||
}
|
||
if(node.nodeType!==1)return 0;
|
||
if(node.classList&&node.classList.contains('fd-react-chip'))return 0;
|
||
let t=0;const kids=node.childNodes;
|
||
for(let i=0;i<kids.length;i++)t+=_srcNodeLen(kids[i]);
|
||
return t;
|
||
}
|
||
function _srcOffset(root,container,offset){
|
||
if(!container)return 0;
|
||
if(container.nodeType===1){
|
||
let t=0;const kids=container.childNodes;
|
||
for(let i=0;i<offset&&i<kids.length;i++)t+=_srcNodeLen(kids[i]);
|
||
return t;
|
||
}
|
||
if(container.nodeType!==3)return 0;
|
||
let t=0;
|
||
const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);
|
||
let n;
|
||
while((n=walker.nextNode())){
|
||
if(n===container){
|
||
const pe=n.parentElement;
|
||
if(pe&&pe.closest&&pe.closest('.fd-react-chip'))return t;
|
||
return t+Math.max(0,Math.min(offset,n.nodeValue.length));
|
||
}
|
||
t+=_srcNodeLen(n);
|
||
}
|
||
return t;
|
||
}
|
||
// v7.69 : position de caret à des coordonnées — la sélection native ne
|
||
// franchit pas les blocs (chacun a son contenteditable), on la rebâtit.
|
||
function _caretAt(x,y){
|
||
if(document.caretPositionFromPoint){const q=document.caretPositionFromPoint(x,y);
|
||
if(q&&q.offsetNode){const r=document.createRange();r.setStart(q.offsetNode,q.offset||0);r.collapse(true);return r;}}
|
||
if(document.caretRangeFromPoint)return document.caretRangeFromPoint(x,y);
|
||
return null;
|
||
}
|
||
function _blkOf(n){let e=n&&(n.nodeType===1?n:n.parentElement);return e&&e.closest?e.closest('[data-bid]'):null;}
|
||
function _emojiHtml(v){
|
||
const s=String(v||'');
|
||
if(/^(https?:|\/)/.test(s))return '<img src="'+esc(s)+'" alt="">';
|
||
return esc(s);
|
||
}
|
||
function splitCaret(e){
|
||
const s=window.getSelection(); if(!s.rangeCount||!s.rangeCount)return null; const sel=s.getRangeAt(0);
|
||
const MK='\uF000FD'; const mark=document.createElement('span'); mark.textContent=MK;
|
||
sel.insertNode(mark);
|
||
const full=gtTok(e)||'';
|
||
const parts=full.split(MK);
|
||
mark.remove(); e.normalize();
|
||
return {before:parts[0]||'',after:(parts.length>1?parts[1]:'')||'',content:full.split(MK).join('')};
|
||
}
|
||
function ce(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(false);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||
function cs(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(true);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
// 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,'&').replace(/</g,'<').replace(/>/g,'>')
|
||
.replace(/`([^`]+)`/g,'<code>$1</code>')
|
||
.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
|
||
.replace(/\*([^*]+)\*/g,'<em>$1</em>')
|
||
.replace(/~~([^~]+)~~/g,'<s>$1</s>')
|
||
.replace(/__([^_]+)__/g,'<u>$1</u>') // souligné (toolbar de sélection)
|
||
.replace(/==([^=]+)==/g,'<mark>$1</mark>') // surlignage
|
||
.replace(/\$\$([^$]+)\$\$/g,function(m,t){return '<span class="fd-math-inline" data-tex="'+t.replace(/"/g,'"')+'"></span>';}) // équation inline
|
||
.replace(/\[\[fdpage:(\d+)\]\]/g,function(m,pid){
|
||
return '<a class="fd-wiki-chip" contenteditable="false" href="/pages/'+pid+'" data-token="'+m+'" data-pid="'+pid+'" title="Linked page"><span class="fd-wiki-label">…</span></a>';})
|
||
.replace(/\[\[fddate:([0-9]{4}-[0-9]{2}-[0-9]{2})(?:T[0-9:]{5,8})?\]\]/g,function(m,iso){
|
||
return '<span class="fd-wiki-date" contenteditable="false" data-token="'+m+'">'+fdDateLabel(iso)+'</span>';})
|
||
.replace(/\[([^\]]+)\]\((https?:[^\)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||
.replace(/\n/g,'<br>');
|
||
}
|
||
// v5.11.0: labels of [[fdpage:ID]] chips are resolved from the live DB
|
||
// (rename propagation — tokens store only the id).
|
||
function fdDateLabel(iso){
|
||
try{
|
||
const p=iso.split('-'); const d=new Date(+p[0],+p[1]-1,+p[2]);
|
||
const DOW=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||
const MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||
return DOW[d.getDay()]+' '+(+p[2])+' '+MON[+p[1]-1]+' '+p[0];
|
||
}catch(e){return iso;}
|
||
}
|
||
function _wikiResolve(root){
|
||
const ct=root||document.getElementById('_blocksCt'); if(!ct)return;
|
||
const pids={};
|
||
ct.querySelectorAll('.fd-wiki-chip[data-pid]').forEach(function(a){
|
||
const lab=a.querySelector('.fd-wiki-label');
|
||
if(!lab||lab.getAttribute('data-done'))return;
|
||
lab.setAttribute('data-done','1');
|
||
pids[a.dataset.pid]=true;
|
||
});
|
||
const ids=Object.keys(pids); if(!ids.length)return;
|
||
fetch('/board/api/wiki/titles?ids='+encodeURIComponent(ids.join(',')),{credentials:'same-origin'})
|
||
.then(function(r){return r.json();})
|
||
.then(function(d){
|
||
const titles=d.titles||{};
|
||
ct.querySelectorAll('.fd-wiki-chip[data-pid]').forEach(function(a){
|
||
const t=titles[a.dataset.pid];
|
||
const lab=a.querySelector('.fd-wiki-label');
|
||
if(lab) lab.textContent = t||'Deleted page';
|
||
});
|
||
}).catch(function(){});
|
||
}
|
||
window._wikiResolve=_wikiResolve;
|
||
|
||
const CALLOUT_ICONS=['💡','💡','📌','⚠️','ℹ️','✅','❌','🚀','🔔','📣','⭐','❤️','🔥','💬','👀','🧠','🎯','🔑','📅','💰','💡','📚','🧪','🌍'];
|
||
|
||
/* ═══════════ Simple Table block (v4.8.0) ═══════════ */
|
||
function _tblSplitLine(s){s=(s||'').trim();if(s.charAt(0)==='|')s=s.slice(1);if(s.charAt(s.length-1)==='|')s=s.slice(0,-1);return s.split('|').map(function(x){return x.trim();});}
|
||
// v7.68 : le pipe de tête est OPTIONNEL en GFM — l'IA rend souvent
|
||
// « A | B » / « --- | --- ». Une ligne de séparation doit garder ≥2 cellules,
|
||
// sinon « texte » suivi d'un « --- » deviendrait un tableau.
|
||
function _tblIsSep(s){s=(s||'').trim();if(!s)return false;var cells=_tblSplitLine(s);if(cells.length<2)return false;return cells.every(function(c){return /^:?-{1,}:?$/.test(c);});}
|
||
function _tblIsData(s){s=(s||'').trim();return s.indexOf('|')>=0&&_tblSplitLine(s).length>=2;}
|
||
function _tblNew(rows,cols){var out=[];for(var i=0;i<rows;i++){var r=new Array(cols);for(var j=0;j<cols;j++)r[j]='';out.push(r);}return out;}
|
||
function _tblClone(b){b.rows=(b.rows||[]).map(function(r){return (r||[]).slice();});return b;}
|
||
function _tblAddCol(b,index){_tblClone(b);var w=b.rows.length?b.rows[0].length:1;if(index<0)index=0;if(index>w)index=w;b.rows.forEach(function(r){r.splice(index,0,'');});}
|
||
function _tblAddRow(b,index){_tblClone(b);var w=b.rows.length&&b.rows[0]?b.rows[0].length:1;if(index<0)index=0;if(index>b.rows.length)index=b.rows.length;b.rows.splice(index,0,_tblNew(1,w)[0]);}
|
||
function _tblDupCol(b,index){_tblClone(b);b.rows.forEach(function(r){var v=(index<r.length)?r[index]:'';var at=index+1>r.length?r.length:index+1;r.splice(at,0,v);});}
|
||
function _tblDupRow(b,index){_tblClone(b);if(index<0||index>b.rows.length)return;b.rows.splice(index+1,0,(b.rows[index]||[]).slice());}
|
||
function _tblClearCol(b,index){_tblClone(b);b.rows.forEach(function(r){if(r&&index<r.length)r[index]='';});}
|
||
function _tblClearRow(b,index){_tblClone(b);var w=b.rows[0]?b.rows[0].length:1;if(index>=0&&index<b.rows.length)b.rows[index]=_tblNew(1,w)[0];}
|
||
function _tblDelCol(b,index){_tblClone(b);if(!b.rows.length)return;if(b.rows[0].length<=1)return;b.rows.forEach(function(r){if(index>=0&&index<r.length)r.splice(index,1);});}
|
||
function _tblDelRow(b,index){_tblClone(b);if(b.rows.length<=1)return;if(index>=0&&index<b.rows.length)b.rows.splice(index,1);}
|
||
function _tblGrip(){return '<svg viewBox="0 0 16 16" width="12" height="12" style="display:block"><circle cx="4" cy="3" r="1.4"/><circle cx="11" cy="3" r="1.4"/><circle cx="4" cy="8" r="1.4"/><circle cx="11" cy="8" r="1.4"/><circle cx="4" cy="13" r="1.4"/><circle cx="11" cy="13" r="1.4"/></svg>';}
|
||
function _tblDefault(){return {rows:_tblNew(3,3)};}
|
||
function _tblFocus(bid,r,c){var sel=document.querySelector('.ftable-editor[data-bid="'+bid+'"]');if(!sel)return;var q='[data-c="'+c+'"]';var cell=sel.querySelector(q);if(!cell){var first=sel.querySelector('.ftable-cell[data-r]');if(first){first.focus();ce(first);}return;}cell.focus();if(r===undefined)cs(cell);else ce(cell);}
|
||
function renderTableBlock(b,idx){
|
||
var rows=(b&&Array.isArray(b.rows)&&b.rows.length)?b.rows:_tblNew(2,2);
|
||
var w=0;rows.forEach(function(r){if(r&&r.length>w)w=r.length;});if(w<1)w=2;
|
||
var widths=(b&&Array.isArray(b.colsW))?b.colsW:[];
|
||
var bid=b.id;var h='';
|
||
var hasHeader=(b.has_header!==false);var firstCol=(!!b.first_col_header);
|
||
h+='<div class="block-wrapper" data-id="'+bid+'"><div class="block-content ftable-editor" data-bid="'+bid+'">';
|
||
h+='<div class="ftable-colbar">';
|
||
for(var c=0;c<w;c++){
|
||
h+='<div class="ftable-colcell"><button type="button" class="ftable-colgrip" data-tbl="colmenu" data-bid="'+bid+'" data-col="'+c+'" title="Column options">'+_tblGrip()+'</button></div>';
|
||
}
|
||
h+='</div>';
|
||
h+='<table class="ftable ftable-edit"><tbody>';
|
||
for(var ri=0;ri<rows.length;ri++){
|
||
var isHead=(ri===0&&hasHeader)||(ri>0&&firstCol);var tag='td';
|
||
if(isHead){tag='th';}
|
||
h+='<tr class="ftable-row">';
|
||
for(var ci=0;ci<w;ci++){
|
||
var rcH=((ri===0&&hasHeader)||(ci===0&&firstCol));
|
||
var v=rows[ri]&&ci<rows[ri].length?rows[ri][ci]:'';
|
||
var wpx=(widths[ci]&&widths[ci]>0)?(' style="width:'+widths[ci]+'px"'):'';
|
||
h+='<'+tag+' class="ftable-cell'+(rcH?' head':'')+'" contenteditable="true" data-r="'+ri+'" data-c="'+ci+'"'+wpx+' spellcheck="false">'+esc(v);
|
||
if(ri===0&&hasHeader)h+='<span class="ftable-resize" data-col="'+ci+'" title="Drag to resize column"></span>';
|
||
h+='</'+tag+'>';
|
||
}
|
||
h+='</tr>';
|
||
}
|
||
h+='</tbody></table>';
|
||
h+='<div class="ftable-rowbar"><button type="button" class="ftable-addrow" data-tbl="rowadd" data-bid="'+bid+'" title="Click to add a row">+ Row</button><button type="button" class="ftable-addrow" data-tbl="coladdright" data-bid="'+bid+'" title="Add column on the right">+ Col</button><span class="ftable-hdr-toggle"><label><input type="checkbox" data-tbl="header" data-bid="'+bid+'" '+(hasHeader?'checked':'')+'> Header row</label></span><span class="ftable-hdr-toggle"><label><input type="checkbox" data-tbl="firstcol" data-bid="'+bid+'" '+(firstCol?'checked':'')+'> First col</label></span></div>';
|
||
h+='</div></div>';
|
||
return h;
|
||
}
|
||
function _tblBlockById(bid){var e=window.E;if(!e||!e.blocks)return null;for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return e.blocks[i];}return null;}
|
||
function _tblIdxByBid(bid){var e=window.E;if(!e||!e.blocks)return -1;for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return i;}return -1;}
|
||
function _tblMenuClose(){var m=document.getElementById('_tblMenu');if(m){m.parentNode&&m.parentNode.removeChild(m);}if(window._tblCur)window._tblCur=null;}
|
||
function _tblMenuOpen(x,y,bid,kind,index){
|
||
_tblMenuClose();
|
||
var items=[];
|
||
if(kind==='col'){
|
||
items=[['col-insert-left','Insert left'],['col-insert-right','Insert right'],['col-dup','Duplicate'],null,['col-clear','Clear contents'],['col-del','Delete column',1],null,['tbl-del','Delete table',1]];
|
||
}else{
|
||
items=[['row-insert-above','Insert above'],['row-insert-below','Insert below'],['row-dup','Duplicate'],null,['row-clear','Clear row'],['row-del','Delete row',1],null,['tbl-del','Delete table',1]];
|
||
}
|
||
var d=document.createElement('div');d.className='ftable-menu';d.id='_tblMenu';d.style.cssText='position:fixed;z-index:3000;';
|
||
var html='';
|
||
items.forEach(function(it){
|
||
if(!it){html+='<div class="ftable-menu-sep"></div>';return;}
|
||
html+='<div class="ftable-menu-item'+(it[2]?' danger':'')+'" data-tbl-act="'+it[0]+'">'+it[1]+'</div>';
|
||
});
|
||
d.innerHTML=html;
|
||
document.body.appendChild(d);
|
||
var r=d.getBoundingClientRect();
|
||
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
|
||
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
|
||
d.style.left=x+'px';d.style.top=y+'px';
|
||
window._tblCur={bid:bid,kind:kind,index:index};
|
||
}
|
||
function _tblDo(action,cur){
|
||
if(!cur)return;
|
||
var bid=cur.bid,index=cur.index,b=_tblBlockById(bid),e=window.E;
|
||
if(!b||!e)return;
|
||
e.sync();
|
||
if(action!=='tbl-del')e.pushHistory();
|
||
if(!(b.rows&&b.rows.length))b.rows=_tblNew(2,2);
|
||
var focusR=1,focusC=0;
|
||
if(action==='col-insert-left'){_tblAddCol(b,index);focusC=index;}
|
||
else if(action==='col-insert-right'){_tblAddCol(b,index+1);focusC=index+1;}
|
||
else if(action==='col-dup'){_tblDupCol(b,index);focusC=index+1;}
|
||
else if(action==='col-clear'){_tblClearCol(b,index);focusC=index;}
|
||
else if(action==='col-del'){_tblDelCol(b,index);focusC=Math.min(index,b.rows[0].length-1);}
|
||
else if(action==='row-insert-above'){_tblAddRow(b,index);focusR=index;}
|
||
else if(action==='row-insert-below'){_tblAddRow(b,index+1);focusR=index+1;}
|
||
else if(action==='row-dup'){_tblDupRow(b,index);focusR=index+1;}
|
||
else if(action==='row-clear'){_tblClearRow(b,index);focusR=index;}
|
||
else if(action==='row-del'){_tblDelRow(b,index);focusR=Math.min(index,b.rows.length-1);if(focusR<1)focusR=1;}
|
||
else if(action==='tbl-del'){_tblMenuClose();var bi=_tblIdxByBid(bid);if(bi>=0)e.removeBlock(bi);return;}
|
||
e.render();e.dirty=true;e.autoSave();
|
||
_tblFocus(bid,focusR,focusC);
|
||
}
|
||
function _tblResizeStart(ev){
|
||
var sp=ev.target&&ev.target.closest?ev.target.closest('.ftable-resize'):null;if(!sp)return;
|
||
var th=sp.parentElement;if(!th)return;
|
||
var wrap=th.closest('.ftable-editor');if(!wrap)return;
|
||
var bid=wrap.getAttribute('data-bid');var col=parseInt(sp.getAttribute('data-col')||'0',10);
|
||
var b=_tblBlockById(bid);if(!b)return;
|
||
ev.preventDefault();ev.stopPropagation();
|
||
var E=window.E;if(E&&E.sync)E.sync();
|
||
var widths=(b.colsW&&Array.isArray(b.colsW))?b.colsW.slice():[];
|
||
while(widths.length<=col)widths.push(null);
|
||
var startX=ev.clientX;var base=widths[col]||th.getBoundingClientRect().width;
|
||
document.body.style.cursor='col-resize';document.body.style.userSelect='none';
|
||
function move(me){var dx=me.clientX-startX;var nw=Math.max(48,Math.min(760,base+dx));th.style.width=Math.round(nw)+'px';widths[col]=Math.round(nw);}
|
||
function up(me){
|
||
document.removeEventListener('mousemove',move);document.removeEventListener('mouseup',up);
|
||
document.body.style.cursor='';document.body.style.userSelect='';
|
||
move(me);
|
||
b.colsW=widths;
|
||
if(E){E.dirty=true;E.autoSave();}
|
||
}
|
||
document.addEventListener('mousemove',move);
|
||
document.addEventListener('mouseup',up);
|
||
}
|
||
function _tblInitDelegate(){
|
||
if(window._tblDelegate)return;window._tblDelegate=true;
|
||
document.addEventListener('click',function(ev){
|
||
if(ev.target.closest&&ev.target.closest('#_tblMenu')){var it=ev.target.closest('.ftable-menu-item');if(it){var act=it.getAttribute('data-tbl-act');_tblDo(act,window._tblCur);}return;}
|
||
var g=ev.target.closest?ev.target.closest('[data-tbl]'):null;
|
||
if(g){var act=g.getAttribute('data-tbl'),bid=g.getAttribute('data-bid'),col=parseInt(g.getAttribute('data-col')||'0',10);
|
||
var gr=g.getBoundingClientRect?g.getBoundingClientRect():null;
|
||
if(act==='colmenu'){_tblMenuOpen(gr?gr.left:ev.clientX,(gr?gr.bottom:ev.clientY)+4,bid,'col',col);ev.preventDefault();}
|
||
else if(act==='colplus'){var b=_tblBlockById(bid);if(b){window.E.sync();_tblAddCol(b,col+1);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,col+1);}}
|
||
else if(act==='rowadd'){var b2=_tblBlockById(bid);if(b2){window.E.sync();var n=b2.rows.length;_tblAddRow(b2,n);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,n,0);}}
|
||
else if(act==='coladdright'){var b4=_tblBlockById(bid);if(b4){window.E.sync();var cw=(b4.rows&&b4.rows[0])?b4.rows[0].length:1;_tblAddCol(b4,cw);window.E.render();window.E.dirty=true;window.E.autoSave();_tblFocus(bid,1,cw);}}
|
||
else if(act==='header'||act==='firstcol'){var b5=_tblBlockById(bid);if(b5){window.E.sync();window.E.pushHistory();if(act==='header')b5.has_header=g.checked?true:false;else b5.first_col_header=g.checked?true:false;window.E.render();window.E.dirty=true;window.E.autoSave();_rtSync();}}
|
||
return;
|
||
}
|
||
if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('[data-tbl]')))_tblMenuClose();
|
||
});
|
||
document.addEventListener('contextmenu',function(ev){
|
||
var cell=ev.target.closest?ev.target.closest('.ftable-cell'):null;
|
||
if(!cell)return;
|
||
var w=cell.closest('.ftable-editor');if(!w)return;
|
||
var bid=w.getAttribute('data-bid'),r=parseInt(cell.getAttribute('data-r')||'0',10),c=parseInt(cell.getAttribute('data-c')||'0',10);
|
||
ev.preventDefault();
|
||
if(r===0)_tblMenuOpen(ev.clientX,ev.clientY,bid,'col',c);
|
||
else _tblMenuOpen(ev.clientX,ev.clientY,bid,'row',r);
|
||
});
|
||
document.addEventListener('mousedown',function(ev){
|
||
if(ev.target&&ev.target.closest&&ev.target.closest('.ftable-resize')){_tblResizeStart(ev);return;}
|
||
if(document.getElementById('_tblMenu')&&!(ev.target.closest&&ev.target.closest('#_tblMenu')))_tblMenuClose();
|
||
});
|
||
}
|
||
|
||
// ── Block type shortcuts (shown on the right of each slash item) ──
|
||
// Raccourcis = préfixes markdown (alignés sur la description mobile Notion : -, [], >, ---)
|
||
const _SHORTCUT={text:'T',heading_1:'#',heading_2:'##',heading_3:'###',heading_4:'####',bulleted_list:'-',numbered_list:'1.',to_do:'[]',toggle:'>',quote:'"',callout:'!',table_of_contents:'TOC',math:'∑',columns:'‖',divider:'---',code:'<>',table:'|',image:'[img]',embed:'[🔗]',bookmark:'[🔖]',video:'[🎬]',audio:'[🎵]',database:'DB',ai_write:'/ai',ai_summarize:'/ai',ai_translate:'/ai',ai_continue:'/ai',meeting:'/meet',duplicate:'Ctrl+D',turn_into:'⋯',copy_link:'#',delete:'⌫'};
|
||
const _GROUP_LABEL={BASIC:'Basic blocks',AI:'AI writing',MEDIA:'Media',DATA:'Data',ACTIONS:'Actions',SYNCED:'Synced'};
|
||
// ── Hover preview card (matches Notion's visual hint) ──
|
||
function _previewHtml(blockId){
|
||
const p=function(inner,cls){return '<div class="slash-prev" style="padding:0">'+inner+'</div>';};
|
||
switch(blockId){
|
||
case 'heading_1': return p('<div style="font-size:24px;font-weight:700">Big section heading</div>');
|
||
case 'heading_2': return p('<div style="font-size:19px;font-weight:700;margin-bottom:8px">Our Values</div><div style="font-size:13px;line-height:1.6">• Ownership<br>• Altruism<br>• Integrity</div><div style="font-size:11px;color:var(--text-dim);margin-top:8px">Medium section heading</div>');
|
||
case 'heading_3': return p('<div style="font-size:16px;font-weight:700;margin-bottom:6px">Small section heading</div><div style="font-size:13px;color:var(--text-dim)">Body text goes here…</div >');
|
||
case 'heading_4': return p('<div style="font-size:14px;font-weight:700">Tiny section heading</div>');
|
||
case 'bulleted_list': return p('<div style="font-size:13px;line-height:1.9">• List item<br>• Another item<br>• Third item</div>');
|
||
case 'numbered_list': return p('<div style="font-size:13px;line-height:1.9">1. First<br>2. Second<br>3. Third</div>');
|
||
case 'to_do': return p('<div style="font-size:13px;line-height:1.9">☐ Todo item<br>☑ Checked todo<br>☐ Another</div>');
|
||
case 'toggle': return p('<div style="font-size:13px">▸ Toggle content</div>');
|
||
case 'quote': return p('<div style="border-left:3px solid var(--accent);padding-left:10px;font-size:13px;color:var(--text-dim)">A blockquote to highlight a quotation.</div>');
|
||
case 'callout': return p('<div style="background:var(--blue-bg);padding:12px;border-radius:6px;font-size:13px">💡 A callout draws attention.</div>');
|
||
case 'table_of_contents': return p('<div style="font-size:13px;color:var(--text-dim)">On this page</div><div style="font-size:12px;line-height:1.8">· Heading 1<br>· Heading 2<br>· Heading 3</div>');
|
||
case 'math': return p('<div style="font-size:15px;text-align:center">E = mc²</div>');
|
||
case 'columns': return p('<div style="display:flex;gap:8px"><div style="flex:1;border:1px solid var(--border);border-radius:4px;padding:8px;font-size:12px">col 1</div><div style="flex:1;border:1px solid var(--border);border-radius:4px;padding:8px;font-size:12px">col 2</div></div>');
|
||
case 'divider': return p('<hr style="border-top:1px solid var(--border);margin:6px 0">');
|
||
case 'code': return p('<pre style="background:#111;padding:10px;border-radius:6px;font-size:12px"><code>const x = 1;</code></pre>');
|
||
case 'table': return p('<div style="font-size:12px"><table style="width:100%;border-collapse:collapse"><tr><td style="border:1px solid var(--border);padding:4px">A</td><td style="border:1px solid var(--border);padding:4px">B</td></tr><tr><td style="border:1px solid var(--border);padding:4px">1</td><td style="border:1px solid var(--border);padding:4px">2</td></tr></table></div>');
|
||
case 'image': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">[Image]</div>');
|
||
case 'embed': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🔗 Embed (YouTube, Figma, Maps…)</div>');
|
||
case 'bookmark': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🔖 Bookmark card with preview</div>');
|
||
case 'video': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🎬 Inline video player</div>');
|
||
case 'audio': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:14px;text-align:center;color:var(--text-dim);font-size:12px">🎵 Inline audio player</div>');
|
||
case 'database': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">📊 Inline database table</div>');
|
||
case 'meeting': return p('<div style="border:1px solid var(--border);border-radius:6px;padding:10px;font-size:12px">🎙️ <b>AI Meeting Notes</b><br><span style="color:var(--text-dim)">Record, transcribe and summarize this meeting.</span></div>');
|
||
default: return p('<div style="font-size:13px;color:var(--text-dim)">Text block</div>');
|
||
}
|
||
}
|
||
|
||
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],_pv:null,
|
||
init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML='';
|
||
const list=document.createElement('div');list.className='sm-list';list.style.cssText='overflow-y:auto;flex:1;min-height:120px;max-height:360px;padding:6px';
|
||
list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}});
|
||
list.addEventListener('mouseover',ev=>{const it=ev.target.closest('.slash-item');if(it)SM._hover(it);});
|
||
const footer=document.createElement('div');footer.className='sm-footer';footer.style.cssText='display:flex;align-items:center;justify-content:space-between;padding:7px 12px;border-top:1px solid var(--border);font-size:12px;color:var(--text-dim);cursor:pointer';
|
||
footer.innerHTML='<span style="display:flex;align-items:center;gap:8px"><span style="opacity:.6">⌘</span> Close menu</span><span style="color:var(--text-dim);opacity:.7">esc</span>';
|
||
footer.onclick=()=>SM.close();
|
||
const inp=document.createElement('input');inp.type='text';inp.placeholder='Type to search';
|
||
inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
|
||
inp.oninput=()=>SM.flt();
|
||
e.appendChild(list);e.appendChild(footer);e.appendChild(inp);
|
||
SM._e=e;SM._in=inp;SM._li=list;SM._f=footer;
|
||
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target)&&!MTB.contains(ev.target))SM.close();});
|
||
if(!SM._doc){
|
||
SM._doc=true;
|
||
document.addEventListener('keydown',function(ev){
|
||
if(!SM._o)return;
|
||
if(ev.metaKey||ev.ctrlKey||ev.altKey)return;
|
||
if(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'){
|
||
ev.preventDefault();ev.stopPropagation();SM._docKd(ev);
|
||
}
|
||
});
|
||
}
|
||
SM._docKd=function(ev){
|
||
if(SM._o&&(ev.key==='ArrowDown'||ev.key==='ArrowUp'||ev.key==='Enter'||ev.key==='Escape'))SM._kd(ev);
|
||
};
|
||
},
|
||
_hover(it){ if(!it) return; const id=it.dataset.sid; if(!id) return;
|
||
if(!SM._pv){ const d=document.createElement('div'); d.className='sm-preview'; d.style.cssText='position:fixed;z-index:1000;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);padding:12px;width:220px;pointer-events:none;'; document.body.appendChild(d); SM._pv=d; }
|
||
SM._pv.innerHTML=_previewHtml(id);
|
||
const r=SM._e.getBoundingClientRect(); const ir=it.getBoundingClientRect();
|
||
let left = r.right + 8; let top = ir.top - 12;
|
||
if(left+220 > window.innerWidth-8) left = r.left - 228;
|
||
top = Math.max(8, Math.min(top, window.innerHeight - SM._pv.offsetHeight - 8));
|
||
SM._pv.style.left=left+'px'; SM._pv.style.top=top+'px'; SM._pv.style.display='block';
|
||
},
|
||
open(i,el){if(!SM._e)SM.init();SM.close();SM._i=i;SM._s=0;SM._in.value='';SM.flt();
|
||
// v7.63 : mobile → barre horizontale façon Notion sous le curseur
|
||
if(_fdMobile()){SM._o=true;MTB.open(i,el);return;}
|
||
const r=el.getBoundingClientRect();SM._e.style.display='flex';
|
||
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-360)?r.top-364:r.bottom+4)+'px';
|
||
SM._e.style.left=Math.min(r.left,window.innerWidth-340)+'px';
|
||
SM._rs();setTimeout(()=>SM._in.focus(),10);SM._o=true;},
|
||
close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');if(SM._pv)SM._pv.style.display='none';MTB.close();},
|
||
// v7.66 : Escape ferme le menu ET retire le « / » qui l'a ouvert.
|
||
escClose(){
|
||
const i=SM._i;SM.close();
|
||
const E=window.E;if(!E||i<0||i>=E.blocks.length)return;
|
||
const el=E.getEl(E.blocks[i].id);if(!el)return;
|
||
const w=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);let n;
|
||
while((n=w.nextNode())){
|
||
if(n.parentElement&&n.parentElement.closest('.fd-react-chip'))continue;
|
||
const v=n.nodeValue,at=v.indexOf('/');
|
||
if(at>=0){
|
||
if(v.slice(0,at).trim())return; // pas le / d'ouverture → on ne touche à rien
|
||
n.nodeValue=v.slice(0,at)+v.slice(at+1);
|
||
E.sync();E.dirty=true;E.autoSave();
|
||
el.focus();
|
||
const r=document.createRange();r.setStart(n,Math.min(at,n.nodeValue.length));r.collapse(true);
|
||
const s=window.getSelection();s.removeAllRanges();s.addRange(r);
|
||
return;
|
||
}
|
||
if(v.trim())return; // 1er texte non vide sans / → rien à retirer
|
||
}
|
||
},
|
||
// Ligne d'item du slash (partagée par la popup desktop et la feuille mobile)
|
||
_slashItemHtml(c){return `<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:7px 10px;font-size:14px;color:var(--text-primary);cursor:pointer;border-radius:5px">
|
||
<span class="slash-item-icon" style="width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:15px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
|
||
<span class="slash-item-name" style="flex:1;font-weight:500">${c.name}</span>
|
||
<span class="slash-item-hint" style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);opacity:.7">${_SHORTCUT[c.id]||''}</span></div>`;},
|
||
flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h='';
|
||
for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue;
|
||
h+=`<div class="sm-group" style="padding:6px 10px 3px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px">${_GROUP_LABEL[g.name]||g.name}</div>`;
|
||
items.forEach(c=>{SM._it.push(c);h+=SM._slashItemHtml(c);});}
|
||
SM._li.innerHTML=h;SM._rs();},
|
||
_kd(e){if(e.key==='ArrowDown'){e.preventDefault();SM._s=Math.min(SM._s+1,SM._it.length-1);SM._rs();}
|
||
else if(e.key==='ArrowUp'){e.preventDefault();SM._s=Math.max(SM._s-1,0);SM._rs();}
|
||
else if(e.key==='Enter'){e.preventDefault();const c=SM._it[SM._s];if(c)SM.sel(c.id);}
|
||
else if(e.key==='Escape'){e.preventDefault();SM.escClose();}},
|
||
_rs(){const its=SM._li.querySelectorAll('.slash-item');its.forEach((el,i)=>{el.classList.toggle('selected',i===SM._s);});const cur=its[SM._s];if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'});},
|
||
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
|
||
get isOpen(){return SM._o;}};
|
||
|
||
// ═══════════ v7.63.0 — Menus mobiles façon Notion : barre horizontale sous le curseur
|
||
// + feuille plein écran « Insert block » / « Turn into » / « Color ». Tout est
|
||
// branché sur les actions existantes (applySlash, turnInto, blockMenu, moveBlock…).
|
||
function _fdMobile(){try{return window.matchMedia('(max-width: 768px)').matches;}catch(e){return false;}}
|
||
const MTB={
|
||
_b:null,_sheet:null,
|
||
contains(t){return !!t&&((this._b&&this._b.contains(t))||(this._sheet&&this._sheet.contains(t)));},
|
||
el(){ if(this._b)return this._b;
|
||
const wide=['plus','turn','color','more'];
|
||
const items=[['plus','+<span class="fd-mtb-caret">▾</span>'],['turn','Turn into<span class="fd-mtb-caret">▾</span>'],
|
||
['at','@'],['comment','💬'],['image','🖼️'],['trash','🗑️'],
|
||
['left','←'],['right','→'],['up','↑'],['down','↓'],['undo','↩'],['redo','↪'],
|
||
['color','Color<span class="fd-mtb-caret">▾</span>'],['more','More<span class="fd-mtb-caret">▾</span>'],['kbd','⌨️']];
|
||
const d=document.createElement('div');d.id='fdMobileBar';d.className='fd-mtb';
|
||
d.innerHTML=items.map(it=>'<button type="button" class="fd-mtb-item'+(wide.indexOf(it[0])>=0?' fd-mtb-wide':'')+'" data-mtb="'+it[0]+'">'+it[1]+'</button>').join('');
|
||
d.addEventListener('click',ev=>{const bt=ev.target.closest('[data-mtb]');if(bt)MTB.act(bt.getAttribute('data-mtb'),bt);});
|
||
document.body.appendChild(d);this._b=d;return d;},
|
||
open(i,el){const d=this.el();d.style.display='flex';
|
||
const r=el.getBoundingClientRect();
|
||
const w=Math.min(d.scrollWidth||280,window.innerWidth-16);
|
||
d.style.left=Math.max(8,Math.min(r.left,window.innerWidth-w-8))+'px';
|
||
let top=r.bottom+6;if(top+54>window.innerHeight-8)top=Math.max(8,r.top-54);
|
||
d.style.top=top+'px';d.scrollLeft=0;},
|
||
close(){if(this._b)this._b.style.display='none';this.closeSheet();},
|
||
closeSheet(){if(this._sheet){this._sheet.remove();this._sheet=null;}},
|
||
sheet(title,html,handler){
|
||
this.closeSheet();
|
||
const s=document.createElement('div');s.className='fd-msheet';
|
||
s.innerHTML='<div class="fd-msheet-head"><span class="fd-msheet-title">'+title+'</span><button type="button" class="fd-msheet-cancel">Cancel</button></div><div class="fd-msheet-list block-menu">'+html+'</div>';
|
||
s.querySelector('.fd-msheet-cancel').addEventListener('click',()=>{const E=window.E;this.closeSheet();if(E&&SM._i>=0&&E.blocks[SM._i])E._focusBid(E.blocks[SM._i].id);});
|
||
s.addEventListener('click',handler);
|
||
document.body.appendChild(s);this._sheet=s;},
|
||
_insertHtml(){let h='';for(const g of CMDS){if(!g.items||!g.items.length)continue;
|
||
h+='<div class="sm-group">'+(_GROUP_LABEL[g.name]||g.name)+'</div>';
|
||
g.items.forEach(c=>{h+=SM._slashItemHtml(c);});}return h;},
|
||
_bmAct(ev){if(!ev.target||!ev.target.closest||!ev.target.closest('[data-bm-act]'))return;_blockMenuClick(ev);this.closeSheet();},
|
||
act(k,btn){
|
||
const E=window.E;if(!E)return;
|
||
const idx=SM._i;const b=(idx>=0&&idx<E.blocks.length)?E.blocks[idx]:null;
|
||
const bid=b?b.id:'';
|
||
if(k==='plus'){this.sheet('Insert block',this._insertHtml(),ev=>{const it=ev.target.closest('.slash-item');if(it)SM.sel(it.dataset.sid);});return;}
|
||
if(k==='turn'){this.sheet('Turn into',_blockTurnItems(bid),ev=>this._bmAct(ev));return;}
|
||
if(k==='color'){this.sheet('Color',_blockColorItems(bid),ev=>this._bmAct(ev));return;}
|
||
if(k==='more'){const el=E.getEl(bid);const r=el?el.getBoundingClientRect():btn.getBoundingClientRect();
|
||
this.close();blockMenuOpen(Math.max(8,Math.min(r.left,window.innerWidth-250)),Math.min(r.bottom+6,window.innerHeight-120),bid);return;}
|
||
if(k==='kbd'){const a=document.activeElement;if(a&&a.blur)a.blur();return;}
|
||
if(!b)return;
|
||
if(k==='at'){const el=E.getEl(bid);if(!el)return;el.focus();document.execCommand('insertText',false,'@');
|
||
E.dirty=true;E.autoSave();E.openMention(el);return;}
|
||
if(k==='comment'){E.commentBlock(idx);return;}
|
||
if(k==='image'){E.applySlash('image');return;}
|
||
if(k==='trash'){E.deleteSelected(idx);return;}
|
||
if(k==='up'||k==='down'||k==='left'||k==='right'){E.moveBlock(bid,k);return;}
|
||
if(k==='undo'){E.undo();return;}
|
||
if(k==='redo'){E.redo();return;}
|
||
}
|
||
};
|
||
|
||
// ═══════════ v5.11.0 Wiki-link picker ([[ → page search) ═══════════
|
||
const WM={_o:false,_idx:-1,_s:0,_it:[],_root:null,_in:null,_li:null,_pv:null,
|
||
mk(){ if(this._root)return this._root;
|
||
const d=document.createElement('div'); d.id='_wikiMenu';
|
||
d.style.cssText='position:fixed;z-index:1250;display:none;width:340px;max-height:320px;flex-direction:column;background:var(--bg-modal,#1f1f1f);border:1px solid var(--border,#333);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden';
|
||
const list=document.createElement('div'); list.style.cssText='overflow-y:auto;flex:1;min-height:60px;max-height:250px;padding:6px';
|
||
const inp=document.createElement('input'); inp.type='text'; inp.placeholder='Search pages…';
|
||
inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
|
||
d.appendChild(list); d.appendChild(inp); document.body.appendChild(d);
|
||
this._root=d; this._li=list; this._in=inp;
|
||
inp.oninput=()=>{clearTimeout(this._t);this._t=setTimeout(()=>this.load(),180);};
|
||
list.addEventListener('click',ev=>{const it=ev.target.closest('.wm-item');if(it){ev.preventDefault();WM.sel(parseInt(it.dataset.wpid,10));}});
|
||
document.addEventListener('mousedown',ev=>{if(!d.contains(ev.target))WM.close();});
|
||
document.addEventListener('keydown',function(ev){
|
||
if(!WM._o)return;
|
||
if(ev.metaKey||ev.ctrlKey||ev.altKey)return;
|
||
if(ev.key==='Escape'){ev.preventDefault();ev.stopPropagation();WM.close();}
|
||
else if(ev.key==='ArrowDown'){ev.preventDefault();ev.stopPropagation();WM._mv(1);}
|
||
else if(ev.key==='ArrowUp'){ev.preventDefault();ev.stopPropagation();WM._mv(-1);}
|
||
else if(ev.key==='Enter'){ev.preventDefault();ev.stopPropagation();const c=WM._it[WM._s];if(c)WM.sel(c.id);}
|
||
},true);
|
||
return d; },
|
||
open(i,el){ this.mk(); const r=el.getBoundingClientRect();
|
||
this._root.style.display='flex';
|
||
this._root.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
|
||
this._root.style.left=Math.min(r.left,window.innerWidth-350)+'px';
|
||
this._idx=i; this._s=0; this._in.value=''; this._o=true;
|
||
this.load(); setTimeout(()=>this._in.focus(),10); },
|
||
close(){ if(this._root)this._root.style.display='none'; this._o=false; },
|
||
load(){ const self=this; const q=(this._in.value||'').toLowerCase();
|
||
fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'})
|
||
.then(r=>r.json()).then(d=>{
|
||
if(!self._o)return;
|
||
self._it=d.pages||[]; self._s=0;
|
||
let h='<div style="padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase">Pages</div>';
|
||
self._it.forEach(function(p,idx){
|
||
h+='<div class="wm-item" data-wpid="'+p.id+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;'+(idx===0?'background:rgba(35,131,226,.15)':'')+'">'
|
||
+'<span style="width:18px;text-align:center;flex-shrink:0">'+esc(p.icon||'📄')+'</span>'
|
||
+'<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(p.title)+'</span>'
|
||
+'<span style="font-size:11px;color:var(--text-dim)">'+esc(p.workspace||'')+'</span></div>';
|
||
});
|
||
if(!self._it.length)h+='<div style="padding:12px;color:var(--text-dim);font-size:13px">No page found</div>';
|
||
self._li.innerHTML=h;
|
||
}).catch(()=>{}); },
|
||
_mv(dir){ if(!this._it.length)return;
|
||
this._s=(this._s+dir+this._it.length)%this._it.length;
|
||
this._li.querySelectorAll('.wm-item').forEach((el,i)=>{el.style.background=i===this._s?'rgba(35,131,226,.15)':'';});
|
||
const cur=this._li.querySelectorAll('.wm-item')[this._s]; if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'}); },
|
||
sel(pid){ const E=window.E; if(!E||this._idx<0)return;
|
||
const b=E.blocks[this._idx]; if(!b)return;
|
||
// Drop the typed "[[" from the live DOM text, append the token chip.
|
||
const el=E.getEl(b.id); let text=(el?gtTok(el):b.content)||'';
|
||
if(text.endsWith('[[')) text=text.slice(0,-2);
|
||
else text=text.replace(/\[\[[^\]]*$/,'');
|
||
E._commitBlockText(b, text+'[[fdpage:'+pid+']] ');
|
||
this.close(); },
|
||
get isOpen(){return this._o;}};
|
||
|
||
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
|
||
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
|
||
// → framed result → "Insert below" → applyAIBlocks.
|
||
const AIC={_open:false,_idx:-1,_stage:'',_html:'',_md:'',
|
||
ROOT:null,
|
||
mk(){ if(this.ROOT) return this.ROOT;
|
||
const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none';
|
||
document.body.appendChild(d); this.ROOT=d; return d; },
|
||
open(idx){ const self=this; this._idx=idx; this._stage='input'; this.mk();
|
||
const r=this._anchorRect(idx);
|
||
const w=Math.max(r.width||0,280); this.ROOT.style.width=Math.min(w,1200)+'px';
|
||
this.ROOT.style.left=Math.max(8,Math.min(r.left,window.innerWidth-r.width-16))+ 'px';
|
||
this.ROOT.style.top=Math.max(8,(r.bottom+6>window.innerHeight-260)?r.top-180:r.bottom+6)+'px';
|
||
this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest();
|
||
document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } });
|
||
},
|
||
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; this._md=''; if(window.E)window.E.dirty=true; this._stage=''; },
|
||
_anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420};
|
||
const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180,width:420}; },
|
||
_focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} },
|
||
_escTag(tag,s){return '<'+tag+'>'+s+'</'+tag+'>';},
|
||
_mdToHtml(md){ // compact block render (headings, lists, bold, paragraphs)
|
||
const lines=String(md||'').split(/\r?\n/); let out=[]; let i=0;
|
||
const inline=function(t){return esc(t).replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank">$1</a>');};
|
||
while(i<lines.length){ const t=lines[i].replace(/\r$/,''); const s=t.trim();
|
||
if(!s){i++;continue;}
|
||
if(/^#{1,4}\s/.test(s)){ const lvl=s.match(/^(#+)\s/)[1].length; out.push('<h'+lvl+'>'+inline(s.replace(/^#+\s+/,''))+'</h'+lvl+'>'); i++; continue; }
|
||
if(/^[-*+] /.test(s)){ const items=[]; while(i<lines.length&&/^[-*+] /.test(lines[i].trim())){items.push('<li>'+inline(lines[i].trim().replace(/^[-*+] /,''))+'</li>'); i++;} out.push('<ul>'+items.join('')+'</ul>'); continue; }
|
||
if(/^\d+\. /.test(s)){ const items=[]; while(i<lines.length&&/^\d+\. /.test(lines[i].trim())){items.push('<li>'+inline(lines[i].trim().replace(/^\d+\. /,''))+'</li>'); i++;} out.push('<ol>'+items.join('')+'</ol>'); continue; }
|
||
if(s.startsWith('> ')){ out.push('<blockquote>'+inline(s.replace(/^> /,''))+'</blockquote>'); i++; continue; }
|
||
out.push('<p>'+inline(s)+'</p>'); i++;
|
||
}
|
||
return out.join(''); },
|
||
paint(){ const self=this; const h=this.ROOT;
|
||
if(this._stage==='input'){
|
||
h.innerHTML='<div style="display:flex;align-items:center;gap:10px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:999px;padding:8px 14px;box-shadow:0 8px 30px rgba(0,0,0,.45)">'
|
||
+'<span style="font-size:18px;color:var(--text-primary)">✨</span>'
|
||
+'<input class="aici-input" type="text" placeholder="Edit with AI" autocomplete="off" spellcheck="false" style="flex:1;background:none;border:none;outline:none;color:var(--text);font-size:14px">'
|
||
+'<button class="aici-send" title="Generate" style="background:var(--bg-secondary);border:none;border-radius:50%;width:26px;height:26px;color:var(--text);cursor:pointer;font-size:14px">↑</button>'
|
||
+'</div>';
|
||
const inp=h.querySelector('.aici-input'); if(inp){inp.focus(); const go=function(){const v=inp.value.trim(); if(v){self._idx=self._idx>=0?self._idx:0; self._run(v);} }; inp.onkeydown=function(ev){ if(ev.key==='Enter'){ev.preventDefault();go();} else if(ev.key==='Escape'){ev.preventDefault();self.close();} }; h.querySelector('.aici-send').onclick=go; }
|
||
} else if(this._stage==='brewing'){
|
||
h.innerHTML='<div style="display:flex;align-items:center;gap:12px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:14px;padding:12px 16px;box-shadow:0 8px 30px rgba(0,0,0,.45)">'
|
||
+'<span style="font-size:20px">✨</span><span style="color:var(--text-dim);font-size:14px;animation:ai-pulse 1.2s ease-in-out infinite">Brewing…</span>'
|
||
+'<button class="aici-stop" title="Stop" style="margin-left:auto;background:var(--bg-secondary);border:none;border-radius:6px;width:26px;height:26px;color:var(--text);cursor:pointer">■</button></div>';
|
||
h.querySelector('.aici-stop').onclick=function(){ self.close(); if(window.fdAgent&&window.fdAgent.cancel)window.fdAgent.cancel(); };
|
||
} else if(this._stage==='result'){
|
||
h.innerHTML='<div style="background:var(--bg-modal);border:1px solid var(--border-light);border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,.5);overflow:hidden">'
|
||
+'<div class="aici-result" style="padding:14px 16px;font-size:14px;line-height:1.6;color:var(--text);max-height:300px;overflow-y:auto">'+this._html+'</div>'
|
||
+'<div style="display:flex;align-items:center;gap:6px;padding:8px 12px;border-top:1px solid var(--border)">'
|
||
+'<button class="aici-fb" data-fb="up" title="Good" style="background:none;border:none;color:var(--text-dim);cursor:pointer">👍</button>'
|
||
+'<button class="aici-fb" data-fb="down" title="Bad" style="background:none;border:none;color:var(--text-dim);cursor:pointer">👎</button>'
|
||
+'<span style="flex:1"></span>'
|
||
+'<button class="aici-insert" style="background:var(--accent,#2383E2);color:#fff;border:none;border-radius:8px;padding:6px 16px;font-size:13px;cursor:pointer">Insert below</button>'
|
||
+'</div></div>';
|
||
const self2=this; h.querySelector('.aici-insert').onclick=function(){ self2.insert(); }; h.querySelectorAll('.aici-fb').forEach(function(b){ b.onclick=function(){ self2.toast(b.dataset.fb==='up'?'👍 Merci':'👎 Enregistré'); }; });
|
||
}
|
||
},
|
||
_run(prompt){ const self=this; this._stage='brewing'; this.paint();
|
||
const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
||
if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; }
|
||
window.fdAgent.generate(prompt, ctx).then(function(text){
|
||
// On conserve le MARKDOWN source : c'est lui qui est inséré (md2b),
|
||
// pas le texte extrait du HTML rendu qui perd titres/listes/gras.
|
||
self._md=String(text||'');
|
||
self._html=self._mdToHtml(self._md); self._stage='result'; self.paint();
|
||
}).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); });
|
||
},
|
||
_err(msg){ this._md=''; this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); },
|
||
insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync();
|
||
// Markdown source d'abord ; repli sur le texte visible (erreurs).
|
||
const result=String(this._md||'').trim()||this._resultText(); if(!result)return;
|
||
E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); },
|
||
_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); },
|
||
toast(m){ if(window.showToast)window.showToast(m,'info'); }
|
||
};
|
||
window.AIC=AIC;
|
||
|
||
// ═══════════ AI Autocomplete (v5.9.0) ═══════════
|
||
// After a short idle while typing in a text block, ask the AI for a short
|
||
// continuation and surface it as a "Tab to accept" pill anchored to the
|
||
// block. Tab inserts the suggestion; Escape dismisses it.
|
||
const AIAC={_timer:null,_sug:'',_bid:null,_busy:false,_enabled:true,_req:0,
|
||
ROOT:null,
|
||
_allowed:function(b){ if(!b)return false; return ['paragraph','heading_1','heading_2','heading_3','heading_4','bulleted_list','numbered_list','quote','callout','to_do'].indexOf(b.type)>=0; },
|
||
mk(){ if(this.ROOT)return this.ROOT; const d=document.createElement('div'); d.id='_aiAC'; d.style.cssText='position:fixed;z-index:1150;display:none;max-width:min(520px,90vw)'; document.body.appendChild(d); this.ROOT=d;
|
||
document.addEventListener('mousedown',function(ev){ if(AIAC.ROOT&&!AIAC.ROOT.contains(ev.target))AIAC.clear(); });
|
||
return d; },
|
||
schedule(bid){ if(!this._enabled)return; const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear();
|
||
clearTimeout(this._timer); const self=this; this._timer=setTimeout(function(){ self.fetch(bid); },900); },
|
||
fetch(bid){ const E=window.E; if(!E)return; const idx=E.getIdx(bid); const b=idx>=0?E.blocks[idx]:null; if(!this._allowed(b))return this.clear();
|
||
const el=E.getEl(bid); const prefix=((el&&el.textContent)||b.content||'').trim(); if(prefix.length<8)return this.clear();
|
||
const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
||
const req=++this._req; this._busy=true; const self=this; const csrf=(E.csrfTok?E.csrfTok():'');
|
||
fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({action:'autocomplete',prefix:prefix,context:ctx})})
|
||
.then(function(r){return r.json();})
|
||
.then(function(d){ self._busy=false; if(req!==self._req)return; if(d&&d.ok&&d.text){ self.show(bid,d.text); } else { self.clear(); } })
|
||
.catch(function(){ self._busy=false; if(req===self._req)self.clear(); });
|
||
},
|
||
show(bid,text){ text=(text||'').trim(); if(!text){return this.clear();} const E=window.E; const el=E&&E.getEl(bid); if(!el)return;
|
||
this._sug=text; this._bid=bid; this.mk(); const r=el.getBoundingClientRect();
|
||
this.ROOT.innerHTML='<div style="display:flex;align-items:center;gap:8px;background:var(--bg-modal);border:1px solid var(--border-light);border-radius:8px;padding:6px 10px;box-shadow:0 6px 24px rgba(0,0,0,.4);font-size:13px;color:var(--text-dim)">'
|
||
+'<span style="opacity:.7">✨</span><span style="color:var(--text);font-style:italic">'+esc(text)+'</span>'
|
||
+'<span style="margin-left:6px;font-family:var(--font-mono);font-size:11px;background:var(--bg-secondary);border-radius:4px;padding:1px 6px">Tab</span>'
|
||
+'<button class="aiac-x" title="Dismiss" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:14px">✕</button></div>';
|
||
const btn=this.ROOT.querySelector('.aiac-x'); if(btn)btn.onclick=function(){ AIAC.clear(); };
|
||
let left=Math.min(Math.max(8,r.left+24),window.innerWidth-360); let top=r.bottom+4;
|
||
if(top+40>window.innerHeight)top=r.top-36;
|
||
this.ROOT.style.left=left+'px'; this.ROOT.style.top=Math.max(8,top)+'px'; this.ROOT.style.display='block';
|
||
},
|
||
accept(){ if(!this._sug)return false; const E=window.E; const idx=E&&E.getIdx(this._bid); if(idx<0){ this.clear(); return false; }
|
||
const b=E.blocks[idx]; const el=E.getEl(this._bid); E.sync(); E.pushHistory();
|
||
const cur=((el&&el.textContent)||b.content||''); const next=(cur+this._sug).replace(/\s+$/,' ');
|
||
b.content=next; if(el){ el.textContent=next; } this.clear(); E.dirty=true; E.autoSave(); if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow();
|
||
if(el){ el.focus(); ce(el); } return true;
|
||
},
|
||
clear(){ this._sug=''; this._bid=null; this._req++; if(this.ROOT){ this.ROOT.style.display='none'; this.ROOT.innerHTML=''; } },
|
||
get active(){ return !!this._sug; }
|
||
};
|
||
window.AIAC=AIAC;
|
||
|
||
// ── Code block renderer (hljs: highlight.js syntax highlighting) — literal
|
||
// text (esc, real newlines — NOT mdEsc, whose <br> would break
|
||
// highlight.js textContent reads) plus a language picker driving
|
||
// FDHighlight (~230 file types). Highlight spans are transparent to
|
||
// sync(): gt()/gtTok() re-read text only.
|
||
function renderCodeBlock(b){
|
||
var rawLang=b.language||'Plain Text';
|
||
var norm=(window.FDHighlight?FDHighlight.normalizeLang(rawLang):String(rawLang||'plaintext').toLowerCase().replace(/[^a-z0-9+#-]+/g,''))||'plaintext';
|
||
var ro=!!(window.E&&E.canEdit===false);
|
||
var head;
|
||
if(!ro&&window.FDHighlight){
|
||
var opts=FDHighlight.PICKER_LANGS.map(function(id){
|
||
return '<option value="'+id+'"'+(id===norm?' selected':'')+'>'+esc(FDHighlight.labelFor(id))+'</option>';
|
||
}).join('');
|
||
head='<div class="code-lang-label code-lang-bar"><select class="code-lang-select" onchange="E.setCodeLang(\''+b.id+'\',this.value)" title="Code language — syntax highlighting">'+opts+'</select></div>';
|
||
}else{
|
||
var label=(window.FDHighlight?FDHighlight.labelFor(norm):rawLang);
|
||
head='<div class="code-lang-label">'+esc(label)+'</div>';
|
||
}
|
||
return head+'<pre><code data-bid="'+b.id+'" data-hljs data-hljs-lang="'+norm+'" class="language-'+norm+'" contenteditable="'+(ro?'false':'true')+'" spellcheck="false">'+esc(b.content||'')+'</code></pre>';
|
||
}
|
||
|
||
function renderBlock(b,idx){
|
||
if(b.type==='meeting')return renderMeetingBlock(b,idx);
|
||
if(b.type==='table')return renderTableBlock(b,idx);
|
||
if(b.type==='divider')return `<div class="block-wrapper" data-id="${b.id}"><hr class="block-divider"></div>`;
|
||
if(b.type==='button')return renderAutomationButton(b,idx);
|
||
// v7.68 : ces trois renderers existaient mais n'étaient JAMAIS appelés —
|
||
// un bloc `columns`/`math`/`table_of_contents` tombait dans le rendu générique
|
||
// (un paragraphe vide). Colonnes (slash + Turn into) = bug rapporté.
|
||
if(b.type==='columns')return renderColumnsBlock(b);
|
||
if(b.type==='math')return renderMathBlock(b);
|
||
if(b.type==='table_of_contents')return renderTOC(b);
|
||
if(b.type==='image')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" class="fd-img" data-full="${b.src||''}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;cursor:zoom-in;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'" onclick="E._openLightbox(this)"></div></div>`;
|
||
if(b.type==='video'){
|
||
const src=b.src||'';return src?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-video"><video controls preload="metadata" style="max-width:100%;border-radius:8px;display:block;margin:0 auto;"><source src="${src}"></video></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-video" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setVideoSrc('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🎬</div><div style="font-size:14px;color:var(--text-dim);">Click to add video URL</div></div></div>`;
|
||
}
|
||
if(b.type==='audio'){
|
||
const src=b.src||'';return src?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-audio"><audio controls preload="metadata" style="width:100%;"><source src="${src}"></audio></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-audio" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setAudioSrc('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🎵</div><div style="font-size:14px;color:var(--text-dim);">Click to add audio URL</div></div></div>`;
|
||
}
|
||
if(b.type==='bookmark'){
|
||
var url=b.url||b.src||'';var title=esc(b.title||url);var desc=esc(b.description||'');var img=b.image||'';var site=esc(b.site_name||'');
|
||
var imgHtml=img?'<img src="'+img.replace(/"/g,'"')+'" 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,'"');
|
||
return url?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark"><a href="${safeUrl}" target="_blank" rel="noopener noreferrer" style="text-decoration:none;color:inherit;"><div style="display:flex;gap:14px;align-items:center;border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin:0;background:var(--bg-secondary);"><div style="flex:1;min-width:0;"><div style="font-weight:600;font-size:15px;">${title}</div>${descHtml}${siteHtml}</div>${imgHtml}</div></a></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setBookmarkUrl('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🔖</div><div style="font-size:14px;color:var(--text-dim);">Click to add bookmark URL</div></div></div>`;
|
||
}
|
||
if(b.type==='embed'){
|
||
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
|
||
if(b.embed_type==='collection')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed-collection" id="inline-db-${b.collection_id}" style="padding:0;min-height:200px"><div style="padding:40px;text-align:center;color:var(--text-dim)">Loading database...</div></div><`+`script>setTimeout(function(){var el=document.getElementById('inline-db-${b.collection_id}');if(el&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,${b.collection_id},null);},50);<`+`/script></div>`;
|
||
if(b.embed_type==='pdf')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="padding:0;"><iframe src="${b.src||''}" style="width:100%;height:80vh;border:none;border-radius:8px;" onerror="this.style.display='none';this.nextElementSibling.style.display='block';"></iframe><div style="display:none;text-align:center;padding:32px;color:var(--text-secondary);"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p style="margin-top:8px;">Failed to load PDF</p><a href="${b.src||''}" target="_blank" style="color:var(--accent);">Open in new tab →</a></div></div></div>`;
|
||
if(b.embed_type==='download'){
|
||
var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB';
|
||
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;"><div style="font-size:48px;margin-bottom:12px;">${PD.clip_icon||''}</div><div style="font-size:16px;font-weight:500;color:var(--text-primary);margin-bottom:4px;">${esc(b.file_name||'File')}</div><div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">${szStr} · ${esc(b.file_mime||'')}</div><a href="${b.src||''}" download style="display:inline-block;padding:8px 20px;background:var(--accent);color:#fff;text-decoration:none;border-radius:8px;font-size:13px;font-weight:500;">⬇ Download</a></div></div>`;
|
||
}
|
||
var url=b.src||'';if(url){
|
||
var src=b.embed_src||url;
|
||
var height=b.height||520;
|
||
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="position:relative;width:100%;height:${height}px;border-radius:8px;overflow:hidden;background:#0a0a0a;"><iframe src="${src}" loading="lazy" frameborder="0" style="position:absolute;inset:0;width:100%;height:100%;" allowfullscreen allow="autoplay; encrypted-media; picture-in-picture"></iframe></div></div>`;
|
||
}
|
||
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setEmbedUrl('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🔗</div><div style="font-size:14px;color:var(--text-dim);">Click to add embed URL</div></div></div>`;
|
||
}
|
||
if(b.type==='synced'){
|
||
if(b._synced_deleted){
|
||
// v6.5.0: source deleted → clear state instead of stale cache.
|
||
return `<div class="block-wrapper synced-block-wrapper" data-id="${b.id}" style="border-left:3px solid #e05e5e;background:var(--bg-secondary);opacity:.8">
|
||
<div class="synced-badge" style="font-size:11px;color:#e05e5e;padding:4px 8px;font-weight:600;">🔗 Deleted synced block</div>
|
||
<div style="color:var(--text-dim);padding:0 8px 8px;font-size:12px;">The original synced block was deleted — this copy no longer updates.</div>
|
||
</div>`;
|
||
}
|
||
var syncedTitle=b._synced_title||'Synced block';
|
||
var syncedContent=b._synced_content||[];
|
||
var sid=b.synced_id||'';
|
||
var syncedInner=syncedContent.map(function(ch){return'<div class="block-content" style="padding-left:16px;border-left:2px solid var(--accent);margin:4px 0;">'+mdEsc(typeof ch==='object'?ch.content||'':ch)+'</div>';}).join('');
|
||
return `<div class="block-wrapper synced-block-wrapper" data-id="${b.id}" style="border-left:3px solid var(--accent);background:var(--synced-bg,var(--bg-secondary))">
|
||
<div class="synced-badge" style="font-size:11px;color:var(--accent);padding:4px 8px;font-weight:600;">🔗 Synced — ${esc(syncedTitle)}</div>
|
||
<div class="synced-content">${syncedInner||'<div style="color:var(--text-dim);padding:8px;">Loading synced content…</div>'}</div>
|
||
</div>`;
|
||
}
|
||
const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':(b.type==='paragraph'&&!b.content?'Press \'space\' for AI or \'/\' for commands':'Press \'/\' for commands...');
|
||
let inner='';
|
||
if(b.type==='to_do')inner=`<input type="checkbox" class="todo-checkbox" ${b.checked?'checked':''} onchange="E._doToggle('${b.id}',this.checked)"><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
|
||
else if(b.type==='toggle')inner=`<div class="toggle-title-row"><button class="toggle-chevron ${b.expanded?'open':''}" onclick="E._doToggleExpand('${b.id}')">▶</button><div class="toggle-title" data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div></div><div class="toggle-children" ${(b.expanded===false)?'style="display:none"':''}>${(b.children||[]).map((ch)=>renderNestedBlock(ch)).join('')}</div>`;
|
||
else if(b.type==='code')inner=renderCodeBlock(b);
|
||
else if(b.type==='callout')inner=`<span class="callout-icon" onclick="E._toggleCalloutIcon('${b.id}')" title="Change icon">${b.icon||'💡'}</span><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div><div class="callout-icon-picker" id="callout-picker-${b.id}" style="display:none">${CALLOUT_ICONS.map(function(i){return '<button type="button" class="callout-icon-opt" onclick="E._setCalloutIcon(\''+b.id+'\',\''+i+'\')">'+i+'</button>';}).join('')}</div>`;
|
||
else inner=`<div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${mdEsc(b.content)}</div>`;
|
||
const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div';
|
||
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
|
||
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:'');
|
||
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
|
||
// Numéro d'ordre : rang des blocs `numbered_list` contigus (1,2,3…) ;
|
||
// passé en attribut data-num → le CSS le rend hors du contenu éditable.
|
||
let numAttr='';
|
||
if(b.type==='numbered_list'&&idx>=0){
|
||
const bs=(window.E&&window.E.blocks)||[];
|
||
let s=idx; while(s>0&&bs[s-1]&&bs[s-1].type==='numbered_list')s--;
|
||
numAttr=` data-num="${idx-s+1}"`;
|
||
}
|
||
// v7.66 : bouton « commenter ce bloc » à droite du bloc (gouttière 64px),
|
||
// icône bulle de conversation + . Remplace l'ancien .block-actions mort
|
||
// (opacity:0 sans règle de survol → un + invisible qui capait les clics).
|
||
const cmtBtn=idx>=0?`<button type="button" class="block-comment-btn" title="Comment on block" aria-label="Comment on block" onclick="E.commentBlock(${idx})"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 8v5M9.5 10.5h5"/></svg></button>`:'';
|
||
// + « ajouter un bloc dessous », juste avant le handle (gouttière 64px)
|
||
const insertBtn=idx>=0?`<button type="button" class="block-insert-btn" title="Add block below" aria-label="Add block below" onclick="E.addAfter(${idx})">+</button>`:'';
|
||
return `<div class="block-wrapper" data-id="${b.id}">
|
||
${insertBtn}
|
||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
|
||
${cmtBtn}
|
||
</div>`;
|
||
}
|
||
|
||
function renderTOC(b){
|
||
var out=[];
|
||
var blocks=window.E?window.E.blocks:[];
|
||
var headings=blocks.filter(function(b){return /^heading_[1-4]$/.test(b.type)&&(b.content||'').trim();});
|
||
if(headings.length===0){
|
||
out.push(`<div class="block-content block-toc block-toc-empty"><span style="color:var(--text-dim);font-size:13px;">Add headings to generate a table of contents</span></div>`);
|
||
}else{
|
||
out.push(`<div class="block-content block-toc"><div class="block-toc-title">On this page</div>`);
|
||
headings.forEach(function(h){
|
||
var lvl=parseInt(h.type.slice(-1),10);
|
||
var id='h-'+ (h.id||'').replace(/[^a-zA-Z0-9]/g,'');
|
||
out.push(`<div class="block-toc-item" style="margin-left:${(lvl-1)*12}px" onclick="var el=document.getElementById('${id}');if(el){el.scrollIntoView({behavior:'smooth',block:'start'});}">${esc(h.content)}</div>`);
|
||
});
|
||
out.push(`</div>`);
|
||
}
|
||
if(blocks.length===0){}
|
||
return `<div class="block-wrapper" data-id="${b.id}">${out.join('')}</div>`;
|
||
}
|
||
|
||
function renderMathBlock(b){
|
||
var html=`<div class="math-display" data-bid="${b.id}"></div>`;
|
||
html+=`<textarea class="math-editor" data-math-bid="${b.id}" data-placeholder="Type a LaTeX math expression (e.g. E = mc^2)..." spellcheck="false">${esc(b.content)}</textarea>`;
|
||
return html;
|
||
}
|
||
|
||
function renderAutomationButton(b,idx){
|
||
var label=(b.content&&b.content.trim())?b.content:((b.automation_name||'Run automation'));
|
||
var bound=b.automation_id>0;
|
||
var badge=bound?'<span style="opacity:.65;font-weight:400;margin-left:8px;font-size:11px;">⚡ '+esc(b.automation_name||'')+'</span>':'<span style="opacity:.65;font-weight:400;margin-left:8px;font-size:11px;">Choose…</span>';
|
||
var actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||
return `<div class="block-wrapper" data-id="${b.id}">
|
||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||
${actions}
|
||
<div class="block-content"><button type="button" class="fd-automation-btn" onclick="E.runAutomationButton('${b.id}')">⚡ ${esc(label)}${badge}</button></div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderColumnsBlock(b){
|
||
var cols=(b.children&&b.children.length)?b.children:[{id:genId(),type:'paragraph',content:''}];
|
||
var cells=cols.map(function(c){
|
||
return `<div class="column-cell">${renderChildColumn(c,b.id)}</div>`;
|
||
}).join('');
|
||
return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-columns"><div class="columns-add-bar"><button class="columns-add-btn" onclick="E._addColumn('${b.id}')">+ Add column</button></div><div class="columns-row">${cells}</div></div></div>`;
|
||
}
|
||
|
||
function renderChildColumn(c,parentId){
|
||
return `<div class="block-column-wrapper" data-parent="${parentId}">
|
||
<div class="block-actions"><button class="block-add-btn" onclick="E._removeColumn('${parentId}','${c.id}')" title="Remove column">×</button></div>
|
||
<div class="block-content block-column" data-bid="${c.id}" contenteditable="true" data-placeholder="${c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':'Type...'}" spellcheck="false">${esc(c.content)}</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderNestedBlock(c){
|
||
var ph=c.type==='heading_1'?'Heading 1':c.type==='heading_2'?'Heading 2':c.type==='heading_3'?'Heading 3':c.type==='heading_4'?'Heading 4':c.type==='bulleted_list'?'List':c.type==='numbered_list'?'List':c.type==='to_do'?'To-do':'Type...';
|
||
return `<div class="block-wrapper nested-block"><div class="block-content block-nested" data-bid="${c.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(c.content)}</div></div>`;
|
||
}
|
||
|
||
// ═══════════ AI Meeting Note — widget de réunion (v4.12.0) ═══════════
|
||
// Bloc interactif « Notion-like » : consentement, transcription live (Web
|
||
// Speech API quand disponible), transcript horodaté + notes, puis résumé
|
||
// généré par le FlowDeck Agent.
|
||
function renderMeetingBlock(b, idx){
|
||
var actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||
return `<div class="block-wrapper" data-id="${b.id}">
|
||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||
${actions}
|
||
<div class="block-content block-meeting" data-meeting-bid="${b.id}"><div class="fm-loading">Préparation de la réunion…</div></div>
|
||
</div>`;
|
||
}
|
||
|
||
function _fmFind(bid){var e=window.E;if(e&&e.blocks){for(var i=0;i<e.blocks.length;i++){if(e.blocks[i].id===bid)return e.blocks[i];}}return null;}
|
||
function _fmDef(){return {title:'Meeting',date:new Date().toISOString().slice(0,10),consent:'self',started:false,status:'idle',lines:[],interim:'',notes:'',manualTranscript:'',summary:'',summaryError:'',summaryLoading:false,lastTab:'transcript'};}
|
||
function _fmEnsure(bid){var b=_fmFind(bid);if(!b)return null;if(!b.meeting)b.meeting=_fmDef();if(!b.meeting.lines)b.meeting.lines=[];if(b.meeting.summaryLoading===undefined)b.meeting.summaryLoading=false;if(b.meeting.manualTranscript===undefined)b.meeting.manualTranscript='';return b;}
|
||
function _fmTime(){var d=new Date();var p=function(n){return (n<10?'0':'')+n;};return p(d.getHours())+':'+p(d.getMinutes())+':'+p(d.getSeconds());}
|
||
function _fmEsc(v){return esc(v==null?'':String(v));}
|
||
function _fmAttr(v){return _fmEsc(v).replace(/"/g,'"');}
|
||
function _fmSave(){var e=window.E;if(e){e.dirty=true;if(e.autoSave)e.autoSave();}}
|
||
function _fmTranscriptText(b){var out=[];(b.meeting.lines||[]).forEach(function(l){out.push('['+l.t+'] '+(l.who||'Vous')+' : '+l.text);});if((b.meeting.manualTranscript||'').trim())out.push('(transcription saisie/collée) '+(b.meeting.manualTranscript||'').trim());if((b.meeting.notes||'').trim())out.push('(notes) '+(b.meeting.notes||'').trim());return out.join('\n');}
|
||
|
||
var FDMeeting = {
|
||
_rt:{},
|
||
_audio(){ return !!(window.SpeechRecognition||window.webkitSpeechRecognition); },
|
||
mount(bid,root){
|
||
var s=this._rt[bid]||(this._rt[bid]={});s.root=root;
|
||
if(!s.wired){
|
||
s.wired=true;
|
||
var self=this;
|
||
root.addEventListener('click',function(ev){var t=ev.target&&ev.target.closest?ev.target.closest('[data-act]'):null;if(t)self.act(bid,t.getAttribute('data-act'),t);});
|
||
root.addEventListener('input',function(ev){self.onInput(bid,ev.target);});
|
||
root.addEventListener('change',function(ev){self.onInput(bid,ev.target);});
|
||
}
|
||
this.paint(bid);
|
||
},
|
||
focus(bid){},
|
||
paint(bid){
|
||
var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;
|
||
s.root.innerHTML=this.html(bid);
|
||
},
|
||
_statusLabel(m){
|
||
if(m.status==='running')return '● Enregistrement';
|
||
if(m.status==='paused')return '⏸ En pause';
|
||
if(m.status==='done')return '✓ Terminée';
|
||
if(m.status==='manual')return '📝 Sans audio';
|
||
return 'Prête';
|
||
},
|
||
html(bid){
|
||
var b=_fmEnsure(bid);if(!b)return '';
|
||
var m=b.meeting;var s=this._rt[bid]||{};
|
||
var audioOk=this._audio();
|
||
var st=this._statusLabel(m);
|
||
var pill=m.status==='running'||m.status==='paused'||m.status==='done'||m.status==='manual'
|
||
? `<span class="fm-pill ${m.status==='done'?'fm-pill-ok':''}">${_fmEsc(st)}</span>`:'';
|
||
var head=`<div class="fm-head">
|
||
<span class="fm-ico">🎙️</span>
|
||
<input class="fm-title" data-fm="title" value="${_fmAttr(m.title)}" placeholder="Titre de la réunion" spellcheck="false">
|
||
<span class="fm-date">📅 ${_fmEsc(m.date)}</span>
|
||
${pill}
|
||
</div>`;
|
||
|
||
var body='';
|
||
if(!m.started){
|
||
body+=`<div class="fm-step">
|
||
<div class="fm-step-title">1 · Consentement des participants</div>
|
||
<label class="fm-radio"><input type="radio" name="fm-consent-${bid}" data-fm="consent" value="self" ${m.consent==='auto'?'':'checked'}> <span>🔘 Obtenir le consentement moi-même</span></label>
|
||
<label class="fm-radio"><input type="radio" name="fm-consent-${bid}" data-fm="consent" value="auto" ${m.consent==='auto'?'checked':''}> <span>🔘 Lire automatiquement un message audio</span></label>
|
||
<div class="fm-step-desc">${audioOk?'Notion AI est prêt : la réunion sera transcrite en direct. Autorisez le micro lorsque le navigateur le demande.':'Ce navigateur ne supporte pas la transcription audio : vous pourrez saisir ou coller la transcription manuellement.'}</div>
|
||
<button class="fm-btn fm-btn-primary" data-act="start" type="button">${audioOk?'🎙 Start transcribing':'Ouvrir la boîte de réunion (sans audio)'}</button>
|
||
</div>`;
|
||
}else{
|
||
var tabs=`<div class="fm-tabs">
|
||
<button class="fm-tab ${m.lastTab==='notes'?'active':''}" data-act="tab" data-tab="notes" type="button">Notes</button>
|
||
<button class="fm-tab ${m.lastTab==='transcript'?'active':''}" data-act="tab" data-tab="transcript" type="button">Transcript</button>
|
||
<button class="fm-tab ${m.lastTab==='summary'?'active':''}" data-act="tab" data-tab="summary" type="button">Summary</button>
|
||
</div>`;
|
||
var ctrl='';
|
||
if(m.status==='running'){
|
||
ctrl=`<div class="fm-wave">•••|ı|ı|ı|ı•••</div>
|
||
<div class="fm-actions">
|
||
<button class="fm-btn" data-act="pause" type="button">⏸ Pause</button>
|
||
<button class="fm-btn fm-btn-primary" data-act="stop" type="button">⏹ Stop</button>
|
||
</div>`;
|
||
}else if(m.status==='paused'){
|
||
ctrl=`<div class="fm-actions">
|
||
<button class="fm-btn fm-btn-primary" data-act="start" type="button">▶ Reprendre</button>
|
||
<button class="fm-btn" data-act="stop" type="button">⏹ Stop</button>
|
||
</div>`;
|
||
}else if(m.status==='manual'){
|
||
ctrl=`<div class="fm-actions"><button class="fm-btn" data-act="stop" type="button">⏹ Terminer la réunion</button></div>`;
|
||
}else if(m.status==='done'){
|
||
ctrl=`<div class="fm-actions"><button class="fm-btn" data-act="new" type="button">🔄 Nouvelle transcription</button></div>`;
|
||
}
|
||
var panel='';
|
||
if(m.lastTab==='notes'){
|
||
panel=`<textarea class="fm-area" data-fm="notes" rows="6" placeholder="Prenez des notes manuelles pendant la réunion…">${_fmEsc(m.notes)}</textarea>`;
|
||
}else if(m.lastTab==='transcript'){
|
||
var lines=(m.lines||[]).map(function(l){return `<div class="fm-line"><span class="fm-ts">${_fmEsc(l.t||'')}</span><span class="fm-who">${_fmEsc(l.who||'Vous')}</span><span class="fm-txt">${_fmEsc(l.text)}</span></div>`;}).join('');
|
||
if(m.status==='running'){lines+=`<div class="fm-line fm-interim"><span class="fm-ts">·</span><span class="fm-who">Vous</span><span class="fm-txt">${_fmEsc(m.interim||'')}${(m.interim||'')?'':'…'}</span></div>`;}
|
||
var manual=m.status!=='running'?`<div class="fm-manual"><div class="fm-manual-label">Transcript saisi ou collé (si aucune capture audio)</div><textarea class="fm-area" data-fm="manual" rows="4" placeholder="Collez ici la transcription de la réunion (optionnel)…">${_fmEsc(m.manualTranscript)}</textarea></div>`:'';
|
||
panel=`<div class="fm-transcript"><div data-fm-transcript-body>${lines||'<div class="fm-empty">La transcription apparaîtra ici en direct. Enregistrez la réunion pour générer un transcript.</div>'}</div>${manual}</div>`;
|
||
}else{
|
||
var summaryHtml='';
|
||
if(m.summaryLoading){
|
||
summaryHtml=`<div class="fm-loading">Génération du résumé…</div>`;
|
||
}else if(m.summary){
|
||
summaryHtml=`<textarea class="fm-area" data-fm="summary" rows="14">${_fmEsc(m.summary)}</textarea>
|
||
<div class="fm-actions"><button class="fm-btn" data-act="copy" type="button">📋 Copier le résumé</button></div>`;
|
||
}else if(m.summaryError){
|
||
summaryHtml=`<div class="fm-error">${_fmEsc(m.summaryError)}</div>
|
||
<button class="fm-btn fm-btn-primary" data-act="generate" type="button">Réessayer</button>`;
|
||
}else{
|
||
var has=!!(_fmTranscriptText(b).trim());
|
||
if(m.status==='done'&&has){
|
||
summaryHtml=`<button class="fm-btn fm-btn-primary" data-act="generate" type="button">✨ Générer le résumé</button>
|
||
<div class="fm-step-desc">L'agent analyse le transcript et produit un aperçu structuré (sujets, décisions, actions).</div>`;
|
||
}else{
|
||
summaryHtml=`<div class="fm-empty">Arrêtez l'enregistrement (ou ajoutez un transcript) puis générez le résumé ici.</div>`;
|
||
}
|
||
}
|
||
panel=summaryHtml;
|
||
}
|
||
body=`<div class="fm-step">${ctrl}${tabs}${panel}</div>`;
|
||
}
|
||
return `<div class="fm">${head}${body}</div>`;
|
||
},
|
||
onInput(bid,el){
|
||
var b=_fmEnsure(bid);if(!b||!el)return;var m=b.meeting;var k=el.getAttribute&&el.getAttribute('data-fm');
|
||
if(!k)return;
|
||
if(k==='title'){m.title=el.value||'';var tEl=document.getElementById('_titleEl');if(tEl&&!((tEl.textContent||'').trim())){tEl.textContent=m.title;if(window.E)window.E.pageTitle=m.title;}}
|
||
else if(k==='notes'){m.notes=el.value;}
|
||
else if(k==='manual'){m.manualTranscript=el.value;}
|
||
else if(k==='summary'){m.summary=el.value;}
|
||
else if(k==='consent'){m.consent=el.value;}
|
||
else return;
|
||
_fmSave();
|
||
},
|
||
act(bid,act,el){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var self=this;
|
||
if(act==='start'){
|
||
if(m.status==='done'||m.status==='idle'||m.status==='paused'){
|
||
if(m.status==='done'){m.lines=[];m.summary='';m.summaryError='';m.manualTranscript='';}
|
||
m.started=true;m.lastTab='transcript';
|
||
if(!self._audio()){m.status='manual';m.interim='';_fmSave();self.paint(bid);return;}
|
||
if(m.consent==='auto'&&window.speechSynthesis){
|
||
var u=new SpeechSynthesisUtterance('Cette réunion est enregistrée et transcrite par FlowDeck. Merci de votre attention.');
|
||
try{window.speechSynthesis.speak(u);}catch(e){}
|
||
}
|
||
setTimeout(function(){self._startRec(bid);},m.consent==='auto'?1300:0);
|
||
}
|
||
}else if(act==='pause'){
|
||
this._stopRec(bid,false);m.status='paused';m.interim='';_fmSave();this.paint(bid);
|
||
}else if(act==='stop'){
|
||
this._stopRec(bid,true);m.status='done';m.lastTab='transcript';m.interim='';_fmSave();this.paint(bid);
|
||
}else if(act==='new'){
|
||
m.lines=[];m.summary='';m.summaryError='';m.status='idle';m.started=false;m.lastTab='transcript';_fmSave();this.paint(bid);
|
||
}else if(act==='tab'){
|
||
m.lastTab=(el&&el.getAttribute&&el.getAttribute('data-tab'))||'transcript';_fmSave();this.paint(bid);
|
||
}else if(act==='generate'){
|
||
this._generate(bid);
|
||
}else if(act==='copy'){
|
||
var txt=(m.summary||'').trim();
|
||
if(!txt)return;
|
||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).catch(function(){});}
|
||
}
|
||
},
|
||
_startRec(bid){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid]||(this._rt[bid]={});
|
||
var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
|
||
if(!SR){m.status='manual';_fmSave();this.paint(bid);return;}
|
||
if(s.rec){try{s.rec.stop();}catch(e){}}
|
||
var rec=new SR();s.rec=rec;s.offset=0;s.stopped=false;
|
||
rec.lang='fr-FR';rec.continuous=true;rec.interimResults=true;rec.maxAlternatives=1;
|
||
var self=this;
|
||
rec.onresult=function(ev){
|
||
var final='',interim='';
|
||
for(var i=(s.offset||0);i<ev.results.length;i++){
|
||
var r=ev.results[i];
|
||
if(r.isFinal)final+=(r[0]&&r[0].transcript||'');
|
||
else interim+=(r[0]&&r[0].transcript||'');
|
||
}
|
||
s.offset=ev.results.length;
|
||
if(final&&final.trim()){
|
||
m.lines.push({t:_fmTime(),who:'Vous',text:final.trim()});
|
||
_fmSave();
|
||
self._updateTranscriptOnly(bid);
|
||
}
|
||
m.interim=interim.trim();
|
||
self._updateInterim(bid);
|
||
};
|
||
rec.onend=function(){
|
||
if(!s.stopped&&m.status==='running'&&self._audio()){try{rec.start();}catch(e){}}
|
||
};
|
||
rec.onerror=function(ev){
|
||
if(ev&&ev.error==='not-allowed'){m.summaryError='Microphone refusé : autorisez le micro ou utilisez la saisie manuelle du transcript.';m.status='manual';s.stopped=true;_fmSave();self.paint(bid);}
|
||
};
|
||
m.status='running';m.interim='';_fmSave();
|
||
this.paint(bid);
|
||
try{rec.start();}catch(e){m.status='manual';s.stopped=true;_fmSave();this.paint(bid);}
|
||
},
|
||
_stopRec(bid,stop){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;var s=this._rt[bid];
|
||
if(s&&s.rec){s.stopped=true;try{s.rec.stop();}catch(e){}}
|
||
if(stop&&m.interim&&m.interim.trim()){m.lines.push({t:_fmTime(),who:'Vous',text:m.interim.trim()});}
|
||
m.interim='';
|
||
},
|
||
_updateTranscriptOnly(bid){var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;var el=s.root.querySelector('[data-fm-transcript-body]');if(el)el.innerHTML=this._transcriptLines(b);},
|
||
_updateInterim(bid){var s=this._rt[bid];var b=_fmEnsure(bid);if(!b||!s||!s.root)return;var el=s.root.querySelector('.fm-interim .fm-txt');if(el&&el.textContent!==(b.meeting.interim||'')){el.textContent=(b.meeting.interim||'')+'…';}},
|
||
_transcriptLines(b){var m=b.meeting;var lines=(m.lines||[]).map(function(l){return `<div class="fm-line"><span class="fm-ts">${_fmEsc(l.t||'')}</span><span class="fm-who">${_fmEsc(l.who||'Vous')}</span><span class="fm-txt">${_fmEsc(l.text)}</span></div>`;}).join('');return lines||'<div class="fm-empty">La transcription apparaîtra ici en direct…</div>';},
|
||
_generate(bid){
|
||
var b=_fmEnsure(bid);if(!b)return;var m=b.meeting;
|
||
var content=_fmTranscriptText(b);
|
||
if(!content.trim()){m.summaryError='Aucun contenu de réunion à résumer.';_fmSave();this.paint(bid);return;}
|
||
m.summaryLoading=true;m.summaryError='';_fmSave();this.paint(bid);
|
||
var self=this;
|
||
var prompt='Rédige le résumé structuré de cette réunion (format AI Meeting Note) : une ligne d\'en-tête avec la date du '+_fmEsc(m.date)+', puis les sections « Résumé » (Overview), « Décisions » et « Action items » avec des puces concrètes. Si le contenu est trop court ou fragmentaire, indique « No meaningful content could be extracted ». Ne précède pas la réponse de commentaires.';
|
||
if(!(window.fdAgent&&window.fdAgent.generate)){
|
||
m.summaryLoading=false;m.summaryError='Agent IA indisponible — configurez-le dans Paramètres → Agent & IA.';_fmSave();this.paint(bid);return;
|
||
}
|
||
window.fdAgent.generate(prompt, content).then(function(text){
|
||
m.summary=(text||'').trim();m.summaryLoading=false;m.summaryError='';_fmSave();self.paint(bid);
|
||
}).catch(function(err){
|
||
m.summaryLoading=false;m.summaryError=(err&&err.message)?err.message:'Échec de génération du résumé';_fmSave();self.paint(bid);
|
||
});
|
||
}
|
||
};
|
||
window.FDMeeting=FDMeeting;
|
||
|
||
// ═══════════ v5.10.0 — Interactions de bloc : sélection multiple, menu ⋮⋮, undo/redo ═══════════
|
||
let _selSet=new Set();
|
||
var _mdSelDown=null; // v7.69 : point de départ du glisser de sélection
|
||
var _mdSelRange=null; // v7.69 : plage reconstruite, à restaurer après le collapse du focus
|
||
function _selHas(bid){return _selSet.has(bid);}
|
||
function _selToggle(bid){if(!bid)return;if(_selSet.has(bid))_selSet.delete(bid);else _selSet.add(bid);
|
||
document.querySelectorAll('.block-wrapper[data-id="'+bid+'"]').forEach(function(w){w.classList.toggle('selected',_selSet.has(bid));});}
|
||
function _selClear(){_selSet.clear();document.querySelectorAll('.block-wrapper.selected').forEach(function(w){w.classList.remove('selected');});}
|
||
function _selApply(){document.querySelectorAll('.block-wrapper[data-id]').forEach(function(w){var id=w.getAttribute('data-id');w.classList.toggle('selected',!!(_selSet.has(id)));});}
|
||
function _selIndicesSorted(){var E=window.E;var out=[];if(!E)return out;E.blocks.forEach(function(b,i){if(_selSet.has(b.id))out.push(i);});return out;}
|
||
function _rtSync(){try{var rt=window.__fdRT;if(rt&&rt.syncNow)rt.syncNow();}catch(e){}}
|
||
|
||
const _BLOCK_TURN_TYPES=[
|
||
{id:'paragraph',name:'Text'},{id:'heading_1',name:'Heading 1'},{id:'heading_2',name:'Heading 2'},
|
||
{id:'heading_3',name:'Heading 3'},{id:'heading_4',name:'Heading 4'},{id:'bulleted_list',name:'Bulleted list'},
|
||
{id:'numbered_list',name:'Numbered list'},{id:'to_do',name:'To-do'},{id:'toggle',name:'Toggle'},
|
||
{id:'quote',name:'Quote'},{id:'callout',name:'Callout'},{id:'table',name:'Table'},
|
||
{id:'math',name:'Math equation'},{id:'columns',name:'Column list'},{id:'button',name:'Button / Automation'},
|
||
{id:'code',name:'Code block'},{id:'synced',name:'Synced block'}
|
||
];
|
||
// Sous-menu « Turn into » — ordre Notion ; id ∈ _BLOCK_TURN_TYPES, cols = nb de colonnes
|
||
const _BLOCK_TURN_MENU=[
|
||
{id:'paragraph',name:'Text',icon:'T'},{id:'heading_1',name:'Heading 1',icon:'H1'},
|
||
{id:'heading_2',name:'Heading 2',icon:'H2'},{id:'heading_3',name:'Heading 3',icon:'H3'},
|
||
{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},
|
||
{id:'numbered_list',name:'Numbered list',icon:'1.'},{id:'to_do',name:'To-do list',icon:'☑'},
|
||
{id:'toggle',name:'Toggle list',icon:'▸'},{id:'code',name:'Code',icon:'</>'},
|
||
{id:'quote',name:'Quote',icon:'”'},{id:'callout',name:'Callout',icon:'ⓘ'},
|
||
{id:'math',name:'Block equation',icon:'∑'},{id:'synced',name:'Synced block',icon:'⛓'},
|
||
{id:'columns',name:'2 columns',icon:'▦',cols:2},{id:'columns',name:'3 columns',icon:'▦',cols:3},
|
||
{id:'columns',name:'4 columns',icon:'▦',cols:4},{id:'columns',name:'5 columns',icon:'▦',cols:5}
|
||
];
|
||
// Couleurs texte/fond alignées par index — noms Notion (Dernière utilisée ailleurs)
|
||
const _BLOCK_COLOR_NAMES=['Default','Gray','Brown','Orange','Yellow','Green','Blue','Purple','Pink','Red'];
|
||
const _BLOCK_COLORS_TEXT=['default','#94A3B8','#8A6642','#F76B15','#FFC53D','#46A758','#3E63DD','#8E4EC6','#E5539B','#E5484D'];
|
||
const _BLOCK_COLORS_BG=['default','rgba(148,163,184,.15)','rgba(138,102,66,.15)','rgba(247,107,21,.15)','rgba(255,197,61,.15)','rgba(70,154,88,.15)','rgba(62,99,221,.15)','rgba(142,78,198,.15)','rgba(229,83,155,.15)','rgba(229,72,77,.15)'];
|
||
// Raccourcis affichés — implémentés par le handler clavier en bas de fichier
|
||
const _BM_SHORTCUTS={link:'Alt+⇧+L',duplicate:'Ctrl+D',move:'Ctrl+⇧+P',delete:'Del',comment:'Ctrl+⇧+M',suggest:'Ctrl+⇧+Alt+X',ai:'Ctrl+J',color:'Ctrl+⇧+H'};
|
||
var _bmSubKind='',_bmSubTimer=null;
|
||
function _blockMenuClose(){var m=document.getElementById('blockMenu');if(m)m.remove();m=document.getElementById('blockMenuSub');if(m)m.remove();clearTimeout(_bmSubTimer);_bmSubKind='';}
|
||
function _blockAttr(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
|
||
function _blockMenuBid(){var m=document.getElementById('blockMenu');return m?(m.getAttribute('data-bid')||''):'';}
|
||
// Sous-menu ouvert au survol de l'entrée correspondante (panneau de droite)
|
||
function _blockSubOpen(kind,bid,anchor){
|
||
if(!kind||!bid)return;
|
||
var m=document.getElementById('blockMenu');if(!m)return;
|
||
if(_bmSubKind===kind&&document.getElementById('blockMenuSub'))return;
|
||
clearTimeout(_bmSubTimer);_bmSubKind=kind;
|
||
var r=m.getBoundingClientRect();
|
||
var a=anchor&&anchor.getBoundingClientRect?anchor.getBoundingClientRect():r;
|
||
var top=Math.max(8,Math.min(a.top,window.innerHeight-60));
|
||
if(kind==='turn')_blockTurnMenuOpen(r.right+4,top,bid);
|
||
else if(kind==='color')_blockColorMenuOpen(r.right+4,top,bid);
|
||
else if(kind==='skills')_blockSkillsMenuOpen(r.right+4,top,bid);
|
||
var s=document.getElementById('blockMenuSub');
|
||
if(s){s.addEventListener('mouseenter',function(){clearTimeout(_bmSubTimer);});
|
||
s.addEventListener('mouseleave',function(){_blockSubCloseLater();});}
|
||
}
|
||
function _blockSubCloseLater(){clearTimeout(_bmSubTimer);_bmSubTimer=setTimeout(function(){var s=document.getElementById('blockMenuSub');if(s&&!s.matches(':hover')){s.remove();_bmSubKind='';}},260);}
|
||
// Sous-menu ouvert depuis la toolbar de sélection (pas de #blockMenu parent)
|
||
function _subHoverClose(){var s=document.getElementById('blockMenuSub');if(s)s.addEventListener('mouseleave',function(){_blockSubCloseLater();});}
|
||
function _blockMenuClick(ev){
|
||
var t=ev.target.closest&&ev.target.closest('[data-bm-act]');if(!t)return;
|
||
var act=t.getAttribute('data-bm-act');var bid=t.getAttribute('data-bid')||_blockMenuBid();
|
||
if(!act)return;
|
||
ev.preventDefault();ev.stopPropagation();
|
||
var E=window.E;if(!E)return;
|
||
var idx=bid?E.getIdx(bid):-1;
|
||
if(act==='submenu'){_blockSubOpen(t.getAttribute('data-sub'),bid,t);return;}
|
||
if(act==='duplicate'){E.duplicateBlock(idx);}
|
||
else if(act==='copy-link'){E.copyBlockLink(bid||'');}
|
||
else if(act==='move-to'){_blockMoveToOpen(idx);}
|
||
else if(act==='delete'){E.deleteSelected(idx);}
|
||
else if(act==='comment'){E.commentBlock(idx);}
|
||
else if(act==='suggest'){E.suggestEdit(idx);}
|
||
else if(act==='ai'){E.askAIBlock(idx);}
|
||
else if(act==='skill'){E.runSkill(idx,t.getAttribute('data-prompt')||'');}
|
||
// v7.68 : Clipboard du menu (sélection multi-blocs gérée par copySelectedBlocks)
|
||
else if(act==='clip-cut'){
|
||
if(_selSet.size>1)E.cutSelectedBlocks();
|
||
else{var mst=document.getElementById('blockMenu');const md=E.clipCut?E.clipCut(mst&&mst._clipSt):null;if(md!=null&&navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md);E.showToast('Sélection coupée');}}
|
||
}
|
||
else if(act==='clip-copy'){
|
||
if(_selSet.size>1)E.copySelectedBlocks();
|
||
else{var mst2=document.getElementById('blockMenu');const md=E.clipCopy?E.clipCopy(mst2&&mst2._clipSt):null;if(md!=null&&navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md);E.showToast('Sélection copiée');}}
|
||
}
|
||
else if(act==='clip-paste'){E.pasteBlocks();}
|
||
else if(act==='color'){var key=t.getAttribute('data-color-key'),val=t.getAttribute('data-color-val');E.setBlockColor(idx,key,val);}
|
||
else if(act==='turn-type'){E.turnInto(idx,t.getAttribute('data-type'),parseInt(t.getAttribute('data-cols')||'0',10)||0);_blockMenuClose();return;}
|
||
_blockMenuClose();
|
||
}
|
||
function _blockMenuItem(act,icon,label,opts){
|
||
opts=opts||{};
|
||
var attr=' data-bm-act="'+act+'"';
|
||
if(opts.bid)attr+=' data-bid="'+_blockAttr(opts.bid)+'"';
|
||
if(opts.sub)attr+=' data-sub="'+opts.sub+'"';
|
||
if(opts.colorKey)attr+=' data-color-key="'+opts.colorKey+'" data-color-val="'+_blockAttr(opts.colorVal||'')+'"';
|
||
if(opts.prompt!=null)attr+=' data-prompt="'+_blockAttr(opts.prompt)+'"';
|
||
if(opts.type)attr+=' data-type="'+opts.type+'"';
|
||
if(opts.cols)attr+=' data-cols="'+opts.cols+'"';
|
||
if(opts.title)attr+=' title="'+_blockAttr(opts.title)+'"';
|
||
var right=opts.check?'<span class="bm-check">✓</span>'
|
||
:(opts.sub?'<span class="bm-arrow">▸</span>'
|
||
:(opts.shortcut?'<span class="bm-hint">'+opts.shortcut+'</span>':''));
|
||
return '<div class="bm-item'+(opts.danger?' bm-danger':'')+(opts.current?' bm-current':'')+'"'+attr+'>'
|
||
+(icon!=null?'<span class="bm-ico">'+icon+'</span>':'')+label+right+'</div>';
|
||
}
|
||
// Métadonnées du pied de page : auteur / heure de dernière édition / taille du bloc
|
||
function _blockMenuMeta(b){
|
||
var E=window.E||{};
|
||
var who='';
|
||
try{var n=document.querySelector('.um-name')||document.querySelector('.workspace-name');who=n?(n.textContent||'').trim():'';}catch(e){}
|
||
var when='';
|
||
if(E.updatedAt){var d=new Date(E.updatedAt);if(!isNaN(d)){
|
||
var same=d.toDateString()===new Date().toDateString();
|
||
var hm=d.toLocaleTimeString([],{hour:'numeric',minute:'2-digit'});
|
||
when=(same?'Today at ':(d.toLocaleDateString([])+' at '))+hm;}}
|
||
var content=(b&&b.content)||'';
|
||
var trimmed=content.trim();
|
||
var words=trimmed?trimmed.split(/\s+/).length:0;
|
||
return '<div class="bm-foot"><div>Last edited by '+esc(who||'you')+'</div>'
|
||
+(when?'<div>'+esc(when)+'</div>':'')
|
||
+'<div>'+words+' word'+(words===1?'':'s')+', '+content.length+' character'+(content.length===1?'':'s')+'</div></div>';
|
||
}
|
||
function blockMenuOpen(x,y,bid){
|
||
_blockMenuClose();
|
||
var E=window.E;if(!E)return;
|
||
var idx=E.getIdx(bid);if(idx<0)return;
|
||
var b=E.blocks[idx];if(!b)return;
|
||
var d=document.createElement('div');d.id='blockMenu';d.className='block-menu';d.setAttribute('data-bid',b.id);d.style.cssText='position:fixed;z-index:4000;';
|
||
var S=_BM_SHORTCUTS;
|
||
// v7.68 : Clipboard — une sélection de texte multi-blocs OU des blocs
|
||
// sélectionnés (handle + Maj-clic) donnent Cut/Copy ; Paste colle après.
|
||
// la sélection est détruite quand la recherche prend le focus : on la
|
||
// fige ici et les actions Clipboard la reprennent telle quelle.
|
||
var clipSt=(E._clipState?E._clipState():null);
|
||
d._clipSt=clipSt;
|
||
var clip=(_selSet.size>1||clipSt)
|
||
?'<div class="bm-group">Clipboard</div>'
|
||
+_blockMenuItem('clip-cut','✂','Cut',{shortcut:'Ctrl+X'})
|
||
+_blockMenuItem('clip-copy','⧉','Copy',{shortcut:'Ctrl+C'})
|
||
+_blockMenuItem('clip-paste','📋','Paste',{shortcut:'Ctrl+V'})
|
||
:'';
|
||
d.innerHTML='<input class="bm-search" type="text" placeholder="Search actions..." autocomplete="off" spellcheck="false">'
|
||
+clip
|
||
+'<div class="bm-group">Text</div>'
|
||
+_blockMenuItem('submenu','↩','Turn into',{sub:'turn'})
|
||
+_blockMenuItem('submenu','🎨','Color',{sub:'color'})
|
||
+_blockMenuItem('copy-link','🔗','Copy link to block',{shortcut:S.link})
|
||
+_blockMenuItem('duplicate','▣','Duplicate',{shortcut:S.duplicate})
|
||
+_blockMenuItem('move-to','↪','Move to…',{shortcut:S.move})
|
||
+_blockMenuItem('delete','✕','Delete',{shortcut:S.delete,danger:true})
|
||
+_blockMenuItem('comment','💬','Comment',{shortcut:S.comment})
|
||
+_blockMenuItem('suggest','✎','Suggest edits',{shortcut:S.suggest})
|
||
+_blockMenuItem('ai','✨','Ask AI',{shortcut:S.ai})
|
||
+_blockMenuItem('submenu','📚','Skills',{sub:'skills'})
|
||
+_blockMenuMeta(b);
|
||
d.addEventListener('click',_blockMenuClick);
|
||
d.addEventListener('input',function(ev){
|
||
if(!ev.target.classList||!ev.target.classList.contains('bm-search'))return;
|
||
var q=(ev.target.value||'').toLowerCase();
|
||
d.querySelectorAll('.bm-item').forEach(function(it){
|
||
it.style.display=(q&&it.textContent.toLowerCase().indexOf(q)<0)?'none':'';
|
||
});
|
||
d.querySelectorAll('.bm-group').forEach(function(g){g.style.display=q?'none':'';});
|
||
});
|
||
d.addEventListener('mouseover',function(ev){
|
||
var it=ev.target&&ev.target.closest?ev.target.closest('[data-sub]'):null;
|
||
if(it)_blockSubOpen(it.getAttribute('data-sub'),b.id,it);
|
||
});
|
||
d.addEventListener('mouseleave',function(){_blockSubCloseLater();});
|
||
d.addEventListener('contextmenu',function(ev){ev.preventDefault();_blockMenuClose();});
|
||
document.body.appendChild(d);
|
||
var r=d.getBoundingClientRect();
|
||
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
|
||
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
|
||
d.style.left=x+'px';d.style.top=y+'px';
|
||
// v7.69 : on ne vole PAS le focus quand un texte est sélectionné — sinon
|
||
// le navigateur efface le surlignage dès que le menu apparaît (les
|
||
// actions Clipboard gardent de toute façon la sélection figée ci-dessus).
|
||
var si=d.querySelector('.bm-search');
|
||
var sLive=window.getSelection();
|
||
var hasSel=sLive&&sLive.rangeCount&&!sLive.isCollapsed;
|
||
if(si&&!hasSel)setTimeout(function(){si.focus();},10);
|
||
}
|
||
// Panneau de droite : coquille commune des 3 sous-menus
|
||
function _blockSubShell(x,y){
|
||
var m=document.getElementById('blockMenuSub');if(m)m.remove();
|
||
var d=document.createElement('div');d.id='blockMenuSub';d.className='block-menu block-menu-sub';d.style.cssText='position:fixed;z-index:4001;';
|
||
d.addEventListener('click',_blockMenuClick);
|
||
document.body.appendChild(d);
|
||
var r=d.getBoundingClientRect();
|
||
x=Math.min(Math.max(8,x),window.innerWidth-r.width-8);
|
||
y=Math.min(Math.max(8,y),window.innerHeight-r.height-8);
|
||
d.style.left=x+'px';d.style.top=y+'px';
|
||
return d;
|
||
}
|
||
function _blockTurnItems(bid){
|
||
var E=window.E;var cur='';
|
||
if(E){var i=E.getIdx(bid);if(i>=0&&E.blocks[i])cur=E.blocks[i].type||'';}
|
||
var html='';
|
||
_BLOCK_TURN_MENU.forEach(function(t){
|
||
var on=(t.id===cur&&!t.cols);
|
||
html+=_blockMenuItem('turn-type',t.icon,t.name,{bid:bid,type:t.id,cols:t.cols,
|
||
title:t.cols?('Create '+t.cols+' columns of blocks'):'',
|
||
check:on,current:on});
|
||
});
|
||
return html;
|
||
}
|
||
function _blockTurnMenuOpen(x,y,bid){
|
||
var d=_blockSubShell(x,y);
|
||
d.innerHTML=_blockTurnItems(bid);
|
||
}
|
||
function _blockColorItems(bid){
|
||
var E=window.E;var idx=E?E.getIdx(bid):-1;var b=idx>=0?E.blocks[idx]:null;
|
||
var curT=(b&&b.style&&b.style.color)||'default';
|
||
var curB=(b&&b.style&&b.style.bgColor)||'default';
|
||
var last='';try{last=localStorage.getItem('fdLastBlockColor')||'';}catch(e){}
|
||
var html='';
|
||
if(last){
|
||
var i=last.indexOf('|');var lk=i<0?'color':last.slice(0,i);var lv=last.slice(i+1);
|
||
var isBg=(lk==='bgColor');
|
||
var arr=isBg?_BLOCK_COLORS_BG:_BLOCK_COLORS_TEXT;
|
||
var ni=arr.indexOf(lv);
|
||
var nm=(ni>=0?_BLOCK_COLOR_NAMES[ni]:'Custom')+(isBg?' background':' text');
|
||
html+='<div class="bm-group">Last used</div>'
|
||
+_blockMenuItem('color','<span style="color:'+(lv==='default'?'var(--text-secondary)':lv)+'">A</span>',
|
||
nm,{bid:bid,colorKey:lk,colorVal:lv,shortcut:_BM_SHORTCUTS.color});
|
||
}
|
||
html+='<div class="bm-group">Text color</div>';
|
||
_BLOCK_COLORS_TEXT.forEach(function(v,n){
|
||
html+=_blockMenuItem('color','<span style="color:'+(v==='default'?'var(--text-secondary)':v)+'">A</span>',
|
||
_BLOCK_COLOR_NAMES[n]+' text',{bid:bid,colorKey:'color',colorVal:v,
|
||
check:v===curT,current:v===curT});
|
||
});
|
||
html+='<div class="bm-group">Background color</div>';
|
||
_BLOCK_COLORS_BG.forEach(function(v,n){
|
||
html+=_blockMenuItem('color','<span class="bm-dot" style="background:'+(v==='default'?'var(--text-tertiary)':v)+'"></span>',
|
||
_BLOCK_COLOR_NAMES[n]+' background',{bid:bid,colorKey:'bgColor',colorVal:v,
|
||
check:v===curB,current:v===curB});
|
||
});
|
||
return html;
|
||
}
|
||
function _blockColorMenuOpen(x,y,bid){
|
||
var d=_blockSubShell(x,y);
|
||
d.innerHTML=_blockColorItems(bid);
|
||
}
|
||
function _blockSkillsMenuOpen(x,y,bid){
|
||
var d=_blockSubShell(x,y);
|
||
d.innerHTML='<div class="bm-empty">Chargement…</div>';
|
||
fetch('/api/agent/skills',{credentials:'same-origin'})
|
||
.then(function(r){return r.json();})
|
||
.then(function(res){
|
||
if(!document.body.contains(d))return;
|
||
var list=(res&&res.skills)||[];
|
||
if(!list.length){d.innerHTML='<div class="bm-empty">Aucune compétence enregistrée — créez-en une dans le panneau Agent.</div>';return;}
|
||
var html='';
|
||
list.forEach(function(s){
|
||
html+=_blockMenuItem('skill','📚',esc(String(s.name||'Skill')),{bid:bid,prompt:s.prompt_template||''});
|
||
});
|
||
d.innerHTML=html;
|
||
})
|
||
.catch(function(){if(document.body.contains(d))d.innerHTML='<div class="bm-empty">Compétences indisponibles.</div>';});
|
||
}
|
||
function _rtMac(){return /Mac|iPhone|iPad/.test(navigator.userAgent);}
|
||
function _blockMoveToOpen(idx){
|
||
var E=window.E;if(!E)return;
|
||
var pages=[];var seen={};
|
||
document.querySelectorAll('.sidebar-item.ws-tree-item a.page-name[href^="/pages/"]').forEach(function(a){
|
||
var m=(a.getAttribute('href')||'').match(/\/pages\/(\d+)/);if(!m)return;
|
||
var id=parseInt(m[1],10);var name=(a.textContent||'').trim()||('Page '+id);
|
||
if(id===E.pid||seen[id])return;seen[id]=1;pages.push({id:id,name:name});
|
||
});
|
||
var ov=document.createElement('div');ov.id='fdMoveOverlay';ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;';
|
||
var box=document.createElement('div');box.style.cssText='background:var(--bg-secondary,#1f1f1f);border:1px solid var(--border,#333);border-radius:12px;padding:16px;width:420px;max-width:92vw;max-height:80vh;overflow:auto;';
|
||
var listHtml=pages.length?pages.map(function(p){return '<button type="button" data-pid="'+p.id+'" class="bm-page">'+esc(p.name)+'</button>';}).join(''):'<div class="bm-empty">Aucune autre page dans l\u2019arbre du workspace.<br>Ouvrez la page cible puis réessayez.</div>';
|
||
box.innerHTML='<div style="font-weight:600;font-size:14px;margin-bottom:10px;">Move block to…</div>'
|
||
+'<input type="text" class="bm-search" placeholder="Filter pages…" autofocus>'
|
||
+'<div class="bm-page-list">'+listHtml+'</div>'
|
||
+'<button type="button" data-cancel="1" class="bm-cancel">Annuler</button>';
|
||
box.addEventListener('click',function(ev){
|
||
var p=ev.target.closest('[data-pid]');
|
||
if(p){var pid=parseInt(p.getAttribute('data-pid'),10);ov.remove();E.moveBlockToPage(idx,pid);return;}
|
||
if(ev.target.closest('[data-cancel]')){ov.remove();}
|
||
});
|
||
box.addEventListener('input',function(ev){
|
||
if(ev.target.classList&&ev.target.classList.contains('bm-search')){
|
||
var q=(ev.target.value||'').toLowerCase();
|
||
box.querySelectorAll('.bm-page').forEach(function(b2){b2.style.display=(q&&b2.textContent.toLowerCase().indexOf(q)<0)?'none':'';});
|
||
}
|
||
});
|
||
ov.appendChild(box);
|
||
document.body.appendChild(ov);
|
||
}
|
||
function _blockDocDown(ev){
|
||
if(!(ev.target&&ev.target.closest))return;
|
||
if(ev.target.closest('#blockMenu')||ev.target.closest('#blockMenuSub')||ev.target.closest('#fdMoveOverlay'))return;
|
||
if(ev.target.closest('.block-handle'))return;
|
||
if(ev.button===2){const w0=ev.target.closest('.block-wrapper');if(w0&&_selSet.has(w0.getAttribute('data-id')))return;}
|
||
if(_selSet.size)_selClear();
|
||
_blockMenuClose();
|
||
}
|
||
|
||
window.E=null;
|
||
function editorState(){
|
||
return {
|
||
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
|
||
updatedAt:PD.updated_at||'',
|
||
fmt:{open:false,top:0,left:0,idx:-1},
|
||
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,exportOpen:false,
|
||
aiBusy:false,
|
||
moveOpen:false,moveWorkspaces:[],moveLoading:false,
|
||
activityOpen:false,pageCreated:PD.created_at||'',
|
||
shareTab:'share',favorited:false,
|
||
pagePublished:!!PD.published,
|
||
pageIsShared:!!PD.is_shared,
|
||
generalAccess:PD.share_mode||'private',publicPerm:'viewer',
|
||
pubAllowEdit:false,pubAllowComments:true,
|
||
pageUrl:window.location.href,publishedUrl:'',
|
||
inviteEmail:'',invitePermission:'edit',inviteUserId:null,inviteGroupId:null,inviteGroups:[],inviteUsers:[],inviteSuggestOpen:false,inviteSel:-1,accessList:[],
|
||
toastVisible:false,toastMsg:'',
|
||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||
commentsOpen:false,commentDraft:'',comments:[],commentCount:0,reactions:[],commentMenu:null,
|
||
currentUserId:PD.user_id||0,
|
||
_commentSel:{range:null,bid:null,start:0,end:0},
|
||
_notifiedMentions:{},
|
||
undoStack:[],redoStack:[],_histCap:100,_histLock:false,
|
||
inlineDBs: [], formOpen: false, formName: '',
|
||
// ── 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:[],
|
||
// ── Unified template picker (registre unifié §7.1 : la pilule
|
||
// « Templates » ouvre TOUJOURS ce panneau — jamais un no-op, P5) ──
|
||
tplOpen:false,tplLoading:false,tplItems:[],tplQuery:'',tplKind:'all',
|
||
tplError:'',tplSelected:null,tplPreviewBlocks:[],tplPreviewTitle:'',
|
||
tplPreviewWarnings:[],tplVarDefs:[],tplVars:{},tplApplying:false,
|
||
tplRestrict:null,tplKindOptionsList:['page','database','row','blocks','all'],
|
||
// ── Version history ──
|
||
versionsOpen:false,versionsLoading:false,versionsList:[],
|
||
// ── Backlinks ──
|
||
backlinksOpen:false,backlinksLoading:false,backlinksList:[],
|
||
get timeAgo() {
|
||
if (!this.updatedAt) return 'just now';
|
||
var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000);
|
||
if (diff < 60) return 'just now';
|
||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||
return Math.floor(diff / 86400) + 'd ago';
|
||
},
|
||
formatDate(d) {
|
||
if (!d) return '';
|
||
var dt = new Date(d + 'Z');
|
||
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
|
||
' at ' + dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||
},
|
||
init(){
|
||
_bid=Math.floor(Math.random()*10000);
|
||
const dataEl=document.getElementById('page-data');
|
||
if(dataEl){try{const data=JSON.parse(dataEl.textContent);this.pid=data.id;this.pageTitle=data.title||'';this.favorited=data.favorited||false;const fmt=data.content_format||'blocks';this.contentFormat=fmt;const raw=data.content||'';
|
||
if(fmt==='file'){this.fileData=data;this.fileUrl=data.file_url||'';this.loadFileContent(data);}
|
||
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);ensureBlockIds(this.blocks);}catch(e){this.blocks=[];}}
|
||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);
|
||
// v5.12.0: page lock + layout options
|
||
this.isLocked=!!data.is_locked;this.lockedBy=data.locked_by||null;
|
||
this.canEdit=data.can_edit!==false;this.fullWidth=!!data.full_width;this.fontSmall=!!data.font_small;}catch(e){}}
|
||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||
this.loadInlineDBs();
|
||
window.E=this;
|
||
this.insertTable=function(){this.sync();this.pushHistory();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(function(){_tblFocus(nb.id,0,0);},60);};
|
||
SM.init();_tblInitDelegate();this.render();
|
||
this._applyLockMode();this._applyLayout();_wikiResolve();
|
||
this.loadComments();this.loadReactions(); // ancres jaunes + pastilles (v7.64)
|
||
const tEl=document.getElementById('_titleEl');
|
||
if(tEl){const syncT=()=>{tEl.classList.toggle('empty',!(tEl.textContent||'').trim());};syncT();tEl.addEventListener('input',syncT);tEl.addEventListener('blur',syncT);}
|
||
if(this.contentFormat==='blocks'&&window.__fdRT&&window.__fdRT.start){setTimeout(()=>{try{window.__fdRT.start(this);}catch(e){}},150);}
|
||
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
|
||
ct.addEventListener('keydown',e=>this.onKd(e));
|
||
ct.addEventListener('paste',e=>this.handleSmartPaste(e));
|
||
if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper:not(.nested-block):not(.block-column-wrapper)',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{this._dragEnd(evt);}});
|
||
ct.addEventListener('click',e=>{const h=e.target&&e.target.closest?e.target.closest('.block-handle'):null;if(!h)return;const w=h.closest('.block-wrapper');if(!w)return;const wid=w.getAttribute('data-id');if(!wid)return;
|
||
if(e.shiftKey){_selToggle(wid);return;}
|
||
_selClear();blockMenuOpen(e.clientX,e.clientY,wid);});
|
||
ct.addEventListener('contextmenu',e=>{if(!(e.target&&e.target.closest))return;if(e.target.closest('.ftable-cell')||e.target.closest('#_tblMenu'))return;const c=e.target.closest('.block-content, .block-wrapper, .block-column-wrapper');const el0=e.target.closest('[data-bid]');if(!c)return;const w=e.target.closest('.block-wrapper');const wid=w?w.getAttribute('data-id'):null;const bid=wid||(el0?el0.getAttribute('data-bid'):null);if(!bid)return;e.preventDefault();
|
||
// v7.68 : clic droit SUR un bloc sélectionné → on garde la sélection
|
||
// multi-blocs (le menu Clipboard coupe/copie ces blocs-là)
|
||
if(!(_selSet.size>1&&_selSet.has(wid)))_selClear();
|
||
blockMenuOpen(e.clientX,e.clientY,bid);});
|
||
// v7.68 : Ctrl+C / Ctrl+X sur une sélection multi-blocs → markdown.
|
||
// Sous un bloc unique, on laisse le navigateur (copie native).
|
||
ct.addEventListener('copy',function(ev){const E=window.E;if(!E||!E._clipState)return;const st=E._clipState();if(!st||st.si>=st.ei)return;const md=E.clipCopy(st);if(md==null)return;ev.preventDefault();ev.clipboardData.setData('text/plain',md);E.showToast('Sélection copiée');});
|
||
ct.addEventListener('cut',function(ev){const E=window.E;if(!E||!E._clipState)return;const st=E._clipState();if(!st||st.si>=st.ei)return;const md=E.clipCut(st);if(md==null)return;ev.preventDefault();ev.clipboardData.setData('text/plain',md);E.showToast('Sélection coupée');});
|
||
// v7.69 : Ctrl+C/X sur une sélection MULTI-BLOCS. Le navigateur ne
|
||
// sait pas sérialiser une sélection qui traverse deux contenteditable
|
||
// : il n'émet AUCUN événement copy et le raccourci ne fait rien. On
|
||
// prend donc le raccourci à la main. (Un menu contextuel ne peut pas
|
||
// faire confiance à getSelection(), d'où d._clipSt figé à l'ouverture.)
|
||
document.addEventListener('keydown',function(ev){
|
||
if(!(ev.ctrlKey||ev.metaKey))return;
|
||
const k=(ev.key||'').toLowerCase();
|
||
if(k!=='c'&&k!=='x')return;
|
||
if(!ev.target||!ev.target.closest||!ev.target.closest('#_blocksCt'))return; // pas dans la recherche d'un menu
|
||
const E=window.E;if(!E||!E._clipState)return;
|
||
const st=E._clipState();if(!st||st.si>=st.ei)return; // sélection mono-bloc → natif
|
||
const md=(k==='x'?E.clipCut(st):E.clipCopy(st));
|
||
if(md==null)return;
|
||
ev.preventDefault();ev.stopPropagation();
|
||
const done=()=>E.showToast(k==='x'?'Sélection coupée':'Sélection copiée');
|
||
if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(md).then(done).catch(()=>{});else done();
|
||
},true);
|
||
// v7.69 : glisser de sélection entre blocs + clic droit préservant la sélection
|
||
ct.addEventListener('mousedown',function(ev){
|
||
_mdSelRange=null; // un nouveau geste remet à zéro la sélection en attente
|
||
if(ev.button===2){const s=window.getSelection();if(s&&s.rangeCount&&!s.isCollapsed)ev.preventDefault();return;}
|
||
if(ev.button===0)_mdSelDown={x:ev.clientX,y:ev.clientY};
|
||
},true);
|
||
// en CAPTURE : SortableJS (initié sur ce même conteneur) peut
|
||
// stopPropagation en fin de glisser → le bubble n'atteint plus document
|
||
document.addEventListener('mouseup',function(ev){
|
||
const d=_mdSelDown;_mdSelDown=null;
|
||
if(!d||ev.button!==0)return;
|
||
if(Math.abs(ev.clientX-d.x)<4&&Math.abs(ev.clientY-d.y)<4)return;
|
||
const E=window.E;if(E&&E._buildSelAt)E._buildSelAt(d.x,d.y,ev.clientX,ev.clientY);
|
||
},true);
|
||
// v7.69 : le « click » d'un contenteditable efface toute sélection
|
||
// existante (focusin → collapse). On la remet juste après.
|
||
document.addEventListener('focusin',function(){
|
||
if(!_mdSelRange)return;
|
||
var r=_mdSelRange;_mdSelRange=null;
|
||
setTimeout(function(){
|
||
var s=window.getSelection();
|
||
if(s&&s.rangeCount&&!s.isCollapsed)return; // survécue : rien à faire
|
||
try{s.removeAllRanges();s.addRange(r);}catch(e){}
|
||
},0);
|
||
},true);
|
||
document.addEventListener('mousedown',_blockDocDown);
|
||
if(window.location.hash&&window.location.hash.indexOf('#fdblk-')===0){const hb=window.location.hash.slice(7);setTimeout(()=>{const selEl=document.querySelector(`[data-bid="${hb}"]`);if(selEl&&selEl.scrollIntoView)selEl.scrollIntoView({behavior:'smooth',block:'center'});if(selEl){selEl.focus();ce(selEl);}},500);}
|
||
},100);
|
||
},
|
||
// ── v5.12.0: lock + layout helpers ──
|
||
_applyLockMode(){
|
||
document.body.classList.toggle('page-locked',!!this.isLocked);
|
||
let banner=document.getElementById('_lockBanner');
|
||
if(this.isLocked){
|
||
if(!banner){
|
||
banner=document.createElement('div');banner.id='_lockBanner';
|
||
banner.className='fd-lock-banner';
|
||
banner.innerHTML='<span>🔒</span><span>This page is locked (read-only)</span>'
|
||
+'<button type="button" class="btn-sm" id="_lockUnlockBtn" style="margin-left:auto;font-size:12px;cursor:pointer;">Unlock</button>';
|
||
const wrap=document.querySelector('.page-editor-wrapper');
|
||
if(wrap)wrap.insertBefore(banner,wrap.firstChild);
|
||
}
|
||
const ub=banner.querySelector('#_lockUnlockBtn');
|
||
if(ub)ub.style.display=this.canEdit?'':'none';
|
||
}else if(banner){banner.remove();}
|
||
const ct=document.getElementById('_blocksCt');
|
||
if(ct){ct.querySelectorAll('[data-bid]').forEach(el=>el.setAttribute('contenteditable',this.canEdit?'true':'false'));}
|
||
const t=document.getElementById('_titleEl');
|
||
if(t)t.setAttribute('contenteditable',this.canEdit?'true':'false');
|
||
},
|
||
_applyLayout(){
|
||
const wrap=document.querySelector('.page-editor-wrapper');
|
||
if(!wrap)return;
|
||
wrap.classList.toggle('full-width',!!this.fullWidth);
|
||
wrap.classList.toggle('small-text',!!this.fontSmall);
|
||
// Page dont le premier bloc est une base : layout « page de base »
|
||
// Notion — titre et tableau calés à gauche, pas centrés.
|
||
const b0=this.blocks&&this.blocks[0];
|
||
wrap.classList.toggle('db-first',!!(b0&&b0.type==='embed'&&b0.embed_type==='collection'));
|
||
},
|
||
closeInviteSuggest(){ this.inviteSuggestOpen = false; this.inviteUsers = []; },
|
||
setSharePerm(a, perm){ this.updateShare(a.id, perm); a.permOpen = false; },
|
||
removeShareAndClose(a){ this.removeShare(a.id); a.permOpen = false; },
|
||
pickGeneralAccess(v){ this.generalAccess = v; this.accessMenuOpen = false; },
|
||
applyIcon(){ this.setIcon(this.iconValue); this.iconOpen = false; },
|
||
moreToggleLock(){ this.moreOpen = false; this.toggleLock(); },
|
||
moreFullWidth(){ this.moreOpen = false; this.setPageOption('full_width', !this.fullWidth); },
|
||
moreFontSmall(){ this.moreOpen = false; this.setPageOption('font_small', !this.fontSmall); },
|
||
moreSaveTemplate(){ this.moreOpen = false; this.saveAsTemplate(); },
|
||
markAndSave(){ this.dirty = true; this.save(); },
|
||
async toggleLock(){
|
||
if(!this.canEdit){this.showToast&&this.showToast('Only the person who locked this page (or an admin) can unlock it','error');return;}
|
||
const next=!this.isLocked;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/lock',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({locked:next})});
|
||
if(!r.ok){const d=await r.json().catch(()=>({}));this.showToast((d&&d.detail)||'Lock failed','error');return;}
|
||
this.isLocked=next;this._applyLockMode();
|
||
this.showToast(next?'🔒 Page locked':'🔓 Page unlocked');
|
||
}catch(e){this.showToast('Lock failed','error');}
|
||
},
|
||
async setPageOption(key,val){
|
||
try{
|
||
const body={};body[key]=val;
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/options',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify(body)});
|
||
if(r.ok){this[key]=val;this._applyLayout();}
|
||
}catch(e){}
|
||
},
|
||
async saveAsTemplate(){
|
||
if(!this.pageTitle||!(this.pageTitle||'').trim()){this.showToast('Give the page a title first','error');return;}
|
||
const name=(this.pageTitle||'').trim()+' template';
|
||
this.tplSaving=true;this.sync();
|
||
const blocks=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','children','rows'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;});
|
||
try{
|
||
const r=await fetch('/board/api/page-templates',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({name:name,blocks:blocks})});
|
||
if(r.ok){this.moreOpen=false;this.showToast('📑 Saved as template: '+name);}
|
||
else{const d=await r.json().catch(()=>({}));this.showToast((d&&d.detail)||'Save failed','error');}
|
||
}catch(e){this.showToast('Save failed','error');}
|
||
this.tplSaving=false;
|
||
},
|
||
fileData:null,
|
||
fileUrl:'',
|
||
async loadFileContent(data){
|
||
const url=data.file_url||'';const mime=data.file_mime||'';const name=data.file_name||'';const self=this;
|
||
if(!url){this.blocks=[this.mkB('paragraph','File not available')];this.render();return;}
|
||
const ext=name.toLowerCase().split('.').pop();
|
||
if(mime.startsWith('image/')||['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext)){
|
||
this.blocks=[this.mkB('image','',{src:url,alt:name})];this.render();return;
|
||
}
|
||
if(mime==='application/pdf'||ext==='pdf'){
|
||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return;
|
||
}
|
||
const _fh=(typeof window!=='undefined'&&window.FDHighlight)?window.FDHighlight:null;
|
||
const codeExts=_fh?Object.keys(_fh.EXT_TO_LANG):['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
|
||
const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'};
|
||
if(mime.startsWith('text/')||codeExts.includes(ext)){
|
||
try{const r=await fetch(url);const text=await r.text();
|
||
if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);}
|
||
else{const lang=_fh?_fh.langForFile(name):(langMap[ext]||'Plain Text');this.blocks=[this.mkB('code',text,{language:lang})];}
|
||
this.render();
|
||
}catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();}
|
||
return;
|
||
}
|
||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'download',file_name:name,file_size:data.file_size||0,file_mime:mime})];this.render();
|
||
},
|
||
mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='toggle')b.children=[];if(type==='button'){b.automation_id=0;b.automation_name='';}return b;},
|
||
// v5.11.0: rewrite one block's text (e.g. after inserting a wiki token)
|
||
// and re-render in place, caret at end.
|
||
_commitBlockText(b,text){
|
||
this.sync();this.pushHistory();
|
||
b.content=text;
|
||
this.dirty=true;this.autoSave();this.render();_rtSync();
|
||
const el=this.getEl(b.id);
|
||
if(el){el.focus();const r=document.createRange();r.selectNodeContents(el);r.collapse(false);const s=window.getSelection();s.removeAllRanges();s.addRange(r);}
|
||
if(window.__fdRT&&window.__fdRT.syncNow)window.__fdRT.syncNow();
|
||
},
|
||
getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;},
|
||
getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);},
|
||
getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};},
|
||
_dragEnd(evt){
|
||
const draggedId=evt.item&&evt.item.getAttribute?evt.item.getAttribute('data-id'):null;
|
||
const selected=_selIndicesSorted();
|
||
const group=_selSet.has(draggedId)&&selected.length>1;
|
||
if(evt.oldIndex===evt.newIndex&&!group)return;
|
||
this.sync();this.pushHistory();
|
||
const byId={};this.blocks.forEach(b=>{byId[b.id]=b;});
|
||
if(group){
|
||
const groupIds={};selected.forEach(i=>{const bb=this.blocks[i];if(bb)groupIds[bb.id]=1;});
|
||
const groupBlocks=selected.map(i=>this.blocks[i]).filter(Boolean);
|
||
const ct=document.getElementById('_blocksCt');
|
||
const domIds=[];ct.querySelectorAll('.block-wrapper[data-id]').forEach(w=>{const id=w.getAttribute('data-id');if(byId[id])domIds.push(id);});
|
||
let inserted=false;const out=[];
|
||
for(let i=0;i<domIds.length;i++){
|
||
const id=domIds[i];
|
||
if(groupIds[id]){if(!inserted){groupBlocks.forEach(gb=>out.push(gb));inserted=true;}continue;}
|
||
out.push(byId[id]);
|
||
}
|
||
if(!inserted)groupBlocks.forEach(gb=>out.push(gb));
|
||
if(out.length)this.blocks=out;
|
||
}else{
|
||
const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);
|
||
}
|
||
this.dirty=true;this.autoSave();this.render();_rtSync();
|
||
},
|
||
focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}},
|
||
onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];if(b&&(b.type==='table'||b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='button')){this.addAt(this.blocks.length,'paragraph');}else{const el=b&&this.getEl(b.id);if(el){el.focus();ce(el);}}}}},
|
||
sync(){const ct=document.getElementById('_blocksCt');if(!ct)return;for(const b of this.blocks){if(b.type==='table'){const wrap=ct.querySelector(`[data-bid="${b.id}"].ftable-editor`);if(wrap){const rows=[];wrap.querySelectorAll('tr.ftable-row').forEach(function(tr){const cells=tr.querySelectorAll('th.ftable-cell,td.ftable-cell');if(!cells.length)return;const rowarr=[];cells.forEach(function(cell){rowarr.push(cell.textContent||'');});rows.push(rowarr);});if(rows.length)b.rows=rows;}continue;}
|
||
if(b.type==='divider'||b.type==='image'||b.type==='embed'||b.type==='table_of_contents'||b.type==='button')continue;
|
||
if(b.type==='math'){const te=ct.querySelector(`[data-math-bid="${b.id}"]`);if(te)b.content=te.value||'';continue;}
|
||
const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el&&!el.classList.contains('toggle-title'))b.content=gtMd(el)||'';else if(el)b.content=gtMd(el)||'';
|
||
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtMd(che)||'';});
|
||
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtMd(che)||'';});
|
||
}},
|
||
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
|
||
// hljs: syntax-highlight code blocks (FDHighlight skips unchanged
|
||
// blocks via content fingerprint; spans are sync-transparent).
|
||
try{if(window.FDHighlight)FDHighlight.highlightAllIn(ct);}catch(e){}
|
||
// Render math blocks with KaTeX
|
||
var self=this;
|
||
ct.querySelectorAll('.math-display').forEach(function(el){var bid=el.dataset.bid;var b=self.blocks.find(function(x){return x.id===bid;});self._renderKatex(el,b?b.content:'');});
|
||
ct.querySelectorAll('.fd-math-inline').forEach(function(el){var tex=el.getAttribute('data-tex')||'';if(window.katex&&tex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:false,throwOnError:false});}catch(e){el.textContent=tex;}}else if(tex&&!el.textContent){el.textContent=tex;}});
|
||
ct.querySelectorAll('.math-editor').forEach(function(ta){var disp=ct.querySelector(`.math-display[data-bid="${ta.dataset.mathBid}"]`);ta.addEventListener('input',function(){self._renderKatex(disp,ta.value);self.dirty=true;self.autoSave();});});
|
||
// Initialize inline database blocks (scripts in innerHTML don't execute)
|
||
setTimeout(()=>{ct.querySelectorAll('.block-embed-collection').forEach(el=>{const cid=parseInt(el.id.replace('inline-db-',''));if(cid&&window.FlowDeckDB)window.FlowDeckDB.renderInto(el,cid,null);});},50);
|
||
// Initialize AI Meeting Note widgets (block type 'meeting')
|
||
setTimeout(()=>{ct.querySelectorAll('[data-meeting-bid]').forEach(function(el){if(window.FDMeeting)window.FDMeeting.mount(el.getAttribute('data-meeting-bid'),el);});},50);
|
||
ct.querySelectorAll('[data-bid]').forEach(el=>{el.addEventListener('input',()=>{self.dirty=true;self.autoSave();if(el.hasAttribute&&el.hasAttribute('data-placeholder'))el.classList.toggle('empty',!(el.textContent||'').trim());if(window.AIAC)window.AIAC.schedule(el.getAttribute('data-bid'));})});
|
||
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
|
||
_selApply();
|
||
try{this._paintAnchors();}catch(e){} // ancres jaunes + pastilles (v7.64)
|
||
var self2=this;
|
||
setTimeout(function(){
|
||
if(window._wikiResolve)_wikiResolve();
|
||
if(!self2.canEdit)self2._applyLockMode();
|
||
const ub=document.getElementById('_lockUnlockBtn');
|
||
if(ub&&!ub._wired){ub._wired=true;ub.onclick=function(){window.E.toggleLock();};}
|
||
},30);
|
||
},
|
||
_renderKatex(el,tex){
|
||
if(!el)return;if(!tex||!tex.trim()){el.innerHTML='<span style="color:var(--text-dim);font-style:italic;">Empty math block</span>';return;}
|
||
if(window.katex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:true,throwOnError:false});}catch(e){el.innerHTML='<span style="color:#e5484d;">Invalid LaTeX</span>';}}
|
||
else{el.innerHTML='<span>'+esc(tex)+'</span>';}
|
||
},
|
||
replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pushHistory();b.type=type;b.content='';if(type==='toggle'){b.expanded=true;b.children=[];}if(type==='to_do')b.checked=false;if(type==='columns')b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='math'){}if(type==='button'){b.automation_id=0;b.automation_name='';}if(type==='embed'){b.embed_type='';b.src='';b.height=520;}if(type==='bookmark'){b.url='';b.title='';b.description='';b.image='';b.site_name='';}if(type==='video'){b.src='';b.title='';}if(type==='audio'){b.src='';b.title='';}this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
|
||
addAt(idx,type,content){this.sync();this.pushHistory();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;},
|
||
addAfter(idx,type){return this.addAt(idx+1,type);},
|
||
removeBlock(idx){this.sync();this.pushHistory();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();this._rtSync();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
|
||
|
||
// ── v5.4.0/v5.5.0: Media block URL setters (click empty block → prompt) ──
|
||
_mediaPrompt(bid, field, label){
|
||
const idx=this.getIdx(bid); if(idx<0)return;
|
||
const b=this.blocks[idx]; if(!b)return;
|
||
const cur=b[field]||''; const u=prompt(label+' (URL):', cur);
|
||
if(u===null)return;
|
||
if(u.trim()===cur.trim())return;
|
||
this.sync();this.pushHistory();b[field]=u.trim();this.dirty=true;this.autoSave();this.render();_rtSync();
|
||
},
|
||
async setEmbedUrl(bid){
|
||
const idx=this.getIdx(bid); if(idx<0)return;
|
||
const b=this.blocks[idx]; if(!b)return;
|
||
const cur=b.src||''; const u=prompt('Embed URL (YouTube, Vimeo, Figma, Maps…):', cur);
|
||
if(u===null)return;
|
||
if(u.trim()===cur.trim())return;
|
||
this.sync();this.pushHistory();b.src=u.trim();b.embed_type='';this.dirty=true;
|
||
try{
|
||
const r=await fetch('/board/api/embed/resolve',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({url:u.trim()})});
|
||
const d=await r.json();
|
||
if(d&&d.ok&&d.src){b.embed_src=d.src;b.embed_provider=d.provider||'';}
|
||
}catch(e){}
|
||
this.render();this.autoSave();_rtSync();
|
||
},
|
||
setBookmarkUrl(bid){
|
||
const idx=this.getIdx(bid); if(idx<0)return;
|
||
const b=this.blocks[idx]; if(!b)return;
|
||
const cur=b.url||''; const u=prompt('Bookmark URL:', cur);
|
||
if(u===null)return;
|
||
if(u.trim()===cur.trim())return;
|
||
this.sync();this.pushHistory();b.url=u.trim();
|
||
this._fetchBookmarkPreview(bid, u.trim());
|
||
},
|
||
async _fetchBookmarkPreview(bid, url){
|
||
try{
|
||
const r=await fetch('/board/api/og/metadata',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({url})});
|
||
const d=await r.json();
|
||
if(d&&d.ok){
|
||
const idx=this.getIdx(bid); if(idx<0)return;
|
||
const b=this.blocks[idx];
|
||
if(d.title)b.title=d.title;
|
||
if(d.description)b.description=d.description;
|
||
if(d.image)b.image=d.image;
|
||
if(d.site_name)b.site_name=d.site_name;
|
||
this.render();this.dirty=true;this.autoSave();_rtSync();
|
||
}
|
||
}catch(e){}
|
||
},
|
||
setVideoSrc(bid){this._mediaPrompt(bid,'src','Video');},
|
||
setAudioSrc(bid){this._mediaPrompt(bid,'src','Audio');},
|
||
|
||
// ── v5.5.0: Image lightbox (fullscreen + keyboard navigation) ──
|
||
_openLightbox(imgEl){
|
||
const full=imgEl.dataset.full||imgEl.src;
|
||
if(!full)return;
|
||
const imgs=Array.from(document.querySelectorAll('.block-image img, .fd-img')).filter(i=>i.dataset.full||i.src);
|
||
let idx=imgs.indexOf(imgEl);
|
||
if(idx<0)idx=0;
|
||
const overlay=document.createElement('div');overlay.className='fd-lightbox';overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:5000;display:flex;align-items:center;justify-content:center;flex-direction:column;';
|
||
overlay.innerHTML='<button class="fd-lightbox-nav" data-nav="prev" aria-label="Previous">‹</button>'
|
||
+'<img class="fd-lightbox-img" style="max-width:95vw;max-height:88vh;border-radius:4px;box-shadow:0 0 40px rgba(0,0,0,.8);">'
|
||
+'<button class="fd-lightbox-nav" data-nav="next" aria-label="Next">›</button>'
|
||
+'<div class="fd-lightbox-hint" style="margin-top:14px;color:#999;font-size:13px;"></div>';
|
||
document.body.appendChild(overlay);
|
||
const img=overlay.querySelector('.fd-lightbox-img');
|
||
const hint=overlay.querySelector('.fd-lightbox-hint');
|
||
const show=()=>{const cur=imgs[idx];img.src=cur.dataset.full||cur.src;hint.textContent=(imgs.length>1?(idx+1)+' / '+imgs.length+' — ':'')+'← → to navigate · Esc to close';};
|
||
const step=(d)=>{if(imgs.length<2)return;idx=(idx+d+imgs.length)%imgs.length;show();};
|
||
show();
|
||
const close=()=>{overlay.remove();document.removeEventListener('keydown',kd);};
|
||
const kd=(e)=>{if(e.key==='Escape')close();else if(e.key==='ArrowRight')step(1);else if(e.key==='ArrowLeft')step(-1);};
|
||
document.addEventListener('keydown',kd);
|
||
overlay.querySelectorAll('[data-nav]').forEach(btn=>btn.addEventListener('click',e=>{e.stopPropagation();step(btn.dataset.nav==='next'?1:-1);}));
|
||
overlay.addEventListener('click',e=>{if(e.target===overlay)close();});
|
||
},
|
||
|
||
// ── v5.4.0: Version history UI ──
|
||
versionsOpen:false,versionsLoading:false,versionsList:[],
|
||
async toggleVersions(){
|
||
this.versionsOpen=!this.versionsOpen;
|
||
if(this.versionsOpen){await this.loadVersions();}
|
||
},
|
||
async loadVersions(){
|
||
this.versionsLoading=true;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/versions');
|
||
const d=await r.json();
|
||
this.versionsList=d.versions||[];
|
||
}catch(e){this.versionsList=[];}
|
||
this.versionsLoading=false;
|
||
},
|
||
async restoreVersion(vid){
|
||
if(!confirm('Restore this version? Current content will be replaced.'))return;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/versions/'+vid+'/restore',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()}});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){
|
||
this.showToast('Version restored');this.versionsOpen=false;
|
||
setTimeout(()=>window.location.reload(),300);
|
||
}
|
||
}catch(e){this.showToast('Restore failed','error');}
|
||
},
|
||
|
||
// ── v5.4.0: Backlinks ──
|
||
backlinksOpen:false,backlinksLoading:false,backlinksList:[],
|
||
async toggleBacklinks(){
|
||
this.backlinksOpen=!this.backlinksOpen;
|
||
if(this.backlinksOpen){await this.loadBacklinks();}
|
||
},
|
||
async loadBacklinks(){
|
||
this.backlinksLoading=true;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/backlinks');
|
||
const d=await r.json();
|
||
this.backlinksList=d.backlinks||[];
|
||
}catch(e){this.backlinksList=[];}
|
||
this.backlinksLoading=false;
|
||
},
|
||
|
||
// ── v5.5.0: Cover & Icon ──
|
||
async loadCoverIcon(){
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid);
|
||
const d=await r.json();
|
||
this.coverUrl=d.cover_url||'';
|
||
this.iconValue=d.page_icon||'';
|
||
}catch(e){}
|
||
},
|
||
toggleCover(){this.coverOpen=!this.coverOpen;if(this.coverOpen&&!this.coverUrl){this.loadCoverIcon();}},
|
||
async setCoverUrl(){
|
||
const u=prompt('Cover image URL:', this.coverUrl);
|
||
if(u===null)return;
|
||
this.coverLoading=true;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({cover_url:u})});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){this.coverUrl=d.cover_url;this.coverOpen=false;this.showToast('Cover updated');}
|
||
}catch(e){this.showToast('Cover update failed','error');}
|
||
this.coverLoading=false;
|
||
},
|
||
async uploadCover(e){
|
||
const f=e.target.files?.[0];if(!f)return;
|
||
this.coverLoading=true;
|
||
const fd=new FormData();fd.append('file',f);
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()},body:fd});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){this.coverUrl=d.cover_url;this.coverOpen=false;this.showToast('Cover uploaded');}
|
||
}catch(e){this.showToast('Cover upload failed','error');}
|
||
this.coverLoading=false;
|
||
e.target.value='';
|
||
},
|
||
async removeCover(){
|
||
if(!confirm('Remove cover?'))return;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/cover',{method:'DELETE',headers:{'X-CSRF-Token':this.csrfTok()}});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){this.coverUrl='';this.showToast('Cover removed');}
|
||
}catch(e){this.showToast('Remove failed','error');}
|
||
},
|
||
toggleIcon(){this.iconOpen=!this.iconOpen;},
|
||
openBacklink(b){
|
||
this.backlinksOpen = false;
|
||
window.location.href = '/pages/' + b.id;
|
||
},
|
||
fmtImportSize(f){ return Math.round(f.size / 1024) + ' KB'; },
|
||
// A20 ph3 : x-html interdit sous build CSP → effet réactif
|
||
bindIconHtml(el){
|
||
Alpine.effect(() => { el.innerHTML = this.iconHtml(); });
|
||
},
|
||
iconHtml(){
|
||
var v=this.iconValue;
|
||
if(!v)v=(this.contentFormat==='file')?'paperclip':'file';
|
||
return window.fdIconHtml?window.fdIconHtml(v,36):v;
|
||
},
|
||
openIconPicker(){
|
||
var self=this;
|
||
if(window.FDIconPicker)window.FDIconPicker.openFor({
|
||
x:120,y:160,
|
||
onPick:function(v){self.setIcon(v);},
|
||
onRemove:function(){self.setIcon('');}
|
||
});
|
||
},
|
||
async setIcon(icon){
|
||
this.iconValue=icon;
|
||
try{
|
||
const r=await fetch('/board/api/pages/'+this.pid+'/icon',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({icon})});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){this.iconOpen=false;this.showToast(icon?'Icon updated':'Icon removed');}
|
||
}catch(e){this.showToast('Icon update failed','error');}
|
||
},
|
||
|
||
// ── v5.4.0: Import ──
|
||
importOpen:false,importFile:null,importText:'',
|
||
toggleImport(){this.importOpen=!this.importOpen;},
|
||
handleImportFile(e){
|
||
const f=e.target.files?.[0];if(!f)return;
|
||
this.importFile=f;
|
||
const reader=new FileReader();
|
||
reader.onload=()=>{this.importText=reader.result;};
|
||
reader.readAsText(f);
|
||
},
|
||
async doImport(){
|
||
if(!this.importText.trim()&&!this.importFile){this.showToast('Nothing to import');return;}
|
||
if(this.importFile){
|
||
const fd=new FormData();fd.append('file',this.importFile);
|
||
try{
|
||
const r=await fetch('/board/api/pages/import/file',{method:'POST',headers:{'X-CSRF-Token':this.csrfTok()},body:fd});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){this.showToast(d.count+' page(s) imported');this.importOpen=false;this.importFile=null;this.importText='';}
|
||
}catch(e){this.showToast('Import failed','error');}
|
||
}else{
|
||
try{
|
||
const r=await fetch('/board/api/pages/import',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({markdown:this.importText,title:this.pageTitle||'Import'})});
|
||
const d=await r.json();
|
||
if(d.status==='ok'){this.showToast('Page imported');this.importOpen=false;this.importText='';setTimeout(()=>{window.location.href='/pages/'+d.id;},400);}
|
||
}catch(e){this.showToast('Import failed','error');}
|
||
}
|
||
},
|
||
|
||
// ── v5.10.0: Undo / Redo ──
|
||
pushHistory(){if(this._histLock)return;this.undoStack.push(JSON.parse(JSON.stringify(this.blocks)));if(this.undoStack.length>this._histCap)this.undoStack.shift();this.redoStack.length=0;},
|
||
undo(){if(!this.undoStack.length){this.showToast('Rien à annuler');return;}const fid=this._focusedBid();this.redoStack.push(JSON.parse(JSON.stringify(this.blocks)));this.applySnapshot(this.undoStack.pop());this._focusBid(fid);},
|
||
redo(){if(!this.redoStack.length){this.showToast('Rien à rétablir');return;}const fid=this._focusedBid();this.undoStack.push(JSON.parse(JSON.stringify(this.blocks)));this.applySnapshot(this.redoStack.pop());this._focusBid(fid);},
|
||
applySnapshot(snap){this._histLock=true;this.blocks=snap||[];if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();this._histLock=false;_rtSync();},
|
||
_focusedBid(){const a=this.getActiveBlock();return a?a.bid:null;},
|
||
_focusBid(bid){if(bid)setTimeout(()=>{const el=this.getEl(bid);if(el){el.focus();ce(el);}},60);},
|
||
_rtSync(){_rtSync();},
|
||
|
||
// ── v5.10.0: Duplicate ──
|
||
_deepCloneBlock(b){const c=JSON.parse(JSON.stringify(b));c.id=genId();if(Array.isArray(c.children)){c.children=c.children.map(ch=>{const n=JSON.parse(JSON.stringify(ch));n.id=genId();return n;});}return c;},
|
||
duplicateBlock(idx){if(idx<0||idx>=this.blocks.length)return;const sel=_selIndicesSorted();if(sel.length>1&&sel.includes(idx)){this.duplicateSelected();return;}this.sync();this.pushHistory();const copy=this._deepCloneBlock(this.blocks[idx]);this.blocks.splice(idx+1,0,copy);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(copy.id);_selClear();return copy;},
|
||
duplicateSelected(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const copies=indices.map(i=>this._deepCloneBlock(this.blocks[i]));const pos=indices[indices.length-1]+1;this.blocks.splice(pos,0,...copies);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(copies[0].id);_selClear();},
|
||
|
||
// ── v5.10.0: Turn into / couleurs / liens / déplacement ──
|
||
_blockTargets(idx){const sel=_selIndicesSorted();return(sel.includes(idx))?sel:[idx];},
|
||
// hljs: code-block language picker (drives FDHighlight re-render).
|
||
setCodeLang(bid,lang){const idx=this.getIdx(bid);if(idx<0)return;const b=this.blocks[idx];if(!b||b.type!=='code')return;this.sync();this.pushHistory();b.language=(window.FDHighlight?FDHighlight.normalizeLang(lang):lang)||'Plain Text';this.dirty=true;this.autoSave();this.render();_rtSync();},
|
||
turnInto(idx,type,cols){if(idx<0||idx>=this.blocks.length)return;if(!_BLOCK_TURN_TYPES.some(t=>t.id===type))return;const targets=this._blockTargets(idx);const focusId=this.blocks[idx].id;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(b.type===type)return;const keep=(b.content||'').trim();b.type=type;if(keep&&['image','embed','meeting','divider','table_of_contents','database','button','columns'].indexOf(b.type)<0){b.content=keep;}else{b.content='';}if(type==='toggle'){b.expanded=true;if(!Array.isArray(b.children))b.children=[];}if(type==='to_do')b.checked=false;if(type==='table'&&!Array.isArray(b.rows))b.rows=_tblNew(3,3);if(type==='code'&&!b.language)b.language='Plain Text';if(type==='columns'&&(!Array.isArray(b.children)||!b.children.length))b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='columns'&&cols>=2){b.children=Array.from({length:cols},()=>({id:genId(),type:'paragraph',content:''}));}if(type==='synced'){b.synced_id=0;b._synced_content=[];b._synced_title='';}if(type==='button'){b.automation_id=0;b.automation_name='';}});
|
||
if(type==='synced')setTimeout(()=>this._pickSyncedBlock(idx),80);
|
||
this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();
|
||
setTimeout(()=>{const bid=this.blocks[idx]?this.blocks[idx].id:focusId;if(type==='table'){_tblFocus(bid,0,0);}else{const el=this.getEl(bid);if(el){el.focus();ce(el);}}},60);},
|
||
setBlockColor(idx,key,val){if(idx<0||idx>=this.blocks.length)return;const targets=this._blockTargets(idx);const v=(!val||val==='default')?null:val;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(!b.style)b.style={};if(v==null)delete b.style[key];else b.style[key]=v;});if(v!=null){try{localStorage.setItem('fdLastBlockColor',key+'|'+v);}catch(e){}}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();},
|
||
// ── v7.61 : actions du menu contextuel de bloc ──
|
||
commentBlock(idx){
|
||
// L'ancre est la SÉLECTION de texte vivante dans ce bloc, sinon tout
|
||
// le bloc. La toolbar de sélection restaure sa plage AVANT (fmtComment)
|
||
// — pas de repli sur `this._sel` ici : sinon le bouton de droite / le
|
||
// menu de bloc reprendraient une sélection périmée.
|
||
const b=this.blocks[idx];if(!b)return;const el=this.getEl(b.id);if(!el)return;
|
||
let r=null;const s=window.getSelection();
|
||
if(s&&s.rangeCount&&!s.isCollapsed){
|
||
const rr=s.getRangeAt(0);
|
||
if(el.contains(rr.startContainer)&&el.contains(rr.endContainer))r=rr.cloneRange();
|
||
}
|
||
if(!r){r=document.createRange();r.selectNodeContents(el);}
|
||
const start=_srcOffset(el,r.startContainer,r.startOffset);
|
||
const end=_srcOffset(el,r.endContainer,r.endOffset);
|
||
if(!(end>start)){this.showToast('Sélectionnez du texte pour commenter','error');return;}
|
||
this._commentSel={range:r,bid:el.dataset.bid,start:start,end:end};
|
||
this.commentOnSelection();
|
||
},
|
||
// v7.62 : renommé — « askAI() » (sans argument, plus bas) l'écrasait,
|
||
// le menu de bloc et Ctrl+J ouvraient l'IA de page au lieu du bloc.
|
||
askAIBlock(idx){const b=this.blocks[idx];if(!b)return;if(window.AIC)window.AIC.open(idx);},
|
||
suggestEdit(idx){
|
||
const b=this.blocks[idx];if(!b||!(b.content||'').trim()){this.showToast('Bloc vide','error');return;}
|
||
this.showToast('FlowDeck AI : suggestion en cours…');
|
||
this._aiWritingRun('write',idx,{prompt:'Améliore ce bloc (grammaire, clarté, style) en gardant le sens, la langue et le format. Réponds UNIQUEMENT avec le contenu amélioré, sans préambule.\n\nBloc :\n'+b.content},'insert');
|
||
},
|
||
runSkill(idx,tpl){
|
||
const b=this.blocks[idx];if(!b||!(b.content||'').trim()){this.showToast('Bloc vide','error');return;}
|
||
this.showToast('FlowDeck AI : compétence en cours…');
|
||
this._aiWritingRun('write',idx,{prompt:String(tpl||'')+'\n\nContenu du bloc :\n'+b.content},'insert');
|
||
},
|
||
// v7.63 : déplacement de bloc depuis la barre mobile
|
||
// (↑↓ = ordre, → = indent dans le frère précédent, ← = sortir du parent)
|
||
moveBlock(bid,dir){
|
||
const top=this.blocks.findIndex(b=>b.id===bid);
|
||
const pi=this.blocks.findIndex(b=>Array.isArray(b.children)&&b.children.some(c=>c.id===bid));
|
||
const parent=pi>=0?this.blocks[pi]:null;
|
||
const list=parent?parent.children:this.blocks;
|
||
const k=list.findIndex(b=>b.id===bid);
|
||
if(k<0){this.showToast('Bloc introuvable','error');return;}
|
||
if(dir==='up'||dir==='down'){
|
||
const j=dir==='up'?k-1:k+1;
|
||
if(j<0||j>=list.length){this.showToast(dir==='up'?'Déjà en haut':'Déjà en bas','error');return;}
|
||
this.sync();this.pushHistory();
|
||
const mv=list.splice(k,1)[0];list.splice(j,0,mv);
|
||
}else if(dir==='right'){
|
||
if(parent){this.showToast('Déjà indenté','error');return;}
|
||
const prev=this.blocks[top-1];
|
||
if(!prev||!Array.isArray(prev.children)||(prev.type!=='toggle'&&prev.type!=='columns')){this.showToast('Pas de bloc cible à droite','error');return;}
|
||
this.sync();this.pushHistory();
|
||
if(prev.type==='toggle')prev.expanded=true;
|
||
const mv=this.blocks.splice(top,1)[0];prev.children.push(mv);
|
||
}else{
|
||
if(!parent){this.showToast('Déjà à la racine','error');return;}
|
||
this.sync();this.pushHistory();
|
||
const mv=parent.children.splice(k,1)[0];
|
||
this.blocks.splice(pi+1,0,mv);
|
||
}
|
||
this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(bid);
|
||
},
|
||
applyLastColor(idx){
|
||
let v='';try{v=localStorage.getItem('fdLastBlockColor')||'';}catch(e){}
|
||
const i=v.indexOf('|');
|
||
if(i<0){this.showToast('Aucune couleur récente','error');return;}
|
||
this.setBlockColor(idx,v.slice(0,i),v.slice(i+1));
|
||
},
|
||
copyBlockLink(bid){const url=window.location.origin+window.location.pathname+'#fdblk-'+bid;if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(url).then(()=>this.showToast('Lien du bloc copié')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(url,'error');}},
|
||
deleteSelected(idx){const indices=_selIndicesSorted();if(indices.length<=1)return this.removeBlock(idx);this.sync();this.pushHistory();const keep=this.blocks.filter((b,i)=>indices.indexOf(i)<0);this.blocks=keep.length?keep:[this.mkB('paragraph','')];const fi=Math.min(idx,this.blocks.length-1);this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
|
||
// ── v5.11: Copy / Cut / Paste selected blocks ──
|
||
_blocksToMarkdown(blocks){return blocks.map(blocksToMarkdown).join('\n\n');},
|
||
copySelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>this.showToast('Blocs copiés')).catch(()=>this.showToast('Copie impossible','error'));}else{this.showToast(md,'error');}},
|
||
cutSelectedBlocks(){const indices=_selIndicesSorted();if(!indices.length)return;this.sync();this.pushHistory();const selected=indices.map(i=>this.blocks[i]).filter(Boolean);if(!selected.length)return;const md=this._blocksToMarkdown(selected);if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(md).then(()=>{});}this.blocks=this.blocks.filter((b,i)=>indices.indexOf(i)<0);if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Blocs coupés');},
|
||
async pasteBlocks(){let idx=this._focusedIdx();if(idx<0)idx=this.blocks.length-1;if(idx<0)return;try{const text=await navigator.clipboard.readText();if(!text||!text.trim())return;const parsed=this.md2b(text);if(!parsed.length)return;this.sync();this.pushHistory();this.blocks.splice(idx+1,0,...parsed);this.dirty=true;this.autoSave();this.render();_rtSync();this._focusBid(parsed[parsed.length-1].id);_selClear();}catch(e){if(e.name==='NotAllowedError'){this.showToast('Permission presse-papier refusée');}else{this.showToast('Coller impossible','error');}}},
|
||
// v7.69 : glisser la souris de bloc A vers bloc B — le navigateur ne
|
||
// sélectionne jamais à travers deux contenteditable, on reconstruit la
|
||
// plage au relâchement avec les carets sous le pointeur.
|
||
_buildSelAt(x0,y0,x1,y1){
|
||
const r0=_caretAt(x0,y0),r1=_caretAt(x1,y1);
|
||
if(!r0||!r1)return;
|
||
const b0=_blkOf(r0.startContainer),b1=_blkOf(r1.startContainer);
|
||
if(!b0||!b1||b0===b1)return; // même bloc → la sélection native suffit
|
||
const a=document.createRange();
|
||
if(r0.compareBoundaryPoints(Range.START_TO_START,r1)<=0){
|
||
a.setStart(r0.startContainer,r0.startOffset);a.setEnd(r1.startContainer,r1.startOffset);
|
||
}else{
|
||
a.setStart(r1.startContainer,r1.startOffset);a.setEnd(r0.startContainer,r0.startOffset);
|
||
}
|
||
if(a.collapsed)return;
|
||
const s=window.getSelection();s.removeAllRanges();s.addRange(a);
|
||
_mdSelRange=a; // le « click » suivant va effacer ça : on garde la copie
|
||
},
|
||
// v7.69 : Maj+↓ / Maj+↑ au début/fin d'un bloc → la sélection saute
|
||
// dans le bloc voisin (extension native impossible : blocs séparés).
|
||
_shiftBlockExtend(dir){
|
||
// le bloc de départ est celui du BOUT MOBILE de la sélection (le
|
||
// focus), pas l'élément focalisé : après une première extension le
|
||
// caret vit déjà dans le bloc voisin.
|
||
const s=window.getSelection();if(!s||!s.rangeCount)return false;
|
||
const fn=s.focusNode;if(!fn)return false;
|
||
const sbl=_blkOf(fn);if(!sbl)return false;
|
||
const si=this.getIdx(sbl.dataset.bid);if(si<0)return false;
|
||
const off=_srcOffset(sbl,fn,s.focusOffset),len=_srcNodeLen(sbl);
|
||
if(dir>0&&off<len)return false; // le navigateur étend encore dans le bloc
|
||
if(dir<0&&off>0)return false;
|
||
const nb=this.blocks[si+dir];if(!nb)return false;
|
||
const nel=this.getEl(nb.id);if(!nel)return false;
|
||
const r=s.getRangeAt(0).cloneRange();
|
||
if(dir>0)r.setEnd(nel,nel.childNodes.length);else r.setStart(nel,0);
|
||
if(r.collapsed)return false;
|
||
s.removeAllRanges();s.addRange(r);
|
||
return true;
|
||
},
|
||
// ── v7.68 : copier/couper une SÉLECTION DE TEXTE MULTI-BLOCS ──
|
||
// Sélection vivante qui traverse ≥2 blocs → plage ; sinon null (le
|
||
// navigateur gère tout seul la sélection à l'intérieur d'un bloc).
|
||
_clipState(){
|
||
const s=window.getSelection();
|
||
if(!s||!s.rangeCount||s.isCollapsed)return null;
|
||
const r=s.getRangeAt(0);
|
||
const up=n=>n?(n.nodeType===1?n:n.parentElement):null;
|
||
const se=up(r.startContainer), ee=up(r.endContainer);
|
||
const sblk=se&&se.closest?se.closest('[data-bid]'):null;
|
||
const eblk=ee&&ee.closest?ee.closest('[data-bid]'):null;
|
||
if(!sblk||!eblk)return null;
|
||
const si=this.getIdx(sblk.dataset.bid), ei=this.getIdx(eblk.dataset.bid);
|
||
if(si<0||ei<0||si>ei)return null;
|
||
return {si:si,ei:ei,
|
||
startOff:_srcOffset(sblk,r.startContainer,r.startOffset),
|
||
endOff:_srcOffset(eblk,r.endContainer,r.endOffset)};
|
||
},
|
||
// blocs dont la sélection ne couvre qu'une PARTIE (les autres = entiers)
|
||
_clipHeavy(b){return ['table','code','columns','math','image','embed','divider','meeting','button','table_of_contents'].indexOf(b.type)>=0;},
|
||
_clipMarkdown(st){
|
||
const parts=[];
|
||
for(let i=st.si;i<=st.ei;i++){
|
||
const b=this.blocks[i];if(!b)continue;
|
||
if(i===st.si&&i===st.ei)return this._anchorText(b.id,st.startOff,st.endOff);
|
||
if(i===st.si||i===st.ei){
|
||
if(this._clipHeavy(b)){parts.push(blocksToMarkdown(b));continue;}
|
||
const el=this.getEl(b.id);
|
||
parts.push(i===st.si
|
||
? this._anchorText(b.id,st.startOff,el?_srcNodeLen(el):0)
|
||
: this._anchorText(b.id,0,st.endOff));
|
||
}else parts.push(blocksToMarkdown(b));
|
||
}
|
||
return parts.join('\n\n');
|
||
},
|
||
// renvoie le markdown copié, ou null si la sélection n'est pas multi-blocs
|
||
clipCopy(st){st=st||this._clipState();if(!st)return null;this.sync();return this._clipMarkdown(st);},
|
||
clipCut(st){
|
||
st=st||this._clipState();if(!st)return null;
|
||
this.sync();this.pushHistory();
|
||
const md=this._clipMarkdown(st);
|
||
if(st.si===st.ei){
|
||
const b=this.blocks[st.si],bel=this.getEl(b.id);
|
||
b.content=this._anchorText(b.id,0,st.startOff)+this._anchorText(b.id,st.endOff,bel?_srcNodeLen(bel):0);
|
||
this.dirty=true;this.autoSave();this.render();_rtSync();
|
||
this._focusBid(b.id);
|
||
return md;
|
||
}
|
||
const first=this.blocks[st.si],last=this.blocks[st.ei];
|
||
const lastEl=this.getEl(last.id);
|
||
if(!this._clipHeavy(first))first.content=this._anchorText(first.id,0,st.startOff);
|
||
if(!this._clipHeavy(last))last.content=this._anchorText(last.id,st.endOff,lastEl?_srcNodeLen(lastEl):0);
|
||
this.blocks=this.blocks.slice(0,st.si+1).concat([last],this.blocks.slice(st.ei+1));
|
||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||
this.dirty=true;this.autoSave();this.render();_rtSync();
|
||
this._focusBid(first.id);
|
||
return md;
|
||
},
|
||
_focusedIdx(){const a=this.getActiveBlock();return a?a.idx:-1;},
|
||
async moveBlockToPage(idx,pageId){if(idx<0||idx>=this.blocks.length||pageId===this.pid)return;this.sync();this.pushHistory();const block=this.blocks[idx];const self=this;const csrf = getCsrf();;this.showToast('Déplacement du bloc…');try{const r=await fetch(`/board/api/pages/${pageId}`,{credentials:'same-origin'});const d=await r.json();let tblocks=[];if(d.content_format==='blocks'&&d.content){try{tblocks=JSON.parse(d.content);}catch(e){tblocks=[];}}tblocks.push(block);await fetch(`/board/api/pages/${pageId}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({title:d.title||'',blocks:tblocks})});
|
||
if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();this.showToast('Bloc déplacé');
|
||
}catch(e){this.showToast('Échec du déplacement','error');}},
|
||
|
||
// ── v4.11.1: Inline AI — "Ask AI" & "AI meeting note" (Get started) ──
|
||
// Wire the contextual get-started actions into the FlowDeck Agent
|
||
// (Notion-style inline generation). The reply is streamed in the agent
|
||
// panel and inserted into this page via window.__fdInsertAI.
|
||
askAI(){
|
||
const self=this;
|
||
const title=(this.pageTitle||'Sans titre').trim();
|
||
if(!(window.fdAgent && window.fdAgent.generate)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
|
||
if(self.aiBusy) return;
|
||
self.aiBusy=true;
|
||
self.showToast('FlowDeck Agent : génération en cours…');
|
||
let ctx='';
|
||
try{ if(window.fdGetPageContext) ctx=String(window.fdGetPageContext()||''); }catch(e){}
|
||
const prompt='Rédige le contenu de la page FlowDeck intitulée « '+title+' ». Fournis un texte structuré et directement utilisable : paragraphes et, si utile, des listes à puces. Sépare chaque paragraphe par une ligne vide. Ne précède pas le texte de commentaires.';
|
||
window.fdAgent.generate(prompt, ctx).then(function(text){
|
||
self.aiBusy=false;
|
||
if(text && text.trim()){ self.applyAIBlocks(text); }
|
||
else { self.showToast('Aucun retour généré par l\u2019agent'); }
|
||
}).catch(function(err){
|
||
self.aiBusy=false;
|
||
self.showToast((err && err.message) ? err.message : 'Erreur de génération');
|
||
});
|
||
},
|
||
meetingNote(){
|
||
// v4.12.0: crée une vraie « boîte de réunion » (bloc 'meeting') au lieu
|
||
// d'insérer du texte. La page est renommée « Meeting @date » si elle
|
||
// est vide, puis le widget est ajouté (ou remplace le bloc vide initial).
|
||
var self=this;
|
||
this.gsMoreOpen=false;
|
||
var today=new Date();
|
||
var iso=today.toISOString().slice(0,10);
|
||
var title='Meeting @'+iso;
|
||
var tEl=document.getElementById('_titleEl');
|
||
var curTitle=tEl?(tEl.textContent||'').trim():'';
|
||
var blank=!curTitle||curTitle==='Untitled'||curTitle==='New page';
|
||
if(blank){
|
||
this.pageTitle=title;
|
||
if(tEl)tEl.textContent=title;
|
||
}
|
||
var mb=this.mkB('meeting','');
|
||
mb.meeting={
|
||
title:title,
|
||
date:iso,
|
||
consent:'self',
|
||
started:false,
|
||
status:'idle',
|
||
lines:[],
|
||
interim:'',
|
||
notes:'',
|
||
summary:'',
|
||
summaryError:'',
|
||
lastTab:'transcript'
|
||
};
|
||
this.sync();this.pushHistory();
|
||
var emptyFirst=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
|
||
if(emptyFirst){ this.blocks=[mb]; } else { this.blocks.push(mb); }
|
||
this.dirty=true; this.autoSave(); this.render();
|
||
this.showToast('AI Meeting Note : bloc de réunion ajouté. Cliquez sur « Start transcribing ».');
|
||
var bid=mb.id;
|
||
setTimeout(function(){ var el=document.querySelector('[data-meeting-bid="'+bid+'"]'); if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'}); if(el&&window.FDMeeting)window.FDMeeting.focus(bid); },120);
|
||
},
|
||
aiRun(prompt){
|
||
const self=this;
|
||
if(self.aiBusy) return;
|
||
if(!(window.fdAgent && window.fdAgent.ask)){ self.showToast('Agent IA indisponible — configurez-le dans Paramètres → Agent & IA'); return; }
|
||
self.aiBusy=true;
|
||
window.__fdInsertAI = function(content){ window.__fdInsertAI=null; self.aiBusy=false; self.applyAIBlocks(content); };
|
||
self.showToast('FlowDeck Agent : génération en cours…');
|
||
window.fdAgent.ask(prompt);
|
||
},
|
||
applyAIBlocks(text){
|
||
// Convert agent markdown → real blocks (headings, lists, tables, code…)
|
||
// so it renders naturally (Notion-style) instead of showing raw `## …`.
|
||
const nb=this.md2b(text||'');
|
||
if(!nb.length) return;
|
||
this.sync();
|
||
const startEmpty=this.blocks.length===1 && this.blocks[0].type==='paragraph' && !(this.blocks[0].content||'').trim();
|
||
this.blocks = startEmpty ? nb : this.blocks.concat(nb);
|
||
this.dirty=true; this.autoSave(); this.render();
|
||
},
|
||
|
||
toggleFavorite(){
|
||
const csrf = getCsrf();;
|
||
const m=this.favorited?'DELETE':'POST';
|
||
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf}})
|
||
.then(r=>r.json()).then(()=>{this.favorited=!this.favorited;this.showToast(this.favorited?'Added to favorites':'Removed from favorites');
|
||
if(window.appState&&window.appState.refreshFavorites)window.appState.refreshFavorites();
|
||
}).catch(()=>{this.showToast('Failed to toggle favorite');});
|
||
},
|
||
toggleActivityOpen(){
|
||
if(!this.activityOpen){this.activityOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||
else{this.activityOpen=false;}
|
||
},
|
||
toggleShareOpen(){
|
||
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);this.loadShares();}
|
||
else{this.shareOpen=false;}
|
||
},
|
||
toggleMoreOpen(){
|
||
if(!this.moreOpen){this.moreOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||
else{this.moreOpen=false;}
|
||
},
|
||
_justOpened:false,
|
||
_syncIsShared(){this.pageIsShared=!!(this.pagePublished||this.generalAccess==='anyone'||(this.accessList||[]).length>0);},
|
||
togglePublish(){this.pagePublished=!this.pagePublished;this._syncIsShared();this.saveShare();},
|
||
saveShare(){
|
||
const csrf = getCsrf();;
|
||
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).then(()=>this._syncIsShared()).catch(()=>{});
|
||
},
|
||
async copyPageLink(){
|
||
console.log('copyPageLink invoked');
|
||
const url = this.pageUrl;
|
||
const fallback = () => {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
|
||
document.body.appendChild(ta); ta.select();
|
||
document.execCommand('copy'); document.body.removeChild(ta);
|
||
};
|
||
try {
|
||
await navigator.clipboard.writeText(url);
|
||
this.showToast('Link copied to clipboard');
|
||
} catch(e) { fallback(); this.showToast('Link copied to clipboard'); }
|
||
},
|
||
showToast(msg){
|
||
this.toastMsg = msg; this.toastVisible = true;
|
||
clearTimeout(this._toastTimer);
|
||
this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500);
|
||
},
|
||
publishPage() {
|
||
var self = this;
|
||
var csrf = getCsrf();;
|
||
fetch('/api/pages/' + this.pid + '/publish', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf }
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.is_published) {
|
||
self.pagePublished = true;
|
||
self.publishedUrl = window.location.origin + '/p/' + d.publish_slug;
|
||
self.showToast('Published to web — ' + self.publishedUrl);
|
||
} else {
|
||
self.showToast(d.detail || 'Publish failed');
|
||
}
|
||
}).catch(function(){ self.showToast('Publish failed'); });
|
||
},
|
||
unpublishPage() {
|
||
var self = this;
|
||
var csrf = getCsrf();;
|
||
fetch('/api/pages/' + this.pid + '/publish', {
|
||
method: 'DELETE',
|
||
headers: { 'X-CSRF-Token': csrf }
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (!d.is_published) {
|
||
self.pagePublished = false;
|
||
self.publishedUrl = '';
|
||
self.showToast('Unpublished');
|
||
} else {
|
||
self.showToast(d.detail || 'Unpublish failed');
|
||
}
|
||
}).catch(function(){ self.showToast('Unpublish failed'); });
|
||
},
|
||
shareInvite() {
|
||
var self = this;
|
||
if (this.inviteGroupId) {
|
||
var csrf = getCsrf();;
|
||
fetch('/api/pages/' + this.pid + '/share', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
||
body: JSON.stringify({ group_id: this.inviteGroupId, permission: this.invitePermission })
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.status === 'shared') {
|
||
self.inviteEmail = ''; self.inviteUserId = null; self.inviteGroupId = null;
|
||
self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false;
|
||
self.showToast('Group shared'); self.loadShares();
|
||
} else { self.showToast(d.detail || 'Share failed'); }
|
||
}).catch(function(){ self.showToast('Share failed'); });
|
||
return;
|
||
}
|
||
if (!this.inviteEmail.trim()) return;
|
||
var csrf2 = getCsrf();;
|
||
fetch('/api/pages/' + this.pid + '/share', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf2 },
|
||
body: JSON.stringify({ user_id: this.inviteUserId, email: this.inviteEmail.trim(), permission: this.invitePermission })
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.status === 'shared') {
|
||
self.inviteEmail = ''; self.inviteUserId = null; self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false;
|
||
self.showToast('Invitation sent'); self.loadShares();
|
||
} else { self.showToast(d.detail || 'Share failed'); }
|
||
}).catch(function(){ self.showToast('Share failed'); });
|
||
},
|
||
inviteInput(){
|
||
var self = this;
|
||
var q = (this.inviteEmail || '').trim();
|
||
if (!q) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteUserId = null; this.inviteGroupId = null; this.inviteSel = -1; return; }
|
||
clearTimeout(this._inviteTimer);
|
||
this._inviteTimer = setTimeout(function(){ self._loadInviteUsers(q); }, 150);
|
||
},
|
||
async _loadInviteUsers(q){
|
||
try {
|
||
const r = await fetch('/api/notifications/users/search?q=' + encodeURIComponent(q), { credentials: 'same-origin' });
|
||
const d = await r.json();
|
||
var me = this.currentUserId;
|
||
var shared = {};
|
||
(this.accessList || []).forEach(function(a){ if (a.user_login) shared[a.user_login.toLowerCase()] = true; });
|
||
this.inviteUsers = (d.users || []).filter(function(u){
|
||
return u.id !== me && !shared[(u.login || '').toLowerCase()];
|
||
});
|
||
// Groups: charger les groupes du workspace et filtrer
|
||
try {
|
||
const gr = await fetch('/api/v2/groups', { credentials: 'same-origin' });
|
||
const gd = await gr.json();
|
||
var sharedGroups = {};
|
||
(this.accessList || []).forEach(function(a){ if (a.shared_with_group_id) sharedGroups[a.shared_with_group_id] = true; });
|
||
var ql = (q || '').toLowerCase();
|
||
this.inviteGroups = (gd.groups || []).filter(function(g){
|
||
return !sharedGroups[g.id] && (!ql || (g.name || '').toLowerCase().indexOf(ql) >= 0);
|
||
}).slice(0, 8);
|
||
} catch(e2) { this.inviteGroups = []; }
|
||
this.inviteSel = this.inviteUsers.length ? 0 : -1;
|
||
this.inviteSuggestOpen = true;
|
||
} catch(e) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; }
|
||
},
|
||
inviteMove(dir){
|
||
var el = this.$refs.inviteSuggest;
|
||
var n = this.inviteUsers.length;
|
||
if (!n) return;
|
||
this.inviteSel = (this.inviteSel + dir + n) % n;
|
||
if (el) {
|
||
var item = el.querySelectorAll('.sd-user-item')[this.inviteSel];
|
||
if (item) item.scrollIntoView({ block: 'nearest' });
|
||
}
|
||
},
|
||
inviteEnter(){
|
||
if (this.inviteSuggestOpen && this.inviteSel >= 0 && this.inviteUsers[this.inviteSel]) {
|
||
this.pickInviteUser(this.inviteUsers[this.inviteSel]);
|
||
} else {
|
||
this.inviteSuggestOpen = false;
|
||
this.shareInvite();
|
||
}
|
||
},
|
||
pickInviteUser(u){
|
||
this.inviteUserId = u.id;
|
||
this.inviteGroupId = null;
|
||
this.inviteEmail = u.login || u.full_name || '';
|
||
this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
|
||
var el = this.$refs.inviteInput;
|
||
if (el) el.focus();
|
||
},
|
||
pickInviteGroup(g){
|
||
this.inviteGroupId = g.id;
|
||
this.inviteUserId = null;
|
||
this.inviteEmail = g.name || '';
|
||
this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
|
||
var el = this.$refs.inviteInput;
|
||
if (el) el.focus();
|
||
},
|
||
updateShare(sid, perm) {
|
||
var self = this;
|
||
var csrf = getCsrf();;
|
||
fetch('/api/pages/' + this.pid + '/share/' + sid, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
|
||
body: JSON.stringify({ permission: perm })
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
self.showToast(d.status === 'updated' ? 'Permission updated' : (d.detail || 'Update failed'));
|
||
self.loadShares();
|
||
}).catch(function(){ self.showToast('Update failed'); self.loadShares(); });
|
||
},
|
||
loadShares() {
|
||
var self = this;
|
||
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
|
||
self.accessList = d.shares || [];
|
||
self._syncIsShared();
|
||
}).catch(function(){ self.accessList = []; self._syncIsShared(); });
|
||
},
|
||
removeShare(sid) {
|
||
var self = this;
|
||
var csrf = getCsrf();;
|
||
fetch('/api/pages/' + this.pid + '/share/' + sid, {
|
||
method: 'DELETE',
|
||
headers: { 'X-CSRF-Token': csrf }
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if (d.status === 'removed') { self.showToast('Share removed'); self.loadShares(); }
|
||
else { self.showToast(d.detail || 'Remove failed'); }
|
||
}).catch(function(){ self.showToast('Remove failed'); });
|
||
},
|
||
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
|
||
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
|
||
exportPage(fmt){
|
||
this.moreOpen=false;
|
||
this.exportOpen=false;
|
||
// Ensure latest blocks are persisted before exporting any format
|
||
var csrf = getCsrf();;
|
||
var self = this;
|
||
var doDownload = function(){
|
||
var title = encodeURIComponent((self.pageTitle || 'Untitled').trim() || 'Untitled');
|
||
var a = document.createElement('a');
|
||
a.href = '/api/export/' + (fmt==='markdown'?'markdown':fmt==='html'?'html':fmt==='pdf'?'pdf':'site') + '/' + self.pid;
|
||
a.download = '';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
var label = {markdown:'Markdown',html:'HTML',pdf:'PDF',site:'Site'}[fmt]||'';
|
||
self.showToast('Exported as ' + label);
|
||
};
|
||
if (this.dirty) {
|
||
this.save(function(){ doDownload(); });
|
||
} else {
|
||
doDownload();
|
||
}
|
||
},
|
||
duplicatePage(){this.moreOpen=false;const csrf = getCsrf();;const t=(this.pageTitle||'').trim()||'New Page';fetch(`/board/api/pages?title=${encodeURIComponent(t+' copy')}§ion=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;
|
||
},
|
||
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); }
|
||
},
|
||
// ── Unified template picker (§7.1) ──
|
||
openTplPicker(opts){
|
||
opts=opts||{};
|
||
this.tplRestrict=(opts.kinds&&opts.kinds.length)?opts.kinds:null;
|
||
this.tplKindOptionsList=this.tplRestrict?['all'].concat(this.tplRestrict):['page','database','row','blocks','all'];
|
||
this.tplKind='all';
|
||
this.gsMoreOpen=false; this.moreOpen=false;
|
||
this.tplOpen=true; this.tplSelected=null; this.tplPreviewBlocks=[];
|
||
this.tplPreviewTitle=''; this.tplPreviewWarnings=[]; this.tplVarDefs=[];
|
||
this.tplVars={}; this.tplError='';
|
||
this.loadTplList();
|
||
},
|
||
closeTplPicker(){ this.tplOpen=false; },
|
||
// Build CSP : une seule expression par handler (pas de `;`).
|
||
setTplKind(k){ this.tplKind=k; this.loadTplList(); },
|
||
setTplVar(name,value){ this.tplVars[name]=value; this.previewTpl(); },
|
||
tplKindLabel(k){ return {all:'Tous',page:'Pages',blocks:'Blocs',row:'Lignes',database:'Bases'}[k]||k; },
|
||
async loadTplList(){
|
||
this.tplLoading=true; this.tplError='';
|
||
var self=this;
|
||
try{
|
||
var p=new URLSearchParams();
|
||
if(this.tplKind!=='all') p.set('kind',this.tplKind);
|
||
if(this.tplQuery) p.set('q',this.tplQuery);
|
||
const r=await fetch('/api/templates?'+p.toString(),{credentials:'same-origin'});
|
||
if(!r.ok) throw new Error('HTTP '+r.status);
|
||
const d=await r.json();
|
||
var items=d.templates||[];
|
||
// Vue restreinte (slash /template : base, ligne, blocs — pas de page).
|
||
if(this.tplRestrict) items=items.filter(function(t){return self.tplRestrict.indexOf(t.kind)>=0;});
|
||
self.tplItems=items;
|
||
}catch(e){ self.tplItems=[]; self.tplError='Chargement impossible : '+e.message; }
|
||
self.tplLoading=false;
|
||
},
|
||
async selectTpl(t){
|
||
this.tplSelected=t; this.tplVars={}; this.tplPreviewWarnings=[];
|
||
this.tplPreviewBlocks=[]; this.tplPreviewTitle=t.name;
|
||
try{
|
||
const r=await fetch('/api/templates/'+encodeURIComponent(t.id),{credentials:'same-origin'});
|
||
if(!r.ok) throw new Error('HTTP '+r.status);
|
||
const d=await r.json();
|
||
this.tplVarDefs=d.variables||[];
|
||
this.tplPreviewBlocks=(d.blocks_preview||[]);
|
||
this.tplPreviewTitle=d.name;
|
||
this.previewTpl();
|
||
}catch(e){ this.tplPreviewWarnings=['Aperçu indisponible : '+e.message]; }
|
||
},
|
||
async previewTpl(){
|
||
if(!this.tplSelected) return;
|
||
try{
|
||
const csrf = getCsrf();
|
||
const r=await fetch('/api/templates/'+encodeURIComponent(this.tplSelected.id)+'/preview',{
|
||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},
|
||
credentials:'same-origin',body:JSON.stringify({variables:this.tplVars})
|
||
});
|
||
if(!r.ok) return;
|
||
const d=await r.json();
|
||
this.tplPreviewTitle=d.title||this.tplSelected.name;
|
||
this.tplPreviewBlocks=d.blocks_preview||this.tplPreviewBlocks;
|
||
this.tplPreviewWarnings=d.warnings||[];
|
||
}catch(e){}
|
||
},
|
||
async applyTpl(){
|
||
if(!this.tplSelected||this.tplApplying) return;
|
||
this.tplApplying=true;
|
||
const self=this; const t=this.tplSelected;
|
||
const csrf = getCsrf();
|
||
try{
|
||
var body={variables:this.tplVars,context:{}};
|
||
if(t.kind==='blocks'||t.kind==='page'){
|
||
body.page_id=this.pid; body.mode='fill';
|
||
}
|
||
if(t.kind==='page'){
|
||
// Le titre suit le template ; {{title}} = sujet saisi, sinon le
|
||
// titre actuel de la page s'il est significatif, sinon le nom du
|
||
// template (côté serveur).
|
||
var pt=(this.pageTitle||'').trim();
|
||
if(!body.variables.title && pt && pt!=='Untitled' && pt!=='Sans titre'){ body.variables.title=pt; }
|
||
}
|
||
if(t.kind==='database'){
|
||
// Base INLINE dans le document courant (pas de navigation) :
|
||
// même patron que la pilule « Database » (createDbFromTemplate).
|
||
body.parent_page_id=this.pid;
|
||
}
|
||
// kind=row : le serveur utilise la base propriétaire du template ;
|
||
// kind=database hors page : une base neuve est créée (navigation vers elle).
|
||
const r=await fetch('/api/templates/'+encodeURIComponent(t.id)+'/instantiate',{
|
||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},
|
||
credentials:'same-origin',body:JSON.stringify(body)
|
||
});
|
||
if(!r.ok){ const txt=await r.text(); throw new Error('HTTP '+r.status+' '+txt.slice(0,200)); }
|
||
const d=await r.json();
|
||
(d.warnings||[]).forEach(function(w){ self.showToast(w,'error'); });
|
||
if(t.kind==='blocks'&&d.blocks){
|
||
const idx=self._tplInsertIndex();
|
||
const fresh=d.blocks.map(function(b){ b.id=genId(); return b; });
|
||
self.blocks.splice.apply(self.blocks,[idx,0].concat(fresh));
|
||
self.dirty=true; self.autoSave(); self.render();
|
||
self.showToast('Blocs insérés depuis « '+t.name+' »');
|
||
}else if(t.kind==='database'&&d.created_id){
|
||
const idx=self._tplInsertIndex();
|
||
const nb=self.mkB('embed','',{embed_type:'collection',collection_id:parseInt(d.created_id,10)});
|
||
self.blocks.splice(idx,0,nb);
|
||
self.dirty=true; self.autoSave(); self.render();
|
||
self.showToast('Base « '+t.name+' » insérée dans le document');
|
||
}else if(body.page_id){
|
||
// Page courante remplie en place (pas de navigation, §7.1).
|
||
const fresh=(d.blocks||[]).map(function(b){ if(!b.id) b.id=genId(); return b; });
|
||
if(fresh.length) self.blocks=fresh;
|
||
if(d.title){ self.pageTitle=d.title; var te=document.getElementById('_titleEl'); if(te){ te.textContent=d.title; te.classList.toggle('empty',!(d.title||'').trim()); } }
|
||
self.dirty=true; self.autoSave(); self.render();
|
||
self.showToast('Page remplie depuis « '+t.name+' »');
|
||
}else if(t.kind==='row'&&d.url){
|
||
// Appliqué depuis un document : on ne quitte pas la page, on
|
||
// signale la ligne créée dans sa base.
|
||
self.showToast('Ligne « '+t.name+' » créée dans sa base');
|
||
}else if(d.url){
|
||
if(window.fdNavigate) window.fdNavigate(d.url); else window.location=d.url;
|
||
}else{
|
||
self.showToast('Créé depuis « '+t.name+' »');
|
||
}
|
||
// Undo via le toast : le run est journalisé côté serveur.
|
||
self.tplOpen=false;
|
||
}catch(e){ self.showToast('Échec : '+e.message,'error'); }
|
||
self.tplApplying=false;
|
||
},
|
||
_tplInsertIndex(){
|
||
try{
|
||
const ab=this.getActiveBlock();
|
||
if(ab&&typeof ab.idx==='number') return ab.idx+1;
|
||
}catch(e){}
|
||
return this.blocks.length;
|
||
},
|
||
openFormModal(){
|
||
this.formName='New Form';
|
||
this.formOpen=true;
|
||
this.$nextTick(()=>{ const el=this.$refs.formNameInput; if(el) el.focus(); });
|
||
},
|
||
closeFormModal(){ this.formOpen=false; },
|
||
async confirmCreateForm(){
|
||
const name=this.formName.trim();
|
||
if(!name){ this.showToast('Form name required','error'); return; }
|
||
this.formOpen=false;
|
||
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,parent_page_id:this.pid})
|
||
});
|
||
const d=await r.json();
|
||
if(d.id){
|
||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'});
|
||
this.showToast('Form created — '+d.name);
|
||
this.saveDBsToPage();
|
||
this.renderInlineDBs();
|
||
}else{
|
||
this.showToast('Failed to create form');
|
||
}
|
||
}catch(e){this.showToast('Error: '+e.message);}
|
||
},
|
||
saveDBsToPage(){
|
||
// Store inline DB refs in the page's blocks as a special embed block
|
||
if(this.inlineDBs.length===0)return;
|
||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||
if(existing.length===0){
|
||
this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs}));
|
||
}else{
|
||
existing.forEach(b=>{b.dbs=this.inlineDBs;});
|
||
}
|
||
this.render();
|
||
this.dirty=true;this.autoSave();
|
||
},
|
||
renderInlineDBs(){
|
||
// Re-render with inline DB blocks
|
||
this.render();
|
||
},
|
||
loadInlineDBs(){
|
||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||
if(existing.length>0){
|
||
this.inlineDBs=existing[0].dbs||[];
|
||
}
|
||
},
|
||
|
||
async onKd(e){
|
||
const ab=this.getActiveBlock();if(!ab)return;
|
||
const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return;
|
||
if(SM.isOpen){
|
||
if(e.key==='Escape'){e.preventDefault();SM.escClose();return;}
|
||
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;}
|
||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||
// La requête peut être tapée DANS le bloc (« /numbered list ») si le
|
||
// focus n'a pas rejoint l'entrée du menu → on la répercute au filtre.
|
||
if(SM._in&&(e.key.length===1||e.key==='Backspace')){
|
||
const q=(el.textContent||'').replace(/^\s*\//,'');
|
||
if(SM._in.value!==q){SM._in.value=q;SM.flt();}
|
||
}
|
||
return;
|
||
}
|
||
// v7.69 : Maj+↓ / Maj+↑ franchit le bloc (les blocs sont des
|
||
// contenteditable distincts : le navigateur ne peut pas étendre)
|
||
if(e.shiftKey&&(e.key==='ArrowDown'||e.key==='ArrowUp')){
|
||
if(this._shiftBlockExtend(e.key==='ArrowDown'?1:-1)){e.preventDefault();return;}
|
||
}
|
||
if(window.AIAC&&window.AIAC.active){
|
||
if(e.key==='Tab'){e.preventDefault();e.stopPropagation();window.AIAC.accept();return;}
|
||
if(e.key==='Escape'){e.preventDefault();window.AIAC.clear();return;}
|
||
if(e.key==='Enter'||e.key==='Backspace'){window.AIAC.clear();}
|
||
}
|
||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);
|
||
if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen&&!WM.isOpen){SM.open(i2,e2);}
|
||
else if(SM.isOpen){const t=(e2&&e2.textContent)||'';if(t.charAt(0)!=='/')SM.close();}
|
||
if(e2&&!SM.isOpen&&!WM.isOpen){const t=gtTok(e2)||'';const mm=t.match(/\[\[$/);if(mm){WM.open(i2,e2);}}},15);}
|
||
if(e.key===' '){
|
||
if(block.type==='paragraph'&&!(block.content||'').trim()&&!(el.textContent||'').trim()&&!SM.isOpen&&window.AIC){
|
||
e.preventDefault(); window.AIC.open(idx); return;
|
||
}
|
||
const i2=idx;setTimeout(()=>this.checkMd(i2),15);
|
||
}
|
||
if(e.key==='Enter'){
|
||
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
|
||
e.preventDefault();
|
||
const sp=splitCaret(el);
|
||
const before=sp?sp.before:gtTok(el)||'',after=sp?sp.after:'';
|
||
if(!before.trim()&&!after.trim()){
|
||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||
this.sync();this.pushHistory();this.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();_rtSync();
|
||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();ce(ne);}},60);
|
||
this.dirty=true;this.autoSave();return;
|
||
}
|
||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||
this.sync();this.pushHistory();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();_rtSync();
|
||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();cs(ne);}},60);
|
||
this.dirty=true;this.autoSave();return;
|
||
}
|
||
if(e.key==='ArrowUp'){const _sp=splitCaret(el);if(_sp&&!_sp.before.trim()&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
|
||
if(e.key==='ArrowDown'){const _sp=splitCaret(el);if(_sp&&!_sp.after.trim()&&idx<this.blocks.length-1){e.preventDefault();const next=this.getEl(this.blocks[idx+1]?.id);if(next){next.focus();cs(next);}}return;}
|
||
// Backspace / Delete sur un bloc vide → supprimer le bloc (même garde
|
||
// pour les deux touches ; le texte restant est lu comme à la sauvegarde
|
||
// pour ignorer les annotations .fd-react-chip)
|
||
if(e.key==='Backspace'||e.key==='Delete'){if(!(gtTok(el)||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();this.pushHistory();block.type='paragraph';block.content='';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
|
||
if(e.key===' '){const i3=idx;setTimeout(()=>this.checkMd(i3),15);}
|
||
if((e.ctrlKey||e.metaKey)&&!e.altKey){
|
||
if(e.key.toLowerCase()==='z'){e.preventDefault();if(e.shiftKey)this.redo();else this.undo();return;}
|
||
if(e.key.toLowerCase()==='y'){e.preventDefault();this.redo();return;}
|
||
if(e.key.toLowerCase()==='d'){e.preventDefault();const ab2=this.getActiveBlock();if(ab2)this.duplicateBlock(ab2.idx);return;}
|
||
if(e.key.toLowerCase()==='c'){
|
||
e.preventDefault();
|
||
const sel=_selIndicesSorted();
|
||
if(sel.length>0){this.copySelectedBlocks();return;}
|
||
}
|
||
if(e.key.toLowerCase()==='x'){
|
||
e.preventDefault();
|
||
const sel=_selIndicesSorted();
|
||
if(sel.length>0){this.cutSelectedBlocks();return;}
|
||
}
|
||
if(e.key.toLowerCase()==='v'){
|
||
e.preventDefault();
|
||
await this.pasteBlocks();return;
|
||
}
|
||
const m={b:'bold',i:'italic',u:'underline'};if(m[e.key]){e.preventDefault();document.execCommand(m[e.key]);this.dirty=true;this.autoSave();}}
|
||
if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');}
|
||
this.dirty=true;this.autoSave();
|
||
},
|
||
checkMd(idx){
|
||
if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||'';
|
||
const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'};
|
||
this.sync();
|
||
for(const[pfx,type] of Object.entries(sc)){if(text===pfx||text.startsWith(pfx)){this.pushHistory();block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}}
|
||
if(text==='---'){this.pushHistory();block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
|
||
if(text==='```'){this.pushHistory();block.type='code';block.content='';block.language='Plain Text';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
|
||
},
|
||
applySlash(id){
|
||
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
|
||
if(id==='template'){ this.openTplPicker({kinds:['database','row','blocks']}); return; }
|
||
if(id==='database'){ this.openDbTemplatePicker(idx); return; }
|
||
if(id==='synced'){
|
||
const block=this.blocks[idx];this.sync();this.pushHistory();
|
||
block.type='synced';block.synced_id=0;block._synced_content=[];block._synced_title='';
|
||
this.render();this.dirty=true;this.autoSave();_rtSync();
|
||
// Open synced block picker
|
||
this._pickSyncedBlock(idx);
|
||
return;
|
||
}
|
||
if(id.indexOf('ai_')===0){ this.aiSlash(id,idx); return; }
|
||
if(id==='duplicate'){this.duplicateBlock(idx);return;}
|
||
if(id==='meeting'){
|
||
// v7.1.1 : bloc AI Meeting Notes (façon Notion) — même action que
|
||
// meetingNote(), mais en place dans le bloc courant (/meet).
|
||
const block=this.blocks[idx];this.sync();this.pushHistory();
|
||
const today=new Date();const iso=today.toISOString().slice(0,10);
|
||
block.type='meeting';block.content='';
|
||
block.meeting={v:2,title:'Meeting @'+iso,date:iso,state:'idle',
|
||
mode:'mic_only',instruction:'auto',lang:'fr',consent:null,
|
||
consentAudio:false,tab:'notes',channels:[],segments:[],segSeq:0,
|
||
notes:'',manual:'',interim:'',startedAt:0,elapsedBase:0,pausedMs:0,
|
||
steps:[],summaryMd:'',completeness:'',genTitle:'',titleEdited:false,
|
||
shareHidden:false,bannerHidden:false,transcriptId:null,error:'',
|
||
remoteSilentWarned:false,menuOpen:false,settingsOpen:false};
|
||
this.render();this.dirty=true;this.autoSave();_rtSync();
|
||
setTimeout(()=>{const el=document.querySelector('[data-meeting-bid="'+block.id+'"]');if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth',block:'center'});},60);
|
||
return;
|
||
}
|
||
if(id==='delete'){this.removeBlock(idx);return;}
|
||
if(id==='copy_link'){const blk=this.blocks[idx];if(blk)this.copyBlockLink(blk.id);return;}
|
||
if(id==='turn_into'){const blk=this.blocks[idx];if(!blk)return;const el=this.getEl(blk.id);const rk=el?el.getBoundingClientRect():null;blockMenuOpen(rk?rk.left:window.innerWidth/2,rk?rk.bottom:window.innerHeight/2,blk.id);return;}
|
||
const block=this.blocks[idx];this.sync();this.pushHistory();block.content='';
|
||
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
|
||
else if(id==='embed'){block.type='embed';block.embed_type='';block.src='';block.height=520;}
|
||
else if(id==='bookmark'){block.type='bookmark';block.url='';block.title='';block.description='';block.image='';block.site_name='';}
|
||
else if(id==='video'){block.type='video';block.src='';block.title='';}
|
||
else if(id==='audio'){block.type='audio';block.src='';block.title='';}
|
||
else{block.type=id;if(id==='toggle'){block.expanded=true;block.children=[];}if(id==='to_do')block.checked=false;if(id==='columns')block.children=[{id:genId(),type:'paragraph',content:''}];if(id==='code')block.language='Plain Text';if(id==='table'){block.rows=_tblNew(3,3);block.content='';}if(id==='button'){block.automation_id=0;block.automation_name='';}}
|
||
this.render();this.dirty=true;this.autoSave();_rtSync();
|
||
if(id==='table'){setTimeout(()=>_tblFocus(block.id,1,0),60);}
|
||
else if(id==='math'){setTimeout(()=>{const ne=document.querySelector(`[data-math-bid="${block.id}"]`);if(ne)ne.focus();},60);}
|
||
else if(id==='columns'){setTimeout(()=>{const ne=this.getEl(block.children[0].id);if(ne)ne.focus();},60);}
|
||
else{setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
|
||
},
|
||
// ── v5.9.0: AI writing slash commands ──
|
||
aiSlash(id,idx){
|
||
const action=id.slice(3); const block=this.blocks[idx];
|
||
if(block){ this.sync(); this.pushHistory(); block.content=''; }
|
||
if(action==='write'){ this.render(); this.dirty=true; this.autoSave(); if(window.AIC)window.AIC.open(idx); return; }
|
||
if(action==='translate'){ this.render(); this.dirty=true; this.autoSave();
|
||
const lang=window.prompt('Traduire en…','English'); if(!lang){return;}
|
||
this._aiWritingRun('translate',idx,{target_language:lang},'replace'); return; }
|
||
this.render(); this.dirty=true; this.autoSave();
|
||
this._aiWritingRun(action,idx,{},'insert');
|
||
},
|
||
_aiWritingRun(action,idx,extra,mode){
|
||
const self=this; this.sync();
|
||
const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
||
const payload={action:action,context:ctx};
|
||
if(action==='continue'){ let parts=[]; for(let i=0;i<idx;i++){ const b=this.blocks[i]; if(b&&b.content)parts.push(b.content); } payload.prefix=parts.join('\n').slice(-4000); }
|
||
if(extra&&extra.target_language)payload.target_language=extra.target_language;
|
||
if(extra&&extra.prompt)payload.prompt=extra.prompt;
|
||
if(extra&&extra.title)payload.title=extra.title;
|
||
this.showToast('FlowDeck AI : génération…');
|
||
fetch('/api/agent/writing',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify(payload)})
|
||
.then(function(r){return r.json();})
|
||
.then(function(d){ if(d&&d.ok&&d.text){ self.applyAIBlocksAt(d.text,idx,mode); } else { self.showToast((d&&d.error)||'Échec de la génération','error'); } })
|
||
.catch(function(err){ self.showToast((err&&err.message)||'Erreur réseau','error'); });
|
||
},
|
||
applyAIBlocksAt(text,idx,mode){
|
||
const nb=this.md2b(text||''); if(!nb.length)return;
|
||
this.sync(); this.pushHistory();
|
||
if(mode==='replace'){ this.blocks=nb; }
|
||
else{ const block=this.blocks[idx]; const empty=block&&block.type==='paragraph'&&!(block.content||'').trim();
|
||
if(empty){ this.blocks.splice(idx,1,...nb); } else { this.blocks.splice(idx+1,0,...nb); } }
|
||
this.dirty=true; this.autoSave(); this.render(); _rtSync();
|
||
setTimeout(()=>{ const el=this.getEl(this.blocks[Math.min(idx,this.blocks.length-1)]?.id); if(el){el.focus();ce(el);} },60);
|
||
},
|
||
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].checked=v;this.dirty=true;this.autoSave();_rtSync();}},
|
||
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].expanded=!this.blocks[idx].expanded;this.render();this.dirty=true;this.autoSave();_rtSync();}},
|
||
_addColumn(parentId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children.push({id:genId(),type:'paragraph',content:''});this.render();this.dirty=true;this.autoSave();_rtSync();setTimeout(()=>{const ne=this.getEl(b.children[b.children.length-1].id);if(ne)ne.focus();},60);},
|
||
_removeColumn(parentId,childId){const idx=this.getIdx(parentId);if(idx<0)return;const b=this.blocks[idx];this.pushHistory();b.children=b.children.filter(function(c){return c.id!==childId;});if(b.children.length===0)b.children=[{id:genId(),type:'paragraph',content:''}];this.render();this.dirty=true;this.autoSave();_rtSync();},
|
||
_toggleCalloutIcon(bid){const picker=document.getElementById('callout-picker-'+bid);if(picker)picker.style.display=picker.style.display==='none'?'flex':'none';},
|
||
_setCalloutIcon(bid,icon){const idx=this.getIdx(bid);if(idx>=0){this.pushHistory();this.blocks[idx].icon=icon;this.dirty=true;this.autoSave();this.render();_rtSync();}},
|
||
// ── v5.14.0: Synced blocks picker ──
|
||
async _pickSyncedBlock(idx){
|
||
const block=this.blocks[idx];if(!block)return;
|
||
try{
|
||
const r=await fetch('/board/api/synced-blocks');const d=await r.json();
|
||
const items=d.synced_blocks||[];
|
||
if(!items.length){this.showToast('No synced blocks yet — create one from a Board page first.');return;}
|
||
const names=items.map(function(s){return (s.title||'Untitled')+' (id:'+s.id+')';});
|
||
const choice=window.prompt('Choose a synced block:\n\n'+names.join('\n'));
|
||
if(!choice)return;
|
||
const match=choice.match(/id:(\d+)/);
|
||
if(!match)return;
|
||
const sid=parseInt(match[1]);
|
||
if(!sid){this.showToast('Invalid synced block','error');return;}
|
||
const sr=await fetch('/board/api/synced-blocks/'+sid);const sd=await sr.json();
|
||
if(!sd||!sd.content){this.showToast('Synced block not found','error');return;}
|
||
block.synced_id=sid;
|
||
block._synced_content=sd.content||[];
|
||
block._synced_title=sd.title||'';
|
||
this.render();this.dirty=true;this.autoSave();_rtSync();
|
||
}catch(e){this.showToast('Failed to load synced blocks','error');}
|
||
},
|
||
// ── v7.62.0 : menu contextuel au highlight de texte (façon Notion) ──
|
||
showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;this._sel=null;return;}const r=s.getRangeAt(0).getBoundingClientRect();
|
||
const H=300,W=230;let top=r.bottom+8;if(top+H>window.innerHeight-8)top=Math.max(8,r.top-H);
|
||
this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=top;
|
||
this.fmt.left=Math.max(8,Math.min(r.left+r.width/2-W/2,window.innerWidth-W-8));
|
||
this._sel={range:s.getRangeAt(0).cloneRange(),idx};},
|
||
_fmtRestore(){const s=window.getSelection();if(this._sel&&this._sel.range){s.removeAllRanges();s.addRange(this._sel.range);}},
|
||
_fmtBid(){const b=this.blocks[this.fmt.idx];return b?b.id:'';},
|
||
_fmtSaved(){this.sync();this.dirty=true;this.autoSave();},
|
||
// style de la 1ʳᵉ ligne : libellé du type de bloc courant
|
||
fmtStyleName(){const b=this.blocks[this.fmt.idx];const t=b?b.type:'paragraph';
|
||
const m={paragraph:'Normal Text',heading_1:'Heading 1',heading_2:'Heading 2',heading_3:'Heading 3',heading_4:'Heading 4',
|
||
bulleted_list:'Bulleted list',numbered_list:'Numbered list',to_do:'To-do list',toggle:'Toggle list',quote:'Quote',
|
||
callout:'Callout',code:'Code',math:'Block equation',divider:'Divider',table:'Table',image:'Image',embed:'Embed',button:'Button'};
|
||
return m[t]||'Normal Text';},
|
||
_fmtSubPos(ev){const r=ev.currentTarget.getBoundingClientRect();
|
||
return {x:Math.min(r.right+4,window.innerWidth-250),y:Math.min(Math.max(8,r.top),window.innerHeight-140)};},
|
||
// « Normal Text › » → sous-menu Turn into (menu de bloc existant)
|
||
fmtStyle(ev){const bid=this._fmtBid();if(!bid)return;const p=this._fmtSubPos(ev);_blockTurnMenuOpen(p.x,p.y,bid);_subHoverClose();},
|
||
// « A » → sous-menu Color
|
||
fmtColor(ev){const bid=this._fmtBid();if(!bid)return;const p=this._fmtSubPos(ev);_blockColorMenuOpen(p.x,p.y,bid);_subHoverClose();},
|
||
// mise en forme de base : le navigateur fait le toggle, sync() persiste (gtMd)
|
||
fmtApply(f){if(!this._sel)return;const idx=this.fmt.idx;this._fmtRestore();document.execCommand(f);this._fmtSaved();this.showFmt(idx);},
|
||
fmtCode(){if(!this._sel)return;this._fmtRestore();_selWrapToggle('code');this._fmtSaved();this.fmt.open=false;},
|
||
fmtHighlight(){if(!this._sel)return;this._fmtRestore();_selWrapToggle('mark');this._fmtSaved();this.fmt.open=false;},
|
||
fmtCodeBlock(){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.turnInto(i,'code');},
|
||
fmtLink(){if(!this._sel)return;this._fmtRestore();const u=prompt('URL:');if(!u)return;
|
||
document.execCommand('createLink',false,u);this._fmtSaved();this.fmt.open=false;},
|
||
fmtEquation(){if(!this._sel)return;this._fmtRestore();const s=window.getSelection();if(!s.rangeCount)return;
|
||
const tex=prompt('Equation (LaTeX):',s.toString()||'x^2');if(tex===null||!tex.trim())return;
|
||
const span=document.createElement('span');span.className='fd-math-inline';span.setAttribute('data-tex',tex.trim());
|
||
if(window.katex){try{span.innerHTML=window.katex.renderToString(tex.trim(),{displayMode:false,throwOnError:false});}catch(e){span.textContent=tex;}}
|
||
else span.textContent=tex;
|
||
const r=s.getRangeAt(0);r.deleteContents();r.insertNode(span);
|
||
this._fmtSaved();this.fmt.open=false;},
|
||
// « … » → le menu contextuel de bloc (recherche + Text + pied métadonnées)
|
||
fmtMore(ev){const bid=this._fmtBid();if(!bid)return;const r=ev.currentTarget.getBoundingClientRect();
|
||
this.fmt.open=false;blockMenuOpen(Math.max(8,Math.min(r.left-190,window.innerWidth-250)),r.bottom+6,bid);},
|
||
fmtComment(){this.fmt.open=false;const i=this.fmt.idx;if(i<0)return;this._fmtRestore();this.commentBlock(i);},
|
||
fmtAskAI(){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.askAIBlock(i);},
|
||
fmtEditAI(){this.fmtAskAI();},
|
||
fmtSkill(tpl){this.fmt.open=false;const i=this.fmt.idx;if(i>=0)this.runSkill(i,tpl);},
|
||
// offsets d'une Range dans le texte rendu d'un bloc (partagé bouton 💬 + réaction)
|
||
_selSpan(bel,range){return {start:_srcOffset(bel,range.startContainer,range.startOffset),
|
||
end:_srcOffset(bel,range.endContainer,range.endOffset)};},
|
||
// « React to selected text » (v7.64) : sélecteur d'émoji avec le texte
|
||
// sélectionné en en-tête → toggle de la réaction ancrée sur la plage
|
||
fmtReact(){
|
||
if(!this._sel)return;
|
||
const E=this;const bid=this._fmtBid();const bel=this.getEl(bid);
|
||
if(!bel||!window.FDIconPicker)return;
|
||
const span=this._selSpan(bel,this._sel.range);
|
||
if(span.end<=span.start)return;
|
||
const text=this._sel.range.toString();
|
||
const r=this._sel.range.getBoundingClientRect();
|
||
this.fmt.open=false;
|
||
window.FDIconPicker.openFor({
|
||
x:Math.min(Math.max(8,r.left-40),window.innerWidth-360),
|
||
y:Math.min(Math.max(8,r.bottom+8),window.innerHeight-260),
|
||
title:text.length>90?text.slice(0,90)+'…':text,
|
||
onPick:function(emoji){E.toggleReaction(bid,span.start,span.end,emoji);}
|
||
});
|
||
},
|
||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||
toggleComments(){if(this.commentsOpen){this.commentsOpen=false;this._commentSel={range:null,bid:null,start:0,end:0};return;}this.commentsOpen=true;this.loadComments();},
|
||
async loadComments(){
|
||
try{const r=await fetch('/api/pages/'+this.pid+'/comments',{credentials:'same-origin'});const d=await r.json();
|
||
this.comments=d.comments||[];this.commentCount=this.comments.length;}
|
||
catch(e){this.comments=[];}
|
||
this._paintAnchors();
|
||
// #cmt-<id> (copié depuis le menu « … » du fil) → ouvre et centre le fil
|
||
if(!this._cmtHashDone&&location.hash.indexOf('#cmt-')===0){
|
||
this._cmtHashDone=true;
|
||
const cid=parseInt(location.hash.slice(5),10);
|
||
if(!isNaN(cid))setTimeout(()=>this.openThread(cid),300);
|
||
}
|
||
},
|
||
fmtTime(s){if(!s)return '';const t=new Date((String(s).includes('T')||String(s).includes('Z'))?s:(s+'Z'));if(isNaN(t.getTime()))t=new Date(s);const diff=Math.floor((Date.now()-t.getTime())/1000);if(diff<60)return 'just now';if(diff<3600)return Math.floor(diff/60)+'m ago';if(diff<86400)return Math.floor(diff/3600)+'h ago';return Math.floor(diff/86400)+'d ago';},
|
||
csrfTok(){return getCsrf();},
|
||
async addPageComment(){
|
||
const text=(this.commentDraft||'').trim();if(!text)return;
|
||
const self=this;const sel=this._commentSel;
|
||
try{
|
||
const r=await fetch('/api/pages/'+this.pid+'/comments',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({body:text,anchor_block_id:sel.bid||null,anchor_start:sel.bid?sel.start:null,anchor_end:sel.bid?sel.end:null})});
|
||
const d=await r.json();
|
||
if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};await this.loadComments();this.showToast('Comment added');}
|
||
else{this.showToast(d.detail||'Failed to add comment');}
|
||
}catch(e){this.showToast('Failed to add comment');}
|
||
},
|
||
async resolveComment(c){
|
||
// accepte l'objet (le fil → bascule resolve/reopen) ou un id brut
|
||
let id=c,resolved=true;
|
||
if(c&&typeof c==='object'){id=c.id;resolved=!c.resolved;}
|
||
if(!id)return;
|
||
try{await fetch('/api/comments/'+id,{method:'PUT',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({resolved:resolved})});
|
||
this.commentMenu=null;await this.loadComments();}catch(e){}
|
||
},
|
||
async deleteComment(id){if(!confirm('Delete this comment?'))return;try{await fetch('/api/comments/'+id,{method:'DELETE',credentials:'same-origin',headers:{'X-CSRF-Token':this.csrfTok()}});this.commentMenu=null;await this.loadComments();}catch(e){}},
|
||
// ── v7.64 : réactions emoji sur texte + ancres commentées ──
|
||
async loadReactions(){
|
||
try{const r=await fetch('/api/pages/'+this.pid+'/reactions',{credentials:'same-origin'});
|
||
const d=await r.json();this.reactions=(d&&d.reactions)||[];}catch(e){this.reactions=[];}
|
||
this._paintAnchors();
|
||
},
|
||
async toggleReaction(bid,start,end,emoji){
|
||
try{
|
||
const r=await fetch('/api/pages/'+this.pid+'/reactions',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({block_id:bid,anchor_start:start,anchor_end:end,emoji:emoji})});
|
||
const d=await r.json();
|
||
if(d.status==='added'||d.status==='removed'){await this.loadReactions();}
|
||
else this.showToast((d&&d.detail)||'Réaction impossible','error');
|
||
}catch(e){this.showToast('Réaction impossible','error');}
|
||
},
|
||
// ── v7.67 : les réactions vivent aussi dans le tiroir Comments ──
|
||
// (une ligne par plage × émoji, déjà groupées par l'API)
|
||
reactionList(){
|
||
return (this.reactions||[]).map(r=>{
|
||
const bid=r.block_id,start=parseInt(r.anchor_start,10),end=parseInt(r.anchor_end,10);
|
||
return {key:bid+':'+start+':'+end+':'+r.emoji,bid:bid,start:start,end:end,emoji:r.emoji,
|
||
count:parseInt(r.count,10)||1,mine:!!parseInt(r.mine,10),
|
||
text:this._anchorText(bid,start,end)};
|
||
});
|
||
},
|
||
// suppression : POST toggle (le backend retire MA ligne si elle existe)
|
||
removeReaction(r){if(!r)return;this.toggleReaction(r.bid,r.start,r.end,r.emoji);},
|
||
// texte source d'une plage ancrée (hors pastilles .fd-react-chip)
|
||
_anchorText(bid,start,end){
|
||
const el=this.getEl(bid);if(!el||!(end>start))return '';
|
||
const w=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);let n,pos=0,txt='';
|
||
while((n=w.nextNode())){
|
||
if(n.parentElement&&n.parentElement.closest('.fd-react-chip'))continue;
|
||
const len=n.nodeValue.length,a=Math.max(start,pos),b=Math.min(end,pos+len);
|
||
if(b>a)txt+=n.nodeValue.slice(a-pos,b-pos);
|
||
pos+=len;if(pos>=end)break;
|
||
}
|
||
return txt;
|
||
},
|
||
openThread(cid){
|
||
this.commentsOpen=true;this.commentMenu=null;
|
||
const self=this;
|
||
this.loadComments().then(function(){self.$nextTick(function(){
|
||
const el=document.querySelector('[data-thread="'+cid+'"]');
|
||
if(el&&el.scrollIntoView){el.scrollIntoView({block:'center',behavior:'smooth'});
|
||
el.classList.add('thread-flash');setTimeout(function(){el.classList.remove('thread-flash');},1500);}
|
||
});});
|
||
},
|
||
commentAt(){
|
||
this.commentDraft=(this.commentDraft||'')+'@';
|
||
this.$nextTick(function(){const ta=document.querySelector('.comments-drawer textarea');
|
||
if(ta){ta.focus();ta.selectionStart=ta.selectionEnd=ta.value.length;}});
|
||
},
|
||
copyCommentLink(id){
|
||
const url=location.origin+location.pathname+'#cmt-'+id;
|
||
this.commentMenu=null;
|
||
if(navigator.clipboard&&navigator.clipboard.writeText){
|
||
navigator.clipboard.writeText(url).then(()=>this.showToast('Lien du commentaire copié')).catch(()=>this.showToast(url,'error'));
|
||
} else this.showToast(url,'error');
|
||
},
|
||
// Recalcule les ancres jaunes (commentaires) + pastilles de réaction.
|
||
// Le texte des blocs est inchangé : sync() lit la même chaîne.
|
||
_paintAnchors(){
|
||
const ct=document.getElementById('_blocksCt');if(!ct)return;
|
||
ct.querySelectorAll('.fd-react-chip').forEach(function(el){el.remove();});
|
||
ct.querySelectorAll('.fd-anchor').forEach(function(el){
|
||
const par=el.parentNode;if(!par)return;
|
||
while(el.firstChild)par.insertBefore(el.firstChild,el);
|
||
par.removeChild(el);par.normalize();
|
||
});
|
||
const self=this;
|
||
(this.comments||[]).forEach(function(c){
|
||
if(!c.anchor_block_id||c.anchor_start==null||c.anchor_end==null)return;
|
||
const el=self.getEl(c.anchor_block_id);if(!el)return;
|
||
_wrapAnchored(el,parseInt(c.anchor_start,10),parseInt(c.anchor_end,10),
|
||
'fd-anchor'+(c.resolved?' fd-anchor-done':''),{cid:c.id});
|
||
});
|
||
const groups={};
|
||
(this.reactions||[]).forEach(function(r){
|
||
const k=r.block_id+':'+r.anchor_start+':'+r.anchor_end;
|
||
if(!groups[k])groups[k]={bid:r.block_id,start:parseInt(r.anchor_start,10),end:parseInt(r.anchor_end,10),items:[]};
|
||
groups[k].items.push(r);
|
||
});
|
||
Object.keys(groups).forEach(function(k){
|
||
const g=groups[k];
|
||
const el=self.getEl(g.bid);if(!el)return;
|
||
const sp=_wrapAnchored(el,g.start,g.end,'fd-anchor fd-anchor-react',null);
|
||
if(!sp||!sp.parentNode)return;
|
||
const n=g.items.reduce(function(a,i){return a+(parseInt(i.count,10)||1);},0);
|
||
const chip=document.createElement('span');
|
||
chip.className='fd-react-chip';
|
||
chip.setAttribute('contenteditable','false');
|
||
chip.setAttribute('data-react-bid',g.bid);
|
||
chip.setAttribute('data-react-start',String(g.start));
|
||
chip.setAttribute('data-react-end',String(g.end));
|
||
// chaque émoji porte son data-emoji ; `is-mine` = j'ai réagi →
|
||
// la croix de suppression (CSS ::after au survol) ne s'affiche
|
||
// que là, un clic sur les autres rouvre le sélecteur.
|
||
chip.innerHTML=g.items.map(function(i){
|
||
const q=String(i.emoji||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<');
|
||
return '<span class="fd-react-emo'+(parseInt(i.mine,10)?' is-mine':'')+'" data-emoji="'+q+'">'+_emojiHtml(i.emoji)+'</span>';
|
||
}).join('')
|
||
+(n>1?'<span class="fd-react-n">'+n+'</span>':'');
|
||
sp.parentNode.insertBefore(chip,sp.nextSibling);
|
||
});
|
||
},
|
||
commentOnSelection(){
|
||
const sel=this._commentSel;if(!sel.range)return;
|
||
this.commentsOpen=true;this.commentDraft='';
|
||
this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();});
|
||
},
|
||
// ── @mention autocomplete (v4.9.0 users + v5.11.0 pages & dates) ──
|
||
mentionOpen:false,mentionQuery:'',mentionResults:[],mentionPages:[],mentionEl:null,
|
||
openMention(el){
|
||
const s=window.getSelection();if(!s.rangeCount)return;
|
||
const range=s.getRangeAt(0).cloneRange();
|
||
range.selectNodeContents(el);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset);
|
||
const before=range.toString();
|
||
const m=before.match(/@([\w\-\.]*)$/);
|
||
if(!m){return;}
|
||
this.mentionQuery=m[1];this.mentionEl=el;
|
||
const r=s.getRangeAt(0).getBoundingClientRect();
|
||
const menu=document.getElementById('_mentionMenu');
|
||
menu.style.display='block';
|
||
menu.style.top=Math.max(4,(r.bottom+4>window.innerHeight-300)?r.top-304:r.bottom+4)+'px';
|
||
menu.style.left=Math.min(r.left,window.innerWidth-260)+'px';
|
||
this.mentionOpen=true;this.loadMentionResults();
|
||
},
|
||
async loadMentionResults(){
|
||
const q=this.mentionQuery||'';
|
||
const self=this;
|
||
const [u,p]=await Promise.all([
|
||
fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'}).then(r=>r.ok?r.json():{users:[]}).catch(()=>({users:[]})),
|
||
fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'}).then(r=>r.ok?r.json():{pages:[]}).catch(()=>({pages:[]}))
|
||
]);
|
||
if(!this.mentionOpen)return;
|
||
this.mentionResults=u.users||[];this.mentionPages=p.pages||[];
|
||
this.renderMentionMenu();
|
||
},
|
||
_dateSuggestions(q){
|
||
const out=[];const lq=(q||'').toLowerCase();
|
||
const iso=s=>s.toISOString().slice(0,10);
|
||
const now=new Date();
|
||
if(!lq||'today'.startsWith(lq))out.push({label:'Today',iso:iso(now)});
|
||
if(!lq||'tomorrow'.startsWith(lq))out.push({label:'Tomorrow',iso:iso(new Date(now.getTime()+864e5))});
|
||
if(!lq||'yesterday'.startsWith(lq))out.push({label:'Yesterday',iso:iso(new Date(now.getTime()-864e5))});
|
||
const m=lq.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
||
if(m)out.push({label:'Set date',iso:m[1]+'-'+m[2].padStart(2,'0')+'-'+m[3].padStart(2,'0')});
|
||
return out;
|
||
},
|
||
renderMentionMenu(){
|
||
const menu=document.getElementById('_mentionMenu');if(!menu)return;
|
||
let h='';const self=this;
|
||
h+='<div class="mention-group-label">People</div>';
|
||
(this.mentionResults||[]).slice(0,6).forEach(function(u){
|
||
h+='<div class="mention-item" data-mkind="user" data-mval="'+esc(u.login)+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
|
||
+'<div style="width:24px;height:24px;border-radius:50%;background:'+(u.avatar_color||'#3A3A3A')+';color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;overflow:hidden;">'
|
||
+((u.avatar_url)?'<img src="'+u.avatar_url+'" style="width:24px;height:24px;border-radius:50%;object-fit:cover;">':((u.full_name||u.login||'?').charAt(0).toUpperCase()))
|
||
+'</div><div><div style="font-weight:500">'+esc(u.login)+'</div><div style="font-size:11px;color:var(--text-dim,#999)">'+esc(u.full_name||'')+'</div></div></div>';
|
||
});
|
||
if((this.mentionPages||[]).length){
|
||
h+='<div class="mention-group-label">Pages</div>';
|
||
this.mentionPages.slice(0,6).forEach(function(p){
|
||
h+='<div class="mention-item" data-mkind="page" data-mval="'+p.id+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
|
||
+'<span style="width:24px;text-align:center;flex-shrink:0">'+esc(p.icon||'📄')+'</span>'
|
||
+'<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(p.title)+'</span></div>';
|
||
});
|
||
}
|
||
const ds=this._dateSuggestions(this.mentionQuery);
|
||
if(ds.length){
|
||
h+='<div class="mention-group-label">Date</div>';
|
||
ds.forEach(function(d){
|
||
h+='<div class="mention-item" data-mkind="date" data-mval="'+esc(d.iso)+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
|
||
+'<span style="width:24px;text-align:center;flex-shrink:0">📅</span>'
|
||
+'<span style="flex:1">'+esc(d.label)+'</span><span style="font-size:11px;color:var(--text-dim)">'+esc(d.iso)+'</span></div>';
|
||
});
|
||
}
|
||
if(this.mentionResults.length===0&&this.mentionPages.length===0&&ds.length===0)h='<div style="padding:10px;color:var(--text-dim,#999);font-size:13px;">No matches</div>';
|
||
menu.innerHTML=h;
|
||
menu.querySelectorAll('.mention-item').forEach(function(it){
|
||
it.addEventListener('click',function(){
|
||
const kind=it.getAttribute('data-mkind');const val=it.getAttribute('data-mval');
|
||
if(kind==='user')self.insertMention(val);
|
||
else if(kind==='page')self.insertMentionToken('[[fdpage:'+val+']] ');
|
||
else if(kind==='date')self.insertMentionToken('[[fddate:'+val+']] ');
|
||
});
|
||
});
|
||
},
|
||
// Shared non-destructive insertion: replaces the typed "@query" right
|
||
// before the caret inside the token-reconstructed text (chips kept).
|
||
_replaceAtMention(replacement){
|
||
const el=this.mentionEl;if(!el)return null;
|
||
const idx=this.getIdx(el.dataset.bid);if(idx<0)return null;
|
||
const b=this.blocks[idx];
|
||
const text=gtTok(el)||'';
|
||
// robust: compute caret by walking with a marker
|
||
let caretPos=text.length;
|
||
try{
|
||
const MK='\uF000AT';const mark=document.createElement('span');mark.textContent=MK;
|
||
const live=s.getRangeAt(0).cloneRange();live.collapse(true);live.insertNode(mark);
|
||
const full=gtTok(el)||'';const at=full.indexOf(MK);
|
||
if(at>=0)caretPos=at;
|
||
mark.remove();el.normalize();
|
||
}catch(e){}
|
||
const before=text.slice(0,caretPos);
|
||
const m=before.match(/@([\w\-\.]*)$/);
|
||
if(!m)return null;
|
||
const start=caretPos-m[0].length;
|
||
const newText=text.slice(0,start)+replacement+text.slice(caretPos);
|
||
this._commitBlockText(b,newText);
|
||
return true;
|
||
},
|
||
insertMentionToken(token){
|
||
if(this._replaceAtMention(token)){this.closeMention();this.dirty=true;this.autoSave();}
|
||
},
|
||
insertMention(login){
|
||
if(this._replaceAtMention('@'+login+' ')){
|
||
this.closeMention();this.dirty=true;this.autoSave();
|
||
this.notifyNewMentions('@'+login);
|
||
}
|
||
},
|
||
closeMention(){const menu=document.getElementById('_mentionMenu');if(menu)menu.style.display='none';this.mentionOpen=false;this.mentionEl=null;},
|
||
notifyNewMentions(text){
|
||
const self=this;
|
||
const handles=[];const re=/@([\w\-\.]+)/g;let mm;
|
||
while((mm=re.exec(text)))handles.push(mm[1].toLowerCase());
|
||
const fresh=handles.filter(function(h){return !self._notifiedMentions[h];});
|
||
if(!fresh.length)return;
|
||
fetch('/api/pages/'+this.pid+'/mentions',{method:'POST',credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||
body:JSON.stringify({text:'@'+fresh.join(' @')})}).catch(function(){});
|
||
},
|
||
pastePlain(e){e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();},
|
||
// ── Smart paste: analyze pasted text and split it into multiple blocks ──
|
||
paste2b(md){
|
||
const out=[];let para='';
|
||
const lines=String(md||'').replace(/\r\n/g,'\n').split('\n');
|
||
const flush=()=>{
|
||
if(!para.trim()){para='';return;}
|
||
const pa=para.split('\n').map(s=>s.trim()).filter(Boolean);
|
||
if(pa.length>1&&pa.every(l=>l.length<=55)){pa.forEach(l=>out.push(this.mkB('paragraph',l)));}
|
||
else out.push(this.mkB('paragraph',para.trim()));
|
||
para='';
|
||
};
|
||
let i=0;
|
||
for(;i<lines.length;i++){
|
||
const line=lines[i];const t=line.trim();
|
||
if(!t){flush();continue;}
|
||
if(t.startsWith('```')){
|
||
flush();
|
||
const lang=t.substring(3).trim()||'Plain Text';
|
||
let code='';i++;
|
||
for(;i<lines.length;i++){if(lines[i].trim()==='```')break;code+=(code?'\n':'')+lines[i];}
|
||
const cb=this.mkB('code',code);cb.language=lang;out.push(cb);
|
||
continue;
|
||
}
|
||
if(_tblIsData(t)&&i+1<lines.length&&_tblIsSep(lines[i+1])){
|
||
flush();
|
||
const header=_tblSplitLine(t);const rows=[header];let j=i+2;
|
||
while(j<lines.length&&_tblIsData(lines[j])){rows.push(_tblSplitLine(lines[j]));j++;}
|
||
let w=0;rows.forEach(r=>{if(r.length>w)w=r.length;});if(w<1)w=1;
|
||
const rows2=rows.map(r=>{const a=r.slice();while(a.length<w)a.push('');return a;});
|
||
out.push(this.mkB('table','',{rows:rows2}));i=j-1;
|
||
continue;
|
||
}
|
||
const hmm=t.match(/^(#{1,6})\s+(.*)$/);
|
||
if(hmm){flush();out.push(this.mkB('heading_'+Math.min(hmm[1].length,4),hmm[2]));continue;}
|
||
if(/^[-*+]\s+\[[ xX]\]\s+/.test(t)){
|
||
flush();
|
||
const nb=this.mkB('to_do',t.replace(/^[-*+]\s+\[[ xX]\]\s+/,''));nb.checked=/^[-*+]\s+\[x\]/.test(t)||/^[-*+]\s+\[X\]/.test(t);
|
||
out.push(nb);continue;
|
||
}
|
||
if(/^[-*+]\s+/.test(t)){flush();out.push(this.mkB('bulleted_list',t.replace(/^[-*+]\s+/,'')));continue;}
|
||
if(/^\d+\.\s+/.test(t)){flush();out.push(this.mkB('numbered_list',t.replace(/^\d+\.\s+/,'')));continue;}
|
||
if(t.substring(0,2)==='> '){flush();out.push(this.mkB('quote',t.substring(2)));continue;}
|
||
if(t==='---'||t==='***'){flush();out.push(this.mkB('divider',''));continue;}
|
||
para+=(para?'\n':'')+line.trim();
|
||
}
|
||
flush();
|
||
return out;
|
||
},
|
||
handleSmartPaste(e){
|
||
e.preventDefault();
|
||
const text=(((e.clipboardData||window.clipboardData))||{}).getData('text/plain')||'';
|
||
const fallback=()=>{document.execCommand('insertText',false,text);this.dirty=true;this.autoSave();};
|
||
const ab=this.getActiveBlock();
|
||
if(!ab)return fallback();
|
||
const bel=ab.el,idx=ab.idx,block=this.blocks[idx];
|
||
if(!block)return fallback();
|
||
const btype=block.type||'paragraph';
|
||
if(['code','table','math','image','embed','meeting','divider','table_of_contents','columns'].indexOf(btype)>=0)return fallback();
|
||
const parsed=this.paste2b(text);
|
||
if(!parsed.length)return fallback();
|
||
// un seul bloc PARAGRAPHE = coller une ligne → texte inline (comme avant) ;
|
||
// un seul bloc structuré (table, code, titre…) = vraie insertion de bloc.
|
||
if(parsed.length===1&&parsed[0].type==='paragraph')return fallback();
|
||
const sp=splitCaret(bel);
|
||
let before='',after='';
|
||
if(sp){before=sp.before;after=sp.after;}else{before=gt(bel)||'';}
|
||
this.sync();this.pushHistory();
|
||
const beforeHasText=!!(before||'').trim();
|
||
block.content=before;
|
||
const ins=[];
|
||
parsed.forEach((pb,k)=>{
|
||
if(k===0&&!beforeHasText){
|
||
const nbf={id:block.id,type:pb.type,content:pb.content};
|
||
for(const key in pb){if(key==='id'||key==='type'||key==='content')continue;nbf[key]=pb[key];}
|
||
this.blocks[idx]=nbf;
|
||
}else{
|
||
const nb=this.mkB(pb.type,pb.content);
|
||
if(pb.language!==undefined)nb.language=pb.language;
|
||
if(pb.checked!==undefined)nb.checked=pb.checked;
|
||
if(pb.children)nb.children=pb.children;
|
||
ins.push(nb);
|
||
}
|
||
});
|
||
if((after||'').trim())ins.push(this.mkB('paragraph',after.trim()));
|
||
this.blocks.splice(idx+1,0,...ins);
|
||
const focusId=ins.length?ins[ins.length-1].id:block.id;
|
||
this.render();this.dirty=true;this.autoSave();
|
||
setTimeout(()=>{const fe=this.getEl(focusId);if(fe){fe.focus();ce(fe);}},60);
|
||
},
|
||
autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
|
||
// Reflect the page title in the left navigation, the header breadcrumb
|
||
// and the browser tab as soon as it changes.
|
||
_syncTitleUI(){
|
||
const title=(this.pageTitle||'').trim()||'Untitled';
|
||
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=title;});
|
||
document.querySelectorAll(`.sidebar-item.ws-tree-item[data-id="${this.pid}"]`).forEach(item=>{item.setAttribute('data-name',title);const n=item.querySelector('.page-name');if(n)n.textContent=title;});
|
||
document.title='FlowDeck — '+title;
|
||
window.dispatchEvent(new CustomEvent('flowdeck:page-renamed',{detail:{id:this.pid,title:title}}));
|
||
if(window.parent&&window.parent!==window){try{window.parent.postMessage({type:'fd-page-renamed',id:this.pid,title:title},window.location.origin);}catch(e){}}
|
||
},
|
||
save(cb){
|
||
if(this.fileData){
|
||
// File page: only the title is editable here. Sync the sidebar,
|
||
// header breadcrumb and browser tab live, and persist the title
|
||
// without rewriting the file's blocks.
|
||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
|
||
this._syncTitleUI();
|
||
if(this._fileTitleT)clearTimeout(this._fileTitleT);
|
||
this._fileTitleT=setTimeout(()=>{
|
||
this._fileTitleT=null;
|
||
const title=(this.pageTitle||'').trim();
|
||
if(!title)return;
|
||
const csrf = getCsrf();;
|
||
fetch(`/board/api/pages/${this.pid}?title=${encodeURIComponent(title)}`,{method:'PUT',headers:{'X-CSRF-Token':csrf}})
|
||
.then(()=>{this.dirty=false;})
|
||
.catch(()=>{});
|
||
},600);
|
||
if(cb)cb();
|
||
return;
|
||
}
|
||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
|
||
this._syncTitleUI();
|
||
if(this.saving){ if(cb) cb(); return; }
|
||
this.sync();
|
||
this.saving=true;
|
||
const blocksArr=this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','first_col_header','colsW','meeting','automation_id','automation_name','url','title','description','image','site_name'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;});
|
||
// v6.0.0 PWA: hors ligne (ou échec réseau) → file IndexedDB rejouée au retour du réseau
|
||
const queueOffline=()=>{
|
||
if(!window.FlowOffline){this.saving=false;return;}
|
||
window.FlowOffline.savePageOffline({id:this.pid,title:this.pageTitle,content:JSON.stringify(blocksArr),content_format:'blocks'})
|
||
.then(()=>{this.saving=false;this.dirty=false;this.lastSaved='offline';if(cb)cb();})
|
||
.catch(()=>{this.saving=false;});
|
||
};
|
||
if(!navigator.onLine){queueOffline();return;}
|
||
const csrf = getCsrf();;
|
||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({title:this.pageTitle,blocks:blocksArr})})
|
||
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
|
||
this._syncTitleUI();
|
||
if(cb)cb();
|
||
}).catch(()=>{this.saving=false;queueOffline();});
|
||
},
|
||
runAutomationButton(bid){
|
||
const idx=this.getIdx(bid);if(idx<0)return;
|
||
const b=this.blocks[idx];
|
||
if(!b.automation_id){this.pickAutomation(bid);return;}
|
||
const csrf = getCsrf();;
|
||
const self=this;
|
||
this.showToast('⚡ '+ (b.automation_name||'Automation') + '…');
|
||
fetch('/api/automations/'+b.automation_id+'/run',{method:'POST',
|
||
headers:{'Content-Type':'application/json','X-CSRF-Token':csrf},
|
||
body:JSON.stringify({page_id:this.pid})})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
if(d.status==='fired'){self.showToast('✅ Automation exécutée');}
|
||
else if(d.status==='skipped'){self.showToast('⏭️ Condition non remplie (skippée)');}
|
||
else{self.showToast((d.detail||'Erreur')+'','error');}
|
||
})
|
||
.catch(()=>{self.showToast('Échec de l\u2019exécution','error');});
|
||
},
|
||
pickAutomation(bid){
|
||
const self=this;
|
||
fetch('/workspace/automations',{credentials:'same-origin'})
|
||
.then(r=>r.json())
|
||
.then(d=>{
|
||
const list=d.automations||[];
|
||
if(!list.length){self.showToast('Aucune automation — créez-en dans Paramètres → Automations','error');return;}
|
||
const ov=document.createElement('div');ov.id='fdAutomationOverlay';
|
||
ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;';
|
||
const box=document.createElement('div');box.style.cssText='background:var(--bg-secondary,#1f1f1f);border:1px solid var(--border,#333);border-radius:12px;padding:20px;width:420px;max-width:92vw;max-height:80vh;overflow:auto;';
|
||
box.innerHTML='<div style="font-weight:600;font-size:14px;margin-bottom:12px;">Choisir une automation</div>'+
|
||
list.filter(a=>a.enabled).map(function(a){
|
||
return `<button type="button" data-id="${a.id}" style="display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:9px 10px;background:var(--bg-hover,transparent);border:1px solid var(--border,#333);color:var(--text-primary);border-radius:8px;margin-bottom:8px;cursor:pointer;"><span>⚡</span><span style="flex:1;font-size:13px;">${esc(a.name||'Automation')}</span><span style="font-size:11px;color:var(--text-tertiary);">${esc(a.trigger_type||'event')}</span></button>`;
|
||
}).join('')+
|
||
'<button type="button" data-cancel="1" style="width:100%;text-align:center;padding:8px;border:none;background:transparent;color:var(--text-secondary);cursor:pointer;font-size:12px;">Annuler</button>';
|
||
ov.appendChild(box);
|
||
document.body.appendChild(ov);
|
||
box.addEventListener('click',function(ev){
|
||
const btn=ev.target.closest('[data-id]');if(btn){
|
||
const idx=self.getIdx(bid);if(idx>=0){self.blocks[idx].automation_id=parseInt(btn.getAttribute('data-id'),10);const a=list.find(function(x){return x.id===parseInt(btn.getAttribute('data-id'),10);});self.blocks[idx].automation_name=a?a.name:'';self.dirty=true;self.autoSave();self.render();}
|
||
ov.remove();
|
||
}else if(ev.target.closest('[data-cancel]')){ov.remove();}
|
||
});
|
||
})
|
||
.catch(()=>{self.showToast('Erreur de chargement','error');});
|
||
},
|
||
md2b(md){const bl=[];const lines=String(md).split('\n');let i=0;for(;i<lines.length;i++){const raw=lines[i];const t=raw.trim();if(!t)continue;
|
||
if(t.startsWith('```')){const lang=t.substring(3).trim()||'Plain Text';let code='';i++;for(;i<lines.length;i++){if(lines[i].trim()==='```')break;code+=(code?'\n':'')+lines[i];}bl.push(this.mkB('code',code,{language:lang}));continue;}
|
||
if(_tblIsData(t)&&i+1<lines.length&&_tblIsSep(lines[i+1])){const header=_tblSplitLine(t);const rows=[header];let j=i+2;while(j<lines.length&&_tblIsData(lines[j])){rows.push(_tblSplitLine(lines[j]));j++;}let w=0;rows.forEach(function(r){if(r.length>w)w=r.length;});if(w<1)w=1;const rows2=rows.map(function(r){const a=r.slice();while(a.length<w)a.push('');return a;});bl.push(this.mkB('table','',{rows:rows2}));i=j-1;continue;}
|
||
if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));
|
||
else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));
|
||
else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));
|
||
else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));
|
||
else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
|
||
// AVANT les puces : « - [ ] … » commence aussi par « - » →
|
||
// sinon la case à cocher de l'agent devenait une puce.
|
||
else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));
|
||
else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));
|
||
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
|
||
else if(/^\d+[.)] /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+[.)] /,'')));
|
||
else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));
|
||
else bl.push(this.mkB('paragraph',t));
|
||
}return bl;},
|
||
};
|
||
}
|
||
// v7.62 : data Alpine du wrapper éditeur — Alpine v3 n'expose plus __x
|
||
// (l'ancien accès rendait muets showFmt/mentions).
|
||
function _xed(el){ if(!el) return null; if(el.__x&&el.__x.$data) return el.__x.$data;
|
||
return (el._x_dataStack&&el._x_dataStack[0])||null; }
|
||
// v7.64 : clic sur une ancre jaune → ouvre et centre le fil de discussion ;
|
||
// clic sur une pastille de réaction → réouvre le sélecteur pour cette plage.
|
||
document.addEventListener('click',function(ev){
|
||
const t=ev.target;if(!t||!t.closest)return;
|
||
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
|
||
const chip=t.closest('.fd-react-chip');
|
||
if(chip){
|
||
const bid=chip.getAttribute('data-react-bid');
|
||
const start=parseInt(chip.getAttribute('data-react-start'),10);
|
||
const end=parseInt(chip.getAttribute('data-react-end'),10);
|
||
// v7.67 : clic sur la croix d'un émoji à moi → retire MA réaction
|
||
const emo=t.closest('.fd-react-emo');
|
||
if(emo&&emo.classList.contains('is-mine')){
|
||
d.removeReaction({bid:bid,start:start,end:end,emoji:emo.getAttribute('data-emoji')});
|
||
return;
|
||
}
|
||
const el=d.getEl(bid);const rect=chip.getBoundingClientRect();
|
||
if(!window.FDIconPicker||!el||isNaN(start)||isNaN(end))return;
|
||
window.FDIconPicker.openFor({
|
||
x:Math.min(Math.max(8,rect.left-40),window.innerWidth-360),
|
||
y:Math.min(Math.max(8,rect.bottom+8),window.innerHeight-260),
|
||
title:(el.textContent||'').slice(start,end),
|
||
onPick:function(emoji){d.toggleReaction(bid,start,end,emoji);}
|
||
});
|
||
return;
|
||
}
|
||
const anch=t.closest('.fd-anchor[data-cid]');
|
||
if(anch&&d.openThread){
|
||
const cid=parseInt(anch.getAttribute('data-cid'),10);
|
||
if(!isNaN(cid))d.openThread(cid);
|
||
}
|
||
});
|
||
document.addEventListener('mouseup',(ev)=>{setTimeout(()=>{
|
||
// v7.62 : clic dans la toolbar → on laisse la sélection (et le menu) intacts ;
|
||
// sélection relâchée/cliquée ailleurs → le menu de sélection se ferme.
|
||
if(ev.target&&ev.target.closest&&ev.target.closest('.format-toolbar'))return;
|
||
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
|
||
const s=window.getSelection();
|
||
if(!s||s.isCollapsed){d.fmt.open=false;d._sel=null;return;}
|
||
const ct=document.getElementById('_blocksCt');
|
||
if(!ct?.contains(s.anchorNode)){d.fmt.open=false;d._sel=null;return;}
|
||
const bel0=s.anchorNode&&(s.anchorNode.nodeType===1?s.anchorNode:s.anchorNode.parentElement);
|
||
const bel=bel0?bel0.closest('[data-bid]'):null;
|
||
if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}
|
||
},80);});
|
||
document.addEventListener('keyup',(e)=>{
|
||
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
|
||
if(!d.mentionOpen){if(e.key!=='@')return;}
|
||
const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return;
|
||
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(el))return;
|
||
if(d.mentionOpen){
|
||
if(e.key==='Escape'){d.closeMention();return;}
|
||
if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const items=document.querySelectorAll('#_mentionMenu .mention-item');if(!items.length)return;let idx=[].indexOf.call(items,document.querySelector('#_mentionMenu .mention-item.hover'));idx+= (e.key==='ArrowDown'?1:-1);if(idx<0)idx=items.length-1;if(idx>=items.length)idx=0;items.forEach(function(x){x.classList.remove('hover');});items[idx].classList.add('hover');return;}
|
||
if(e.key==='Enter'){e.preventDefault();const hover=document.querySelector('#_mentionMenu .mention-item.hover')||document.querySelector('#_mentionMenu .mention-item');if(hover)hover.click();return;}
|
||
}
|
||
d.openMention(el);
|
||
},true);
|
||
document.addEventListener('click',(e)=>{
|
||
const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return;
|
||
if(!d.mentionOpen)return;
|
||
const menu=document.getElementById('_mentionMenu');
|
||
if(menu&&!menu.contains(e.target))d.closeMention();
|
||
});
|
||
var fdStyle=document.createElement('style');
|
||
fdStyle.textContent='#_mentionMenu .mention-item:hover,#_mentionMenu .mention-item.hover{background:rgba(35,131,226,.15);}';
|
||
document.head.appendChild(fdStyle);
|
||
function blocksToMarkdown(b) {
|
||
var c = b.content || '';
|
||
switch(b.type) {
|
||
case 'heading_1': return '# ' + c;
|
||
case 'heading_2': return '## ' + c;
|
||
case 'heading_3': return '### ' + c;
|
||
case 'heading_4': return '#### ' + c;
|
||
case 'bulleted_list': return '- ' + c;
|
||
case 'numbered_list': return '1. ' + c;
|
||
case 'to_do': return (b.checked ? '- [x] ' : '- [ ] ') + c;
|
||
case 'quote': return '> ' + c;
|
||
case 'divider': return '---';
|
||
case 'code': return '```' + (b.language || '') + '\n' + c + '\n```';
|
||
case 'toggle': return (b.children && b.children.length ? '## ' : '## ') + c + (b.children && b.children.length ? '\n\n' + b.children.map(blocksToMarkdown).join('\n\n') : '');
|
||
case 'math': return '$$\n' + c + '\n$$';
|
||
case 'table_of_contents': return '[TOC]';
|
||
case 'columns': return (b.children || []).map(blocksToMarkdown).join('\n\n');
|
||
case 'table': { var rows=b.rows||[]; if(!rows.length)return ''; var w=0; rows.forEach(function(r){if(r&&r.length>w)w=r.length;}); var rl=function(r){var cells=[];for(var k=0;k<w;k++)cells.push((r&&k<r.length)?String(r[k]==null?'':r[k]):'');return '| '+cells.join(' | ')+' |';}; var hasH=(b.has_header!==false),fc=!!b.first_col_header; var startRows=[],sep=false; var out=''; var ri0=0; if(hasH){startRows.push(rows[0]);ri0=1;} for(var si=0;si<startRows.length;si++){out+=(out?'\n':'')+rl(startRows[si]);} if(hasH){var seps=[];for(var k2=0;k2<w;k2++)seps.push('---');out+=(out?'\n':'')+'| '+seps.join(' | ')+' |';} for(var ri2=ri0;ri2<rows.length;ri2++)out+='\n'+rl(rows[ri2]); return out; }
|
||
default: return c;
|
||
}
|
||
}
|
||
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
||
|
||
// globales window — probe « Undefined variable: editorState »).
|
||
|
||
// Navigation partielle (fdLoad) : Alpine démarre déjà → 'alpine:init'
|
||
// ne sera plus émis, on enregistre tout de suite (pattern _ctx_menu.js)
|
||
// et on remonte la racine montée avant l'enregistrement.
|
||
if (window.Alpine) { Alpine.data('editorState', editorState); fdRefreshXData('editorState'); }
|
||
else document.addEventListener('alpine:init', function () { Alpine.data('editorState', editorState); });
|
||
window.editorState = editorState;
|
||
|
||
// ── Ctrl/Cmd + S : sauvegarder le document ────────────────────────
|
||
// L'autosave (1,5 s) suffit pour la frappe au quotidien, mais on ne peut
|
||
// pas fermer l'onglet « au bout d'un moment » en etant sur que tout est
|
||
// parti. Le raccourci est attendu par tout le monde : il appelle le même
|
||
// `save()` que l'autosave (meme endpoint, meme file hors-ligne) et rend
|
||
// la main explicitement.
|
||
// `window.E` survit aux navigations partielles : on exige donc que
|
||
// l'editeur soit reellement monte (`#_blocksCt`) avant d'agir, sinon
|
||
// Ctrl+S sauvegarderait un editeur detache depuis une autre page.
|
||
// ── v7.61 : raccourcis du menu contextuel de bloc ──
|
||
document.addEventListener('keydown', function (e) {
|
||
var E = window.E;
|
||
if (!E || !document.getElementById('_blocksCt')) return;
|
||
var k = String(e.key || '').toLowerCase();
|
||
var ctrl = e.ctrlKey || e.metaKey, sh = e.shiftKey, alt = e.altKey;
|
||
var menu = document.getElementById('blockMenu');
|
||
var menuBid = menu ? (menu.getAttribute('data-bid') || '') : '';
|
||
function curIdx(){
|
||
if (menuBid) { var mi = E.getIdx(menuBid); if (mi >= 0) return mi; }
|
||
var ab = E.getActiveBlock();
|
||
return ab && ab.idx >= 0 ? ab.idx : -1;
|
||
}
|
||
if (ctrl && !sh && !alt && k === 'j') { e.preventDefault(); var i1 = curIdx(); if (i1 >= 0) E.askAIBlock(i1); return; }
|
||
if (ctrl && sh && !alt && k === 'h') { e.preventDefault(); var i2 = curIdx(); if (i2 >= 0) E.applyLastColor(i2); return; }
|
||
if (ctrl && sh && !alt && k === 'm') { e.preventDefault(); var i3 = curIdx(); if (i3 >= 0) E.commentBlock(i3); return; }
|
||
if (ctrl && sh && alt && k === 'x') { e.preventDefault(); var i4 = curIdx(); if (i4 >= 0) E.suggestEdit(i4); return; }
|
||
if (alt && sh && !ctrl && k === 'l') { e.preventDefault(); var i5 = curIdx(); if (i5 >= 0) E.copyBlockLink(E.blocks[i5].id); return; }
|
||
if (alt && sh && !ctrl && k === 'e') { e.preventDefault(); var i7 = curIdx(); if (i7 >= 0) E.askAIBlock(i7); return; }
|
||
if (ctrl && sh && !alt && k === 'p') { e.preventDefault(); var i6 = curIdx(); if (i6 >= 0) _blockMoveToOpen(i6); return; }
|
||
// Ctrl+D : déjà géré par la saisie quand le bloc a le focus (pas de doublon)
|
||
if (ctrl && !sh && !alt && k === 'd') {
|
||
var ab = E.getActiveBlock();
|
||
if (ab && document.activeElement === ab.el) return;
|
||
var idc = ab && ab.idx >= 0 ? ab.idx : (menuBid ? E.getIdx(menuBid) : -1);
|
||
if (idc >= 0) { e.preventDefault(); E.duplicateBlock(idc); }
|
||
return;
|
||
}
|
||
// Del : supprime le bloc SEULEMENT avec le menu ouvert (jamais pendant la saisie)
|
||
if (k === 'delete' && !ctrl && !sh && !alt && menu) {
|
||
var tgt = e.target;
|
||
if (tgt && (tgt.tagName === 'INPUT' || tgt.tagName === 'TEXTAREA' || tgt.isContentEditable)) return;
|
||
var i8 = curIdx();
|
||
if (i8 >= 0) { e.preventDefault(); E.deleteSelected(i8); }
|
||
return;
|
||
}
|
||
if (k === 'escape' && menu) { _blockMenuClose(); return; }
|
||
});
|
||
document.addEventListener('keydown', function (e) {
|
||
if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey) return;
|
||
if (String(e.key).toLowerCase() !== 's') return;
|
||
var E = window.E;
|
||
if (!E || !document.getElementById('_blocksCt') || typeof E.save !== 'function') return;
|
||
e.preventDefault(); // sinon le navigateur ouvre « Enregistrer la page »
|
||
if (E.canEdit === false) {
|
||
if (E.showToast) E.showToast('This page is locked (read-only)', 'error');
|
||
return;
|
||
}
|
||
if (E.saving) { if (E.showToast) E.showToast('Saving…'); return; }
|
||
E.dirty = true;
|
||
var notify = function () {
|
||
if (E.showToast) E.showToast('Saved', 'success');
|
||
else if (window.showToast) window.showToast('Saved', 'success');
|
||
};
|
||
try { E.save(notify); } catch (err) {
|
||
if (E.showToast) E.showToast('Save failed', 'error');
|
||
}
|
||
});
|
||
}
|