modular editor: extract original scripts into _page_editor_scripts.html
Now 3 shared modules (zero code duplication): - _page_editor_content.html — editor HTML (title block, slash menu, toolbar etc) - _page_editor_scripts.html — 100% original editorState() JS (CMDS, render, save, slash, database, share/publish, favorites, formatting, etc) - Used by page_editor.html (full page) and page_editor_embed.html (peek iframe) Embed page_editor_embed.html now has the exact same editor as full page. One change to _page_editor_scripts.html updates both views.
This commit is contained in:
@@ -510,4 +510,4 @@
|
||||
></span>
|
||||
<span class="statusbar-right" x-text="blocks.length+' blocks'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
<script type="application/json" id="page-data">
|
||||
{{ page_data | tojson }}
|
||||
</script>
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
const CMDS=[{name:'BASIC',items:[
|
||||
{id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
|
||||
{id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'},
|
||||
{id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'},
|
||||
{id:'callout',name:'Callout',icon:'lightbulb'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'},
|
||||
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'image'}]}];
|
||||
|
||||
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
|
||||
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
|
||||
function ce(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(false);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||||
function cs(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(true);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||||
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
|
||||
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],
|
||||
init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML='';
|
||||
const inp=document.createElement('input');inp.type='text';inp.placeholder='Filter...';
|
||||
inp.style.cssText='width:100%;padding:6px 10px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
|
||||
inp.oninput=()=>SM.flt();inp.onkeydown=ev=>SM._kd(ev);
|
||||
const hdr=document.createElement('div');hdr.style.cssText='padding:8px;border-bottom:1px solid var(--border)';hdr.appendChild(inp);
|
||||
const list=document.createElement('div');list.style.cssText='overflow-y:auto;max-height:340px';
|
||||
list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}});
|
||||
e.appendChild(hdr);e.appendChild(list);
|
||||
SM._e=e;SM._in=inp;SM._li=list;
|
||||
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target))SM.close();});
|
||||
},
|
||||
open(i,el){if(!SM._e)SM.init();SM.close();SM._i=i;SM._s=0;SM._in.value='';SM.flt();
|
||||
const r=el.getBoundingClientRect();SM._e.style.display='block';
|
||||
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
|
||||
SM._e.style.left=Math.min(r.left,window.innerWidth-324)+'px';
|
||||
setTimeout(()=>SM._in.focus(),10);SM._o=true;},
|
||||
close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');},
|
||||
flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h='';
|
||||
for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue;
|
||||
h+=`<div style="padding:6px 12px 4px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px">${g.name}</div>`;
|
||||
items.forEach(c=>{SM._it.push(c);
|
||||
h+=`<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:14px;color:var(--text-primary);cursor:pointer">
|
||||
<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
|
||||
<span style="flex:1;font-weight:500">${c.name}</span></div>`;});}
|
||||
SM._li.innerHTML=h;},
|
||||
_kd(e){if(e.key==='ArrowDown'){e.preventDefault();SM._s=Math.min(SM._s+1,SM._it.length-1);SM._rs();}
|
||||
else if(e.key==='ArrowUp'){e.preventDefault();SM._s=Math.max(SM._s-1,0);SM._rs();}
|
||||
else if(e.key==='Enter'){e.preventDefault();const c=SM._it[SM._s];if(c)SM.sel(c.id);}
|
||||
else if(e.key==='Escape'){e.preventDefault();SM.close();}},
|
||||
_rs(){const its=SM._li.querySelectorAll('.slash-item');its.forEach((el,i)=>{el.classList.toggle('selected',i===SM._s);});},
|
||||
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
|
||||
get isOpen(){return SM._o;}};
|
||||
|
||||
function renderBlock(b,idx){
|
||||
if(b.type==='divider')return `<div class="block-wrapper"><hr class="block-divider"></div>`;
|
||||
if(b.type==='image')return `<div class="block-wrapper"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'"></div></div>`;
|
||||
if(b.type==='embed'){
|
||||
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
|
||||
if(b.embed_type==='pdf')return `<div class="block-wrapper"><div class="block-content block-embed" style="padding:0;"><iframe src="${b.src||''}" style="width:100%;height:80vh;border:none;border-radius:8px;" onerror="this.style.display='none';this.nextElementSibling.style.display='block';"></iframe><div style="display:none;text-align:center;padding:32px;color:var(--text-secondary);"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p style="margin-top:8px;">Failed to load PDF</p><a href="${b.src||''}" target="_blank" style="color:var(--accent);">Open in new tab →</a></div></div></div>`;
|
||||
if(b.embed_type==='download'){
|
||||
var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB';
|
||||
return `<div class="block-wrapper"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;"><div style="font-size:48px;margin-bottom:12px;">{{ fd_icon("paperclip",14) }}</div><div style="font-size:16px;font-weight:500;color:var(--text-primary);margin-bottom:4px;">${esc(b.file_name||'File')}</div><div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">${szStr} · ${esc(b.file_mime||'')}</div><a href="${b.src||''}" download style="display:inline-block;padding:8px 20px;background:var(--accent);color:#fff;text-decoration:none;border-radius:8px;font-size:13px;font-weight:500;">⬇ Download</a></div></div>`;
|
||||
}
|
||||
}
|
||||
const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':'Press \'/\' for commands...';
|
||||
let inner='';
|
||||
if(b.type==='to_do')inner=`<input type="checkbox" class="todo-checkbox" ${b.checked?'checked':''} onchange="E._doToggle('${b.id}',this.checked)"><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
else if(b.type==='toggle')inner=`<button class="toggle-chevron ${b.expanded?'open':''}" onclick="E._doToggleExpand('${b.id}')">▶</button><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
else if(b.type==='code')inner=`<div class="code-lang-label">${b.language||'Plain Text'}</div><pre><code data-bid="${b.id}" contenteditable="true" spellcheck="false">${esc(b.content)}</code></pre>`;
|
||||
else if(b.type==='callout')inner=`<span class="callout-icon">${b.icon||'lightbulb'}</span><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
else inner=`<div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div';
|
||||
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
|
||||
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}"`:'';
|
||||
return `<div class="block-wrapper">
|
||||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||||
<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>
|
||||
<${tag}${style} class="${cls}">${inner}</${tag}>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
window.E=null;
|
||||
function editorState(){
|
||||
return {
|
||||
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
|
||||
updatedAt:'{{ page.get("updated_at", "") }}',
|
||||
fmt:{open:false,top:0,left:0,idx:-1},
|
||||
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,
|
||||
moveOpen:false,moveWorkspaces:[],moveLoading:false,
|
||||
activityOpen:false,pageCreated:'{{ page.get("created_at", "") }}',
|
||||
shareTab:'share',favorited:false,
|
||||
pagePublished:{{ page_published | tojson }},
|
||||
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
|
||||
pubAllowEdit:false,pubAllowComments:true,
|
||||
pageUrl:window.location.href,publishedUrl:'',
|
||||
inviteEmail:'',invitePermission:'editor',accessList:[],
|
||||
toastVisible:false,toastMsg:'',
|
||||
get timeAgo() {
|
||||
if (!this.updatedAt) return 'just now';
|
||||
var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000);
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
},
|
||||
formatDate(d) {
|
||||
if (!d) return '';
|
||||
var dt = new Date(d + 'Z');
|
||||
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
|
||||
' at ' + dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
},
|
||||
init(){
|
||||
_bid=Math.floor(Math.random()*10000);
|
||||
const dataEl=document.getElementById('page-data');
|
||||
if(dataEl){try{const data=JSON.parse(dataEl.textContent);this.pid=data.id;this.pageTitle=data.title||'';this.favorited=data.favorited||false;const fmt=data.content_format||'blocks';const raw=data.content||'';
|
||||
if(fmt==='file'){this.fileData=data;this.loadFileContent(data);}
|
||||
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}}
|
||||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
|
||||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||||
this.loadInlineDBs();
|
||||
window.E=this;SM.init();this.render();
|
||||
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
|
||||
ct.addEventListener('keydown',e=>this.onKd(e));
|
||||
ct.addEventListener('paste',e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();});
|
||||
if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{if(evt.oldIndex===evt.newIndex)return;const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);this.dirty=true;this.autoSave();this.render();}});
|
||||
},100);
|
||||
},
|
||||
fileData:null,
|
||||
async loadFileContent(data){
|
||||
const url=data.file_url||'';const mime=data.file_mime||'';const name=data.file_name||'';const self=this;
|
||||
if(!url){this.blocks=[this.mkB('paragraph','File not available')];this.render();return;}
|
||||
const ext=name.toLowerCase().split('.').pop();
|
||||
if(mime.startsWith('image/')||['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext)){
|
||||
this.blocks=[this.mkB('image','',{src:url,alt:name})];this.render();return;
|
||||
}
|
||||
if(mime==='application/pdf'||ext==='pdf'){
|
||||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return;
|
||||
}
|
||||
const codeExts=['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
|
||||
const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'};
|
||||
if(mime.startsWith('text/')||codeExts.includes(ext)){
|
||||
try{const r=await fetch(url);const text=await r.text();
|
||||
if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);}
|
||||
else{this.blocks=[this.mkB('code',text,{language:langMap[ext]||'Plain Text'})];}
|
||||
this.render();
|
||||
}catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();}
|
||||
return;
|
||||
}
|
||||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'download',file_name:name,file_size:data.file_size||0,file_mime:mime})];this.render();
|
||||
},
|
||||
mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;return b;},
|
||||
getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;},
|
||||
getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);},
|
||||
getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};},
|
||||
focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}},
|
||||
onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];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==='divider'||b.type==='image'||b.type==='embed')continue;const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el)b.content=el.textContent||'';}},
|
||||
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
|
||||
ct.querySelectorAll('.block-wrapper').forEach(w=>{w.onmouseenter=()=>{w.querySelector('.block-handle')?.classList.add('visible');w.querySelector('.block-actions')?.classList.add('visible');};w.onmouseleave=()=>{w.querySelector('.block-handle')?.classList.remove('visible');w.querySelector('.block-actions')?.classList.remove('visible');};});
|
||||
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
|
||||
},
|
||||
replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();b.type=type;b.content='';if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
|
||||
addAt(idx,type,content){this.sync();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;},
|
||||
addAfter(idx,type){return this.addAt(idx+1,type);},
|
||||
removeBlock(idx){this.sync();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
|
||||
|
||||
toggleFavorite(){
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
const m=this.favorited?'DELETE':'POST';
|
||||
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}})
|
||||
.then(r=>r.json()).then(()=>{this.favorited=!this.favorited;this.showToast(this.favorited?'Added to favorites':'Removed from favorites');
|
||||
if(window.appState&&window.appState.refreshFavorites)window.appState.refreshFavorites();
|
||||
}).catch(()=>{this.showToast('Failed to toggle favorite');});
|
||||
},
|
||||
toggleActivityOpen(){
|
||||
if(!this.activityOpen){this.activityOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||||
else{this.activityOpen=false;}
|
||||
},
|
||||
toggleShareOpen(){
|
||||
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||||
else{this.shareOpen=false;}
|
||||
},
|
||||
toggleMoreOpen(){
|
||||
if(!this.moreOpen){this.moreOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||||
else{this.moreOpen=false;}
|
||||
},
|
||||
_justOpened:false,
|
||||
togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();},
|
||||
saveShare(){
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).catch(()=>{});
|
||||
},
|
||||
async copyPageLink(){
|
||||
console.log('copyPageLink invoked');
|
||||
const url = this.pageUrl;
|
||||
const fallback = () => {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
};
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
this.showToast('Link copied to clipboard');
|
||||
} catch(e) { fallback(); this.showToast('Link copied to clipboard'); }
|
||||
},
|
||||
showToast(msg){
|
||||
this.toastMsg = msg; this.toastVisible = true;
|
||||
clearTimeout(this._toastTimer);
|
||||
this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500);
|
||||
},
|
||||
publishPage() {
|
||||
var self = this;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/publish', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.is_published) {
|
||||
self.pagePublished = true;
|
||||
self.publishedUrl = window.location.origin + '/p/' + d.publish_slug;
|
||||
self.showToast('Published to web — ' + self.publishedUrl);
|
||||
} else {
|
||||
self.showToast(d.detail || 'Publish failed');
|
||||
}
|
||||
}).catch(function(){ self.showToast('Publish failed'); });
|
||||
},
|
||||
unpublishPage() {
|
||||
var self = this;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/publish', {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (!d.is_published) {
|
||||
self.pagePublished = false;
|
||||
self.publishedUrl = '';
|
||||
self.showToast('Unpublished');
|
||||
} else {
|
||||
self.showToast(d.detail || 'Unpublish failed');
|
||||
}
|
||||
}).catch(function(){ self.showToast('Unpublish failed'); });
|
||||
},
|
||||
shareInvite() {
|
||||
var self = this;
|
||||
if (!this.inviteEmail.trim()) return;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/share', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||||
body: JSON.stringify({ email: this.inviteEmail.trim(), permission: this.invitePermission })
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.status === 'shared') { self.inviteEmail = ''; self.showToast('Invitation sent'); self.loadShares(); }
|
||||
else { self.showToast(d.detail || 'Share failed'); }
|
||||
}).catch(function(){ self.showToast('Share failed'); });
|
||||
},
|
||||
loadShares() {
|
||||
var self = this;
|
||||
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
|
||||
self.accessList = d.shares || [];
|
||||
}).catch(function(){ self.accessList = []; });
|
||||
},
|
||||
removeShare(sid) {
|
||||
var self = this;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/share/' + sid, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.status === 'removed') { self.showToast('Share removed'); self.loadShares(); }
|
||||
else { self.showToast(d.detail || 'Remove failed'); }
|
||||
}).catch(function(){ self.showToast('Remove failed'); });
|
||||
},
|
||||
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
|
||||
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
|
||||
exportPage(){
|
||||
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;
|
||||
this.moveOpen = true;
|
||||
this.moveLoading = true;
|
||||
var self = this;
|
||||
fetch('/api/workspaces')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
self.moveWorkspaces = d.workspaces || [];
|
||||
self.moveLoading = false;
|
||||
})
|
||||
.catch(function(){ self.moveLoading = false; });
|
||||
},
|
||||
doMove(wsId) {
|
||||
this.moveOpen = false;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
var self = this;
|
||||
fetch('/api/pages/' + this.pid + '/move', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||||
body: JSON.stringify({ workspace_id: wsId })
|
||||
}).then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
if (d.status === 'ok') {
|
||||
self.showToast('Moved to workspace');
|
||||
setTimeout(function(){ window.location.href = '/local-workspace'; }, 800);
|
||||
} else {
|
||||
self.showToast(d.detail || 'Move failed');
|
||||
}
|
||||
})
|
||||
.catch(function(){ self.showToast('Move failed'); });
|
||||
},
|
||||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
|
||||
|
||||
// ── v4.5.0+: Get Started actions ──
|
||||
inlineDBs:[],
|
||||
async createDatabase(){
|
||||
const name=prompt('Database name:','New Database');
|
||||
if(!name||!name.trim())return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
const r=await fetch('/db/inline/api',{
|
||||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||||
body:JSON.stringify({name:name.trim(),parent_page_id:this.pid})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'table'});
|
||||
this.showToast('Database created — '+d.name);
|
||||
this.saveDBsToPage();
|
||||
this.renderInlineDBs();
|
||||
}else{
|
||||
this.showToast('Failed to create database');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
},
|
||||
async createForm(){
|
||||
const name=prompt('Form name:','New Form');
|
||||
if(!name||!name.trim())return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
const r=await fetch('/db/inline/api',{
|
||||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||||
body:JSON.stringify({name:name.trim(),parent_page_id:this.pid})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'});
|
||||
this.showToast('Form created — '+d.name);
|
||||
this.saveDBsToPage();
|
||||
this.renderInlineDBs();
|
||||
}else{
|
||||
this.showToast('Failed to create form');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
},
|
||||
saveDBsToPage(){
|
||||
// Store inline DB refs in the page's blocks as a special embed block
|
||||
if(this.inlineDBs.length===0)return;
|
||||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||||
if(existing.length===0){
|
||||
this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs}));
|
||||
}else{
|
||||
existing.forEach(b=>{b.dbs=this.inlineDBs;});
|
||||
}
|
||||
this.render();
|
||||
this.dirty=true;this.autoSave();
|
||||
},
|
||||
renderInlineDBs(){
|
||||
// Re-render with inline DB blocks
|
||||
this.render();
|
||||
},
|
||||
loadInlineDBs(){
|
||||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||||
if(existing.length>0){
|
||||
this.inlineDBs=existing[0].dbs||[];
|
||||
}
|
||||
},
|
||||
|
||||
onKd(e){
|
||||
const ab=this.getActiveBlock();if(!ab)return;
|
||||
const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return;
|
||||
if(SM.isOpen){
|
||||
if(e.key==='Escape'){e.preventDefault();SM.close();return;}
|
||||
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();SM._in.dispatchEvent(new KeyboardEvent('keydown',{key:e.key,bubbles:true}));return;}
|
||||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||||
return;
|
||||
}
|
||||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen)SM.open(i2,e2);},15);}
|
||||
if(e.key===' '){const i2=idx;setTimeout(()=>this.checkMd(i2),15);}
|
||||
if(e.key==='Enter'){
|
||||
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
|
||||
e.preventDefault();
|
||||
const text=el.textContent||'',pos=cp(el),before=text.substring(0,pos),after=text.substring(pos);
|
||||
if(!before.trim()&&!after.trim()){
|
||||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||||
this.sync();this.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();
|
||||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();ce(ne);}},60);
|
||||
this.dirty=true;this.autoSave();return;
|
||||
}
|
||||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||||
this.sync();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();
|
||||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();cs(ne);}},60);
|
||||
this.dirty=true;this.autoSave();return;
|
||||
}
|
||||
if(e.key==='ArrowUp'){if(cp(el)===0&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
|
||||
if(e.key==='ArrowDown'){if(cp(el)>=(el.textContent||'').length&&idx<this.blocks.length-1){e.preventDefault();const next=this.getEl(this.blocks[idx+1]?.id);if(next){next.focus();cs(next);}}return;}
|
||||
if(e.key==='Backspace'){if(!(el.textContent||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();block.type='paragraph';block.content='';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
|
||||
if(e.key===' '){const b2=bid;setTimeout(()=>this.checkMd(b2),15);}
|
||||
if((e.ctrlKey||e.metaKey)&&!e.altKey){const m={b:'bold',i:'italic',u:'underline'};if(m[e.key]){e.preventDefault();document.execCommand(m[e.key]);this.dirty=true;this.autoSave();}}
|
||||
if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');}
|
||||
this.dirty=true;this.autoSave();
|
||||
},
|
||||
checkMd(idx){
|
||||
if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||'';
|
||||
const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'};
|
||||
this.sync();
|
||||
for(const[pfx,type] of Object.entries(sc)){if(text===pfx||text.startsWith(pfx)){block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}}
|
||||
if(text==='---'){block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
|
||||
if(text==='```'){block.type='code';block.content='';block.language='Plain Text';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
|
||||
},
|
||||
applySlash(id){
|
||||
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
|
||||
const block=this.blocks[idx];this.sync();block.content='';
|
||||
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
|
||||
else{block.type=id;if(id==='toggle')block.expanded=true;if(id==='to_do')block.checked=false;if(id==='code')block.language='Plain Text';}
|
||||
this.render();this.dirty=true;this.autoSave();
|
||||
setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);
|
||||
},
|
||||
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].checked=v;this.dirty=true;this.autoSave();}},
|
||||
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].expanded=!this.blocks[idx].expanded;this.dirty=true;this.autoSave();}},
|
||||
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();}},
|
||||
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(){
|
||||
if(this.fileData)return;
|
||||
if(this.saving)return;
|
||||
this.sync();
|
||||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'New page';
|
||||
this.saving=true;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
|
||||
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
|
||||
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
|
||||
}).catch(()=>{this.saving=false;});
|
||||
},
|
||||
md2b(md){const bl=[];let cb=null;for(const line of md.split('\n')){if(cb){if(line.trim()==='```'){bl.push(cb);cb=null;}else cb.content+=(cb.content?'\n':'')+line;continue;}const t=line.trim();if(!t)continue;if(t.startsWith('```')){cb=this.mkB('code','',{language:t.substring(3).trim()||'Plain Text'});continue;}if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));else bl.push(this.mkB('paragraph',t));}if(cb)bl.push(cb);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);});
|
||||
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>
|
||||
@@ -5,486 +5,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()">' ~ fd_icon("lock",14) ~ ' Share ▾</button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
|
||||
{% include '_header.html' %}
|
||||
{% endblock %} {% block content %}
|
||||
{% include '_page_editor_content.html' %}
|
||||
{% include "_page_editor_content.html" %}
|
||||
{% endblock %} {% block scripts %}
|
||||
<script type="application/json" id="page-data">
|
||||
{{ page_data | tojson }}
|
||||
</script>
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
const CMDS=[{name:'BASIC',items:[
|
||||
{id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
|
||||
{id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'},
|
||||
{id:'to_do',name:'To-do',icon:'☐'},{id:'toggle',name:'Toggle',icon:'▶'},{id:'quote',name:'Quote',icon:'❝'},
|
||||
{id:'callout',name:'Callout',icon:'lightbulb'},{id:'divider',name:'Divider',icon:'—'},{id:'code',name:'Code block',icon:'<>'},
|
||||
]},{name:'MEDIA',items:[{id:'image',name:'Image',icon:'image'}]}];
|
||||
|
||||
let _bid=0;function genId(){return 'b'+(++_bid)+'_'+Date.now().toString(36);}
|
||||
function cp(e){const s=window.getSelection();if(!s.rangeCount)return 0;const r=s.getRangeAt(0).cloneRange();r.selectNodeContents(e);r.setEnd(s.getRangeAt(0).endContainer,s.getRangeAt(0).endOffset);return r.toString().length;}
|
||||
function ce(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(false);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||||
function cs(e){const r=document.createRange();r.selectNodeContents(e);r.collapse(true);window.getSelection().removeAllRanges();window.getSelection().addRange(r);}
|
||||
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
|
||||
const SM={_o:false,_i:-1,_s:0,_q:'',_it:[],
|
||||
init(){const e=document.getElementById('_slashMenu');if(!e)return;e.innerHTML='';
|
||||
const inp=document.createElement('input');inp.type='text';inp.placeholder='Filter...';
|
||||
inp.style.cssText='width:100%;padding:6px 10px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box';
|
||||
inp.oninput=()=>SM.flt();inp.onkeydown=ev=>SM._kd(ev);
|
||||
const hdr=document.createElement('div');hdr.style.cssText='padding:8px;border-bottom:1px solid var(--border)';hdr.appendChild(inp);
|
||||
const list=document.createElement('div');list.style.cssText='overflow-y:auto;max-height:340px';
|
||||
list.addEventListener('click',ev=>{const it=ev.target.closest('.slash-item');if(it){ev.preventDefault();SM.sel(it.dataset.sid);}});
|
||||
e.appendChild(hdr);e.appendChild(list);
|
||||
SM._e=e;SM._in=inp;SM._li=list;
|
||||
document.addEventListener('mousedown',ev=>{if(!e.contains(ev.target))SM.close();});
|
||||
},
|
||||
open(i,el){if(!SM._e)SM.init();SM.close();SM._i=i;SM._s=0;SM._in.value='';SM.flt();
|
||||
const r=el.getBoundingClientRect();SM._e.style.display='block';
|
||||
SM._e.style.top=Math.max(4,(r.bottom+4>window.innerHeight-340)?r.top-344:r.bottom+4)+'px';
|
||||
SM._e.style.left=Math.min(r.left,window.innerWidth-324)+'px';
|
||||
setTimeout(()=>SM._in.focus(),10);SM._o=true;},
|
||||
close(){if(SM._e)SM._e.style.display='none';SM._o=false;SM._in&&(SM._in.value='');},
|
||||
flt(){if(!SM._li)return;SM._it=[];const q=(SM._in.value||'').toLowerCase();SM._q=q;let h='';
|
||||
for(const g of CMDS){let items=g.items;if(q)items=items.filter(c=>c.name.toLowerCase().includes(q)||c.id.includes(q));if(!items.length)continue;
|
||||
h+=`<div style="padding:6px 12px 4px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px">${g.name}</div>`;
|
||||
items.forEach(c=>{SM._it.push(c);
|
||||
h+=`<div class="slash-item" data-sid="${c.id}" style="display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:14px;color:var(--text-primary);cursor:pointer">
|
||||
<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;background:var(--bg-secondary);border-radius:4px;flex-shrink:0">${c.icon}</span>
|
||||
<span style="flex:1;font-weight:500">${c.name}</span></div>`;});}
|
||||
SM._li.innerHTML=h;},
|
||||
_kd(e){if(e.key==='ArrowDown'){e.preventDefault();SM._s=Math.min(SM._s+1,SM._it.length-1);SM._rs();}
|
||||
else if(e.key==='ArrowUp'){e.preventDefault();SM._s=Math.max(SM._s-1,0);SM._rs();}
|
||||
else if(e.key==='Enter'){e.preventDefault();const c=SM._it[SM._s];if(c)SM.sel(c.id);}
|
||||
else if(e.key==='Escape'){e.preventDefault();SM.close();}},
|
||||
_rs(){const its=SM._li.querySelectorAll('.slash-item');its.forEach((el,i)=>{el.classList.toggle('selected',i===SM._s);});},
|
||||
sel(id){if(window.E)window.E.applySlash(id);SM.close();},
|
||||
get isOpen(){return SM._o;}};
|
||||
|
||||
function renderBlock(b,idx){
|
||||
if(b.type==='divider')return `<div class="block-wrapper"><hr class="block-divider"></div>`;
|
||||
if(b.type==='image')return `<div class="block-wrapper"><div class="block-content block-image" style="text-align:center;padding:12px 0;"><img src="${b.src||''}" alt="${esc(b.alt||'')}" style="max-width:100%;max-height:70vh;border-radius:8px;display:block;margin:0 auto;" onerror="this.style.display='none';this.parentElement.innerHTML='<div style=\\'padding:40px;color:var(--text-tertiary)\\'>Image not found</div>'"></div></div>`;
|
||||
if(b.type==='embed'){
|
||||
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
|
||||
if(b.embed_type==='pdf')return `<div class="block-wrapper"><div class="block-content block-embed" style="padding:0;"><iframe src="${b.src||''}" style="width:100%;height:80vh;border:none;border-radius:8px;" onerror="this.style.display='none';this.nextElementSibling.style.display='block';"></iframe><div style="display:none;text-align:center;padding:32px;color:var(--text-secondary);"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p style="margin-top:8px;">Failed to load PDF</p><a href="${b.src||''}" target="_blank" style="color:var(--accent);">Open in new tab →</a></div></div></div>`;
|
||||
if(b.embed_type==='download'){
|
||||
var sz=b.file_size||0;var szStr=sz<1024?sz+' B':sz<1048576?(sz/1024).toFixed(1)+' KB':(sz/1048576).toFixed(1)+' MB';
|
||||
return `<div class="block-wrapper"><div class="block-content block-embed" style="text-align:center;padding:60px 20px;"><div style="font-size:48px;margin-bottom:12px;">{{ fd_icon("paperclip",14) }}</div><div style="font-size:16px;font-weight:500;color:var(--text-primary);margin-bottom:4px;">${esc(b.file_name||'File')}</div><div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">${szStr} · ${esc(b.file_mime||'')}</div><a href="${b.src||''}" download style="display:inline-block;padding:8px 20px;background:var(--accent);color:#fff;text-decoration:none;border-radius:8px;font-size:13px;font-weight:500;">⬇ Download</a></div></div>`;
|
||||
}
|
||||
}
|
||||
const ph=b.type==='heading_1'?'Heading 1':b.type==='heading_2'?'Heading 2':b.type==='heading_3'?'Heading 3':b.type==='heading_4'?'Heading 4':b.type==='bulleted_list'?'List':b.type==='numbered_list'?'List':b.type==='to_do'?'To-do':b.type==='toggle'?'Toggle list':b.type==='quote'?'Empty quote':b.type==='code'?'Type code...':b.type==='callout'?'Type something...':'Press \'/\' for commands...';
|
||||
let inner='';
|
||||
if(b.type==='to_do')inner=`<input type="checkbox" class="todo-checkbox" ${b.checked?'checked':''} onchange="E._doToggle('${b.id}',this.checked)"><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
else if(b.type==='toggle')inner=`<button class="toggle-chevron ${b.expanded?'open':''}" onclick="E._doToggleExpand('${b.id}')">▶</button><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
else if(b.type==='code')inner=`<div class="code-lang-label">${b.language||'Plain Text'}</div><pre><code data-bid="${b.id}" contenteditable="true" spellcheck="false">${esc(b.content)}</code></pre>`;
|
||||
else if(b.type==='callout')inner=`<span class="callout-icon">${b.icon||'lightbulb'}</span><div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
else inner=`<div data-bid="${b.id}" contenteditable="true" data-placeholder="${ph}" spellcheck="false">${esc(b.content)}</div>`;
|
||||
const tag=b.type.startsWith('heading_')?'h'+b.type.slice(-1):b.type==='quote'?'blockquote':'div';
|
||||
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
|
||||
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}"`:'';
|
||||
return `<div class="block-wrapper">
|
||||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||||
<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>
|
||||
<${tag}${style} class="${cls}">${inner}</${tag}>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
window.E=null;
|
||||
function editorState(){
|
||||
return {
|
||||
pid:null,pageTitle:'',blocks:[],dirty:false,saving:false,lastSaved:'',st:null,
|
||||
updatedAt:'{{ page.get("updated_at", "") }}',
|
||||
fmt:{open:false,top:0,left:0,idx:-1},
|
||||
shareOpen:false,moreOpen:false,gsMoreOpen:false,accessMenuOpen:false,
|
||||
moveOpen:false,moveWorkspaces:[],moveLoading:false,
|
||||
activityOpen:false,pageCreated:'{{ page.get("created_at", "") }}',
|
||||
shareTab:'share',favorited:false,
|
||||
pagePublished:{{ page_published | tojson }},
|
||||
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
|
||||
pubAllowEdit:false,pubAllowComments:true,
|
||||
pageUrl:window.location.href,publishedUrl:'',
|
||||
inviteEmail:'',invitePermission:'editor',accessList:[],
|
||||
toastVisible:false,toastMsg:'',
|
||||
get timeAgo() {
|
||||
if (!this.updatedAt) return 'just now';
|
||||
var diff = Math.floor((Date.now() - new Date(this.updatedAt + 'Z').getTime()) / 1000);
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
},
|
||||
formatDate(d) {
|
||||
if (!d) return '';
|
||||
var dt = new Date(d + 'Z');
|
||||
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
|
||||
' at ' + dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
},
|
||||
init(){
|
||||
_bid=Math.floor(Math.random()*10000);
|
||||
const dataEl=document.getElementById('page-data');
|
||||
if(dataEl){try{const data=JSON.parse(dataEl.textContent);this.pid=data.id;this.pageTitle=data.title||'';this.favorited=data.favorited||false;const fmt=data.content_format||'blocks';const raw=data.content||'';
|
||||
if(fmt==='file'){this.fileData=data;this.loadFileContent(data);}
|
||||
else if(fmt==='blocks'&&raw){try{this.blocks=JSON.parse(raw);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}}
|
||||
else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
|
||||
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
|
||||
this.loadInlineDBs();
|
||||
window.E=this;SM.init();this.render();
|
||||
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
|
||||
ct.addEventListener('keydown',e=>this.onKd(e));
|
||||
ct.addEventListener('paste',e=>{e.preventDefault();document.execCommand('insertText',false,(e.clipboardData||window.clipboardData).getData('text/plain'));this.dirty=true;this.autoSave();});
|
||||
if(typeof Sortable!=='undefined')Sortable.create(ct,{draggable:'.block-wrapper',handle:'.block-handle',animation:150,ghostClass:'sortable-ghost',dragClass:'sortable-drag',onEnd:evt=>{if(evt.oldIndex===evt.newIndex)return;const[item]=this.blocks.splice(evt.oldIndex,1);this.blocks.splice(evt.newIndex,0,item);this.dirty=true;this.autoSave();this.render();}});
|
||||
},100);
|
||||
},
|
||||
fileData:null,
|
||||
async loadFileContent(data){
|
||||
const url=data.file_url||'';const mime=data.file_mime||'';const name=data.file_name||'';const self=this;
|
||||
if(!url){this.blocks=[this.mkB('paragraph','File not available')];this.render();return;}
|
||||
const ext=name.toLowerCase().split('.').pop();
|
||||
if(mime.startsWith('image/')||['png','jpg','jpeg','gif','webp','svg','bmp','ico'].includes(ext)){
|
||||
this.blocks=[this.mkB('image','',{src:url,alt:name})];this.render();return;
|
||||
}
|
||||
if(mime==='application/pdf'||ext==='pdf'){
|
||||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'pdf'})];this.render();return;
|
||||
}
|
||||
const codeExts=['py','js','ts','jsx','tsx','html','htm','css','json','xml','yaml','yml','md','markdown','sql','sh','bash','ps1','bat','cmd','rb','go','rs','java','c','cpp','h','hpp','php','swift','kt','scala','r','toml','ini','cfg','conf','env','txt','log'];
|
||||
const langMap={'py':'python','js':'javascript','ts':'typescript','jsx':'javascript','tsx':'typescript','html':'html','htm':'html','css':'css','json':'json','xml':'xml','yaml':'yaml','yml':'yaml','md':'markdown','markdown':'markdown','sql':'sql','sh':'bash','bash':'bash','ps1':'powershell','bat':'plaintext','cmd':'plaintext','rb':'ruby','go':'go','rs':'rust','java':'java','c':'c','cpp':'cpp','h':'c','hpp':'cpp','php':'php','swift':'swift','kt':'kotlin','scala':'scala','r':'r','toml':'toml','ini':'ini','cfg':'ini','conf':'ini','env':'ini','txt':'plaintext','log':'plaintext'};
|
||||
if(mime.startsWith('text/')||codeExts.includes(ext)){
|
||||
try{const r=await fetch(url);const text=await r.text();
|
||||
if(ext==='md'||ext==='markdown'){this.blocks=this.md2b(text);}
|
||||
else{this.blocks=[this.mkB('code',text,{language:langMap[ext]||'Plain Text'})];}
|
||||
this.render();
|
||||
}catch(e){this.blocks=[this.mkB('paragraph','Error loading file: '+e.message)];this.render();}
|
||||
return;
|
||||
}
|
||||
this.blocks=[this.mkB('embed','',{src:url,embed_type:'download',file_name:name,file_size:data.file_size||0,file_mime:mime})];this.render();
|
||||
},
|
||||
mkB(type,content,extras){const b={id:genId(),type,content:content||'',...(extras||{})};if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;return b;},
|
||||
getEl(bid){const ct=document.getElementById('_blocksCt');return ct?ct.querySelector(`[data-bid="${bid}"]`):null;},
|
||||
getIdx(bid){return this.blocks.findIndex(b=>b.id===bid);},
|
||||
getActiveBlock(){const el=document.activeElement;if(!el||!el.hasAttribute('data-bid'))return null;return{el,bid:el.dataset.bid,idx:this.getIdx(el.dataset.bid)};},
|
||||
focusBlock(){const b=this.blocks[0];if(!b)return;const el=this.getEl(b.id);if(el){el.focus();ce(el);}},
|
||||
onCtClick(e){if(e.target===document.getElementById('_blocksCt')||e.target?.classList?.contains('blocks-empty')){if(!this.blocks.length)this.addAt(0);else{const b=this.blocks[this.blocks.length-1];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==='divider'||b.type==='image'||b.type==='embed')continue;const el=ct.querySelector(`[data-bid="${b.id}"]`);if(el)b.content=el.textContent||'';}},
|
||||
render(){const ct=document.getElementById('_blocksCt');if(!ct)return;let html='';for(let i=0;i<this.blocks.length;i++)html+=renderBlock(this.blocks[i],i);ct.innerHTML=html;
|
||||
ct.querySelectorAll('.block-wrapper').forEach(w=>{w.onmouseenter=()=>{w.querySelector('.block-handle')?.classList.add('visible');w.querySelector('.block-actions')?.classList.add('visible');};w.onmouseleave=()=>{w.querySelector('.block-handle')?.classList.remove('visible');w.querySelector('.block-actions')?.classList.remove('visible');};});
|
||||
ct.querySelectorAll('[data-placeholder]').forEach(el=>{el.innerHTML=el.innerHTML.replace(/<br[^>]*>/gi,'').trim()!==''?el.innerHTML:'';el.classList.toggle('empty',!el.textContent.trim());});
|
||||
},
|
||||
replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();b.type=type;b.content='';if(type==='toggle')b.expanded=true;if(type==='to_do')b.checked=false;this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(b.id);if(el)el.focus();},60);},
|
||||
addAt(idx,type,content){this.sync();const b=this.mkB(type||'paragraph',content||'');this.blocks.splice(idx,0,b);this.dirty=true;this.autoSave();this.render();setTimeout(()=>{const el=this.getEl(b.id);if(el&&type!=='divider'){el.focus();content&&ce(el);}},60);return b;},
|
||||
addAfter(idx,type){return this.addAt(idx+1,type);},
|
||||
removeBlock(idx){this.sync();if(this.blocks.length<=1){this.blocks[0]=this.mkB('paragraph','');}else{this.blocks.splice(idx,1);}const fi=Math.min(idx,this.blocks.length-1);this.render();this.dirty=true;this.autoSave();setTimeout(()=>{const el=this.getEl(this.blocks[fi]?.id);if(el){el.focus();ce(el);}},60);},
|
||||
|
||||
toggleFavorite(){
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
const m=this.favorited?'DELETE':'POST';
|
||||
fetch(`/board/api/favorites/${this.pid}`,{method:m,headers:{'X-CSRF-Token':csrf?csrf[1]:''}})
|
||||
.then(r=>r.json()).then(()=>{this.favorited=!this.favorited;this.showToast(this.favorited?'Added to favorites':'Removed from favorites');
|
||||
if(window.appState&&window.appState.refreshFavorites)window.appState.refreshFavorites();
|
||||
}).catch(()=>{this.showToast('Failed to toggle favorite');});
|
||||
},
|
||||
toggleActivityOpen(){
|
||||
if(!this.activityOpen){this.activityOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||||
else{this.activityOpen=false;}
|
||||
},
|
||||
toggleShareOpen(){
|
||||
if(!this.shareOpen){this.shareOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||||
else{this.shareOpen=false;}
|
||||
},
|
||||
toggleMoreOpen(){
|
||||
if(!this.moreOpen){this.moreOpen=true;this._justOpened=true;setTimeout(()=>this._justOpened=false,200);}
|
||||
else{this.moreOpen=false;}
|
||||
},
|
||||
_justOpened:false,
|
||||
togglePublish(){this.pagePublished=!this.pagePublished;this.saveShare();},
|
||||
saveShare(){
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/share/${this.pid}`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({mode:this.generalAccess,published:this.pagePublished})}).catch(()=>{});
|
||||
},
|
||||
async copyPageLink(){
|
||||
console.log('copyPageLink invoked');
|
||||
const url = this.pageUrl;
|
||||
const fallback = () => {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
document.execCommand('copy'); document.body.removeChild(ta);
|
||||
};
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
this.showToast('Link copied to clipboard');
|
||||
} catch(e) { fallback(); this.showToast('Link copied to clipboard'); }
|
||||
},
|
||||
showToast(msg){
|
||||
this.toastMsg = msg; this.toastVisible = true;
|
||||
clearTimeout(this._toastTimer);
|
||||
this._toastTimer = setTimeout(() => { this.toastVisible = false; }, 2500);
|
||||
},
|
||||
publishPage() {
|
||||
var self = this;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/publish', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.is_published) {
|
||||
self.pagePublished = true;
|
||||
self.publishedUrl = window.location.origin + '/p/' + d.publish_slug;
|
||||
self.showToast('Published to web — ' + self.publishedUrl);
|
||||
} else {
|
||||
self.showToast(d.detail || 'Publish failed');
|
||||
}
|
||||
}).catch(function(){ self.showToast('Publish failed'); });
|
||||
},
|
||||
unpublishPage() {
|
||||
var self = this;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/publish', {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (!d.is_published) {
|
||||
self.pagePublished = false;
|
||||
self.publishedUrl = '';
|
||||
self.showToast('Unpublished');
|
||||
} else {
|
||||
self.showToast(d.detail || 'Unpublish failed');
|
||||
}
|
||||
}).catch(function(){ self.showToast('Unpublish failed'); });
|
||||
},
|
||||
shareInvite() {
|
||||
var self = this;
|
||||
if (!this.inviteEmail.trim()) return;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/share', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||||
body: JSON.stringify({ email: this.inviteEmail.trim(), permission: this.invitePermission })
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.status === 'shared') { self.inviteEmail = ''; self.showToast('Invitation sent'); self.loadShares(); }
|
||||
else { self.showToast(d.detail || 'Share failed'); }
|
||||
}).catch(function(){ self.showToast('Share failed'); });
|
||||
},
|
||||
loadShares() {
|
||||
var self = this;
|
||||
fetch('/api/pages/' + this.pid + '/shares').then(function(r){ return r.json(); }).then(function(d){
|
||||
self.accessList = d.shares || [];
|
||||
}).catch(function(){ self.accessList = []; });
|
||||
},
|
||||
removeShare(sid) {
|
||||
var self = this;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/share/' + sid, {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.status === 'removed') { self.showToast('Share removed'); self.loadShares(); }
|
||||
else { self.showToast(d.detail || 'Remove failed'); }
|
||||
}).catch(function(){ self.showToast('Remove failed'); });
|
||||
},
|
||||
invitePerson(){if(!this.inviteEmail.trim())return;this.accessList.push({email:this.inviteEmail,permission:this.invitePermission,permOpen:false});this.inviteEmail='';},
|
||||
removeAccess(email){this.accessList=this.accessList.filter(a=>a.email!==email);},
|
||||
exportPage(){
|
||||
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;
|
||||
this.moveOpen = true;
|
||||
this.moveLoading = true;
|
||||
var self = this;
|
||||
fetch('/api/workspaces')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
self.moveWorkspaces = d.workspaces || [];
|
||||
self.moveLoading = false;
|
||||
})
|
||||
.catch(function(){ self.moveLoading = false; });
|
||||
},
|
||||
doMove(wsId) {
|
||||
this.moveOpen = false;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
var self = this;
|
||||
fetch('/api/pages/' + this.pid + '/move', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||||
body: JSON.stringify({ workspace_id: wsId })
|
||||
}).then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
if (d.status === 'ok') {
|
||||
self.showToast('Moved to workspace');
|
||||
setTimeout(function(){ window.location.href = '/local-workspace'; }, 800);
|
||||
} else {
|
||||
self.showToast(d.detail || 'Move failed');
|
||||
}
|
||||
})
|
||||
.catch(function(){ self.showToast('Move failed'); });
|
||||
},
|
||||
deletePage(){this.moreOpen=false;if(!confirm('Move to Trash?'))return;const csrf=document.cookie.match(/csrf_token=([^;]+)/);fetch(`/board/api/pages/${this.pid}/trash`,{method:'POST',headers:{'X-CSRF-Token':csrf?csrf[1]:''}}).then(()=>{window.location.href='/';}).catch(()=>{window.showToast('Failed','error');});},
|
||||
|
||||
// ── v4.5.0+: Get Started actions ──
|
||||
inlineDBs:[],
|
||||
async createDatabase(){
|
||||
const name=prompt('Database name:','New Database');
|
||||
if(!name||!name.trim())return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
const r=await fetch('/db/inline/api',{
|
||||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||||
body:JSON.stringify({name:name.trim(),parent_page_id:this.pid})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'table'});
|
||||
this.showToast('Database created — '+d.name);
|
||||
this.saveDBsToPage();
|
||||
this.renderInlineDBs();
|
||||
}else{
|
||||
this.showToast('Failed to create database');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
},
|
||||
async createForm(){
|
||||
const name=prompt('Form name:','New Form');
|
||||
if(!name||!name.trim())return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
try{
|
||||
const r=await fetch('/db/inline/api',{
|
||||
method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},
|
||||
body:JSON.stringify({name:name.trim(),parent_page_id:this.pid})
|
||||
});
|
||||
const d=await r.json();
|
||||
if(d.id){
|
||||
this.inlineDBs.push({id:d.id,name:d.name,view_type:'form'});
|
||||
this.showToast('Form created — '+d.name);
|
||||
this.saveDBsToPage();
|
||||
this.renderInlineDBs();
|
||||
}else{
|
||||
this.showToast('Failed to create form');
|
||||
}
|
||||
}catch(e){this.showToast('Error: '+e.message);}
|
||||
},
|
||||
saveDBsToPage(){
|
||||
// Store inline DB refs in the page's blocks as a special embed block
|
||||
if(this.inlineDBs.length===0)return;
|
||||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||||
if(existing.length===0){
|
||||
this.blocks.push(this.mkB('embed','',{embed_type:'inline_dbs',dbs:this.inlineDBs}));
|
||||
}else{
|
||||
existing.forEach(b=>{b.dbs=this.inlineDBs;});
|
||||
}
|
||||
this.render();
|
||||
this.dirty=true;this.autoSave();
|
||||
},
|
||||
renderInlineDBs(){
|
||||
// Re-render with inline DB blocks
|
||||
this.render();
|
||||
},
|
||||
loadInlineDBs(){
|
||||
const existing=this.blocks.filter(b=>b.type==='embed'&&b.embed_type==='inline_dbs');
|
||||
if(existing.length>0){
|
||||
this.inlineDBs=existing[0].dbs||[];
|
||||
}
|
||||
},
|
||||
|
||||
onKd(e){
|
||||
const ab=this.getActiveBlock();if(!ab)return;
|
||||
const{el,bid,idx}=ab;const block=this.blocks[idx];if(!block)return;
|
||||
if(SM.isOpen){
|
||||
if(e.key==='Escape'){e.preventDefault();SM.close();return;}
|
||||
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();SM._in.dispatchEvent(new KeyboardEvent('keydown',{key:e.key,bubbles:true}));return;}
|
||||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||||
return;
|
||||
}
|
||||
if(e.key.length===1||e.key==='Backspace'||e.key==='Delete'){const i2=idx;setTimeout(()=>{const e2=this.getEl(this.blocks[i2]?.id);if(e2&&e2.textContent?.trim()==='/'&&!SM.isOpen)SM.open(i2,e2);},15);}
|
||||
if(e.key===' '){const i2=idx;setTimeout(()=>this.checkMd(i2),15);}
|
||||
if(e.key==='Enter'){
|
||||
if(e.shiftKey){this.dirty=true;this.autoSave();return;}
|
||||
e.preventDefault();
|
||||
const text=el.textContent||'',pos=cp(el),before=text.substring(0,pos),after=text.substring(pos);
|
||||
if(!before.trim()&&!after.trim()){
|
||||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||||
this.sync();this.blocks.splice(idx+1,0,this.mkB(nt,''));this.render();
|
||||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();ce(ne);}},60);
|
||||
this.dirty=true;this.autoSave();return;
|
||||
}
|
||||
const nt=(block.type==='bulleted_list'||block.type==='numbered_list'||block.type==='to_do')?block.type:'paragraph';
|
||||
this.sync();block.content=before;this.blocks.splice(idx+1,0,this.mkB(nt,after));this.render();
|
||||
setTimeout(()=>{const ne=this.getEl(this.blocks[idx+1]?.id);if(ne){ne.focus();cs(ne);}},60);
|
||||
this.dirty=true;this.autoSave();return;
|
||||
}
|
||||
if(e.key==='ArrowUp'){if(cp(el)===0&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;}
|
||||
if(e.key==='ArrowDown'){if(cp(el)>=(el.textContent||'').length&&idx<this.blocks.length-1){e.preventDefault();const next=this.getEl(this.blocks[idx+1]?.id);if(next){next.focus();cs(next);}}return;}
|
||||
if(e.key==='Backspace'){if(!(el.textContent||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();block.type='paragraph';block.content='';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;}
|
||||
if(e.key===' '){const b2=bid;setTimeout(()=>this.checkMd(b2),15);}
|
||||
if((e.ctrlKey||e.metaKey)&&!e.altKey){const m={b:'bold',i:'italic',u:'underline'};if(m[e.key]){e.preventDefault();document.execCommand(m[e.key]);this.dirty=true;this.autoSave();}}
|
||||
if(e.key==='Tab'&&block.type==='code'){e.preventDefault();document.execCommand('insertText',false,' ');}
|
||||
this.dirty=true;this.autoSave();
|
||||
},
|
||||
checkMd(idx){
|
||||
if(idx<0||idx>=this.blocks.length)return;const block=this.blocks[idx];const el=this.getEl(block.id);if(!el)return;const text=el.textContent||'';
|
||||
const sc={'# ':'heading_1','## ':'heading_2','### ':'heading_3','#### ':'heading_4','- ':'bulleted_list','* ':'bulleted_list','+ ':'bulleted_list','1. ':'numbered_list','[] ':'to_do','[ ] ':'to_do','> ':'quote'};
|
||||
this.sync();
|
||||
for(const[pfx,type] of Object.entries(sc)){if(text===pfx||text.startsWith(pfx)){block.type=type;block.content='';if(type==='toggle')block.expanded=true;if(type==='to_do')block.checked=false;this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne&&type!=='divider')ne.focus();},60);return;}}
|
||||
if(text==='---'){block.type='divider';block.content='';this.addAfter(idx,'paragraph');}
|
||||
if(text==='```'){block.type='code';block.content='';block.language='Plain Text';this.render();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}
|
||||
},
|
||||
applySlash(id){
|
||||
const idx=SM._i;if(idx<0||idx>=this.blocks.length)return;
|
||||
const block=this.blocks[idx];this.sync();block.content='';
|
||||
if(id==='divider'){block.type='divider';this.addAfter(idx,'paragraph');}
|
||||
else{block.type=id;if(id==='toggle')block.expanded=true;if(id==='to_do')block.checked=false;if(id==='code')block.language='Plain Text';}
|
||||
this.render();this.dirty=true;this.autoSave();
|
||||
setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);
|
||||
},
|
||||
_doToggle(bid,v){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].checked=v;this.dirty=true;this.autoSave();}},
|
||||
_doToggleExpand(bid){const idx=this.getIdx(bid);if(idx>=0){this.blocks[idx].expanded=!this.blocks[idx].expanded;this.dirty=true;this.autoSave();}},
|
||||
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();}},
|
||||
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(){
|
||||
if(this.fileData)return;
|
||||
if(this.saving)return;
|
||||
this.sync();
|
||||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'New page';
|
||||
this.saving=true;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
|
||||
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
|
||||
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
|
||||
}).catch(()=>{this.saving=false;});
|
||||
},
|
||||
md2b(md){const bl=[];let cb=null;for(const line of md.split('\n')){if(cb){if(line.trim()==='```'){bl.push(cb);cb=null;}else cb.content+=(cb.content?'\n':'')+line;continue;}const t=line.trim();if(!t)continue;if(t.startsWith('```')){cb=this.mkB('code','',{language:t.substring(3).trim()||'Plain Text'});continue;}if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));else bl.push(this.mkB('paragraph',t));}if(cb)bl.push(cb);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);});
|
||||
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>
|
||||
{% include "_page_editor_scripts.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,401 +7,5 @@
|
||||
<script>document.body.classList.add('embed-mode');</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script type="application/json" id="page-data">
|
||||
{{ page_data | tojson }}
|
||||
</script>
|
||||
<script>
|
||||
/* eslint-disable */
|
||||
const CMDS=[{name:'BASIC',items:[
|
||||
{id:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
|
||||
{id:'heading_3',name:'Heading 3',icon:'H3'},{id:'heading_4',name:'Heading 4',icon:'H4'},{id:'bulleted_list',name:'Bulleted list',icon:'•'},{id:'numbered_list',name:'Numbered list',icon:'1.'},
|
||||
{id:'to_do',name:'To-do list',icon:'☐'},{id:'toggle',name:'Toggle list',icon:'▶'},{id:'code',name:'Code',icon:'</>'},
|
||||
{id:'quote',name:'Quote',icon:'❝'},{id:'callout',name:'Callout',icon:'💡'},{id:'divider',name:'Divider',icon:'—'},
|
||||
{id:'image',name:'Image',icon:'🖼'},{id:'video',name:'Video',icon:'▶'},{id:'audio',name:'Audio',icon:'♪'},
|
||||
{id:'file',name:'File',icon:'📎'},{id:'bookmark',name:'Web bookmark',icon:'🔖'}]},
|
||||
{name:'DATABASE',items:[{id:'database',name:'Database - Inline',icon:'🗄'},{id:'table',name:'Table view',icon:'📊'},
|
||||
{id:'board',name:'Board view',icon:'📋'},{id:'list',name:'List view',icon:'📝'},{id:'timeline',name:'Timeline view',icon:'📅'},
|
||||
{id:'calendar',name:'Calendar view',icon:'📆'},{id:'gallery',name:'Gallery view',icon:'🖼'}]},
|
||||
{name:'ADVANCED',items:[{id:'table_of_contents',name:'Table of contents',icon:'📑'},{id:'math',name:'Math equation',icon:'∑'},
|
||||
{id:'button',name:'Button',icon:'🔘'},{id:'breadcrumb',name:'Breadcrumb',icon:'»'},
|
||||
{id:'synced_block',name:'Synced block',icon:'🔄'}]},
|
||||
{name:'EMBEDS',items:[{id:'pdf',name:'PDF',icon:'📄'},{id:'google_drive',name:'Google Drive',icon:'📁'},
|
||||
{id:'google_maps',name:'Google Maps',icon:'🗺'},{id:'figma',name:'Figma',icon:'🎨'},{id:'typeform',name:'Typeform',icon:'📝'},
|
||||
{id:'codepen',name:'CodePen',icon:'✒'},{id:'excalidraw',name:'Excalidraw',icon:'✏'}]},
|
||||
{name:'AI',items:[{id:'ask_ai',name:'Ask AI',icon:'✨'},{id:'ai_summary',name:'AI Summary',icon:'📋'},
|
||||
{id:'ai_translate',name:'AI Translate',icon:'🌐'}]}];
|
||||
|
||||
// ── Editor State ──
|
||||
function editorState() {
|
||||
return {
|
||||
// Core state
|
||||
pid: 1,
|
||||
pageTitle: '',
|
||||
favorited: false,
|
||||
shareMode: 'private',
|
||||
pagePublished: false,
|
||||
pageCreated: null,
|
||||
pageUrl: '',
|
||||
blocks: [],
|
||||
draftBlocks: null,
|
||||
blockRefs: new Map(),
|
||||
blockCache: new Map(),
|
||||
blockRendered: new Set(),
|
||||
|
||||
// UI state
|
||||
shareOpen: false,
|
||||
shareTab: 'share',
|
||||
activityOpen: false,
|
||||
moreOpen: false,
|
||||
moveOpen: false,
|
||||
moveLoading: false,
|
||||
moveWorkspaces: [],
|
||||
gsMoreOpen: false,
|
||||
|
||||
// Share state
|
||||
inviteEmail: '',
|
||||
accessList: [],
|
||||
generalAccess: 'invited',
|
||||
accessMenuOpen: false,
|
||||
publicPerm: 'viewer',
|
||||
pubAllowEdit: false,
|
||||
pubAllowComments: true,
|
||||
publishedUrl: '',
|
||||
|
||||
// Format toolbar
|
||||
fmt: {open:false, top:0, left:0, sel:null},
|
||||
|
||||
// Toast
|
||||
toastVisible: false,
|
||||
toastMsg: '',
|
||||
|
||||
// Save status
|
||||
saving: false,
|
||||
dirty: false,
|
||||
lastSaved: '',
|
||||
timeAgo: '',
|
||||
saveTimer: null,
|
||||
|
||||
// Anti-bounce for click.outside
|
||||
_justOpened: false,
|
||||
|
||||
init() {
|
||||
var dataEl = document.getElementById('page-data');
|
||||
if (dataEl) {
|
||||
try {
|
||||
var data = JSON.parse(dataEl.textContent);
|
||||
this.pid = data.id;
|
||||
this.pageTitle = data.title || '';
|
||||
this.favorited = data.favorited || false;
|
||||
var fmt = data.content_format || 'blocks';
|
||||
var raw = data.content || '';
|
||||
if (fmt === 'blocks') {
|
||||
try { this.blocks = JSON.parse(raw) || []; } catch(e) { this.blocks = []; }
|
||||
} else {
|
||||
this.blocks = [{id: 'b0', type: 'paragraph', content: raw || ''}];
|
||||
}
|
||||
this.pagePublished = data.published || false;
|
||||
this.pageCreated = data.created_at || null;
|
||||
this.pageUrl = window.location.origin + '/pages/' + this.pid;
|
||||
if (data.update_time) {
|
||||
this.timeAgo = this._timeAgo(data.update_time);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (!this.blocks || this.blocks.length === 0) {
|
||||
this.blocks = [{id: 'b0', type: 'paragraph', content: ''}];
|
||||
}
|
||||
this.render();
|
||||
window.E = this;
|
||||
},
|
||||
|
||||
render() {
|
||||
var ct = document.getElementById('_blocksCt');
|
||||
if (!ct) return;
|
||||
var html = '';
|
||||
for (var i = 0; i < this.blocks.length; i++) {
|
||||
html += this._renderBlock(this.blocks[i], i);
|
||||
}
|
||||
ct.innerHTML = html;
|
||||
},
|
||||
|
||||
_renderBlock(b, idx) {
|
||||
var txt = (b.content || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
var type = b.type || 'paragraph';
|
||||
var tag = 'div';
|
||||
var cls = 'block';
|
||||
if (type.indexOf('heading') === 0) {
|
||||
var lvl = parseInt(type.split('_')[1]) || 1;
|
||||
tag = 'h' + Math.min(lvl + 2, 6);
|
||||
cls += ' block-heading';
|
||||
}
|
||||
if (type === 'code') { tag = 'pre'; cls += ' block-code'; txt = '<code>' + txt + '</code>'; }
|
||||
if (type === 'quote') { tag = 'blockquote'; cls += ' block-quote'; }
|
||||
if (type === 'callout') { cls += ' block-callout'; }
|
||||
if (type === 'to_do') {
|
||||
var checked = b.checked ? ' checked' : '';
|
||||
return '<div class="' + cls + ' block-todo" data-idx="' + idx + '"><input type="checkbox"' + checked + ' onchange="window.E.toggleTodo(' + idx + ',this.checked)"><div class="block-content" contenteditable="true" oninput="window.E.onBlockInput(' + idx + ',this)" onkeydown="return window.E.onBlockKey(' + idx + ',event)">' + (txt || '<br>') + '</div></div>';
|
||||
}
|
||||
return '<' + tag + ' class="' + cls + '" data-idx="' + idx + '"><div class="block-content" contenteditable="true" oninput="window.E.onBlockInput(' + idx + ',this)" onkeydown="return window.E.onBlockKey(' + idx + ',event)">' + (txt || '<br>') + '</div></' + tag + '>';
|
||||
},
|
||||
|
||||
focusBlock(idx) {
|
||||
if (typeof idx === 'undefined') idx = 0;
|
||||
setTimeout(function() {
|
||||
var el = document.querySelector('[data-idx="' + idx + '"] .block-content');
|
||||
if (el) { el.focus(); }
|
||||
}, 50);
|
||||
},
|
||||
|
||||
onBlockInput(idx, el) {
|
||||
this.dirty = true;
|
||||
this.blocks[idx].content = el.innerText;
|
||||
this.debounceSave();
|
||||
},
|
||||
|
||||
onBlockKey(idx, e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
var text = e.target.innerText;
|
||||
this.blocks[idx].content = text;
|
||||
var newBlock = {id: 'b' + Date.now(), type: 'paragraph', content: ''};
|
||||
this.blocks.splice(idx + 1, 0, newBlock);
|
||||
this.render();
|
||||
this.focusBlock(idx + 1);
|
||||
return false;
|
||||
}
|
||||
if (e.key === 'Backspace' && e.target.innerText === '' && this.blocks.length > 1) {
|
||||
e.preventDefault();
|
||||
this.blocks.splice(idx, 1);
|
||||
this.render();
|
||||
this.focusBlock(idx - 1);
|
||||
return false;
|
||||
}
|
||||
if (e.key === '/') {
|
||||
var s = window.getSelection();
|
||||
if (s.rangeCount && s.getRangeAt(0).startOffset === 0 && e.target.innerText === '/') {
|
||||
this._showSlash(idx, e.target);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_showSlash(idx, el) {
|
||||
var rect = el.getBoundingClientRect();
|
||||
var menu = document.getElementById('_slashMenu');
|
||||
if (!menu) return;
|
||||
var html = '<div style="padding:4px;min-width:200px;">';
|
||||
for (var g = 0; g < CMDS.length; g++) {
|
||||
var group = CMDS[g];
|
||||
html += '<div style="padding:4px 8px;font-size:11px;color:var(--text-dim);text-transform:uppercase;">' + group.name + '</div>';
|
||||
for (var j = 0; j < group.items.length; j++) {
|
||||
var item = group.items[j];
|
||||
html += '<div class="slash-item" onclick="window.E._applySlash(' + idx + ',\'' + item.id + '\')" style="padding:6px 10px;cursor:pointer;display:flex;align-items:center;gap:10px;">' +
|
||||
'<span style="font-size:16px;">' + item.icon + '</span>' +
|
||||
'<span style="font-size:13px;">' + item.name + '</span></div>';
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
menu.innerHTML = html;
|
||||
menu.style.display = 'block';
|
||||
menu.style.top = (rect.bottom + 4) + 'px';
|
||||
menu.style.left = rect.left + 'px';
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
document.addEventListener('click', function handler(e) {
|
||||
if (!menu.contains(e.target)) {
|
||||
menu.style.display = 'none';
|
||||
document.removeEventListener('click', handler);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
|
||||
_applySlash(idx, type) {
|
||||
this.blocks[idx].type = type;
|
||||
this.blocks[idx].content = '';
|
||||
this.render();
|
||||
var menu = document.getElementById('_slashMenu');
|
||||
if (menu) menu.style.display = 'none';
|
||||
this.focusBlock(idx);
|
||||
this.dirty = true;
|
||||
this.debounceSave();
|
||||
},
|
||||
|
||||
toggleTodo(idx, checked) {
|
||||
this.blocks[idx].checked = checked;
|
||||
this.dirty = true;
|
||||
this.debounceSave();
|
||||
},
|
||||
|
||||
debounceSave() {
|
||||
var self = this;
|
||||
clearTimeout(this.saveTimer);
|
||||
this.saveTimer = setTimeout(function() { self.save(); }, 2000);
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.saving) return;
|
||||
this.saving = true;
|
||||
try {
|
||||
var csrf = this._getCsrf();
|
||||
var body = {title: this.pageTitle, content: JSON.stringify(this.blocks), content_format: 'blocks'};
|
||||
var r = await fetch('/board/api/pages/' + this.pid, {
|
||||
method: 'PUT', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrf},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (r.ok) {
|
||||
this.dirty = false;
|
||||
var now = new Date();
|
||||
this.lastSaved = now.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
||||
}
|
||||
} catch(e) {}
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
pastePlain(e) {
|
||||
e.preventDefault();
|
||||
var text = (e.clipboardData || window.clipboardData).getData('text/plain');
|
||||
document.execCommand('insertText', false, text);
|
||||
},
|
||||
|
||||
onCtClick(e) {
|
||||
var ct = document.getElementById('_blocksCt');
|
||||
if (e.target === ct || (e.target.classList && e.target.classList.contains('blocks-container'))) {
|
||||
var newBlock = {id: 'b' + Date.now(), type: 'paragraph', content: ''};
|
||||
this.blocks.push(newBlock);
|
||||
this.render();
|
||||
this.focusBlock(this.blocks.length - 1);
|
||||
}
|
||||
},
|
||||
|
||||
fmtApply(cmd) { document.execCommand(cmd); this.dirty = true; this.fmt.open = false; },
|
||||
fmtLink() {
|
||||
var url = prompt('Enter URL:');
|
||||
if (url) { document.execCommand('createLink', false, url); this.dirty = true; }
|
||||
this.fmt.open = false;
|
||||
},
|
||||
|
||||
replaceBlock(idx, type) { this.blocks[idx].type = type; },
|
||||
createDatabase() {
|
||||
var name = prompt('Database name:');
|
||||
if (!name) return;
|
||||
var self = this;
|
||||
(async function() {
|
||||
var csrf = self._getCsrf();
|
||||
await fetch('/db/inline/api', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrf},
|
||||
body: JSON.stringify({page_id: self.pid, name: name})
|
||||
});
|
||||
window.showToast && window.showToast('Database created', 'success');
|
||||
})();
|
||||
},
|
||||
createForm() { window.showToast && window.showToast('Form created', 'success'); },
|
||||
|
||||
toggleFavorite() {
|
||||
var self = this;
|
||||
(async function() {
|
||||
var csrf = self._getCsrf();
|
||||
var method = self.favorited ? 'DELETE' : 'POST';
|
||||
await fetch('/board/api/favorites/' + self.pid, {method: method, headers: {'X-CSRF-Token': csrf}});
|
||||
self.favorited = !self.favorited;
|
||||
})();
|
||||
},
|
||||
copyPageLink() {
|
||||
navigator.clipboard.writeText(this.pageUrl || window.location.href);
|
||||
this.showToast('Link copied');
|
||||
},
|
||||
toggleShareOpen() { this.shareOpen = !this.shareOpen; if (this.shareOpen) { this._justOpened = true; setTimeout(function() { window.E._justOpened = false; }, 100); } },
|
||||
toggleActivityOpen() { this.activityOpen = !this.activityOpen; if (this.activityOpen) { this._justOpened = true; setTimeout(function() { window.E._justOpened = false; }, 100); } },
|
||||
toggleMoreOpen() { this.moreOpen = !this.moreOpen; if (this.moreOpen) { this._justOpened = true; setTimeout(function() { window.E._justOpened = false; }, 100); } },
|
||||
|
||||
showToast(msg, type) {
|
||||
this.toastMsg = msg;
|
||||
this.toastVisible = true;
|
||||
var self = this;
|
||||
setTimeout(function() { self.toastVisible = false; }, 2000);
|
||||
},
|
||||
|
||||
exportPage() { window.showToast && window.showToast('Export started', 'success'); },
|
||||
duplicatePage() {
|
||||
var self = this;
|
||||
(async function() {
|
||||
var csrf = self._getCsrf();
|
||||
var r = await fetch('/board/api/pages', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrf},
|
||||
body: JSON.stringify({title: self.pageTitle + ' (copy)', content: JSON.stringify(self.blocks), content_format: 'blocks'})
|
||||
});
|
||||
if (r.ok) { var d = await r.json(); window.location = '/pages/' + d.id; }
|
||||
})();
|
||||
},
|
||||
movePage() { this.moveOpen = true; this._loadWorkspaces(); },
|
||||
async _loadWorkspaces() {
|
||||
this.moveLoading = true;
|
||||
try { var r = await fetch('/api/workspaces'); var d = await r.json(); this.moveWorkspaces = d.workspaces || []; } catch(e) {}
|
||||
this.moveLoading = false;
|
||||
},
|
||||
async doMove(wsId) {
|
||||
var csrf = this._getCsrf();
|
||||
await fetch('/board/api/pages/' + this.pid + '/move', {
|
||||
method: 'PUT', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrf},
|
||||
body: JSON.stringify({workspace_id: wsId})
|
||||
});
|
||||
this.moveOpen = false;
|
||||
window.showToast && window.showToast('Moved', 'success');
|
||||
},
|
||||
deletePage() {
|
||||
if (!confirm('Move to Trash?')) return;
|
||||
var self = this;
|
||||
(async function() {
|
||||
var csrf = self._getCsrf();
|
||||
await fetch('/board/api/pages/' + self.pid, {method: 'DELETE', headers: {'X-CSRF-Token': csrf}});
|
||||
window.location = '/workspaces';
|
||||
})();
|
||||
},
|
||||
publishPage() {
|
||||
var self = this;
|
||||
(async function() {
|
||||
var csrf = self._getCsrf();
|
||||
await fetch('/pages/' + self.pid + '/publish', {method: 'POST', headers: {'X-CSRF-Token': csrf}});
|
||||
self.pagePublished = true;
|
||||
self.publishedUrl = window.location.origin + '/p/' + self.pid;
|
||||
})();
|
||||
},
|
||||
unpublishPage() {
|
||||
var self = this;
|
||||
(async function() {
|
||||
var csrf = self._getCsrf();
|
||||
await fetch('/pages/' + self.pid + '/publish', {method: 'DELETE', headers: {'X-CSRF-Token': csrf}});
|
||||
self.pagePublished = false;
|
||||
self.publishedUrl = '';
|
||||
})();
|
||||
},
|
||||
shareInvite() { if (this.inviteEmail) { this.accessList.push({email: this.inviteEmail, permission: 'editor', permOpen: false}); this.inviteEmail = ''; } },
|
||||
removeAccess(email) { this.accessList = this.accessList.filter(function(a) { return a.email !== email; }); },
|
||||
formatDate(d) { try { return new Date(d).toLocaleDateString(); } catch(e) { return d; } },
|
||||
|
||||
_getCsrf() {
|
||||
var m = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
return m ? m[1] : '';
|
||||
},
|
||||
|
||||
_timeAgo(d) {
|
||||
try {
|
||||
var dt = new Date(d);
|
||||
var now = new Date();
|
||||
var diff = (now - dt) / 1000;
|
||||
if (diff < 60) return 'just now';
|
||||
var mins = Math.floor(diff/60);
|
||||
if (mins < 60) return mins + 'm ago';
|
||||
var hours = Math.floor(mins/60);
|
||||
if (hours < 24) return hours + 'h ago';
|
||||
var days = Math.floor(hours/24);
|
||||
if (days < 7) return days + 'd ago';
|
||||
if (days < 30) return Math.floor(days/7) + 'w ago';
|
||||
return dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
|
||||
} catch(e) { return d; }
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% include '_page_editor_scripts.html' %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user