feat(wiki,templates): v5.11.0 wiki-links & mentions + v5.12.0 templates & page lock (release 5.12.0)
v5.11.0 Wiki-links & mentions de page :
- tokens [[fdpage:ID]] / [[fddate:ISO]] dans le texte des blocs,
service app/services/wiki_links.py (labels, rendu HTML, extraction)
- taper [[ ouvre le picker de pages (recherche floue, clavier) ;
le menu @ gagne les sections Pages et Date (today/tomorrow/YYYY-MM-DD)
- chips atomiques contenteditable=false relues en tokens par gtTok()
(autosave/drag/undo preservent les liens) ; renommage propage via
GET /board/api/wiki/titles ; backlinks reconnaissent les tokens ;
page publique rend les chips (echopee)
v5.12.0 Templates & verrouillage :
- template picker global sur + New page : 5 built-in
(app/services/block_templates.py) + templates perso
(table page_global_templates, migration 13)
- POST /board/api/page-templates (save current page) + /{id}/use
(instantiate, id 0 = built-in par cle)
- page lock : POST /api/pages/{id}/lock, garde _ensure_page_editable
-> 423 en ecriture pour les non-privileged, deblocage par
locked_by ou admin seulement (403 sinon), banniere + read-only UI
- full-width / small text par page (pages.full_width/font_small,
POST /api/pages/{id}/options, classes CSS)
- migration 13 : is_locked, locked_by, full_width, font_small,
page_global_templates
Tests : tests/test_v511_v512_wiki_templates.py (15) ; suite complete
538 verte ; ruff OK ; node --check des templates JS OK.
This commit is contained in:
@@ -27,11 +27,31 @@ window.__fdEditorScriptsLoaded = true;
|
||||
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
|
||||
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
|
||||
function gt(e){return (e&&e.innerText!==undefined)?e.innerText:(e?e.textContent:'');}
|
||||
// v5.11.0: read a contenteditable block back to plain text, converting
|
||||
// wiki chip nodes (contenteditable=false spans) back into their raw
|
||||
// [[fdpage:ID]] / [[fddate:YYYY-MM-DD]] tokens.
|
||||
function gtTok(e){
|
||||
if(!e) return '';
|
||||
if(!e.querySelector||!e.querySelector('.fd-wiki-chip,.fd-wiki-date')) return gt(e);
|
||||
let out='';
|
||||
(function walk(node){
|
||||
for(const n of node.childNodes){
|
||||
if(n.nodeType===3){out+=n.nodeValue;continue;}
|
||||
if(n.nodeType===1){
|
||||
if(n.classList&&n.classList.contains('fd-wiki-chip')){out+=n.getAttribute('data-token')||'';continue;}
|
||||
if(n.tagName==='BR'){out+='\n';continue;}
|
||||
walk(n);
|
||||
}
|
||||
}
|
||||
})(e);
|
||||
return out;
|
||||
}
|
||||
window.gtTok=gtTok;
|
||||
function splitCaret(e){
|
||||
const s=window.getSelection(); if(!s.rangeCount||!s.rangeCount)return null; const sel=s.getRangeAt(0);
|
||||
const MK='\uF000FD'; const mark=document.createElement('span'); mark.textContent=MK;
|
||||
sel.insertNode(mark);
|
||||
const full=gt(e)||'';
|
||||
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('')};
|
||||
@@ -48,9 +68,45 @@ window.__fdEditorScriptsLoaded = true;
|
||||
.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
|
||||
.replace(/\*([^*]+)\*/g,'<em>$1</em>')
|
||||
.replace(/~~([^~]+)~~/g,'<s>$1</s>')
|
||||
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
.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=['💡','💡','📌','⚠️','ℹ️','✅','❌','🚀','🔔','📣','⭐','❤️','🔥','💬','👀','🧠','🎯','🔑','📅','💰','💡','📚','🧪','🌍'];
|
||||
|
||||
@@ -297,6 +353,64 @@ window.__fdEditorScriptsLoaded = true;
|
||||
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
|
||||
get isOpen(){return SM._o;}};
|
||||
|
||||
// ═══════════ v5.11.0 Wiki-link picker ([[ → page search) ═══════════
|
||||
const WM={_o:false,_idx:-1,_s:0,_it:[],_root:null,_in:null,_li:null,_pv:null,
|
||||
mk(){ if(this._root)return this._root;
|
||||
const d=document.createElement('div'); d.id='_wikiMenu';
|
||||
d.style.cssText='position:fixed;z-index:1250;display:none;width:340px;max-height:320px;flex-direction:column;background:var(--bg-modal,#1f1f1f);border:1px solid var(--border,#333);border-radius:10px;box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden';
|
||||
const list=document.createElement('div'); list.style.cssText='overflow-y:auto;flex:1;min-height:60px;max-height:250px;padding:6px';
|
||||
const inp=document.createElement('input'); inp.type='text'; inp.placeholder='Search pages…';
|
||||
inp.style.cssText='width:100%;padding:9px 12px;background:var(--bg-secondary);border:none;border-top:1px solid var(--border);color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
|
||||
d.appendChild(list); d.appendChild(inp); document.body.appendChild(d);
|
||||
this._root=d; this._li=list; this._in=inp;
|
||||
inp.oninput=()=>{clearTimeout(this._t);this._t=setTimeout(()=>this.load(),180);};
|
||||
list.addEventListener('click',ev=>{const it=ev.target.closest('.wm-item');if(it){ev.preventDefault();WM.sel(parseInt(it.dataset.wpid,10));}});
|
||||
document.addEventListener('mousedown',ev=>{if(!d.contains(ev.target))WM.close();});
|
||||
document.addEventListener('keydown',function(ev){
|
||||
if(!WM._o)return;
|
||||
if(ev.metaKey||ev.ctrlKey||ev.altKey)return;
|
||||
if(ev.key==='Escape'){ev.preventDefault();ev.stopPropagation();WM.close();}
|
||||
else if(ev.key==='ArrowDown'){ev.preventDefault();ev.stopPropagation();WM._mv(1);}
|
||||
else if(ev.key==='ArrowUp'){ev.preventDefault();ev.stopPropagation();WM._mv(-1);}
|
||||
else if(ev.key==='Enter'){ev.preventDefault();ev.stopPropagation();const c=WM._it[WM._s];if(c)WM.sel(c.id);}
|
||||
},true);
|
||||
return d; },
|
||||
open(i,el){ this.mk(); const r=el.getBoundingClientRect();
|
||||
this._root.style.display='flex';
|
||||
this._root.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
|
||||
this._root.style.left=Math.min(r.left,window.innerWidth-350)+'px';
|
||||
this._idx=i; this._s=0; this._in.value=''; this._o=true;
|
||||
this.load(); setTimeout(()=>this._in.focus(),10); },
|
||||
close(){ if(this._root)this._root.style.display='none'; this._o=false; },
|
||||
load(){ const self=this; const q=(this._in.value||'').toLowerCase();
|
||||
fetch('/board/api/wiki/pages?q='+encodeURIComponent(q),{credentials:'same-origin'})
|
||||
.then(r=>r.json()).then(d=>{
|
||||
if(!self._o)return;
|
||||
self._it=d.pages||[]; self._s=0;
|
||||
let h='<div style="padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase">Pages</div>';
|
||||
self._it.forEach(function(p,idx){
|
||||
h+='<div class="wm-item" data-wpid="'+p.id+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;'+(idx===0?'background:rgba(35,131,226,.15)':'')+'">'
|
||||
+'<span style="width:18px;text-align:center;flex-shrink:0">'+esc(p.icon||'📄')+'</span>'
|
||||
+'<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(p.title)+'</span>'
|
||||
+'<span style="font-size:11px;color:var(--text-dim)">'+esc(p.workspace||'')+'</span></div>';
|
||||
});
|
||||
if(!self._it.length)h+='<div style="padding:12px;color:var(--text-dim);font-size:13px">No page found</div>';
|
||||
self._li.innerHTML=h;
|
||||
}).catch(()=>{}); },
|
||||
_mv(dir){ if(!this._it.length)return;
|
||||
this._s=(this._s+dir+this._it.length)%this._it.length;
|
||||
this._li.querySelectorAll('.wm-item').forEach((el,i)=>{el.style.background=i===this._s?'rgba(35,131,226,.15)':'';});
|
||||
const cur=this._li.querySelectorAll('.wm-item')[this._s]; if(cur&&cur.scrollIntoView)cur.scrollIntoView({block:'nearest'}); },
|
||||
sel(pid){ const E=window.E; if(!E||this._idx<0)return;
|
||||
const b=E.blocks[this._idx]; if(!b)return;
|
||||
// Drop the typed "[[" from the live DOM text, append the token chip.
|
||||
const el=E.getEl(b.id); let text=(el?gtTok(el):b.content)||'';
|
||||
if(text.endsWith('[[')) text=text.slice(0,-2);
|
||||
else text=text.replace(/\[\[[^\]]*$/,'');
|
||||
E._commitBlockText(b, text+'[[fdpage:'+pid+']] ');
|
||||
this.close(); },
|
||||
get isOpen(){return this._o;}};
|
||||
|
||||
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
|
||||
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
|
||||
// → framed result → "Insert below" → applyAIBlocks.
|
||||
@@ -907,6 +1021,9 @@ window.__fdEditorScriptsLoaded = true;
|
||||
iconValue:'',
|
||||
// ── Import ──
|
||||
importOpen:false,importText:'',importFile:null,
|
||||
// ── v5.11.0 wiki-links & v5.12.0 page options ──
|
||||
isLocked:false,lockedBy:null,canEdit:true,fullWidth:false,fontSmall:false,
|
||||
tplSaving:false,
|
||||
// ── DB Templates ──
|
||||
dbTplOpen:false,dbTplLoading:false,dbTemplates:[],
|
||||
// ── Version history ──
|
||||
@@ -933,12 +1050,16 @@ window.__fdEditorScriptsLoaded = true;
|
||||
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);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}}
|
||||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
|
||||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);
|
||||
// v5.12.0: page lock + layout options
|
||||
this.isLocked=!!data.is_locked;this.lockedBy=data.locked_by||null;
|
||||
this.canEdit=data.can_edit!==false;this.fullWidth=!!data.full_width;this.fontSmall=!!data.font_small;}catch(e){}}
|
||||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||||
this.loadInlineDBs();
|
||||
window.E=this;
|
||||
this.insertTable=function(){this.sync();this.pushHistory();var nb={id:genId(),type:'table',content:'',rows:_tblNew(3,3)};this.blocks.push(nb);this.dirty=true;this.autoSave();this.render();_rtSync();setTimeout(function(){_tblFocus(nb.id,0,0);},60);};
|
||||
SM.init();_tblInitDelegate();this.render();
|
||||
this._applyLockMode();this._applyLayout();_wikiResolve();
|
||||
const tEl=document.getElementById('_titleEl');
|
||||
if(tEl){const syncT=()=>{tEl.classList.toggle('empty',!(tEl.textContent||'').trim());};syncT();tEl.addEventListener('input',syncT);tEl.addEventListener('blur',syncT);}
|
||||
if(this.contentFormat==='blocks'&&window.__fdRT&&window.__fdRT.start){setTimeout(()=>{try{window.__fdRT.start(this);}catch(e){}},150);}
|
||||
@@ -954,6 +1075,67 @@ window.__fdEditorScriptsLoaded = true;
|
||||
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);
|
||||
},
|
||||
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){
|
||||
@@ -979,6 +1161,16 @@ window.__fdEditorScriptsLoaded = true;
|
||||
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)};},
|
||||
@@ -1012,9 +1204,9 @@ window.__fdEditorScriptsLoaded = true;
|
||||
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=gt(el)||'';else if(el)b.content=gt(el)||'';
|
||||
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gt(che)||'';});
|
||||
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gt(che)||'';});
|
||||
const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el&&!el.classList.contains('toggle-title'))b.content=gtTok(el)||'';else if(el)b.content=gtTok(el)||'';
|
||||
if(b.type==='toggle'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtTok(che)||'';});
|
||||
if(b.type==='columns'&&b.children)b.children.forEach(function(ch){const che=ct.querySelector(`[data-bid="${ch.id}"]`);if(che)ch.content=gtTok(che)||'';});
|
||||
}},
|
||||
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
|
||||
// Render math blocks with KaTeX
|
||||
@@ -1027,8 +1219,15 @@ window.__fdEditorScriptsLoaded = true;
|
||||
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();
|
||||
},
|
||||
_selApply();
|
||||
var self2=this;
|
||||
setTimeout(function(){
|
||||
if(window._wikiResolve)_wikiResolve();
|
||||
if(!self2.canEdit)self2._applyLockMode();
|
||||
const ub=document.getElementById('_lockUnlockBtn');
|
||||
if(ub&&!ub._wired){ub._wired=true;ub.onclick=function(){window.E.toggleLock();};}
|
||||
},30);
|
||||
},
|
||||
_renderKatex(el,tex){
|
||||
if(!el)return;if(!tex||!tex.trim()){el.innerHTML='<span style="color:var(--text-dim);font-style:italic;">Empty math block</span>';return;}
|
||||
if(window.katex){try{el.innerHTML=window.katex.renderToString(tex,{displayMode:true,throwOnError:false});}catch(e){el.innerHTML='<span style="color:#e5484d;">Invalid LaTeX</span>';}}
|
||||
@@ -1693,7 +1892,8 @@ applyAIBlocks(text){
|
||||
if(e.key==='Escape'){e.preventDefault();window.AIAC.clear();return;}
|
||||
if(e.key==='Enter'||e.key==='Backspace'){window.AIAC.clear();}
|
||||
}
|
||||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen)SM.open(i2,e2);},15);}
|
||||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen&&!WM.isOpen)SM.open(i2,e2);
|
||||
if(e2&&!SM.isOpen&&!WM.isOpen){const t=gtTok(e2)||'';const mm=t.match(/\[\[$/);if(mm){WM.open(i2,e2);}}},15);}
|
||||
if(e.key===' '){
|
||||
if(block.type==='paragraph'&&!(block.content||'').trim()&&!(el.textContent||'').trim()&&!SM.isOpen&&window.AIC){
|
||||
e.preventDefault(); window.AIC.open(idx); return;
|
||||
@@ -1704,7 +1904,7 @@ applyAIBlocks(text){
|
||||
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
|
||||
e.preventDefault();
|
||||
const sp=splitCaret(el);
|
||||
const before=sp?sp.before:gt(el)||'',after=sp?sp.after:'';
|
||||
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();
|
||||
@@ -1859,53 +2059,115 @@ applyAIBlocks(text){
|
||||
btn.style.left=x+'px';btn.style.top=y+'px';
|
||||
},
|
||||
hideCommentBtn(){const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';},
|
||||
// ── @mention autocomplete ──
|
||||
mentionOpen:false,mentionQuery:'',mentionResults:[],mentionEl:null,
|
||||
// ── @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 text=el.textContent||'';const pos=s.getRangeAt(0).startOffset;
|
||||
const before=text.substring(0,pos);
|
||||
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;
|
||||
if(!m){return;}
|
||||
this.mentionQuery=m[1];this.mentionEl=el;
|
||||
const r=s.getRangeAt(0).getBoundingClientRect();
|
||||
const menu=document.getElementById('_mentionMenu');
|
||||
menu.style.display='block';
|
||||
menu.style.top=Math.max(4,(r.bottom+4>window.innerHeight-280)?r.top-284:r.bottom+4)+'px';
|
||||
menu.style.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.loadMentionUsers();
|
||||
this.mentionOpen=true;this.loadMentionResults();
|
||||
},
|
||||
async loadMentionUsers(){
|
||||
async loadMentionResults(){
|
||||
const q=this.mentionQuery||'';
|
||||
try{const r=await fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'});const d=await r.json();this.mentionResults=d.users||[];this.renderMentionMenu();}catch(e){this.mentionResults=[];}
|
||||
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;
|
||||
(this.mentionResults||[]).forEach(function(u,idx){
|
||||
h+='<div class="mention-item" data-login="'+u.login+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
|
||||
+'<div style="width:24px;height:24px;border-radius:50%;background:'+(u.avatar_color||'#3A3A3A')+';color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;">'
|
||||
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">'+u.login+'</div><div style="font-size:11px;color:var(--text-dim,#999)">'+(u.full_name||'')+'</div></div></div>';
|
||||
+'</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(!h)h='<div style="padding:10px;color:var(--text-dim,#999);font-size:13px;">No users found</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(){self.insertMention(it.getAttribute('data-login'));});
|
||||
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){
|
||||
const el=this.mentionEl;if(!el)return;
|
||||
const s=window.getSelection();if(s.rangeCount){s.deleteFromDocument();}
|
||||
const text=el.textContent||'';const pos=s.rangeCount? (()=>{const r=s.getRangeAt(0);return r.startOffset;} )():text.length;
|
||||
const before=text.substring(0,pos);
|
||||
const at=before.lastIndexOf('@');
|
||||
const full=before.substring(0,at)+'@'+login+' ';
|
||||
const after=text.substring(pos);
|
||||
el.textContent=full+after;
|
||||
const ns=window.getSelection();const nr=document.createRange();nr.selectNodeContents(el);nr.collapse(false);ns.removeAllRanges();ns.addRange(nr);
|
||||
this.closeMention();this.dirty=true;this.autoSave();
|
||||
this.notifyNewMentions(full);
|
||||
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){
|
||||
|
||||
Reference in New Issue
Block a user