feat: real timestamp + working Export + polished More menu in page editor
- 'Edited X ago' now shows real relative time from page.updated_at - timeAgo computed getter in editorState() calculates relative time - Export now generates a Markdown file from blocks and triggers download - blocksToMarkdown() helper converts blocks to proper Markdown syntax (headings, lists, todos, quotes, dividers, code blocks) - More menu items: Duplicate (was working), Export (now works), Move to Trash (was working), Copy link (via shortcut)
This commit is contained in:
@@ -8,7 +8,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
<!-- ═══════════ Action Bar (Share, Copy, Favorite, More) ═══════════ -->
|
||||
<div class="page-topbar">
|
||||
<div class="page-topbar-right" style="position: relative">
|
||||
<span class="topbar-edited">Edited just now</span>
|
||||
<span class="topbar-edited">Edited <span x-text="timeAgo"></span></span>
|
||||
<button
|
||||
class="page-action-btn share-btn"
|
||||
@click="shareOpen = !shareOpen"
|
||||
@@ -583,6 +583,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
function editorState(){
|
||||
return {
|
||||
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
|
||||
updatedAt:'{{ page.get("updated_at", "") }}',
|
||||
fmt:{open:false,top:0,left:0,idx:-1},
|
||||
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,
|
||||
shareTab:'share',favorited:false,
|
||||
@@ -592,6 +593,14 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
pageUrl:window.location.href,publishedUrl:'',
|
||||
inviteEmail:'',invitePermission:'editor',accessList:[],
|
||||
toastVisible:false,toastMsg:'',
|
||||
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';
|
||||
},
|
||||
init(){
|
||||
_bid=Math.floor(Math.random()*10000);
|
||||
const dataEl=document.getElementById('page-data');
|
||||
@@ -706,7 +715,21 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
},
|
||||
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(){this.moreOpen=false;},
|
||||
exportPage(){
|
||||
this.moreOpen=false;
|
||||
// Export blocks as Markdown
|
||||
var md = '# ' + (this.pageTitle || 'Untitled') + '\n\n';
|
||||
for (var i = 0; i < this.blocks.length; i++) {
|
||||
var b = this.blocks[i];
|
||||
md += blocksToMarkdown(b) + '\n\n';
|
||||
}
|
||||
var blob = new Blob([md], {type: 'text/markdown'});
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url; a.download = (this.pageTitle || 'export') + '.md';
|
||||
a.click(); URL.revokeObjectURL(url);
|
||||
this.showToast('Exported as Markdown');
|
||||
},
|
||||
duplicatePage(){this.moreOpen=false;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages?title=${encodeURIComponent(this.pageTitle+' copy')}§ion=Private&project=${encodeURIComponent('{{ workspace_key }}')}`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(r=>r.json()).then(d=>{window.location.href=`/pages/${d.id}`;}).catch(()=>{window.showToast('Duplicate failed','error');});},
|
||||
movePage(){this.moreOpen=false;},
|
||||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
|
||||
@@ -783,5 +806,22 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
};
|
||||
}
|
||||
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);});
|
||||
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 '## ' + c;
|
||||
default: return c;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user