Reformat HTML in page editor template
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

This commit is contained in:
2026-07-10 14:45:17 -04:00
parent 806ec495f0
commit e0102d2617
+345 -97
View File
@@ -1,51 +1,84 @@
{% extends "base.html" %} {% extends "base.html" %} {% block page_icon %}📄{% endblock %} {% block
{% block page_icon %}📄{% endblock %} page_title %}{{ page.title }}{% endblock %} {% block topbar %}
{% block page_title %}{{ page.title }}{% endblock %}
{% block topbar %}
<!-- Page editor uses its own topbar --> <!-- Page editor uses its own topbar -->
{% endblock %} {% endblock %} {% block content %}
<div class="page-editor-wrapper" x-data="editorState()">
{% block content %}
<div class="page-editor-wrapper" x-data="editorState()" x-init="loadPage()">
<!-- ═══════════ Top Action Bar ═══════════ --> <!-- ═══════════ Top Action Bar ═══════════ -->
<div class="page-topbar"> <div class="page-topbar">
<div class="page-topbar-right" style="position:relative;"> <div class="page-topbar-right" style="position: relative">
<span class="topbar-edited">Edited just now</span> <span class="topbar-edited">Edited just now</span>
<button class="page-action-btn share-btn" @click="shareOpen = !shareOpen"> <button
class="page-action-btn share-btn"
@click="shareOpen = !shareOpen"
>
🔒 Share <span class="chevron-down">▾</span> 🔒 Share <span class="chevron-down">▾</span>
</button> </button>
<button class="page-action-btn" @click="copyPageLink" title="Copy link">🔗</button> <button
class="page-action-btn"
@click="copyPageLink"
title="Copy link"
>
🔗
</button>
<button class="page-action-btn star-btn" @click="toggleFavorite"> <button class="page-action-btn star-btn" @click="toggleFavorite">
<span x-text="favorited ? '⭐' : '☆'" :class="{ starred: favorited }"></span> <span
x-text="favorited ? '⭐' : '☆'"
:class="{ starred: favorited }"
></span>
</button>
<button class="page-action-btn" @click="moreOpen = !moreOpen">
⋮
</button> </button>
<button class="page-action-btn" @click="moreOpen = !moreOpen">⋮</button>
<!-- ═══════════ Share / Publish Dialog ═══════════ --> <!-- ═══════════ Share / Publish Dialog ═══════════ -->
<div class="share-dialog" x-show="shareOpen" @click.away="shareOpen = false" x-transition.opacity.scale.origin.top.right> <div
class="share-dialog"
x-show="shareOpen"
@click.away="shareOpen = false"
x-transition.opacity.scale.origin.top.right
>
<!-- Header with tabs --> <!-- Header with tabs -->
<div class="sd-header"> <div class="sd-header">
<button class="sd-tab" :class="{ active: shareTab === 'share' }" @click="shareTab = 'share'">Share</button> <button
<button class="sd-tab" :class="{ active: shareTab === 'publish' }" @click="shareTab = 'publish'"> class="sd-tab"
:class="{ active: shareTab === 'share' }"
@click="shareTab = 'share'"
>
Share
</button>
<button
class="sd-tab"
:class="{ active: shareTab === 'publish' }"
@click="shareTab = 'publish'"
>
Publish Publish
<span class="sd-badge" x-show="pagePublished"></span> <span class="sd-badge" x-show="pagePublished"></span>
</button> </button>
<button class="sd-close" @click="shareOpen = false">✕</button> <button class="sd-close" @click="shareOpen = false">
✕
</button>
</div> </div>
<!-- ═══════ SHARE TAB ═══════ --> <!-- ═══════ SHARE TAB ═══════ -->
<div class="sd-body" x-show="shareTab === 'share'" x-transition> <div class="sd-body" x-show="shareTab === 'share'" x-transition>
<!-- Invite input --> <!-- Invite input -->
<div class="sd-invite-row"> <div class="sd-invite-row">
<input type="email" class="sd-input" placeholder="Add people, groups or emails..." x-model="inviteEmail" @keydown.enter="invitePerson"> <input
<button class="sd-btn-primary" @click="invitePerson">Invite</button> type="email"
class="sd-input"
placeholder="Add people, groups or emails..."
x-model="inviteEmail"
@keydown.enter="invitePerson"
/>
<button class="sd-btn-primary" @click="invitePerson">
Invite
</button>
</div> </div>
<!-- Context card (shown when independent permissions) --> <!-- Context card (shown when independent permissions) -->
<div class="sd-context-card" x-show="false"> <div class="sd-context-card" x-show="false">
Share settings on this page are unlinked from parent page Share settings on this page are unlinked from parent
page
</div> </div>
<!-- Participants list --> <!-- Participants list -->
@@ -54,30 +87,72 @@
{% raw %} {% raw %}
<div class="sd-participant"> <div class="sd-participant">
<div class="sd-participant-info"> <div class="sd-participant-info">
<div class="sd-avatar" x-text="a.email[0].toUpperCase()"></div> <div
class="sd-avatar"
x-text="a.email[0].toUpperCase()"
></div>
<div> <div>
<div class="sd-participant-name" x-text="a.email"></div> <div
<div class="sd-participant-email" x-text="a.email"></div> class="sd-participant-name"
x-text="a.email"
></div>
<div
class="sd-participant-email"
x-text="a.email"
></div>
</div> </div>
</div> </div>
<button class="sd-perm-btn" @click="a.permOpen = !a.permOpen"> <button
<span x-text="a.permission === 'editor' ? 'Full access' : a.permission === 'commenter' ? 'Can comment' : 'Can view'"></span> ▾ class="sd-perm-btn"
@click="a.permOpen = !a.permOpen"
>
<span
x-text="a.permission === 'editor' ? 'Full access' : a.permission === 'commenter' ? 'Can comment' : 'Can view'"
></span>
▾
</button> </button>
<div class="sd-perm-menu" x-show="a.permOpen" @click.away="a.permOpen = false"> <div
<div class="sd-perm-option" :class="{ active: a.permission === 'editor' }" @click="a.permission='editor'; a.permOpen=false"> class="sd-perm-menu"
x-show="a.permOpen"
@click.away="a.permOpen = false"
>
<div
class="sd-perm-option"
:class="{ active: a.permission === 'editor' }"
@click="a.permission='editor'; a.permOpen=false"
>
<span>Can edit</span> <span>Can edit</span>
<span class="sd-perm-desc">Edit, suggest and comment</span> <span class="sd-perm-desc"
>Edit, suggest and comment</span
>
</div> </div>
<div class="sd-perm-option" :class="{ active: a.permission === 'commenter' }" @click="a.permission='commenter'; a.permOpen=false"> <div
class="sd-perm-option"
:class="{ active: a.permission === 'commenter' }"
@click="a.permission='commenter'; a.permOpen=false"
>
<span>Can comment</span> <span>Can comment</span>
<span class="sd-perm-desc">Suggest and comment only</span> <span class="sd-perm-desc"
>Suggest and comment only</span
>
</div> </div>
<div class="sd-perm-option" :class="{ active: a.permission === 'viewer' }" @click="a.permission='viewer'; a.permOpen=false"> <div
class="sd-perm-option"
:class="{ active: a.permission === 'viewer' }"
@click="a.permission='viewer'; a.permOpen=false"
>
<span>Can view</span> <span>Can view</span>
<span class="sd-perm-desc">Read only</span> <span class="sd-perm-desc"
>Read only</span
>
</div> </div>
<div class="sd-perm-sep"></div> <div class="sd-perm-sep"></div>
<div class="sd-perm-option danger" @click="removeAccess(a.email); a.permOpen=false">Remove</div> <div
class="sd-perm-option danger"
@click="removeAccess(a.email); a.permOpen=false"
>
Remove
</div>
</div> </div>
</div> </div>
{% endraw %} {% endraw %}
@@ -89,25 +164,61 @@
<!-- General Access --> <!-- General Access -->
<div class="sd-section"> <div class="sd-section">
<div class="sd-section-title">General access</div> <div class="sd-section-title">General access</div>
<div style="position:relative;"> <div style="position: relative">
<button class="sd-access-btn" @click="accessMenuOpen = !accessMenuOpen"> <button
<span x-text="generalAccess === 'invited' ? '🔒 Only people invited' : '🌐 Anyone with the link'"></span> class="sd-access-btn"
@click="accessMenuOpen = !accessMenuOpen"
>
<span
x-text="generalAccess === 'invited' ? '🔒 Only people invited' : '🌐 Anyone with the link'"
></span>
<span class="chevron-down">▾</span> <span class="chevron-down">▾</span>
</button> </button>
<div class="sd-access-menu" x-show="accessMenuOpen" @click.away="accessMenuOpen = false" x-transition> <div
<div class="sd-perm-option" :class="{ active: generalAccess === 'invited' }" @click="generalAccess='invited'; accessMenuOpen=false"> class="sd-access-menu"
x-show="accessMenuOpen"
@click.away="accessMenuOpen = false"
x-transition
>
<div
class="sd-perm-option"
:class="{ active: generalAccess === 'invited' }"
@click="generalAccess='invited'; accessMenuOpen=false"
>
<span>🔒 Only people invited</span> <span>🔒 Only people invited</span>
<span class="sd-perm-desc">People need to be invited</span> <span class="sd-perm-desc"
>People need to be invited</span
>
</div> </div>
<div class="sd-perm-option" :class="{ active: generalAccess === 'anyone' }" @click="generalAccess='anyone'; accessMenuOpen=false"> <div
class="sd-perm-option"
:class="{ active: generalAccess === 'anyone' }"
@click="generalAccess='anyone'; accessMenuOpen=false"
>
<span>🌐 Anyone with the link</span> <span>🌐 Anyone with the link</span>
<span class="sd-perm-desc">Can view the page</span> <span class="sd-perm-desc"
>Can view the page</span
>
</div> </div>
</div> </div>
</div> </div>
<!-- Public permission when link is open --> <!-- Public permission when link is open -->
<div x-show="generalAccess === 'anyone'" style="margin-top:6px;display:flex;align-items:center;gap:8px;"> <div
<span style="font-size:13px;color:var(--text-primary);">Anyone with the link</span> x-show="generalAccess === 'anyone'"
style="
margin-top: 6px;
display: flex;
align-items: center;
gap: 8px;
"
>
<span
style="
font-size: 13px;
color: var(--text-primary);
"
>Anyone with the link</span
>
<select class="sd-select-sm" x-model="publicPerm"> <select class="sd-select-sm" x-model="publicPerm">
<option value="viewer">Can view</option> <option value="viewer">Can view</option>
<option value="commenter">Can comment</option> <option value="commenter">Can comment</option>
@@ -120,32 +231,70 @@
<!-- Footer --> <!-- Footer -->
<div class="sd-footer"> <div class="sd-footer">
<a href="#" class="sd-footer-link" @click.prevent>? Learn about sharing</a> <a href="#" class="sd-footer-link" @click.prevent
<button class="sd-footer-action" @click="copyPageLink">🔗 Copy link</button> >? Learn about sharing</a
>
<button class="sd-footer-action" @click="copyPageLink">
🔗 Copy link
</button>
</div> </div>
</div> </div>
<!-- ═══════ PUBLISH TAB ═══════ --> <!-- ═══════ PUBLISH TAB ═══════ -->
<div class="sd-body" x-show="shareTab === 'publish'" x-transition> <div
class="sd-body"
x-show="shareTab === 'publish'"
x-transition
>
<!-- Before publishing --> <!-- Before publishing -->
<div x-show="!pagePublished" class="sd-publish-hero"> <div x-show="!pagePublished" class="sd-publish-hero">
<div class="sd-publish-preview"> <div class="sd-publish-preview">
<div class="sd-preview-bar"></div> <div class="sd-preview-bar"></div>
<div class="sd-preview-title">🌐 {{ page.title }}</div> <div class="sd-preview-title">
🌐 {{ page.title }}
</div>
</div> </div>
<h3 class="sd-publish-heading">Publish to web</h3> <h3 class="sd-publish-heading">Publish to web</h3>
<p class="sd-publish-desc">Make this page visible to anyone on the internet. You can share the link with anyone.</p> <p class="sd-publish-desc">
<button class="sd-btn-primary sd-btn-full" @click="pagePublished = true">Publish</button> Make this page visible to anyone on the internet.
<p class="sd-publish-note">Your page will be visible to anyone with the link.</p> You can share the link with anyone.
</p>
<button
class="sd-btn-primary sd-btn-full"
@click="pagePublished = true"
>
Publish
</button>
<p class="sd-publish-note">
Your page will be visible to anyone with the link.
</p>
</div> </div>
<!-- After publishing --> <!-- After publishing -->
<div x-show="pagePublished"> <div x-show="pagePublished">
<div class="sd-url-bar"> <div class="sd-url-bar">
<span style="font-size:13px;color:var(--text-dim);">{{ workspace_key or 'flowdeck' }}</span> <span
<input type="text" class="sd-url-input" :value="publishedUrl || pageUrl" readonly @click="$event.target.select()"> style="font-size: 13px; color: var(--text-dim)"
<button class="sd-btn-primary" style="height:32px;padding:0 12px;font-size:12px;" @click="copyPageLink">Copy link</button> >{{ workspace_key or 'flowdeck' }}</span
>
<input
type="text"
class="sd-url-input"
:value="publishedUrl || pageUrl"
readonly
@click="$event.target.select()"
/>
<button
class="sd-btn-primary"
style="
height: 32px;
padding: 0 12px;
font-size: 12px;
"
@click="copyPageLink"
>
Copy link
</button>
</div> </div>
<div class="sd-sep"></div> <div class="sd-sep"></div>
@@ -154,30 +303,41 @@
<div class="sd-settings"> <div class="sd-settings">
<div class="sd-setting-row"> <div class="sd-setting-row">
<span>🔗 Link expires</span> <span>🔗 Link expires</span>
<span style="color:var(--text-dim);">Never ▾</span> <span style="color: var(--text-dim)"
>Never ▾</span
>
</div> </div>
<div class="sd-setting-row"> <div class="sd-setting-row">
<span>🔍 Search engine indexing</span> <span>🔍 Search engine indexing</span>
<span style="color:var(--text-dim);">Off ▾</span> <span style="color: var(--text-dim)"
>Off ▾</span
>
</div> </div>
<div class="sd-setting-row"> <div class="sd-setting-row">
<span>📄 Allow duplicate as template</span> <span>📄 Allow duplicate as template</span>
<label class="toggle-switch sm"> <label class="toggle-switch sm">
<input type="checkbox" checked> <input type="checkbox" checked />
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
<div class="sd-setting-row"> <div class="sd-setting-row">
<span>✏️ Allow editing</span> <span>✏️ Allow editing</span>
<label class="toggle-switch sm"> <label class="toggle-switch sm">
<input type="checkbox" x-model="pubAllowEdit"> <input
type="checkbox"
x-model="pubAllowEdit"
/>
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
<div class="sd-setting-row"> <div class="sd-setting-row">
<span>💬 Allow comments</span> <span>💬 Allow comments</span>
<label class="toggle-switch sm"> <label class="toggle-switch sm">
<input type="checkbox" x-model="pubAllowComments" checked> <input
type="checkbox"
x-model="pubAllowComments"
checked
/>
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
@@ -185,18 +345,32 @@
<div class="sd-sep"></div> <div class="sd-sep"></div>
<button class="sd-danger-btn" @click="pagePublished = false">Unpublish</button> <button
class="sd-danger-btn"
@click="pagePublished = false"
>
Unpublish
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- More menu dropdown --> <!-- More menu dropdown -->
<div class="more-menu" x-show="moreOpen" @click.away="moreOpen = false" x-transition> <div
class="more-menu"
x-show="moreOpen"
@click.away="moreOpen = false"
x-transition
>
<div class="more-menu-item" @click="exportPage">↗ Export</div> <div class="more-menu-item" @click="exportPage">↗ Export</div>
<div class="more-menu-item" @click="duplicatePage">📋 Duplicate</div> <div class="more-menu-item" @click="duplicatePage">
📋 Duplicate
</div>
<div class="more-menu-item" @click="movePage">↗ Move to</div> <div class="more-menu-item" @click="movePage">↗ Move to</div>
<div class="more-menu-sep"></div> <div class="more-menu-sep"></div>
<div class="more-menu-item danger" @click="deletePage">🗑️ Move to Trash</div> <div class="more-menu-item danger" @click="deletePage">
🗑️ Move to Trash
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -205,18 +379,37 @@
<div class="page-cover-area"> <div class="page-cover-area">
<div class="page-title-block"> <div class="page-title-block">
<span class="page-icon-emoji">📄</span> <span class="page-icon-emoji">📄</span>
<div class="page-title-input" contenteditable="true" id="_titleEl" <div
class="page-title-input"
contenteditable="true"
id="_titleEl"
@input="dirty=true;save()" @input="dirty=true;save()"
@keydown.enter.prevent="focusBlock()" @keydown.enter.prevent="focusBlock()"
@paste.prevent="pastePlain($event)" @paste.prevent="pastePlain($event)"
spellcheck="false">{{ page.title }}</div> spellcheck="false"
>
{{ page.title }}
</div>
</div> </div>
</div> </div>
<div class="blocks-container" id="_blocksCt" @click="onCtClick($event)"></div> <div
<div id="_slashMenu" class="slash-menu" style="display:none;position:fixed;z-index:999"></div> class="blocks-container"
id="_blocksCt"
@click="onCtClick($event)"
></div>
<div
id="_slashMenu"
class="slash-menu"
style="display: none; position: fixed; z-index: 999"
></div>
<div class="format-toolbar" x-show="fmt.open" :style="{top:fmt.top+'px',left:fmt.left+'px'}" @mousedown.prevent> <div
class="format-toolbar"
x-show="fmt.open"
:style="{top:fmt.top+'px',left:fmt.left+'px'}"
@mousedown.prevent
>
<button @click="fmtApply('bold')"><strong>B</strong></button> <button @click="fmtApply('bold')"><strong>B</strong></button>
<button @click="fmtApply('italic')"><em>I</em></button> <button @click="fmtApply('italic')"><em>I</em></button>
<button @click="fmtApply('underline')"><u>U</u></button> <button @click="fmtApply('underline')"><u>U</u></button>
@@ -225,27 +418,81 @@
</div> </div>
<!-- ═══════════ Copy Link Toast ═══════════ --> <!-- ═══════════ Copy Link Toast ═══════════ -->
<div class="sd-toast" x-show="toastVisible" x-transition x-text="toastMsg"></div> <div
class="sd-toast"
x-show="toastVisible"
x-transition
x-text="toastMsg"
></div>
<!-- ═══════════ Get Started Toolbar (bottom) ═══════════ --> <!-- ═══════════ Get Started Toolbar (bottom) ═══════════ -->
<div class="get-started-toolbar" x-show="blocks.length === 1 && blocks[0].type === 'paragraph' && !blocks[0].content.trim()"> <div
class="get-started-toolbar"
x-show="blocks.length === 1 && blocks[0].type === 'paragraph' && !blocks[0].content.trim()"
>
<span class="gs-label">Get started with</span> <span class="gs-label">Get started with</span>
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Ask AI to write...';render()"><span class="gs-icon">✨</span> Ask AI</button> <button
<button class="gs-pill" @click="replaceBlock(0, 'heading_1');blocks[0].content='AI Meeting Note';render()"><span class="gs-icon">📝</span> AI meeting note</button> class="gs-pill"
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Database';render()"><span class="gs-icon">🗄️</span> Database</button> @click="replaceBlock(0, 'paragraph');blocks[0].content='Ask AI to write...';render()"
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Form';render()"><span class="gs-icon">📋</span> Form</button> >
<button class="gs-pill" @click="replaceBlock(0, 'paragraph');blocks[0].content='Templates';render()"><span class="gs-icon">📑</span> Templates</button> <span class="gs-icon">✨</span> Ask AI
<div style="position:relative;"> </button>
<button class="gs-pill" @click="gsMoreOpen = !gsMoreOpen"><span>⋯</span></button> <button
<div class="gs-more-dropdown" x-show="gsMoreOpen" @click.away="gsMoreOpen = false" x-transition> class="gs-pill"
<div class="gs-more-item" @click="gsMoreOpen=false">📊 Table</div> @click="replaceBlock(0, 'heading_1');blocks[0].content='AI Meeting Note';render()"
<div class="gs-more-item" @click="gsMoreOpen=false">📋 Board</div> >
<div class="gs-more-item" @click="gsMoreOpen=false">📝 List</div> <span class="gs-icon">📝</span> AI meeting note
<div class="gs-more-item" @click="gsMoreOpen=false">📅 Timeline</div> </button>
<div class="gs-more-item" @click="gsMoreOpen=false">🗓️ Calendar</div> <button
<div class="gs-more-item" @click="gsMoreOpen=false">🖼️ Gallery</div> class="gs-pill"
@click="replaceBlock(0, 'paragraph');blocks[0].content='Database';render()"
>
<span class="gs-icon">🗄️</span> Database
</button>
<button
class="gs-pill"
@click="replaceBlock(0, 'paragraph');blocks[0].content='Form';render()"
>
<span class="gs-icon">📋</span> Form
</button>
<button
class="gs-pill"
@click="replaceBlock(0, 'paragraph');blocks[0].content='Templates';render()"
>
<span class="gs-icon">📑</span> Templates
</button>
<div style="position: relative">
<button class="gs-pill" @click="gsMoreOpen = !gsMoreOpen">
<span>⋯</span>
</button>
<div
class="gs-more-dropdown"
x-show="gsMoreOpen"
@click.away="gsMoreOpen = false"
x-transition
>
<div class="gs-more-item" @click="gsMoreOpen=false">
📊 Table
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
📋 Board
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
📝 List
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
📅 Timeline
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
🗓️ Calendar
</div>
<div class="gs-more-item" @click="gsMoreOpen=false">
🖼️ Gallery
</div>
<div class="gs-more-sep"></div> <div class="gs-more-sep"></div>
<div class="gs-more-item" @click="gsMoreOpen=false">📥 Import</div> <div class="gs-more-item" @click="gsMoreOpen=false">
📥 Import
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -253,15 +500,16 @@
<div class="editor-statusbar"> <div class="editor-statusbar">
<span x-show="saving">Saving...</span> <span x-show="saving">Saving...</span>
<span x-show="!saving&&dirty">Unsaved</span> <span x-show="!saving&&dirty">Unsaved</span>
<span x-show="!saving&&!dirty&&lastSaved">Saved <span x-text="lastSaved"></span></span> <span x-show="!saving&&!dirty&&lastSaved"
>Saved <span x-text="lastSaved"></span
></span>
<span class="statusbar-right" x-text="blocks.length+' blocks'"></span> <span class="statusbar-right" x-text="blocks.length+' blocks'"></span>
</div> </div>
</div> </div>
{% endblock %} {% endblock %} {% block scripts %}
<script type="application/json" id="page-data">
{% block scripts %} {{ page_data | tojson }}
<script type="application/json" id="page-data">{{ page_data | tojson }}</script> </script>
<script> <script>
const CMDS=[{name:'BASIC',items:[ 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:'paragraph',name:'Text',icon:'¶'},{id:'heading_1',name:'Heading 1',icon:'H1'},{id:'heading_2',name:'Heading 2',icon:'H2'},
@@ -342,11 +590,12 @@ function editorState(){
pageUrl:window.location.href,publishedUrl:'', pageUrl:window.location.href,publishedUrl:'',
inviteEmail:'',invitePermission:'editor',accessList:[], inviteEmail:'',invitePermission:'editor',accessList:[],
toastVisible:false,toastMsg:'', toastVisible:false,toastMsg:'',
init(){ init(){
_bid=Math.floor(Math.random()*10000); _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==='blocks'&&raw){try{this.blocks=JSON.parse(raw);this.blocks.forEach(b=>{if(!b.id)b.id=genId()});}catch(e){this.blocks=[];}} 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); else if(raw&&raw.trim())this.blocks=this.md2b(raw);}catch(e){}}
if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')]; if(!this.blocks.length)this.blocks=[this.mkB('paragraph','')];
window.E=this;SM.init();this.render(); window.E=this;SM.init();this.render();
setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return; setTimeout(()=>{this.focusBlock();const ct=document.getElementById('_blocksCt');if(!ct)return;
@@ -478,7 +727,6 @@ function editorState(){
}, },
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;}, 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;},
}; };
return self;
} }
document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}},80);}); document.addEventListener('mouseup',()=>{setTimeout(()=>{const s=window.getSelection();if(!s||s.isCollapsed)return;const ed=document.querySelector('.page-editor-wrapper');if(!ed?.__x)return;const d=ed.__x.$data;const ct=document.getElementById('_blocksCt');if(!ct?.contains(s.anchorNode))return;const bel=s.anchorNode.parentElement?.closest('[data-bid]');if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);}},80);});
</script> </script>