feat(collab): v4.9.0 Collaboration - commentaires inline, mentions @, notifications in-app + email
This commit is contained in:
@@ -298,6 +298,11 @@
|
||||
pageUrl:window.location.href,publishedUrl:'',
|
||||
inviteEmail:'',invitePermission:'editor',accessList:[],
|
||||
toastVisible:false,toastMsg:'',
|
||||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||||
commentsOpen:false,commentDraft:'',comments:[],commentCount:0,
|
||||
currentUserId:{{ user.get('id', 0) if user else 0 }},
|
||||
_commentSel:{range:null,bid:null,start:0,end:0},
|
||||
_notifiedMentions:{},
|
||||
get timeAgo() {
|
||||
if (!this.updatedAt) return 'just now';
|
||||
var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000);
|
||||
@@ -679,6 +684,110 @@
|
||||
showFmt(idx){const s=window.getSelection();if(!s.rangeCount||s.isCollapsed){this.fmt.open=false;return;}const r=s.getRangeAt(0).getBoundingClientRect();this.fmt.open=true;this.fmt.idx=idx;this.fmt.top=Math.max(r.top-44,0);this.fmt.left=Math.min(r.left+r.width/2-120,window.innerWidth-260);this._sel={range:s.getRangeAt(0).cloneRange(),idx};},
|
||||
fmtApply(f){this.fmt.open=false;if(!this._sel)return;const s=window.getSelection();s.removeAllRanges();s.addRange(this._sel.range);document.execCommand(f==='strikeThrough'?'strikeThrough':f);this._sel=null;this.dirty=true;this.autoSave();},
|
||||
fmtLink(){this.fmt.open=false;const u=prompt('URL:');if(u){document.execCommand('createLink',false,u);this.dirty=true;this.autoSave();}},
|
||||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||||
toggleComments(){this.commentsOpen=!this.commentsOpen;if(this.commentsOpen)this.loadComments();},
|
||||
async loadComments(){try{const r=await fetch('/api/pages/'+this.pid+'/comments',{credentials:'same-origin'});const d=await r.json();this.comments=d.comments||[];this.commentCount=this.comments.length;}catch(e){this.comments=[];}},
|
||||
fmtTime(s){if(!s)return '';const t=new Date((String(s).includes('T')||String(s).includes('Z'))?s:(s+'Z'));if(isNaN(t.getTime()))t=new Date(s);const diff=Math.floor((Date.now()-t.getTime())/1000);if(diff<60)return 'just now';if(diff<3600)return Math.floor(diff/60)+'m ago';if(diff<86400)return Math.floor(diff/3600)+'h ago';return Math.floor(diff/86400)+'d ago';},
|
||||
csrfTok(){return (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'';},
|
||||
async addPageComment(){
|
||||
const text=(this.commentDraft||'').trim();if(!text)return;
|
||||
const self=this;const sel=this._commentSel;
|
||||
try{
|
||||
const r=await fetch('/api/pages/'+this.pid+'/comments',{method:'POST',credentials:'same-origin',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||||
body:JSON.stringify({body:text,anchor_block_id:sel.bid||null,anchor_start:sel.bid?sel.start:null,anchor_end:sel.bid?sel.end:null})});
|
||||
const d=await r.json();
|
||||
if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};this.hideCommentBtn();await this.loadComments();this.showToast('Comment added');}
|
||||
else{this.showToast(d.detail||'Failed to add comment');}
|
||||
}catch(e){this.showToast('Failed to add comment');}
|
||||
},
|
||||
async resolveComment(id){try{await fetch('/api/comments/'+id,{method:'PUT',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},body:JSON.stringify({resolved:true})});await this.loadComments();}catch(e){}},
|
||||
async deleteComment(id){if(!confirm('Delete this comment?'))return;try{await fetch('/api/comments/'+id,{method:'DELETE',credentials:'same-origin',headers:{'X-CSRF-Token':this.csrfTok()}});await this.loadComments();}catch(e){}},
|
||||
commentOnSelection(){
|
||||
const sel=this._commentSel;if(!sel.range)return;
|
||||
const bid=sel.bid||'';this.commentsOpen=true;this.commentDraft='';
|
||||
const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';
|
||||
this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();});
|
||||
},
|
||||
showCommentBtn(){
|
||||
const s=window.getSelection();if(!s||s.isCollapsed)return;
|
||||
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(s.anchorNode))return;
|
||||
const bel=s.anchorNode.parentElement? s.anchorNode.parentElement.closest('[data-bid]'):null;
|
||||
if(!bel)return;
|
||||
const bid=bel.dataset.bid;
|
||||
const range=s.getRangeAt(0).cloneRange();range.selectNodeContents(bel);range.setEnd(s.getRangeAt(0).startContainer,s.getRangeAt(0).startOffset);
|
||||
const start=range.toString().length;
|
||||
const end=start+s.toString().length;
|
||||
if(end<=start)return;
|
||||
this._commentSel={range:s.getRangeAt(0).cloneRange(),bid:bid,start:start,end:end};
|
||||
const r=s.getRangeAt(0).getBoundingClientRect();
|
||||
const btn=document.getElementById('_commentSelBtn');if(!btn)return;
|
||||
btn.style.display='block';
|
||||
const bw=btn.offsetWidth||110,bh=btn.offsetHeight||34;
|
||||
let x=r.left+r.width/2-bw/2,y=r.top-bh-8;
|
||||
x=Math.min(Math.max(8,x),window.innerWidth-bw-8);if(y<8)y=r.bottom+8;
|
||||
btn.style.left=x+'px';btn.style.top=y+'px';
|
||||
},
|
||||
hideCommentBtn(){const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';},
|
||||
// ── @mention autocomplete ──
|
||||
mentionOpen:false,mentionQuery:'',mentionResults:[],mentionEl:null,
|
||||
openMention(el){
|
||||
const s=window.getSelection();if(!s.rangeCount)return;
|
||||
const text=el.textContent||'';const pos=s.getRangeAt(0).startOffset;
|
||||
const before=text.substring(0,pos);
|
||||
const m=before.match(/@([\w\-\.]*)$/);
|
||||
if(!m)return;
|
||||
this.mentionQuery=m[1];this.mentionEl=el;
|
||||
const r=s.getRangeAt(0).getBoundingClientRect();
|
||||
const menu=document.getElementById('_mentionMenu');
|
||||
menu.style.display='block';
|
||||
menu.style.top=Math.max(4,(r.bottom+4>window.innerHeight-280)?r.top-284:r.bottom+4)+'px';
|
||||
menu.style.left=Math.min(r.left,window.innerWidth-260)+'px';
|
||||
this.mentionOpen=true;this.loadMentionUsers();
|
||||
},
|
||||
async loadMentionUsers(){
|
||||
const q=this.mentionQuery||'';
|
||||
try{const r=await fetch('/api/notifications/users/search?q='+encodeURIComponent(q),{credentials:'same-origin'});const d=await r.json();this.mentionResults=d.users||[];this.renderMentionMenu();}catch(e){this.mentionResults=[];}
|
||||
},
|
||||
renderMentionMenu(){
|
||||
const menu=document.getElementById('_mentionMenu');if(!menu)return;
|
||||
let h='';const self=this;
|
||||
(this.mentionResults||[]).forEach(function(u,idx){
|
||||
h+='<div class="mention-item" data-login="'+u.login+'" style="display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:13px;color:var(--text);cursor:pointer;border-radius:6px;">'
|
||||
+'<div style="width:24px;height:24px;border-radius:50%;background:'+(u.avatar_color||'#3A3A3A')+';color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;">'
|
||||
+((u.avatar_url)?'<img src="'+u.avatar_url+'" style="width:24px;height:24px;border-radius:50%;object-fit:cover;">':((u.full_name||u.login||'?').charAt(0).toUpperCase()))
|
||||
+'</div><div><div style="font-weight:500">'+u.login+'</div><div style="font-size:11px;color:var(--text-dim,#999)">'+(u.full_name||'')+'</div></div></div>';
|
||||
});
|
||||
if(!h)h='<div style="padding:10px;color:var(--text-dim,#999);font-size:13px;">No users found</div>';
|
||||
menu.innerHTML=h;
|
||||
menu.querySelectorAll('.mention-item').forEach(function(it){
|
||||
it.addEventListener('click',function(){self.insertMention(it.getAttribute('data-login'));});
|
||||
});
|
||||
},
|
||||
insertMention(login){
|
||||
const el=this.mentionEl;if(!el)return;
|
||||
const s=window.getSelection();if(s.rangeCount){s.deleteFromDocument();}
|
||||
const text=el.textContent||'';const pos=s.rangeCount? (()=>{const r=s.getRangeAt(0);return r.startOffset;} )():text.length;
|
||||
const before=text.substring(0,pos);
|
||||
const at=before.lastIndexOf('@');
|
||||
const full=before.substring(0,at)+'@'+login+' ';
|
||||
const after=text.substring(pos);
|
||||
el.textContent=full+after;
|
||||
const ns=window.getSelection();const nr=document.createRange();nr.selectNodeContents(el);nr.collapse(false);ns.removeAllRanges();ns.addRange(nr);
|
||||
this.closeMention();this.dirty=true;this.autoSave();
|
||||
this.notifyNewMentions(full);
|
||||
},
|
||||
closeMention(){const menu=document.getElementById('_mentionMenu');if(menu)menu.style.display='none';this.mentionOpen=false;this.mentionEl=null;},
|
||||
notifyNewMentions(text){
|
||||
const self=this;
|
||||
const handles=[];const re=/@([\w\-\.]+)/g;let mm;
|
||||
while((mm=re.exec(text)))handles.push(mm[1].toLowerCase());
|
||||
const fresh=handles.filter(function(h){return !self._notifiedMentions[h];});
|
||||
if(!fresh.length)return;
|
||||
fetch('/api/pages/'+this.pid+'/mentions',{method:'POST',credentials:'same-origin',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()},
|
||||
body:JSON.stringify({text:'@'+fresh.join(' @')})}).catch(function(){});
|
||||
},
|
||||
pastePlain(e){e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();},
|
||||
autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
|
||||
save(cb){
|
||||
@@ -711,7 +820,29 @@
|
||||
}return bl;},
|
||||
};
|
||||
}
|
||||
document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}},80);});
|
||||
document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);d.showCommentBtn();}else{d.hideCommentBtn();}},80);});
|
||||
document.addEventListener('mousedown',()=>{setTimeout(()=>{const ed=document.querySelector('.page-editor-wrapper');if(ed?.__x)ed.__x.$data.hideCommentBtn();},10);});
|
||||
document.addEventListener('keyup',(e)=>{
|
||||
const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;
|
||||
const d=ed.__x.$data;if(!d.mentionOpen){if(e.key!=='@')return;}
|
||||
const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return;
|
||||
const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(el))return;
|
||||
if(d.mentionOpen){
|
||||
if(e.key==='Escape'){d.closeMention();return;}
|
||||
if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const items=document.querySelectorAll('#_mentionMenu .mention-item');if(!items.length)return;let idx=[].indexOf.call(items,document.querySelector('#_mentionMenu .mention-item.hover'));idx+= (e.key==='ArrowDown'?1:-1);if(idx<0)idx=items.length-1;if(idx>=items.length)idx=0;items.forEach(function(x){x.classList.remove('hover');});items[idx].classList.add('hover');return;}
|
||||
if(e.key==='Enter'){e.preventDefault();const hover=document.querySelector('#_mentionMenu .mention-item.hover')||document.querySelector('#_mentionMenu .mention-item');if(hover)hover.click();return;}
|
||||
}
|
||||
d.openMention(el);
|
||||
},true);
|
||||
document.addEventListener('click',(e)=>{
|
||||
const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;
|
||||
const d=ed.__x.$data;if(!d.mentionOpen)return;
|
||||
const menu=document.getElementById('_mentionMenu');
|
||||
if(menu&&!menu.contains(e.target))d.closeMention();
|
||||
});
|
||||
var fdStyle=document.createElement('style');
|
||||
fdStyle.textContent='#_mentionMenu .mention-item:hover,#_mentionMenu .mention-item.hover{background:rgba(35,131,226,.15);}';
|
||||
document.head.appendChild(fdStyle);
|
||||
function blocksToMarkdown(b) {
|
||||
var c = b.content || '';
|
||||
switch(b.type) {
|
||||
|
||||
Reference in New Issue
Block a user