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:
@@ -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