domma-cms 1.10.32 → 1.10.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.34] - 2026-10-27
4
+
5
+ - Scan rendered page previews for external image domains, show blocked hosts in the editor, and let users with site settings permission allow individual domains. Image policy updates apply without restarting; an open editor can reload its preview policy while retaining unsaved drafts.
6
+
7
+ ## [1.10.33] - 2026-10-26
8
+
9
+ - Group newer Tools into purpose-based submenus, including business, marketing, publishing, maintenance and games. Upgrade saved loose entries once while preserving labels, visibility, permissions and custom submenus.
10
+
3
11
  ## [1.10.32] - 2026-10-26
4
12
 
5
13
  - Add clickable (?) explanations throughout the Forms editor, including submission actions, triggers, conditions, field options and chooser controls. Newly added controls receive help immediately; help buttons preserve checkbox and panel state.
@@ -0,0 +1,2 @@
1
+ export function imageReferences(o){const s=[];for(const t of o.querySelectorAll('img[src], input[type="image"][src], video[poster], svg image')){const r=t.getAttribute("src")||t.getAttribute("poster")||t.getAttribute("href")||t.getAttribute("xlink:href");r&&s.push(r)}for(const t of o.querySelectorAll("img[srcset], picture source[srcset]"))for(const r of t.getAttribute("srcset").matchAll(/(?:^|,\s*)((?:https?:)?\/\/[^\s,]+)/g))s.push(r[1]);const i=[...o.querySelectorAll("[style]")].map(t=>t.getAttribute("style")).concat([...o.querySelectorAll("style")].map(t=>t.textContent)).join(`
2
+ `);for(const t of i.matchAll(/url\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^\s)]+))\s*\)/gi))s.push(t[1]??t[2]??t[3]);return s}export function externalImageHosts(o,s,i=[]){const t=new URL(s),r=new Set(i),n=new Map;for(const c of o){let e;try{e=new URL(c,t)}catch{continue}if(!["https:","http:"].includes(e.protocol)||e.origin===t.origin)continue;const l=e.protocol==="https:"&&!e.port&&/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(e.hostname),a=l&&r.has(e.hostname);n.has(e.origin)||n.set(e.origin,{origin:e.origin,host:e.hostname,allowed:a,eligible:l,count:0,status:a?"Allowed":l?"Blocked by image policy":"Use an HTTPS URL with a standard hostname and port"}),n.get(e.origin).count++}return[...n.values()].sort((c,e)=>c.origin.localeCompare(e.origin))}
@@ -118,6 +118,30 @@
118
118
  <div class="pe-stage" hidden data-bind-hidden="!active.value">
119
119
  <!-- On a block's or a component's tab the Markdown tools step aside; the layout buttons and fullscreen stay. -->
120
120
  <div class="editor-toolbar" id="editor-toolbar" data-bind-class="(blockTab.value || compTab.value) && 'pe-toolbar--block'"></div>
121
+ <details class="card" id="page-image-permissions" style="flex:none;max-height:30vh;overflow:auto;margin:0">
122
+ <summary class="p-2">External images <span data-bind-text="imagePolicyReady.value && imageScanned.value ? (imageBlocked.value ? '(' + imageBlocked.value + ' blocked)' : (imageHosts.value.length ? '(all allowed)' : '(none found)')) : ''"></span></summary>
123
+ <div class="card-body">
124
+ <p class="form-hint">Checks the current rendered page, including blocks, shortcodes and inline backgrounds. Allowing a domain lets images from it appear anywhere on this site.</p>
125
+ <button type="button" class="btn btn-secondary btn-sm" data-on-click="scanImages" data-bind-disabled="imageScanning.value"
126
+ data-bind-text="imageScanning.value ? 'Scanning...' : 'Scan images'">Scan images</button>
127
+ <p role="status" hidden data-bind-hidden="!imageError.value" data-bind-text="imageError.value"></p>
128
+ <p hidden data-bind-hidden="!imageScanned.value || imageHosts.value.length">No external images found in the rendered markup.</p>
129
+ <p hidden data-bind-hidden="!imagePolicyReady.value || imageCanAllow.value">Permission to update site settings is required to allow a domain.</p>
130
+ <div data-each="imageHosts.value key=origin">
131
+ <div class="mt-2" style="display:flex;flex-wrap:wrap;align-items:center;gap:.5rem">
132
+ <code style="overflow-wrap:anywhere" data-bind-text="origin"></code>
133
+ <span data-bind-text="$root.imagePolicyReady.value ? status : 'Checking permissions...'"></span>
134
+ <button type="button" class="btn btn-secondary btn-sm"
135
+ hidden data-bind-hidden="allowed || !eligible || !$root.imageCanAllow.value || !$root.imagePolicyReady.value"
136
+ data-bind-disabled="$root.imageAllowing.value"
137
+ data-on-click="$root.allowImageHost(host)">Allow images from this domain</button>
138
+ </div>
139
+ </div>
140
+ <p hidden data-bind-hidden="!imageReload.value" role="status">The domain is allowed. Reload this editor to refresh its preview policy. Unsaved changes are kept as a draft.
141
+ <button type="button" class="btn btn-ghost btn-sm" data-on-click="reloadImagePolicy">Reload editor</button>
142
+ </p>
143
+ </div>
144
+ </details>
121
145
  <div class="editor-body" id="editor-body" data-bind-class="'editor-mode-' + viewMode.value">
122
146
  <div class="editor-pane editor-pane--write">
123
147
  <!-- The body is not a data-model: the folding manager owns what the textarea shows. -->
@@ -1,4 +1,4 @@
1
- import{api as h}from"../api.js";import{CardBuilder as tn}from"../lib/card-builder.js";import{TimelineBuilder as on}from"../lib/timeline-builder.js";import{attachEditorKeybindings as an,createToolbar as sn,insertAtCursor as nn}from"../lib/markdown-toolbar.js";import{EffectsBuilder as rn}from"../lib/effects-builder.js";import{attachContextMenu as ln}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as cn}from"../lib/shortcode-insert.js";import{populateThemeSelect as dn}from"../lib/themes.js";import{createFoldingManager as pn}from"../lib/editor-folding.js";import{openScribeComposer as un}from"../lib/scribe-composer.js";import{openCrudTutorial as mn}from"../lib/crud-tutorial.js";import{addBannerAction as dt}from"../lib/plugin-chrome.js";import{exampleJson as fn,parseSchema as Po,schemaText as bn}from"../lib/seo-structured-data.js";import{helpPopovers as qa}from"../lib/help-popover.js";import{onceClosed as Bt,openMenuAt as Ce,sidebarCount as pt}from"../lib/tool-kit.js";import{pickMedia as vn}from"../lib/media-picker.js";import{openPageClone as gn}from"../lib/page-clone.js";import{openEditorReference as Ha}from"../lib/page-editor-reference.js";import{dconfigNotes as hn}from"../lib/dconfig-check.js";import{catalogueNow as yn,setCatalogue as kn}from"../lib/domma-catalogue.js";import{cleanType as It,DEFAULT_TYPE as Ft,EDITOR_FONTS as wn,fontHref as Sn,isDefaultType as En,LINE_HEIGHTS as Mn,SIZES as Tn,typeVars as Va}from"../lib/editor-type.js";import{getProjectFromHash as zt}from"../lib/project-context.js";import{attachSmoothCaret as _a,getCaretPreference as Cn,setCaretPreference as xn}from"../lib/smooth-caret.js";import{SCREEN as Oo,bannerSection as Pn,draftIsStale as On,draftKey as ze,initialProject as Rn,makeDraft as Ka,problems as Ro,readDraft as An,snapshot as W}from"../lib/page-editor-arrange.js";import{fmtDateTime as qe}from"../lib/dates.js";import{hasDraft as xe,heldBlockEdits as $n,hold as Ao,takeHeld as $o,wireHeldRouter as Ln}from"../lib/editor-hop.js";import{hopTarget as Ua}from"../lib/editor-hop-arrange.js";import{checkNewName as Nn,copyName as Lo,usageText as Dn}from"../lib/blocks-arrange.js";import{isCollapsed as Ja,setCollapsed as Wa}from"../lib/sidebar-size.js";import{blockTagDefs as Ya,blocksInBody as jn}from"../lib/block-tags.js";import{draftKey as He,draftOffer as Bn,extractPlaceholders as Ga,formFromBlock as Xa,payloadFromForm as Za,rawPlaceholders as No,shortcodeFor as In,snapshot as Do}from"../lib/block-editor-arrange.js";import{blockTabs as Fn,notHereLine as zn,otherUses as Qa,outlineBlock as qn,previewBlocks as Hn,saveQuestion as Vn,savedMessage as _n,usesLine as Kn}from"../lib/page-blocks-arrange.js";import{addressFor as es,blockAloneBody as Un,blockRows as Jn,cleanLayout as jo,closeTab as Wn,componentRows as Yn,componentStatusText as Gn,componentsInBody as Xn,moveTab as Zn,nextLayout as Qn,openTab as Bo,pageRows as er,pageStatusText as tr,rememberedTabs as or,renameTab as ar,sampleRows as ts,saveAllLabel as sr,startingTabs as nr,tabId as X,tabItem as I,tabsToClose as Io,workspaceKey as rr}from"../lib/editor-workspace-arrange.js";import{PANES as qt,STARTER_SOURCE as ir,assembleSource as os,attributeFor as lr,draftKey as Ve,draftStatus as cr,nameProblem as dr,previewRows as as,readValue as pr,snapshot as Fo,splitSource as ut,writeValue as ss}from"../lib/component-editor-arrange.js";import{attachColourLayer as ns,createPlainEditor as zo}from"../lib/plain-editor.js";import{cleanCodePrefs as Ht,codePrefs as ur,DEFAULT_CODE_PREFS as Vt,formatTextarea as _e,INDENT_SIZES as mr,setCodePrefs as fr}from"../lib/code-format.js";function mt(d){const b=document.createElement("template");return b.innerHTML=d.trim(),b.content.firstElementChild}const br=qe,rs=d=>qe(d.createdAt)+(d.label?` - ${d.label}`:""),vr=`
1
+ import{api as g}from"../api.js";import{CardBuilder as ln}from"../lib/card-builder.js";import{TimelineBuilder as cn}from"../lib/timeline-builder.js";import{attachEditorKeybindings as dn,createToolbar as pn,insertAtCursor as un}from"../lib/markdown-toolbar.js";import{EffectsBuilder as mn}from"../lib/effects-builder.js";import{attachContextMenu as fn}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as bn}from"../lib/shortcode-insert.js";import{populateThemeSelect as vn}from"../lib/themes.js";import{createFoldingManager as gn}from"../lib/editor-folding.js";import{openScribeComposer as hn}from"../lib/scribe-composer.js";import{openCrudTutorial as yn}from"../lib/crud-tutorial.js";import{addBannerAction as ut}from"../lib/plugin-chrome.js";import{exampleJson as kn,parseSchema as Aa,schemaText as wn}from"../lib/seo-structured-data.js";import{helpPopovers as Uo}from"../lib/help-popover.js";import{onceClosed as qt,openMenuAt as Ce,sidebarCount as mt}from"../lib/tool-kit.js";import{pickMedia as Sn}from"../lib/media-picker.js";import{openPageClone as En}from"../lib/page-clone.js";import{openEditorReference as Jo}from"../lib/page-editor-reference.js";import{dconfigNotes as Mn}from"../lib/dconfig-check.js";import{catalogueNow as Tn,setCatalogue as Cn}from"../lib/domma-catalogue.js";import{cleanType as Ht,DEFAULT_TYPE as _t,EDITOR_FONTS as xn,fontHref as Pn,isDefaultType as On,LINE_HEIGHTS as An,SIZES as Rn,typeVars as Wo}from"../lib/editor-type.js";import{getProjectFromHash as Vt}from"../lib/project-context.js";import{attachSmoothCaret as Yo,getCaretPreference as $n,setCaretPreference as Ln}from"../lib/smooth-caret.js";import{SCREEN as Ra,bannerSection as Nn,draftIsStale as Dn,draftKey as ze,initialProject as In,makeDraft as Go,problems as $a,readDraft as Bn,snapshot as W}from"../lib/page-editor-arrange.js";import{externalImageHosts as jn,imageReferences as Fn}from"../lib/page-image-hosts.js";import{fmtDateTime as qe}from"../lib/dates.js";import{hasDraft as xe,heldBlockEdits as zn,hold as La,takeHeld as Na,wireHeldRouter as qn}from"../lib/editor-hop.js";import{hopTarget as Xo}from"../lib/editor-hop-arrange.js";import{checkNewName as Hn,copyName as Da,usageText as _n}from"../lib/blocks-arrange.js";import{isCollapsed as Zo,setCollapsed as Qo}from"../lib/sidebar-size.js";import{blockTagDefs as es,blocksInBody as Vn}from"../lib/block-tags.js";import{draftKey as He,draftOffer as Kn,extractPlaceholders as ts,formFromBlock as as,payloadFromForm as os,rawPlaceholders as Ia,shortcodeFor as Un,snapshot as Ba}from"../lib/block-editor-arrange.js";import{blockTabs as Jn,notHereLine as Wn,otherUses as ss,outlineBlock as Yn,previewBlocks as Gn,saveQuestion as Xn,savedMessage as Zn,usesLine as Qn}from"../lib/page-blocks-arrange.js";import{addressFor as ns,blockAloneBody as er,blockRows as tr,cleanLayout as ja,closeTab as ar,componentRows as or,componentStatusText as sr,componentsInBody as nr,moveTab as rr,nextLayout as lr,openTab as Fa,pageRows as ir,pageStatusText as cr,rememberedTabs as dr,renameTab as pr,sampleRows as rs,saveAllLabel as ur,startingTabs as mr,tabId as X,tabItem as j,tabsToClose as za,workspaceKey as fr}from"../lib/editor-workspace-arrange.js";import{PANES as Kt,STARTER_SOURCE as br,assembleSource as ls,attributeFor as vr,draftKey as _e,draftStatus as gr,nameProblem as hr,previewRows as is,readValue as yr,snapshot as qa,splitSource as ft,writeValue as cs}from"../lib/component-editor-arrange.js";import{attachColourLayer as ds,createPlainEditor as Ha}from"../lib/plain-editor.js";import{cleanCodePrefs as Ut,codePrefs as kr,DEFAULT_CODE_PREFS as Jt,formatTextarea as Ve,INDENT_SIZES as wr,setCodePrefs as Sr}from"../lib/code-format.js";function bt(d){const b=document.createElement("template");return b.innerHTML=d.trim(),b.content.firstElementChild}const Er=qe,ps=d=>qe(d.createdAt)+(d.label?` - ${d.label}`:""),Mr=`
2
2
  <div class="pe-share">
3
3
  <p class="form-hint" style="margin-top:0">Anyone with the link can view this page even while it is unpublished, without
4
4
  signing in. The link works for this page only, expires on its own, and can be revoked at any time.</p>
@@ -30,7 +30,7 @@ import{api as h}from"../api.js";import{CardBuilder as tn}from"../lib/card-builde
30
30
  .pe-share [hidden] { display: none !important; }
31
31
  .pe-share-row { display: flex; align-items: center; gap: .6rem; padding: .5rem 0; border-bottom: 1px solid var(--dm-border-light); }
32
32
  .pe-share-text { flex: 1 1 auto; min-width: 0; font-size: .82rem; }
33
- </style>`;function is(d){const b=Sn(d);if(!b||document.querySelector(`link[data-pe-font="${CSS.escape(d)}"]`))return;const v=Object.assign(document.createElement("link"),{rel:"stylesheet",href:b});v.dataset.peFont=d,document.head.appendChild(v)}const gr=`
33
+ </style>`;function us(d){const b=Pn(d);if(!b||document.querySelector(`link[data-pe-font="${CSS.escape(d)}"]`))return;const v=Object.assign(document.createElement("link"),{rel:"stylesheet",href:b});v.dataset.peFont=d,document.head.appendChild(v)}const Tr=`
34
34
  <div class="pe-settings">
35
35
  <p class="pe-settings-note">How the editor looks to you - kept in this browser, for every page.</p>
36
36
  <label class="form-label" for="pe-set-font">Font <span data-help="The lettering in the editor (not on the site). Monospace keeps every character the same width, so shortcodes and Markdown line up."></span></label>
@@ -94,17 +94,17 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
94
94
  .pe-settings-sample { white-space: pre-wrap; padding: .75rem 1rem; border-radius: var(--dm-radius-md); border: 1px solid var(--dm-border);
95
95
  background: var(--dm-background-alt); font-family: var(--pe-font); font-size: var(--pe-size); line-height: var(--pe-line); }
96
96
  .pe-settings-actions { display: flex; gap: .5rem; border-top: 1px solid var(--dm-border); padding-top: .75rem; }
97
- </style>`;function hr(d,b){const v=E.slideover({title:"Editor settings",size:"sm",position:"right"}),i=mt(`<div>${gr}</div>`),k=It(d.peek()),u={font:M.observable(k.font),size:M.observable(String(k.size)),line:M.observable(String(k.line)),wrap:M.observable(k.wrap),caret:M.observable(Cn()),sidebar:M.observable(Pe().closeSidebar===!0)},w=Ht(b.peek());u.indent=M.observable(w.indent),u.indentSize=M.observable(String(w.size));for(const T of["detect","autoIndent","continueLists","shortTags","tables","html","formatOnSave"])u[T]=M.observable(w[T]);const y=M.computed(()=>Ht({indent:u.indent.value,size:u.indentSize.value,detect:u.detect.value,autoIndent:u.autoIndent.value,continueLists:u.continueLists.value,shortTags:u.shortTags.value,tables:u.tables.value,html:u.html.value,formatOnSave:u.formatOnSave.value})),x=M.effect(()=>{const T=y.value;JSON.stringify(T)!==JSON.stringify(Ht(b.peek()))&&(b.value=T,Ke("code",T))}),j=M.effect(()=>{const T=u.sidebar.value;T!==(Pe().closeSidebar===!0)&&Ke("closeSidebar",T)}),c=M.computed(()=>It({font:u.font.value,size:u.size.value,line:u.line.value,wrap:u.wrap.value}));u.isDefault=M.computed(()=>En(c.value)&&u.caret.value==="auto"&&!u.sidebar.value&&JSON.stringify(y.value)===JSON.stringify(Vt));const A=M.effect(()=>xn(u.caret.value)),H=i.querySelector("#pe-set-font");for(const[T,ne]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const Oe=wn.filter(re=>re.kind===T),O=ne?Object.assign(document.createElement("optgroup"),{label:ne}):H.querySelector("optgroup:last-of-type");O.append(...Oe.map(re=>new Option(re.label,re.id))),ne&&H.appendChild(O)}i.querySelector("#pe-set-indent-size").append(...mr.map(T=>new Option(String(T),String(T)))),i.querySelector("#pe-set-size").append(...Tn.map(T=>new Option(`${T}px`,String(T)))),i.querySelector("#pe-set-line").append(...Mn.map(T=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[T]||String(T),String(T))));const Y=i.querySelector(".pe-settings-sample"),D=M.effect(()=>{const T=c.value;is(T.font);for(const[ne,Oe]of Object.entries(Va(T)))Y.style.setProperty(ne,Oe);Y.style.whiteSpace=T.wrap?"pre-wrap":"pre",Y.style.overflowX=T.wrap?"":"auto",JSON.stringify(T)!==JSON.stringify(It(d.peek()))&&(d.value=T,Ke("type",T))});u.reset=()=>{u.font.value=Ft.font,u.size.value=String(Ft.size),u.line.value=String(Ft.line),u.wrap.value=Ft.wrap,u.caret.value="auto",u.sidebar.value=!1,u.indent.value=Vt.indent,u.indentSize.value=String(Vt.size);for(const T of["detect","autoIndent","continueLists","shortTags","tables","html","formatOnSave"])u[T].value=Vt[T]},u.done=()=>v.close(),v.element.appendChild(i);const se=M.applyBindings(u,i),_t=qa(i);return Bt(v,()=>{se.dispose(),_t(),D?.(),A?.(),j?.(),x?.()}),Domma.icons?.scan?.(i),v.open(),v}async function yr(d){const b=E.slideover({title:"Share preview link",size:"md",position:"right"}),v=mt(`<div>${vr}</div>`),i={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},k=async()=>{try{const w=await h.pages.previewLinks.list(d);i.links.value=w.filter(y=>!y.revokedAt&&!y.expired).map(y=>({id:y.id,title:y.label||"Untitled link",meta:`Expires ${br(y.expiresAt)} - ${y.useCount===1?"1 view":`${y.useCount||0} views`}`})),i.listError.value=""}catch(w){i.listError.value="Could not load existing links: "+(w.message||"unknown error")}finally{i.loaded.value=!0}};i.create=async()=>{i.creating.value=!0;try{const w=await h.pages.previewLinks.create(d,i.expiry.peek(),i.note.peek().trim());i.url.value=w.url,i.note.value="",E.toast("Share link created.",{type:"success"}),await k(),v.querySelector(".pe-share-url")?.select()}catch(w){E.toast("Could not create link: "+(w.message||"unknown error"),{type:"error"})}finally{i.creating.value=!1}},i.copy=async()=>{try{await navigator.clipboard.writeText(i.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{v.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},i.revoke=async w=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await h.pages.previewLinks.revoke(w),E.toast("Link revoked.",{type:"success"}),await k()}catch(y){E.toast("Could not revoke: "+(y.message||"unknown error"),{type:"error"})}};const u=M.applyBindings(i,v);Bt(b,()=>u.dispose()),b.element.appendChild(v),b.open(),await k()}function kr(d){return new Promise(b=>{const v=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),i=mt(`
97
+ </style>`;function Cr(d,b){const v=E.slideover({title:"Editor settings",size:"sm",position:"right"}),l=bt(`<div>${Tr}</div>`),k=Ht(d.peek()),u={font:M.observable(k.font),size:M.observable(String(k.size)),line:M.observable(String(k.line)),wrap:M.observable(k.wrap),caret:M.observable($n()),sidebar:M.observable(Pe().closeSidebar===!0)},w=Ut(b.peek());u.indent=M.observable(w.indent),u.indentSize=M.observable(String(w.size));for(const T of["detect","autoIndent","continueLists","shortTags","tables","html","formatOnSave"])u[T]=M.observable(w[T]);const y=M.computed(()=>Ut({indent:u.indent.value,size:u.indentSize.value,detect:u.detect.value,autoIndent:u.autoIndent.value,continueLists:u.continueLists.value,shortTags:u.shortTags.value,tables:u.tables.value,html:u.html.value,formatOnSave:u.formatOnSave.value})),x=M.effect(()=>{const T=y.value;JSON.stringify(T)!==JSON.stringify(Ut(b.peek()))&&(b.value=T,Ke("code",T))}),I=M.effect(()=>{const T=u.sidebar.value;T!==(Pe().closeSidebar===!0)&&Ke("closeSidebar",T)}),i=M.computed(()=>Ht({font:u.font.value,size:u.size.value,line:u.line.value,wrap:u.wrap.value}));u.isDefault=M.computed(()=>On(i.value)&&u.caret.value==="auto"&&!u.sidebar.value&&JSON.stringify(y.value)===JSON.stringify(Jt));const A=M.effect(()=>Ln(u.caret.value)),H=l.querySelector("#pe-set-font");for(const[T,ne]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const Oe=xn.filter(re=>re.kind===T),O=ne?Object.assign(document.createElement("optgroup"),{label:ne}):H.querySelector("optgroup:last-of-type");O.append(...Oe.map(re=>new Option(re.label,re.id))),ne&&H.appendChild(O)}l.querySelector("#pe-set-indent-size").append(...wr.map(T=>new Option(String(T),String(T)))),l.querySelector("#pe-set-size").append(...Rn.map(T=>new Option(`${T}px`,String(T)))),l.querySelector("#pe-set-line").append(...An.map(T=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[T]||String(T),String(T))));const Y=l.querySelector(".pe-settings-sample"),D=M.effect(()=>{const T=i.value;us(T.font);for(const[ne,Oe]of Object.entries(Wo(T)))Y.style.setProperty(ne,Oe);Y.style.whiteSpace=T.wrap?"pre-wrap":"pre",Y.style.overflowX=T.wrap?"":"auto",JSON.stringify(T)!==JSON.stringify(Ht(d.peek()))&&(d.value=T,Ke("type",T))});u.reset=()=>{u.font.value=_t.font,u.size.value=String(_t.size),u.line.value=String(_t.line),u.wrap.value=_t.wrap,u.caret.value="auto",u.sidebar.value=!1,u.indent.value=Jt.indent,u.indentSize.value=String(Jt.size);for(const T of["detect","autoIndent","continueLists","shortTags","tables","html","formatOnSave"])u[T].value=Jt[T]},u.done=()=>v.close(),v.element.appendChild(l);const se=M.applyBindings(u,l),Wt=Uo(l);return qt(v,()=>{se.dispose(),Wt(),D?.(),A?.(),I?.(),x?.()}),Domma.icons?.scan?.(l),v.open(),v}async function xr(d){const b=E.slideover({title:"Share preview link",size:"md",position:"right"}),v=bt(`<div>${Mr}</div>`),l={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},k=async()=>{try{const w=await g.pages.previewLinks.list(d);l.links.value=w.filter(y=>!y.revokedAt&&!y.expired).map(y=>({id:y.id,title:y.label||"Untitled link",meta:`Expires ${Er(y.expiresAt)} - ${y.useCount===1?"1 view":`${y.useCount||0} views`}`})),l.listError.value=""}catch(w){l.listError.value="Could not load existing links: "+(w.message||"unknown error")}finally{l.loaded.value=!0}};l.create=async()=>{l.creating.value=!0;try{const w=await g.pages.previewLinks.create(d,l.expiry.peek(),l.note.peek().trim());l.url.value=w.url,l.note.value="",E.toast("Share link created.",{type:"success"}),await k(),v.querySelector(".pe-share-url")?.select()}catch(w){E.toast("Could not create link: "+(w.message||"unknown error"),{type:"error"})}finally{l.creating.value=!1}},l.copy=async()=>{try{await navigator.clipboard.writeText(l.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{v.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},l.revoke=async w=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await g.pages.previewLinks.revoke(w),E.toast("Link revoked.",{type:"success"}),await k()}catch(y){E.toast("Could not revoke: "+(y.message||"unknown error"),{type:"error"})}};const u=M.applyBindings(l,v);qt(b,()=>u.dispose()),b.element.appendChild(v),b.open(),await k()}function Pr(d){return new Promise(b=>{const v=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),l=bt(`
98
98
  <div style="padding:1rem">
99
99
  <label class="form-label" for="pe-version-label">Version Label</label>
100
100
  <input id="pe-version-label" type="text" class="form-input" style="margin-bottom:.75rem"
101
101
  placeholder="e.g. Initial design, Before restructure" data-model="label.value" data-on-keydown="key">
102
102
  <button type="button" class="btn btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save Version</button>
103
- </div>`);let k=!1;const u={label:M.observable(""),saving:M.observable(!1)};u.save=async()=>{u.saving.value=!0;try{await h.versions.create(d,u.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),k=!0,v.close()}catch(y){E.toast("Failed to save version: "+y.message,{type:"error"})}finally{u.saving.value=!1}},u.key=y=>{y.key==="Enter"&&(y.preventDefault(),u.save())};const w=M.applyBindings(u,i);Bt(v,()=>{w.dispose(),b(k)}),v.element.appendChild(i),v.open(),setTimeout(()=>i.querySelector("input")?.focus(),100)})}const wr=`<div class="card">
103
+ </div>`);let k=!1;const u={label:M.observable(""),saving:M.observable(!1)};u.save=async()=>{u.saving.value=!0;try{await g.versions.create(d,u.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),k=!0,v.close()}catch(y){E.toast("Failed to save version: "+y.message,{type:"error"})}finally{u.saving.value=!1}},u.key=y=>{y.key==="Enter"&&(y.preventDefault(),u.save())};const w=M.applyBindings(u,l);qt(v,()=>{w.dispose(),b(k)}),v.element.appendChild(l),v.open(),setTimeout(()=>l.querySelector("input")?.focus(),100)})}const Or=`<div class="card">
104
104
  <h3>{{title}}</h3>
105
105
  <p>{{description}}</p>
106
106
  </div>
107
- `,Sr=`
107
+ `,Ar=`
108
108
  <div class="pe-new">
109
109
  <p class="form-hint" style="margin-top:0" data-bind-text="hint.value"></p>
110
110
  <label class="form-label" for="pe-new-name">Name</label>
@@ -124,7 +124,7 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
124
124
  .pe-new [hidden] { display: none !important; }
125
125
  .pe-new-err { display: block; margin-top: .3rem; color: var(--dm-danger); font-size: var(--dm-text-xs); }
126
126
  .pe-new-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: 1.25rem; }
127
- </style>`;function ls({kind:d,projects:b,taken:v,project:i="",name:k="",check:u,create:w,register:y=()=>{}}){return new Promise(x=>{const j=E.slideover({title:d==="block"?"New block":"New component",size:"sm",position:"right"});y(()=>{try{j.close()}catch{}});const c=mt(`<div>${Sr}</div>`);for(const D of b)c.querySelector("#pe-new-project").append(new Option(D.name||D.slug,D.slug));const A={name:M.observable(k),project:M.observable(i),busy:M.observable(!1),hint:M.observable(d==="block"?"A block is a reusable piece of HTML with {{placeholders}}, used on pages with its short tag or by a collection display. Its name is its tag - fixed once created.":"A component is a custom element - <dm-name> - with a template, props, a script and a style. Its name is its tag - fixed once created."),placeholder:M.observable(d==="block"?"e.g. team-card":"e.g. pricing-table")};A.error=M.computed(()=>A.name.value.trim()?u(A.name.value.trim(),v):""),A.canCreate=M.computed(()=>!A.busy.value&&!!A.name.value.trim()&&!u(A.name.value.trim(),v));let H="";A.create=async()=>{const D=A.name.peek().trim();if(!(!D||A.error.peek()||A.busy.peek())){A.busy.value=!0;try{await w(D,A.project.peek()),H=D,j.close()}catch(se){E.toast(`Not created: ${se.message||se}`,{type:"error"})}finally{A.busy.value=!1}}},A.cancel=()=>j.close(),A.key=D=>{D.key==="Enter"&&(D.preventDefault(),A.create())};const Y=M.applyBindings(A,c);Bt(j,()=>{Y.dispose(),x(H)}),j.element.appendChild(c),c.querySelector("#pe-new-project").value=i,j.open(),setTimeout(()=>{const D=c.querySelector("#pe-new-name");D?.focus(),D?.select()},100)})}let F=null,cs=!1;function Er(){cs||!window.R?.use||(cs=!0,Ln(),R.use((d,b,v)=>{if(F&&!F.alive()){const u=F;F=null,u.dispose()}const i=b?.path||"",k=d?.path||"";if(!F||!Oo.test(i))return v();if(Oo.test(k)){const u=Ua(k);if(u){F.open(u,k);return}return v()}return F.dirty()?E.confirm("You have unsaved changes. Leave anyway? They are kept as drafts for your next visit.").then(u=>{u?(F?.keepDraft(),v()):F&&history.replaceState({pe:1},"","#"+F.address)}):v()}),M.subscribe("router:afterChange",({to:d}={})=>{if(F&&!Oo.test(d?.path||"")){const b=F;F=null,b.dispose()}}))}const ds="editor_prefs";function Pe(){return S.get(ds)||{viewMode:"split",fullscreen:!1}}let Mr=null;const Tr=()=>Mr??(Mr=h.pages.dommaCatalogue().then(d=>(kn(d),yn()))),Cr=d=>{try{return document.createDocumentFragment().querySelector(d),!0}catch{return!1}},xr=()=>{try{return Pe()||{}}catch{return{}}};function Ke(d,b){const v=Pe();v[d]=b,S.set(ds,v)}const ps="editor-workspace.tabs",us="editor-workspace.folded",ft="editor-workspace.css",Pr="/__block-preview",Or=d=>d.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function Rr(d,b,v=()=>{}){let i=0;try{i=d.contentWindow.scrollY||0}catch{}d.addEventListener("load",()=>{try{d.contentWindow.scrollTo(0,i)}catch{}v()},{once:!0}),d.srcdoc=b}const Ar=["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","seoSchema","dconfig"],$r=["showInNav","sidebar","breadcrumbs","bundled"],ms=()=>({urlPath:"",title:"",description:"",layout:"",status:"draft",sortOrder:"99",showInNav:!1,sidebar:!1,breadcrumbs:!0,bundled:!1,tags:[],adminTags:[],category:"",visibility:"public",project:"",theme:"",seoTitle:"",seoDesc:"",seoSchema:"",dconfig:"",body:"",css:null});export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(d){if(Er(),F){const e=F;F=null,e.dispose()}const b=window.location.hash.replace(/^#/,""),v=[],i=e=>(v.push(e),e);let k=()=>!1,u=()=>{},w=()=>{};const y=d.get(0),x=y.querySelector("#pe-workspace"),j={address:b,alive:()=>x?.isConnected===!0,dirty:()=>k(),keepDraft:()=>u(),open:(e,t)=>w(e,t),dispose:()=>{v.splice(0).forEach(e=>{try{e()}catch{}})}};F=j;const c=()=>F===j&&j.alive();if(xr().closeSidebar===!0){const e=document.querySelector("#admin-sidebar");e&&!Ja(e)&&!window.matchMedia("(max-width: 768px)").matches&&(Wa(e,!0,!1),i(()=>{e.isConnected&&Ja(e)&&Wa(e,!1,!1)}))}const A=E.loader(y,{type:"dots"});let[H,Y,D,se,_t,T,ne,Oe]=await Promise.all([h.layouts.get().catch(()=>({})),h.settings.get().catch(()=>({})),h.pages.list().catch(()=>[]),h.projects.list().catch(()=>[]),h.pages.tags().catch(()=>[]),h.listTags.list("pages").then(e=>e.map(t=>t.name)).catch(()=>[]),h.get("/blocks?usage=1").catch(()=>h.blocks.list()).catch(()=>[]),h.components.list().catch(()=>[])]);if(!c()){A.destroy();return}A.destroy();const O=e=>y.querySelector(e);if(D=Array.isArray(D)?D:[],dn(O("#field-theme"),{includeDefault:!0}),await Br(O("#field-visibility")),!c())return;const re=Object.keys(H||{});jr(O("#field-layout"),H,re[0]||"default");const Ho=e=>re.includes(e)?e:re[0]||e||"default";for(const e of se)for(const t of["#field-project","#block-project","#comp-project"])O(t).append(new Option(e.name||e.slug,e.slug));let ie=Array.isArray(ne)?ne:[],bt=Ya(ie);const vt=new Map(ie.map(e=>[e.name,e.updatedAt])),Ue=new Map(ie.map(e=>[e.name,e.usage])),be=new Set(ie.map(e=>e.name)),Vo=async()=>{const e=await h.get("/blocks?usage=1").catch(()=>null);if(!(!c()||!Array.isArray(e))){ie=e,bt=Ya(ie);for(const t of ie)be.add(t.name),Ue.set(t.name,t.usage),vt.has(t.name)||vt.set(t.name,t.updatedAt)}},Kt=Array.isArray(Y?.footer?.blocks)?Y.footer.blocks.filter(e=>typeof e=="string"):[];let Je=Array.isArray(Oe)?Oe.filter(e=>typeof e?.name=="string"):[];const le=new Set(Je.map(e=>e.name)),_o=async()=>{const e=await h.components.list().catch(()=>null);if(!(!c()||!Array.isArray(e))){Je=e.filter(t=>typeof t?.name=="string");for(const t of Je)le.add(t.name)}},Z=Pe(),p={urlPath:M.observable(""),title:M.observable(""),description:M.observable(""),layout:M.observable(Ho("default")),status:M.observable("draft"),sortOrder:M.observable("99"),showInNav:M.observable(!1),sidebar:M.observable(!1),breadcrumbs:M.observable(!0),bundled:M.observable(!1),tags:M.observable([]),adminTags:M.observable([]),category:M.observable(""),visibility:M.observable("public"),project:M.observable(""),theme:M.observable(""),seoTitle:M.observable(""),seoDesc:M.observable(""),seoSchema:M.observable(""),dconfig:M.observable(""),css:M.observable(""),cssOpened:M.observable(!1)},o={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),viewMode:M.observable(jo(Z.viewMode)),fullscreen:M.observable(!!Z.fullscreen),explorerOn:M.observable(Z.explorer??!window.matchMedia("(max-width: 1024px)").matches),dockOn:M.observable(!!Z.dock),dockTab:M.observable("details"),seoSchemaNote:M.observable(""),catalogue:M.observable(null),cssMode:M.observable(!1),folded:M.observable(!1),previewStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null),heldNote:M.observable(""),tabs:M.observable([]),active:M.observable(""),curId:M.observable(null),blockTab:M.observable(""),blockPart:M.observable("html"),blockReady:M.observable(!1),blockNote:M.observable(""),blockUses:M.observable(""),blockProject:M.observable(""),blockBundled:M.observable(!1),blockWhere:M.observable(""),compTab:M.observable(""),compPart:M.observable("template"),compReady:M.observable(!1),compNote:M.observable(""),compReadOnly:M.observable(!1),compState:M.observable(""),compErrors:M.observable(""),compProject:M.observable(""),compBundled:M.observable(!1),blockRev:M.observable(0),compRev:M.observable(0),docRev:M.observable(0),filter:M.observable(""),secHere:M.observable(Z.secHere!==!1),secPages:M.observable(Z.secPages!==!1),secBlocks:M.observable(Z.secBlocks!==!1),secComps:M.observable(Z.secComps!==!1),thisPage:M.observable([]),pageRows:M.observable([]),blockRows:M.observable([]),compRows:M.observable([])},V=O("#markdown-editor"),ve=O("#css-editor"),fs=_a(V),bs=_a(ve);i(()=>fs.destroy()),i(()=>bs.destroy());const ce=document.createElement("div");ce.className="editor-line-numbers editor-line-numbers--foldable",V.parentElement.insertBefore(ce,V);const We=ns(V,"md"),Ut=ns(ve,"css");i(()=>{We.destroy(),Ut.destroy()});const Ko=()=>{We.paint(),Ut.paint()},Q=pn(V,ce,{onFoldChange:()=>We.paint()});let gt=!1;Q.setGuard(e=>{gt=e});const ht=()=>Q.getUnfoldedContent(),Jt=()=>{Q.unfoldAll(),o.folded.value=!1,We.paint()},vs=e=>{if(e?.kind==="page")Jt(),_e(V,"md");else if(e?.kind==="block")oe&&_e(oe.textarea,"html"),me&&_e(me.textarea,"css");else if(e?.kind==="component"){if(N.get(e.id)?.readOnly)return;for(const[a,s]of Object.entries({template:"html",props:"json",script:"js",style:"css"}))ae[a]&&_e(ae[a].textarea,s)}},Uo=e=>{e.error?E.toast(e.error,{type:"warning"}):E.toast(e.unchanged?"Already tidy":"Formatted",{type:"info",duration:900})},Jo=M.observable(It(Z.type)),Wo=M.observable(Ht(Z.code));i(M.effect(()=>{const e=Wo.value;fr(e),O("#editor-body")?.style.setProperty("--pe-tab",String(e.size))})),i(M.effect(()=>{const e=Jo.value,t=O("#editor-body");if(t){is(e.font);for(const[a,s]of Object.entries(Va(e)))t.style.setProperty(a,s);t.classList.toggle("pe-nowrap",!e.wrap),V.wrap=e.wrap?"soft":"off",ve.wrap=e.wrap?"soft":"off",requestAnimationFrame(()=>{c()&&Ko()})}}));let Yo=null;i(()=>{try{Yo?.close()}catch{}});const _=()=>(o.bodyRev.value,{urlPath:p.urlPath.value.trim(),title:p.title.value,description:p.description.value,layout:p.layout.value,status:p.status.value,sortOrder:p.sortOrder.value,showInNav:p.showInNav.value,sidebar:p.sidebar.value,breadcrumbs:p.breadcrumbs.value,bundled:p.bundled.value,tags:p.tags.value,adminTags:p.adminTags.value,category:p.category.value,visibility:p.visibility.value,project:p.project.value,theme:p.theme.value,seoTitle:p.seoTitle.value,seoDesc:p.seoDesc.value,seoSchema:p.seoSchema.value,dconfig:p.dconfig.value,body:ht(),css:null}),Go=()=>M.untracked?M.untracked(_):_(),ee=M.observable(""),yt=()=>p.cssOpened.peek()&&p.css.peek()!==ee.peek(),K=new Map;let m=null;const L=new Map,N=new Map;let P=[];const de=e=>Do(e.form)!==e.base,ge=()=>[...L.entries()].filter(([,e])=>de(e)).map(([e])=>e),he=e=>!e.readOnly&&Fo({name:e.name,...e.form})!==e.base,Ye=()=>[...N.values()].filter(he).map(e=>e.name),Wt=()=>W(Go())!==o.base.peek(),Yt=e=>e.loaded&&(e===m?Wt():W(e.state)!==e.base),Re=()=>[...K.values()].filter(Yt),gs=M.computed(()=>W(_())!==o.base.value),hs=M.computed(()=>p.cssOpened.value&&p.css.value!==ee.value),ys=M.computed(()=>(o.blockRev.value,o.compRev.value,o.docRev.value,W(_())!==o.base.value||p.cssOpened.value&&p.css.value!==ee.value||Re().some(e=>e!==m)||ge().length>0||Ye().length>0)),ks=M.computed(()=>{o.blockRev.value,o.compRev.value;const e=I(o.active.value);if(e?.kind==="block"){const t=L.get(e.id);return!!(t&&de(t))}if(e?.kind==="component"){const t=N.get(e.id);return!!(t&&he(t))}return W(_())!==o.base.value||p.cssOpened.value&&p.css.value!==ee.value}),ws=M.computed(()=>o.curId.value!==null),Xo=M.computed(()=>I(o.active.value)?.kind||""),Zo=M.computed(()=>!!o.curId.value&&I(o.active.value)?.kind==="page"),Ss=M.computed(()=>p.urlPath.value.trim()||o.curId.value||""),Es=M.computed(()=>Ro({urlPath:p.urlPath.value,dconfig:p.dconfig.value})[0]?.text||""),Ms=M.computed(()=>(o.bodyRev.value,o.catalogue.value?hn({dconfig:p.dconfig.value,body:ht()},o.catalogue.value,{isSelector:Cr}).map((e,t)=>({key:`${t}:${e}`,text:e})):[]));Tr().then(e=>{o.catalogue.value=e});const Ts=M.computed(()=>{o.docRev.value;const e=I(o.active.value);return e?e.kind==="block"?o.blockUses.value:e.kind==="component"?Gn({name:e.id,readOnly:o.compReadOnly.value,state:o.compReady.value?o.compState.value:""}):tr({urlPath:p.urlPath.value,status:p.status.value,isEdit:!!o.curId.value}):""}),Qo=M.computed(()=>o.compTab.value?"component":o.blockTab.value?"block":o.curId.value!==null?"page":""),Cs=M.computed(()=>o.blockTab.value?`[${o.blockTab.value} /]`:""),xs=M.computed(()=>({compiling:"Compiling\u2026",ok:"Compiles",error:"Does not compile"})[o.compState.value]||""),Ps=M.computed(()=>o.compTab.value?`<dm-${o.compTab.value}>`:""),Os=M.computed(()=>o.versions.value.some(e=>e.checked.value)),Rs=M.computed(()=>o.versions.value.length>0&&o.versions.value.every(e=>e.checked.value)),As=M.computed(()=>{const e=o.versions.value.filter(t=>t.checked.value).length;return e>0&&e<o.versions.value.length});k=()=>Re().length>0||ge().length>0||Ye().length>0||yt();const Gt=(e,t,a)=>({title:String(e.title||"").trim()||"Untitled",description:String(e.description||"").trim(),layout:e.layout,status:e.status,sortOrder:parseInt(String(e.sortOrder),10)||99,showInNav:!!e.showInNav,sidebar:!!e.sidebar,...e.breadcrumbs?{}:{breadcrumbs:!1},...e.bundled?{bundled:!0}:{},tags:Array.isArray(e.tags)?e.tags:[],...t?.isEdit&&JSON.stringify(e.adminTags)===t.savedAdminTags?{}:{adminTags:Array.isArray(e.adminTags)?e.adminTags:[]},category:String(e.category||"").trim()||null,visibility:e.visibility||"public",project:e.project||"",seo:{title:String(e.seoTitle||"").trim(),description:String(e.seoDesc||"").trim(),...ea(e)?{schema:ea(e)}:t?.isEdit?{schema:null}:{}},dconfig:a,...e.theme?{theme:e.theme}:{}}),ea=e=>Po(e.seoSchema).value,Xt=e=>{const t=String(e.dconfig||"").trim();if(!t)return null;try{return JSON.parse(t)}catch{return null}},$s=()=>p.urlPath.peek().trim()||m?.id||"/";let kt=[],ta=[];const wt=e=>kt.includes(e)||Kt.includes(e);let Ae=new Set;const St=()=>{const e=[...new Set([...kt,...Kt,...L.keys()])],t=$n(e.filter(s=>!L.has(s)),vt);Ae=new Set(Object.keys(t));const a=Ae.size?`Preview shows unsaved changes to ${[...Ae].join(", ")}`:"";return a!==o.heldNote.peek()&&(o.heldNote.value=a),Hn(t,[...L.entries()].map(([s,r])=>({name:s,content:r.form.content,css:r.form.css,dirty:de(r)})))};let Zt="";const Ls=()=>{oa();const e=Zt;if(e){Zt="";try{$e.contentDocument?.querySelector(`[data-block="${CSS.escape(e)}"]`)?.scrollIntoView({block:"center"})}catch{}}},$e=O("#markdown-preview");h.settings.getCustomCss().then(({css:e})=>{if(!c())return;ee.value=e||"";let t=null;try{t=S.get(ft)}catch{}typeof t=="string"&&t!==ee.peek()?(ia(),p.css.value=t,E.toast("Your unsaved custom CSS is back - the CSS button shows it.",{type:"info"})):p.cssOpened.peek()||(p.css.value=ee.peek()),J(ft)}).catch(()=>{});const oa=()=>{const e=$e?.contentDocument;if(!e?.head)return;const t=p.cssOpened.peek()?p.css.peek():"";if(!t)return;let a=e.getElementById("preview-custom-css");a||(a=e.createElement("style"),a.id="preview-custom-css",e.head.appendChild(a)),a.textContent=t};let Qt=null,eo=0;const U=(e=!1)=>{clearTimeout(Qt),Qt=setTimeout(async()=>{if(!$e||o.viewMode.peek()==="write"||!o.active.peek()||o.compTab.peek())return;const t=++eo,a=o.blockTab.peek();o.previewStatus.value="Rendering\u2026";try{let s;if(a&&(!m||!wt(a)))({html:s}=await h.pages.previewFull(Pr,{title:a,layout:"blank",breadcrumbs:!1},Un(a,_s(a)),St()));else if(m){const r=_();({html:s}=await h.pages.previewFull($s(),Gt(r,m,Xt(r)),r.body,St()))}else{$e.srcdoc="";return}if(!c()||t!==eo)return;Rr($e,qn(Or(s),a),Ls),o.previewStatus.value="Showing unsaved changes"}catch(s){if(!c()||t!==eo)return;o.previewStatus.value="Preview failed - "+(s?.message||"unknown error"),$e.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(s?.message||"unknown error").replace(/[<&]/g,r=>r==="<"?"&lt;":"&amp;")+"</body>"}},e?0:500)};i(()=>clearTimeout(Qt));const to=sn(d.find("#markdown-editor"),d.find("#editor-toolbar"),{spacerDefault:Y?.layoutOptions?.spacerSize??40,beforeFormat:()=>Jt()});ln(V,{blocks:()=>bt,openBlock:e=>Be(e)});const Ge=y.querySelector(".editor-toolbar-right");Ge.querySelectorAll(".editor-view-btn[data-mode]").forEach(e=>{const t=e.getAttribute("data-mode");e.classList.remove("active"),e.setAttribute("data-bind-class",`viewMode.value === '${t}' && 'active'`),e.setAttribute("data-on-click",`setMode('${t}')`)}),O("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const Ns=mt(`
127
+ </style>`;function ms({kind:d,projects:b,taken:v,project:l="",name:k="",check:u,create:w,register:y=()=>{}}){return new Promise(x=>{const I=E.slideover({title:d==="block"?"New block":"New component",size:"sm",position:"right"});y(()=>{try{I.close()}catch{}});const i=bt(`<div>${Ar}</div>`);for(const D of b)i.querySelector("#pe-new-project").append(new Option(D.name||D.slug,D.slug));const A={name:M.observable(k),project:M.observable(l),busy:M.observable(!1),hint:M.observable(d==="block"?"A block is a reusable piece of HTML with {{placeholders}}, used on pages with its short tag or by a collection display. Its name is its tag - fixed once created.":"A component is a custom element - <dm-name> - with a template, props, a script and a style. Its name is its tag - fixed once created."),placeholder:M.observable(d==="block"?"e.g. team-card":"e.g. pricing-table")};A.error=M.computed(()=>A.name.value.trim()?u(A.name.value.trim(),v):""),A.canCreate=M.computed(()=>!A.busy.value&&!!A.name.value.trim()&&!u(A.name.value.trim(),v));let H="";A.create=async()=>{const D=A.name.peek().trim();if(!(!D||A.error.peek()||A.busy.peek())){A.busy.value=!0;try{await w(D,A.project.peek()),H=D,I.close()}catch(se){E.toast(`Not created: ${se.message||se}`,{type:"error"})}finally{A.busy.value=!1}}},A.cancel=()=>I.close(),A.key=D=>{D.key==="Enter"&&(D.preventDefault(),A.create())};const Y=M.applyBindings(A,i);qt(I,()=>{Y.dispose(),x(H)}),I.element.appendChild(i),i.querySelector("#pe-new-project").value=l,I.open(),setTimeout(()=>{const D=i.querySelector("#pe-new-name");D?.focus(),D?.select()},100)})}let F=null,fs=!1;function Rr(){fs||!window.R?.use||(fs=!0,qn(),R.use((d,b,v)=>{if(F&&!F.alive()){const u=F;F=null,u.dispose()}const l=b?.path||"",k=d?.path||"";if(!F||!Ra.test(l))return v();if(Ra.test(k)){const u=Xo(k);if(u){F.open(u,k);return}return v()}return F.dirty()?E.confirm("You have unsaved changes. Leave anyway? They are kept as drafts for your next visit.").then(u=>{u?(F?.keepDraft(),v()):F&&history.replaceState({pe:1},"","#"+F.address)}):v()}),M.subscribe("router:afterChange",({to:d}={})=>{if(F&&!Ra.test(d?.path||"")){const b=F;F=null,b.dispose()}}))}const bs="editor_prefs";function Pe(){return S.get(bs)||{viewMode:"split",fullscreen:!1}}let $r=null;const Lr=()=>$r??($r=g.pages.dommaCatalogue().then(d=>(Cn(d),Tn()))),Nr=d=>{try{return document.createDocumentFragment().querySelector(d),!0}catch{return!1}},Dr=()=>{try{return Pe()||{}}catch{return{}}};function Ke(d,b){const v=Pe();v[d]=b,S.set(bs,v)}const vs="editor-workspace.tabs",gs="editor-workspace.folded",vt="editor-workspace.css",Ir="/__block-preview",Br=d=>d.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function jr(d,b,v=()=>{}){let l=0;try{l=d.contentWindow.scrollY||0}catch{}d.addEventListener("load",()=>{try{d.contentWindow.scrollTo(0,l)}catch{}v()},{once:!0}),d.srcdoc=b}const Fr=["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","seoSchema","dconfig"],zr=["showInNav","sidebar","breadcrumbs","bundled"],hs=()=>({urlPath:"",title:"",description:"",layout:"",status:"draft",sortOrder:"99",showInNav:!1,sidebar:!1,breadcrumbs:!0,bundled:!1,tags:[],adminTags:[],category:"",visibility:"public",project:"",theme:"",seoTitle:"",seoDesc:"",seoSchema:"",dconfig:"",body:"",css:null});export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(d){if(Rr(),F){const e=F;F=null,e.dispose()}const b=window.location.hash.replace(/^#/,""),v=[],l=e=>(v.push(e),e);let k=()=>!1,u=()=>{},w=()=>{};const y=d.get(0),x=y.querySelector("#pe-workspace"),I={address:b,alive:()=>x?.isConnected===!0,dirty:()=>k(),keepDraft:()=>u(),open:(e,t)=>w(e,t),dispose:()=>{v.splice(0).forEach(e=>{try{e()}catch{}})}};F=I;const i=()=>F===I&&I.alive();if(Dr().closeSidebar===!0){const e=document.querySelector("#admin-sidebar");e&&!Zo(e)&&!window.matchMedia("(max-width: 768px)").matches&&(Qo(e,!0,!1),l(()=>{e.isConnected&&Zo(e)&&Qo(e,!1,!1)}))}const A=E.loader(y,{type:"dots"});let[H,Y,D,se,Wt,T,ne,Oe]=await Promise.all([g.layouts.get().catch(()=>({})),g.settings.get().catch(()=>({})),g.pages.list().catch(()=>[]),g.projects.list().catch(()=>[]),g.pages.tags().catch(()=>[]),g.listTags.list("pages").then(e=>e.map(t=>t.name)).catch(()=>[]),g.get("/blocks?usage=1").catch(()=>g.blocks.list()).catch(()=>[]),g.components.list().catch(()=>[])]);if(!i()){A.destroy();return}A.destroy();const O=e=>y.querySelector(e);if(D=Array.isArray(D)?D:[],vn(O("#field-theme"),{includeDefault:!0}),await Kr(O("#field-visibility")),!i())return;const re=Object.keys(H||{});Vr(O("#field-layout"),H,re[0]||"default");const Va=e=>re.includes(e)?e:re[0]||e||"default";for(const e of se)for(const t of["#field-project","#block-project","#comp-project"])O(t).append(new Option(e.name||e.slug,e.slug));let le=Array.isArray(ne)?ne:[],gt=es(le);const ht=new Map(le.map(e=>[e.name,e.updatedAt])),Ue=new Map(le.map(e=>[e.name,e.usage])),be=new Set(le.map(e=>e.name)),Ka=async()=>{const e=await g.get("/blocks?usage=1").catch(()=>null);if(!(!i()||!Array.isArray(e))){le=e,gt=es(le);for(const t of le)be.add(t.name),Ue.set(t.name,t.usage),ht.has(t.name)||ht.set(t.name,t.updatedAt)}},Yt=Array.isArray(Y?.footer?.blocks)?Y.footer.blocks.filter(e=>typeof e=="string"):[];let Je=Array.isArray(Oe)?Oe.filter(e=>typeof e?.name=="string"):[];const ie=new Set(Je.map(e=>e.name)),Ua=async()=>{const e=await g.components.list().catch(()=>null);if(!(!i()||!Array.isArray(e))){Je=e.filter(t=>typeof t?.name=="string");for(const t of Je)ie.add(t.name)}},Z=Pe(),p={urlPath:M.observable(""),title:M.observable(""),description:M.observable(""),layout:M.observable(Va("default")),status:M.observable("draft"),sortOrder:M.observable("99"),showInNav:M.observable(!1),sidebar:M.observable(!1),breadcrumbs:M.observable(!0),bundled:M.observable(!1),tags:M.observable([]),adminTags:M.observable([]),category:M.observable(""),visibility:M.observable("public"),project:M.observable(""),theme:M.observable(""),seoTitle:M.observable(""),seoDesc:M.observable(""),seoSchema:M.observable(""),dconfig:M.observable(""),css:M.observable(""),cssOpened:M.observable(!1)},a={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),viewMode:M.observable(ja(Z.viewMode)),fullscreen:M.observable(!!Z.fullscreen),explorerOn:M.observable(Z.explorer??!window.matchMedia("(max-width: 1024px)").matches),dockOn:M.observable(!!Z.dock),dockTab:M.observable("details"),seoSchemaNote:M.observable(""),catalogue:M.observable(null),cssMode:M.observable(!1),folded:M.observable(!1),previewStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null),imageHosts:M.observable([]),imagePolicyReady:M.observable(!1),imageCanAllow:M.observable(!1),imageError:M.observable(""),imageScanning:M.observable(!1),imageAllowing:M.observable(""),imageScanned:M.observable(!1),imageReload:M.observable(!1),imageBlocked:M.observable(0),heldNote:M.observable(""),tabs:M.observable([]),active:M.observable(""),curId:M.observable(null),blockTab:M.observable(""),blockPart:M.observable("html"),blockReady:M.observable(!1),blockNote:M.observable(""),blockUses:M.observable(""),blockProject:M.observable(""),blockBundled:M.observable(!1),blockWhere:M.observable(""),compTab:M.observable(""),compPart:M.observable("template"),compReady:M.observable(!1),compNote:M.observable(""),compReadOnly:M.observable(!1),compState:M.observable(""),compErrors:M.observable(""),compProject:M.observable(""),compBundled:M.observable(!1),blockRev:M.observable(0),compRev:M.observable(0),docRev:M.observable(0),filter:M.observable(""),secHere:M.observable(Z.secHere!==!1),secPages:M.observable(Z.secPages!==!1),secBlocks:M.observable(Z.secBlocks!==!1),secComps:M.observable(Z.secComps!==!1),thisPage:M.observable([]),pageRows:M.observable([]),blockRows:M.observable([]),compRows:M.observable([])},_=O("#markdown-editor"),ve=O("#css-editor"),ys=Yo(_),ks=Yo(ve);l(()=>ys.destroy()),l(()=>ks.destroy());const ce=document.createElement("div");ce.className="editor-line-numbers editor-line-numbers--foldable",_.parentElement.insertBefore(ce,_);const We=ds(_,"md"),Gt=ds(ve,"css");l(()=>{We.destroy(),Gt.destroy()});const Ja=()=>{We.paint(),Gt.paint()},Q=gn(_,ce,{onFoldChange:()=>We.paint()});let yt=!1;Q.setGuard(e=>{yt=e});const kt=()=>Q.getUnfoldedContent(),Xt=()=>{Q.unfoldAll(),a.folded.value=!1,We.paint()},ws=e=>{if(e?.kind==="page")Xt(),Ve(_,"md");else if(e?.kind==="block")ae&&Ve(ae.textarea,"html"),me&&Ve(me.textarea,"css");else if(e?.kind==="component"){if(N.get(e.id)?.readOnly)return;for(const[o,s]of Object.entries({template:"html",props:"json",script:"js",style:"css"}))oe[o]&&Ve(oe[o].textarea,s)}},Wa=e=>{e.error?E.toast(e.error,{type:"warning"}):E.toast(e.unchanged?"Already tidy":"Formatted",{type:"info",duration:900})},Ya=M.observable(Ht(Z.type)),Ga=M.observable(Ut(Z.code));l(M.effect(()=>{const e=Ga.value;Sr(e),O("#editor-body")?.style.setProperty("--pe-tab",String(e.size))})),l(M.effect(()=>{const e=Ya.value,t=O("#editor-body");if(t){us(e.font);for(const[o,s]of Object.entries(Wo(e)))t.style.setProperty(o,s);t.classList.toggle("pe-nowrap",!e.wrap),_.wrap=e.wrap?"soft":"off",ve.wrap=e.wrap?"soft":"off",requestAnimationFrame(()=>{i()&&Ja()})}}));let Xa=null;l(()=>{try{Xa?.close()}catch{}});const V=()=>(a.bodyRev.value,{urlPath:p.urlPath.value.trim(),title:p.title.value,description:p.description.value,layout:p.layout.value,status:p.status.value,sortOrder:p.sortOrder.value,showInNav:p.showInNav.value,sidebar:p.sidebar.value,breadcrumbs:p.breadcrumbs.value,bundled:p.bundled.value,tags:p.tags.value,adminTags:p.adminTags.value,category:p.category.value,visibility:p.visibility.value,project:p.project.value,theme:p.theme.value,seoTitle:p.seoTitle.value,seoDesc:p.seoDesc.value,seoSchema:p.seoSchema.value,dconfig:p.dconfig.value,body:kt(),css:null}),Za=()=>M.untracked?M.untracked(V):V(),ee=M.observable(""),wt=()=>p.cssOpened.peek()&&p.css.peek()!==ee.peek(),K=new Map;let m=null;const L=new Map,N=new Map;let P=[];const de=e=>Ba(e.form)!==e.base,ge=()=>[...L.entries()].filter(([,e])=>de(e)).map(([e])=>e),he=e=>!e.readOnly&&qa({name:e.name,...e.form})!==e.base,Ye=()=>[...N.values()].filter(he).map(e=>e.name),Zt=()=>W(Za())!==a.base.peek(),Qt=e=>e.loaded&&(e===m?Zt():W(e.state)!==e.base),Ae=()=>[...K.values()].filter(Qt),Ss=M.computed(()=>W(V())!==a.base.value),Es=M.computed(()=>p.cssOpened.value&&p.css.value!==ee.value),Ms=M.computed(()=>(a.blockRev.value,a.compRev.value,a.docRev.value,W(V())!==a.base.value||p.cssOpened.value&&p.css.value!==ee.value||Ae().some(e=>e!==m)||ge().length>0||Ye().length>0)),Ts=M.computed(()=>{a.blockRev.value,a.compRev.value;const e=j(a.active.value);if(e?.kind==="block"){const t=L.get(e.id);return!!(t&&de(t))}if(e?.kind==="component"){const t=N.get(e.id);return!!(t&&he(t))}return W(V())!==a.base.value||p.cssOpened.value&&p.css.value!==ee.value}),Cs=M.computed(()=>a.curId.value!==null),Qa=M.computed(()=>j(a.active.value)?.kind||""),eo=M.computed(()=>!!a.curId.value&&j(a.active.value)?.kind==="page"),xs=M.computed(()=>p.urlPath.value.trim()||a.curId.value||""),Ps=M.computed(()=>$a({urlPath:p.urlPath.value,dconfig:p.dconfig.value})[0]?.text||""),Os=M.computed(()=>(a.bodyRev.value,a.catalogue.value?Mn({dconfig:p.dconfig.value,body:kt()},a.catalogue.value,{isSelector:Nr}).map((e,t)=>({key:`${t}:${e}`,text:e})):[]));Lr().then(e=>{a.catalogue.value=e});const As=M.computed(()=>{a.docRev.value;const e=j(a.active.value);return e?e.kind==="block"?a.blockUses.value:e.kind==="component"?sr({name:e.id,readOnly:a.compReadOnly.value,state:a.compReady.value?a.compState.value:""}):cr({urlPath:p.urlPath.value,status:p.status.value,isEdit:!!a.curId.value}):""}),to=M.computed(()=>a.compTab.value?"component":a.blockTab.value?"block":a.curId.value!==null?"page":""),Rs=M.computed(()=>a.blockTab.value?`[${a.blockTab.value} /]`:""),$s=M.computed(()=>({compiling:"Compiling\u2026",ok:"Compiles",error:"Does not compile"})[a.compState.value]||""),Ls=M.computed(()=>a.compTab.value?`<dm-${a.compTab.value}>`:""),Ns=M.computed(()=>a.versions.value.some(e=>e.checked.value)),Ds=M.computed(()=>a.versions.value.length>0&&a.versions.value.every(e=>e.checked.value)),Is=M.computed(()=>{const e=a.versions.value.filter(t=>t.checked.value).length;return e>0&&e<a.versions.value.length});k=()=>Ae().length>0||ge().length>0||Ye().length>0||wt();const St=(e,t,o)=>({title:String(e.title||"").trim()||"Untitled",description:String(e.description||"").trim(),layout:e.layout,status:e.status,sortOrder:parseInt(String(e.sortOrder),10)||99,showInNav:!!e.showInNav,sidebar:!!e.sidebar,...e.breadcrumbs?{}:{breadcrumbs:!1},...e.bundled?{bundled:!0}:{},tags:Array.isArray(e.tags)?e.tags:[],...t?.isEdit&&JSON.stringify(e.adminTags)===t.savedAdminTags?{}:{adminTags:Array.isArray(e.adminTags)?e.adminTags:[]},category:String(e.category||"").trim()||null,visibility:e.visibility||"public",project:e.project||"",seo:{title:String(e.seoTitle||"").trim(),description:String(e.seoDesc||"").trim(),...ao(e)?{schema:ao(e)}:t?.isEdit?{schema:null}:{}},dconfig:o,...e.theme?{theme:e.theme}:{}}),ao=e=>Aa(e.seoSchema).value,Et=e=>{const t=String(e.dconfig||"").trim();if(!t)return null;try{return JSON.parse(t)}catch{return null}},oo=()=>p.urlPath.peek().trim()||m?.id||"/";let Mt=[],so=[];const Tt=e=>Mt.includes(e)||Yt.includes(e);let Re=new Set;const Ge=()=>{const e=[...new Set([...Mt,...Yt,...L.keys()])],t=zn(e.filter(s=>!L.has(s)),ht);Re=new Set(Object.keys(t));const o=Re.size?`Preview shows unsaved changes to ${[...Re].join(", ")}`:"";return o!==a.heldNote.peek()&&(a.heldNote.value=o),Gn(t,[...L.entries()].map(([s,r])=>({name:s,content:r.form.content,css:r.form.css,dirty:de(r)})))};let ea="";const Bs=()=>{ro();const e=ea;if(e){ea="";try{$e.contentDocument?.querySelector(`[data-block="${CSS.escape(e)}"]`)?.scrollIntoView({block:"center"})}catch{}}};let ta=[],Xe="";const Ct=e=>{Xe=e;const t=new DOMParser().parseFromString(e,"text/html");if(p.cssOpened.peek()){const o=t.createElement("style");o.textContent=p.css.peek(),t.head.appendChild(o)}a.imageHosts.value=jn(Fn(t),window.location.origin,ta),a.imageBlocked.value=a.imageHosts.peek().filter(o=>!o.allowed).length,a.imageScanned.value=!0};l(M.effect(()=>{a.curId.value,a.blockTab.value,a.compTab.value,Xe="",a.imageHosts.value=[],a.imageScanned.value=!1,a.imageBlocked.value=0}));const no=async()=>{const e=await g.get("/pages/_image-hosts");i()&&(ta=e.hosts,a.imageCanAllow.value=e.canAllow,a.imagePolicyReady.value=!0,Xe&&Ct(Xe))};no().catch(e=>{i()&&(a.imageError.value="Could not read image permissions: "+e.message)});const js=async()=>{const e=m;a.imageScanning.value=!0,a.imageError.value="";try{if(await no(),!m)throw new Error("Open a page to scan its images.");const t=V(),{html:o}=await g.pages.previewFull(oo(),St(t,m,Et(t)),t.body,Ge());i()&&m===e&&Ct(o)}catch(t){i()&&(a.imageError.value="Could not scan images: "+t.message)}finally{i()&&(a.imageScanning.value=!1)}},Fs=async e=>{if(!a.imageAllowing.peek()){a.imageAllowing.value=e,a.imageError.value="";try{const{hosts:t}=await g.post("/pages/_image-hosts",{host:e});if(!i())return;ta=t,Ct(Xe),a.imageReload.value=!0,E.toast("Images from "+e+" are now allowed site-wide.",{type:"success"})}catch(t){i()&&(a.imageError.value="Could not allow images: "+t.message)}finally{i()&&(a.imageAllowing.value="")}}},$e=O("#markdown-preview");g.settings.getCustomCss().then(({css:e})=>{if(!i())return;ee.value=e||"";let t=null;try{t=S.get(vt)}catch{}typeof t=="string"&&t!==ee.peek()?(uo(),p.css.value=t,E.toast("Your unsaved custom CSS is back - the CSS button shows it.",{type:"info"})):p.cssOpened.peek()||(p.css.value=ee.peek()),J(vt)}).catch(()=>{});const ro=()=>{const e=$e?.contentDocument;if(!e?.head)return;const t=p.cssOpened.peek()?p.css.peek():"";if(!t)return;let o=e.getElementById("preview-custom-css");o||(o=e.createElement("style"),o.id="preview-custom-css",e.head.appendChild(o)),o.textContent=t};let aa=null,oa=0;const U=(e=!1)=>{clearTimeout(aa),aa=setTimeout(async()=>{if(!$e||a.viewMode.peek()==="write"||!a.active.peek()||a.compTab.peek())return;const t=++oa,o=a.blockTab.peek();a.previewStatus.value="Rendering\u2026";try{let s;if(o&&(!m||!Tt(o)))({html:s}=await g.pages.previewFull(Ir,{title:o,layout:"blank",breadcrumbs:!1},er(o,Gs(o)),Ge()));else if(m){const r=V();({html:s}=await g.pages.previewFull(oo(),St(r,m,Et(r)),r.body,Ge()))}else{$e.srcdoc="";return}if(!i()||t!==oa)return;Ct(s),jr($e,Yn(Br(s),o),Bs),a.previewStatus.value="Showing unsaved changes"}catch(s){if(!i()||t!==oa)return;a.previewStatus.value="Preview failed - "+(s?.message||"unknown error"),$e.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(s?.message||"unknown error").replace(/[<&]/g,r=>r==="<"?"&lt;":"&amp;")+"</body>"}},e?0:500)};l(()=>clearTimeout(aa));const sa=pn(d.find("#markdown-editor"),d.find("#editor-toolbar"),{spacerDefault:Y?.layoutOptions?.spacerSize??40,beforeFormat:()=>Xt()});fn(_,{blocks:()=>gt,openBlock:e=>Be(e)});const Ze=y.querySelector(".editor-toolbar-right");Ze.querySelectorAll(".editor-view-btn[data-mode]").forEach(e=>{const t=e.getAttribute("data-mode");e.classList.remove("active"),e.setAttribute("data-bind-class",`viewMode.value === '${t}' && 'active'`),e.setAttribute("data-on-click",`setMode('${t}')`)}),O("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const zs=bt(`
128
128
  <span class="pe-toolbar-extras" style="display:contents">
129
129
  <button type="button" class="editor-view-btn" data-tip="Edit Custom CSS" data-on-click="toggleCss"
130
130
  data-bind-class="cssMode.value && 'active'"><span>CSS</span></button>
@@ -136,8 +136,8 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
136
136
  <span class="pe-fold-icon" hidden data-bind-hidden="!folded.value"><span data-icon="chevrons-right" data-icon-size="10"></span></span>
137
137
  </button>
138
138
  <span class="editor-toolbar-sep"></span>
139
- </span>`);Ge.prepend(Ns),Ge.before(O(".pe-block-bar")),Ge.before(O(".pe-comp-bar"));const aa=e=>(Array.isArray(e)?e:[]).map(t=>({value:t,label:t})),sa=O("#field-tags");let Et=null;Et=sa?E.pillbox(sa,{data:aa(_t),value:[],creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{Et&&(p.tags.value=[...Et.getValue()])}}):null;const na=O("#field-admin-tags");let Mt=null;Mt=na?E.pillbox(na,{data:aa(T),value:[],creatable:!0,searchable:!0,placeholder:"Add admin tag\u2026",onChange:()=>{Mt&&(p.adminTags.value=[...Mt.getValue()])}}):null;const ra=()=>{o.bodyRev.value=o.bodyRev.peek()+1},Le=()=>{o.blockRev.value=o.blockRev.peek()+1},ye=()=>{o.compRev.value=o.compRev.peek()+1},Xe=()=>{o.docRev.value=o.docRev.peek()+1},ia=()=>{p.cssOpened.peek()||(p.css.peek()||(p.css.value=ee.peek()),p.cssOpened.value=!0,an(ve,{language:"css"}))},Ze=e=>{for(const t of Ar)p[t].value=e[t]==null?"":String(e[t]);for(const t of $r)p[t].value=!!e[t];p.tags.value=Array.isArray(e.tags)?e.tags:[],Et?.setValue(p.tags.peek()),Array.isArray(e.adminTags)&&(p.adminTags.value=e.adminTags),Mt?.setValue(p.adminTags.peek()),qo(O("#field-visibility"),p.visibility.peek()),Q.unfoldAll(),o.folded.value=!1,gt=!0,V.value=e.body==null?"":String(e.body),gt=!1,Q.refresh(),Ko(),ra()},la=(e,t)=>({urlPath:t||"",title:e?.title||"",description:e?.description||"",layout:Ho(e?.layout||"default"),status:e?.status||"draft",sortOrder:String(e?.sortOrder??99),showInNav:!!e?.showInNav,sidebar:!!e?.sidebar,breadcrumbs:e?e.breadcrumbs!==!1:!0,bundled:!!e?.bundled,tags:Array.isArray(e?.tags)?e.tags:[],adminTags:Array.isArray(e?.adminTags)?e.adminTags:[],category:e?.category||"",visibility:typeof e?.visibility=="string"&&e.visibility?e.visibility:"public",project:Rn({isEdit:!!t,page:e,projects:se,fromHash:t?"":zt()||"",urlPath:t||""}),theme:e?.theme||"",seoTitle:e?.seo?.title||"",seoDesc:e?.seo?.description||"",seoSchema:bn(e?.seo?.schema),dconfig:e?.dconfig?JSON.stringify(e.dconfig,null,2):"",body:e?.content||"",css:null}),ca=()=>{m&&(m.state=_(),m.at=Date.now())},da=e=>{m!==e&&(ca(),m=e,Ze(e.state),o.base.value=e.base,o.curId.value=e.id,e.at=Date.now(),Ct(),Xe())},Ds=()=>{ca(),m=null;const e=ms();Ze(e),o.base.value=W(e),o.curId.value=null,Ct(),Xe()};Ze(ms()),o.base.value=W(_());const J=e=>{try{S.remove?S.remove(e):S.set(e,null)}catch{}},pa=(e,t)=>({name:e.name,...e.form,base:e.base,at:t});u=()=>{try{for(const t of Re())S.set(ze(t.id),Ka(t===m?Go():t.state,t.base));yt()&&S.set(ft,p.css.peek());const e=new Date().toISOString();for(const t of ge()){const a=L.get(t);S.set(He(t),{form:{...a.form},base:a.base,at:e})}for(const t of Ye())S.set(Ve(t),pa(N.get(t),e))}catch{}};const ua=e=>{k()&&(u(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ua),i(()=>window.removeEventListener("beforeunload",ua));let Qe=!1,Tt=null;const Ct=()=>{Qe=!1,Tt=null,o.versions.value=[],o.compare.value=null,o.histError.value="",o.dockOn.peek()&&o.dockTab.peek()==="history"&&(m?.isEdit?pe():o.dockTab.value="details")},pe=async()=>{const e=m;if(e?.isEdit){Qe=!0,o.histLoading.value=!0,o.histError.value="";try{const t=(await h.versions.list(e.id)).versions||[];if(!c()||m!==e)return;o.versions.value=t.map(a=>({...a,when:qe(a.createdAt),author:a.author||"",label:a.label||"",who:[a.author,a.label].filter(Boolean).join(" - "),checked:M.observable(!1)}))}catch(t){m===e&&(o.histError.value="Failed to load versions: "+t.message)}finally{m===e&&(o.histLoading.value=!1)}}},oo=e=>o.versions.peek().find(t=>t.filename===e),ao=async e=>{const t=m;try{const a=await h.versions.get(t.id,e.filename);if(!c()||m!==t)return;Tt=e.filename,o.compare.value={label:rs(e),old:a.content,current:ht()}}catch(a){E.toast("Failed to load version: "+a.message,{type:"error"})}},js=async()=>{const e=m;if(!e?.isEdit)return;const t=await h.pages.get(e.id).catch(()=>null);if(!c()||m!==e||!t)return;e.page=t;const a=la(t,e.id);e.base=W(a),e.savedAdminTags=JSON.stringify(a.adminTags),Ze(a),o.base.value=e.base,Ct(),o.dockOn.peek()&&o.dockTab.peek()==="history"&&pe(),U()},ma=async e=>{if(!(!e||!await E.confirm("Restore this version? The current page content will be overwritten (a pre-restore snapshot will be saved automatically).")))try{await h.versions.restore(m.id,e),E.toast("Page restored.",{type:"success"}),await js()}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},Bs=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await h.versions.delete(m.id,e.filename),E.toast("Version deleted.",{type:"success"}),await pe()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},Ne=new Map,Is=e=>{if(!Ne.has(e)){const t=I(e);Ne.set(e,{id:e,kind:t.kind,kindText:t.kind==="page"?"Page":t.kind==="component"?"Component":"Block",label:M.observable(""),title:M.observable(""),active:M.observable(!1),dirty:M.observable(!1)})}return Ne.get(e)},fa=e=>{const t=e===m?p.title.peek():e?.loaded?e.state.title:D.find(a=>a.urlPath===e?.id)?.title;return String(t||"").trim()||e?.id||"New page"};let ba=!0,va="";const Fs=()=>{if(ba)return;const e={ids:P,active:o.active.peek()},t=JSON.stringify(e);if(t!==va){va=t;try{S.set(ps,e)}catch{}}},q=()=>{const e=P.map(Is);for(const t of e){const a=I(t.id);let s,r;if(a.kind==="page"){const l=K.get(a.id);s=fa(l),r=l?.loaded?Yt(l):xe(a),t.title.value=a.id?`${s} (${a.id})`:"New page"}else if(a.kind==="component"){const l=N.get(a.id);s=a.id,r=l?he(l):xe(a),t.title.value=`Component <dm-${a.id}>`}else{const l=L.get(a.id);s=a.id,r=l?de(l):Ae.has(a.id)||xe(a),t.title.value=`Block ${a.id}`}t.label.peek()!==s&&(t.label.value=s),t.dirty.peek()!==r&&(t.dirty.value=r);const n=t.id===o.active.peek();t.active.peek()!==n&&(t.active.value=n)}e.map(t=>t.id).join("|")!==o.tabs.peek().map(t=>t.id).join("|")&&(o.tabs.value=e),Fs()};let ke=new Set;try{const e=S.get(us);Array.isArray(e)&&(ke=new Set(e.filter(t=>typeof t=="string")))}catch{}const ga=new Map,so=e=>{let t=ga.get(e.key);return t?(t.label.peek()!==e.label&&(t.label.value=e.label),t.meta.peek()!==(e.meta||"")&&(t.meta.value=e.meta||""),t.status.peek()!==(e.status||"")&&(t.status.value=e.status||""),t.folder.peek()!==!!e.folder&&(t.folder.value=!!e.folder)):(t={...e,label:M.observable(e.label),meta:M.observable(e.meta||""),status:M.observable(e.status||""),folder:M.observable(!!e.folder),active:M.observable(!1),dirty:M.observable(!1),folded:M.observable(!1),title:""},ga.set(e.key,t)),t.title=e.kind==="block"||e.kind==="component"?e.meta?`${e.id} - ${e.meta}`:e.id:e.kind==="folder"?e.meta:`${e.label} (${e.meta})`,t},no=new Map,z=()=>{const e=o.filter.peek(),t=I(o.active.peek()),a=t?.kind==="page"?t.id:null,s=t?.kind==="block"?t.id:null,r=t?.kind==="component"?t.id:null,n=new Set(Re().map(f=>f.id)),l=er(D,{filter:e,folded:ke}).map(so);for(const f of l){const C=f.kind==="page"&&f.id===a,B=f.kind==="page"&&(n.has(f.id)||!K.get(f.id)?.loaded&&xe({kind:"page",id:f.id})),fe=ke.has(f.id);f.active.peek()!==C&&(f.active.value=C),f.dirty.peek()!==B&&(f.dirty.value=B),f.folded.peek()!==fe&&(f.folded.value=fe)}const g=Jn(ie,e).map(so),$=f=>{const C=L.get(f);return C?de(C):Ae.has(f)||xe({kind:"block",id:f})};for(const f of g){const C=f.id===s,B=$(f.id);f.active.peek()!==C&&(f.active.value=C),f.dirty.peek()!==B&&(f.dirty.value=B)}const G=Yn(Je,e).map(so),Te=f=>{const C=N.get(f);return C?he(C):xe({kind:"component",id:f})};for(const f of G){const C=f.id===r,B=Te(f.id);f.active.peek()!==C&&(f.active.value=C),f.dirty.peek()!==B&&(f.dirty.value=B)}const Co=m?Fn(kt,Kt,be):[],za=(f,C,B)=>(no.has(f)||no.set(f,{key:f,id:C,label:B,kindText:"",title:"",active:M.observable(!1),dirty:M.observable(!1)}),no.get(f)),xo=Co.map(f=>{const C=za(`block:${f.name}`,f.name,f.name);C.kindText=f.footer?"Footer":"Block",C.title=`${f.name} - ${f.footer?"in the footer, on every page":"a block on this page"}`;const B=f.name===s,fe=$(f.name);return C.active.peek()!==B&&(C.active.value=B),C.dirty.peek()!==fe&&(C.dirty.value=fe),C});for(const f of ta){const C=za(`component:${f}`,f,f);C.kindText="Component",C.title=`<dm-${f}> - a component on this page`;const B=f===r,fe=Te(f);C.active.peek()!==B&&(C.active.value=B),C.dirty.peek()!==fe&&(C.dirty.value=fe),xo.push(C)}const jt=(f,C)=>f.map(B=>B.key).join("|")===C.map(B=>B.key).join("|");jt(l,o.pageRows.peek())||(o.pageRows.value=l),jt(g,o.blockRows.peek())||(o.blockRows.value=g),jt(G,o.compRows.peek())||(o.compRows.value=G),jt(xo,o.thisPage.peek())||(o.thisPage.value=xo)},ha=async()=>{const e=await h.pages.list().catch(()=>null);!c()||!Array.isArray(e)||(D=e,z(),q())},te=()=>{const e=I(o.active.peek()),t=e?es(e):"/pages/new",a=window.location.hash.replace(/^#/,"");j.address=a.split("?")[0]===t?a:t,a.split("?")[0]!==t?history.replaceState({pe:1},"","#"+t):history.state?.pe||history.replaceState({pe:1},"",window.location.hash)},zs=async e=>{if(e.loading)return e.loading;e.loading=(async()=>{let t=null;if(e.isEdit){if(t=await h.pages.get(e.id).catch(()=>null),!c())return!1;if(!t)return E.toast(`Page ${e.id} not found.`,{type:"error"}),!1}e.page=t;const a=la(t,e.id);e.state=a,e.base=W(a),e.savedAdminTags=JSON.stringify(a.adminTags),e.loaded=!0;let s=null;try{s=S.get(ze(e.id))}catch{}const r=An(s),n=$o({kind:"page",id:e.id});if(r){J(ze(e.id));const l=W(r.state)!==e.base,g=On(r,e.base),$={...a,...r.state,adminTags:a.adminTags};if(l&&n&&!g)e.state=$,E.toast(`Your unsaved changes to ${e.id||"the new page"} are back.`,{type:"info"});else if(l){const G=r.at?` (${qe(r.at)})`:"",Te=g?" The saved page has changed since - restoring puts your version back over it (the saved one stays in History).":"";await E.confirm(`You left unsaved changes to ${e.id||"a new page"}${G}. Restore them?${Te}`)&&c()&&(e.state=$)}}return!0})();try{return await e.loading}finally{e.loading=null}},ya=e=>(K.has(e)||K.set(e,{kind:"page",id:e,isEdit:!!e,loaded:!1,page:null,state:null,base:"",savedAdminTags:"[]",at:0}),K.get(e)),ue=async e=>{const t=X({kind:"page",id:e});({ids:P}=Bo(P,t)),o.active.value=t,o.blockTab.value="",vo();const a=ya(e);if(q(),!a.loaded){o.previewStatus.value=`Opening ${e||"a new page"}\u2026`;const s=await zs(a);if(!c()||!P.includes(t)||K.get(e)!==a)return;if(!s){ka(t);return}if(o.active.peek()!==t){q(),z();return}}da(a),te(),q(),z(),U(),Q.refresh(),V.focus()};w=async(e,t)=>{if(j.address=t,e.kind==="page"){ue(e.id);return}if(!e.id){(e.kind==="block"?Nt:nt)({project:zt(t)||""});return}const a=e.kind==="block"?be:le;if(a.has(e.id)||await(e.kind==="block"?Vo:_o)(),!!c()){if(!a.has(e.id)){E.toast(`There is no ${e.kind} called "${e.id}".`,{type:"warning"}),te(),z();return}e.kind==="block"?Be(e.id):st(e.id)}};const ka=e=>{const t=o.active.peek(),a=Wn(P,e,t);P=a.ids,Ne.delete(e);const s=I(e),r=a.active!==t?I(a.active):null;if(s.kind==="page"){if(K.delete(s.id),m?.id===s.id){const n=[...K.values()].filter(l=>l.loaded&&l.id!==s.id).sort((l,g)=>g.at-l.at)[0];n?da(n):Ds()}}else s.kind==="component"?(N.delete(s.id),o.compTab.peek()===s.id&&vo()):(L.delete(s.id),o.blockTab.peek()===s.id&&(o.blockTab.value="",o.blockReady.value=!1));if(o.active.value=a.active,r?.kind==="page"){ue(r.id);return}if(r?.kind==="block"){Be(r.id);return}if(r?.kind==="component"){st(r.id);return}te(),Xe(),Le(),ye(),q(),z(),U()},et=e=>{const t=I(e);if(!t||!P.includes(e))return;let a="";try{if(t.kind==="page"){const s=K.get(t.id);s&&Yt(s)&&(S.set(ze(s.id),Ka(s===m?_():s.state,s.base)),Ao({kind:"page",id:s.id}),a=fa(s))}else if(t.kind==="component"){const s=N.get(t.id);s&&he(s)&&(S.set(Ve(t.id),pa(s,new Date().toISOString())),Ao({kind:"component",id:t.id}),a=t.id)}else{const s=L.get(t.id);s&&de(s)&&(S.set(He(t.id),{form:{...s.form},base:s.base,at:new Date().toISOString()}),Ao({kind:"block",id:t.id}),a=t.id)}}catch{}ka(e),a&&E.toast(`Changes to "${a}" kept as a draft - back when you open it again.`,{type:"info"})},xt=e=>{const t=I(e);t&&(t.kind==="page"?ue(t.id):t.kind==="component"?st(t.id):Be(t.id))},wa=new Set,qs=async e=>{const t=[];for(const a of e){const s=L.get(a);if(!s)continue;const r={...s.form};try{const n=Za(r,s.block?.meta);await h.blocks.put(a,n),s.block={...s.block,meta:n.meta},s.saved=r,s.base=Do(r),vt.set(a,new Date().toISOString()),c()&&J(He(a)),t.push(a)}catch(n){E.toast(`Block "${a}" not saved: ${n.message||"Unknown error"}`,{type:"error"})}}return t.forEach(a=>wa.add(a)),t.length&&(Le(),pt("#/blocks")),t},Hs=async e=>{const t=e===m?_():e.state,a=Po(t.seoSchema).error;if(a)return E.toast(`${e.id||"The new page"} not saved - structured data: ${a}`,{type:"error"}),e===m&&(o.seoSchemaNote.value=a,o.dockOn.value=!0,o.dockTab.value="seo"),!1;const s=Gt(t,e,Xt(t)),r=e.id;try{if(e.isEdit){const n={frontmatter:s,body:t.body};t.urlPath!==e.id&&(n.newUrlPath=t.urlPath),await h.pages.update(e.id,n)}else await h.pages.create({urlPath:t.urlPath,frontmatter:s,body:t.body})}catch(n){return E.toast(`${e.id||"The new page"} not saved: ${n.message||"Unknown error"}`,{type:"error"}),!1}if(!c())return!0;if(e.savedAdminTags=JSON.stringify(t.adminTags),e.base=W(t),e===m?o.base.value=e.base:e.state=t,J(ze(r)),!e.isEdit||t.urlPath!==e.id){K.delete(r),e.id=t.urlPath,e.isEdit=!0,K.set(e.id,e);const n=X({kind:"page",id:r}),l=X({kind:"page",id:e.id});Ne.delete(n),{ids:P}=ar(P,n,l,o.active.peek()),o.active.peek()===n&&(o.active.value=l),e===m&&(o.curId.value=e.id,te())}return!0},Vs=async e=>{const t=[];for(const a of e){const s=N.get(a);if(!s||s.readOnly)continue;if($t===s?(clearTimeout(At),$t=null,ye(),at(s)):s.state||at(s),s.compiling&&await s.compiling,!c())return t;if(s.state!=="ok"){E.toast(`Component "${a}" not saved: ${s.state==="error"?"it does not compile - fix the errors first.":"it has not compiled yet - try again in a moment."}`,{type:"warning"});continue}const r={...s.form},n={...s.meta||{}};delete n.bundled,r.project?n.project=r.project:delete n.project;try{await h.components.put(a,{source:r.source,bundled:r.bundled,meta:n}),s.meta=n,s.saved=r,s.base=Fo({name:a,...r});const l=Je.find(g=>g.name===a);l&&(l.bundled=r.bundled,l.meta=n),c()&&J(Ve(a)),t.push(a)}catch(l){E.toast(`Component "${a}" not saved: ${l.message||"Unknown error"}`,{type:"error"})}}return t.length&&(ye(),pt("#/components")),t};let ro=!1;const io=async(e=!1)=>{if(o.saving.peek()||ro)return;ro=!0;let t,a,s=[];try{if(await Ea(),!c())return;const n=I(o.active.peek());if(ur().formatOnSave&&vs(n),e)t=Re(),a=ge(),s=Ye();else if(n?.kind==="block")t=[],a=L.has(n.id)&&de(L.get(n.id))?[n.id]:[];else if(n?.kind==="component")t=[],a=[],s=N.has(n.id)&&he(N.get(n.id))?[n.id]:[];else if(n?.kind==="page"&&m)a=ge().filter(wt),t=!m.isEdit||Wt()||!a.length?[m]:[];else return;if(!t.length&&!a.length&&!s.length){E.toast("Nothing to save.",{type:"info"});return}for(const g of t){const $=Ro(g===m?_():g.state);if($.length){E.toast(`${g.id||"New page"}: ${$[0].text}`,{type:"warning"}),g!==m&&await ue(g.id),o.dockOn.value=!0,o.dockTab.value=$[0].field==="dconfig"?"dconfig":"details",Ke("dock",!0);return}}const l=Vn(a,Object.fromEntries(a.map(g=>[g,Qa(Ue.get(g),m?.id||"")])),wa);if(l&&(!await E.confirm(l)||!c()))return}finally{ro=!1}const r=t.length>0&&yt();o.saving.value=!0;try{let n=0;for(const $ of t)await Hs($)&&n++;if(!c())return;if(r){const $=p.css.peek();await h.settings.saveCustomCss($),ee.value=$,J(ft)}const l=await qs(a);if(!c())return;const g=await Vs(s);if((n||l.length||g.length)&&E.toast(_n({pages:n,css:r,blocks:l,components:g}),{type:"success"}),Xe(),!c())return;q(),z(),n&&(pt("#/pages"),Ct(),ha())}catch(n){E.toast(`Save failed: ${n.message||"Unknown error"}`,{type:"error"})}finally{o.saving.value=!1}};let oe=null,me=null;const De=new Map,je=new Map,Sa=()=>{const e=L.get(o.blockTab.peek());!e||!oe||(o.blockReady.value=!1,oe.setValue(e.form.content),me.setValue(e.form.css),o.blockProject.value=e.form.project,qo(O("#block-project"),e.form.project),o.blockBundled.value=e.form.bundled,o.blockWhere.value=Dn({usage:Ue.get(e.form.name)})||"Where it is used could not be read.",o.blockNote.value="",o.blockReady.value=!0,uo())},_s=e=>{const t=L.get(e);if(!t)return{};const a=t.form.content;return Object.fromEntries(ts(Ga(a),No(a),t.samples||{}).map(s=>[s.name,s.value]))};i(M.effect(()=>{const e=o.blockProject.value,t=o.blockBundled.value,a=L.get(o.blockTab.peek());!a||!o.blockReady.peek()||a.form.project===e&&a.form.bundled===t||(a.form.project=e,a.form.bundled=t,Le())}));const lo=async(e,{quiet:t=!1}={})=>{for(;De.has(e);)if(await De.get(e),L.has(e)||t)return L.get(e)||null;if(L.has(e))return L.get(e);const a=(async()=>{try{const s=await h.blocks.get(e);if(!c()||!t&&!P.includes(X({kind:"block",id:e})))return null;const r=Xa({...s,name:e}),n={block:s,saved:r,base:Do(r),form:{...r},samples:{}};let l=null;try{l=S.get(He(e))}catch{}const g=Bn(l,n.base);if(t&&g?.stale)return null;if($o({kind:"block",id:e}),g){const $=g.at?` (${qe(g.at)})`:"",G=!g.stale||await E.confirm(`You left unsaved changes to "${e}"${$}, but the block has been saved since. Restore your version anyway?`);if(!c())return null;G&&(n.form={...n.form,...g.form,name:e},g.stale||E.toast(`Your unsaved changes to "${e}" are back.`,{type:"info"}))}return l&&J(He(e)),L.set(e,n),Le(),n}catch(s){return c()&&o.blockTab.peek()===e&&(o.blockNote.value=`Could not open "${e}": ${s.message||"unknown error"}`),null}finally{De.delete(e)}})();return De.set(e,a),a},Ea=async()=>{for(;De.size||je.size;)await Promise.all([...De.values(),...je.values()])},Be=async e=>{if(!e||!be.has(e))return;const t=X({kind:"block",id:e});({ids:P}=Bo(P,t)),o.active.value=t,vo(),o.cssMode.peek()&&(o.cssMode.value=!1),o.viewMode.peek()==="preview"&&(o.viewMode.value="split"),o.blockTab.peek()!==e&&(o.blockReady.value=!1),o.blockTab.value=e,o.blockUses.value=m&&wt(e)?Kn(Qa(Ue.get(e),m.id||"")):zn(Ue.get(e)),te(),q(),z();let a=L.get(e);a||(o.blockReady.value=!1,o.blockNote.value=`Opening ${e}\u2026`,a=await lo(e)),!(!a||!c()||o.blockTab.peek()!==e)&&(Sa(),q(),z(),Zt=e,U(),(o.blockPart.peek()==="css"?me:oe)?.focus())};let co=null;i(()=>clearTimeout(co));const Ma=(e,t)=>{const a=L.get(o.blockTab.peek());!a||!o.blockReady.peek()||(a.form[e]=t,clearTimeout(co),co=setTimeout(()=>{Le(),e==="content"&&uo()},120),U())},we=O("#pe-block-samples"),Ta=e=>{we.replaceChildren();const t=document.createElement("p");t.className="pe-dock-none",t.textContent=e,we.appendChild(t)};let tt=[],po=null;function uo(){const e=L.get(o.blockTab.peek());if(!e){tt=[],po=null,Ta("Open a block to see its placeholders.");return}const t=ts(Ga(e.form.content),No(e.form.content),e.samples);if(!(po===e&&t.map(a=>a.name).join("|")===tt.join("|"))){if(tt=t.map(a=>a.name),po=e,!t.length){Ta("No {{placeholders}} in the template yet.");return}we.replaceChildren();for(const a of t){const s=document.createElement("div");s.className="pe-comp-prop",s.tabIndex=0,s.dataset.sample=a.name;const r=document.createElement("label");r.className="pe-comp-prop-label",r.textContent=a.name,r.title=a.raw?`{{{${a.name}}}} - raw markup`:`{{${a.name}}}`;const n=document.createElement("input");n.type="text",n.className="form-input form-input--sm pe-comp-prop-input",n.value=a.value,n.setAttribute("aria-label",`Sample value for ${a.name}`),n.addEventListener("input",()=>{e.samples[a.name]=n.value,U()});const l=document.createElement("button");l.type="button",l.className="pe-kebab",l.setAttribute("aria-label",`Actions for ${a.name}`),l.title="Actions",l.textContent="\u22EE",s.append(r,n,l),we.appendChild(s)}}}const mo=(e,t)=>No(e.form.content).has(t)?`{{{${t}}}}`:`{{${t}}}`,Ks=e=>{const t=L.get(o.blockTab.peek());return!t||!tt.includes(e)?[]:[{header:mo(t,e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>{const a=oe?.textarea;a&&(o.blockPart.value="html",a.focus(),a.setRangeText(mo(t,e),a.selectionStart,a.selectionEnd,"end"),a.dispatchEvent(new Event("input",{bubbles:!0})))}},{label:"Copy the placeholder",icon:"copy",action:()=>Ie(mo(t,e),"Placeholder")},{type:"divider"},{label:"Reset the sample",icon:"rotate-ccw",action:()=>{delete t.samples[e],tt=[],uo(),U()}}]};{const e=E.contextMenu(we,{cursor:"glyph",match:".pe-comp-prop",inherit:!1,density:"compact",items:t=>Ks(t.dataset.sample)});i(()=>e?.destroy?.()),we.addEventListener("click",t=>{const a=t.target.closest(".pe-kebab");a&&Ce(a)}),we.addEventListener("keydown",t=>{const a=t.target.closest(".pe-comp-prop");a&&t.key==="F10"&&t.shiftKey&&(t.preventDefault(),Ce(a))})}const ae={},Se=O("#component-preview"),Pt=e=>{try{Se?.contentWindow?.postMessage(e,"*")}catch{}};let fo=Se.contentDocument?.readyState==="complete"&&/component-preview/.test(Se.contentDocument?.URL||""),Ot=!1,ot=null;const Ca=()=>{fo=!0,ot&&c()&&(Pt({type:"mount",payload:ot}),Ot=!0),ot=null};Se.addEventListener("load",Ca),i(()=>Se.removeEventListener("load",Ca));const Us=e=>{if(!fo){ot=e;return}if(!Ot){Ot=!0,Pt({type:"mount",payload:e});return}ot=e,fo=!1,Ot=!1,Se.src=Se.src};let Rt="";const bo=(e=!1)=>{const t=N.get(o.compTab.peek());if(!(!t||t.state!=="ok"||!t.js)){if(o.viewMode.peek()==="write"){t.previewStale=!0;return}!e&&Rt===t.name&&!t.previewStale||(t.previewStale=!1,Rt=t.name,Us({compiledJs:t.js,tagName:`dm-${t.name}`,props:t.previewProps}))}},at=e=>{const t=++e.seq;return e.state="compiling",o.compTab.peek()===e.name&&(o.compState.value="compiling"),e.compiling=(async()=>{let a;try{a=await h.components.compile(e.name,e.form.source)}catch(s){a={errors:[{type:"request",message:s.message||"Compile request failed."}]}}if(!(t!==e.seq||!c())){if(a.errors?.length?(e.state="error",e.errors=a.errors.map(s=>`[${s.type}] ${s.message}`).join(`
140
- `),e.js=null):(e.state="ok",e.errors="",e.js=a.js),o.compTab.peek()!==e.name){e.previewStale=!0;return}o.compState.value=e.state,o.compErrors.value=e.errors,e.state==="ok"&&(ko(),bo(!0))}})().finally(()=>{e.compiling&&t===e.seq&&(e.compiling=null)}),e.compiling};let At=null,$t=null;i(()=>clearTimeout(At));const xa=()=>{const e=N.get(o.compTab.peek());if(!e||!ae.template)return;o.compReady.value=!1;const t=ut(e.form.source);for(const a of qt)ae[a].setValue(t[a]),ae[a].textarea.readOnly=e.readOnly;o.compReadOnly.value=e.readOnly,o.compProject.value=e.form.project,qo(O("#comp-project"),e.form.project),o.compBundled.value=e.form.bundled,o.compState.value=e.state,o.compErrors.value=e.errors,o.compNote.value="",o.compReady.value=!0,ko(),e.state==="ok"?bo():e.state||at(e)},vo=()=>{o.compTab.peek()&&(o.compTab.value="",o.compReady.value=!1)},Pa={page:"details",block:"details",component:"details"};let Lt="";i(M.effect(()=>{const e=Qo.value;e!==Lt&&(Lt&&(Pa[Lt]=o.dockTab.peek()),Lt=e,e&&(o.dockTab.value=Pa[e]||"details",e==="page"&&o.dockTab.peek()==="history"&&o.dockOn.peek()&&m?.isEdit&&!Qe&&pe()))}));const go=async(e,{quiet:t=!1}={})=>{if(je.has(e))return je.get(e);if(N.has(e))return N.get(e);const a=(async()=>{try{const s=await h.components.get(e);if(!c()||!P.includes(X({kind:"component",id:e})))return null;const r=s.readOnly===!0||s.origin==="plugin",n={project:s.meta?.project||"",bundled:!!s.bundled,source:os(ut(s.source))},l={name:e,comp:s,saved:n,base:Fo({name:e,...n}),form:{...n},meta:s.meta||{},readOnly:r,state:"",errors:"",js:null,seq:0,compiling:null,previewProps:{},previewStale:!1};let g=null;try{g=S.get(Ve(e))}catch{}const $=cr(g,l.base);if(t&&(!$.ok||$.stale||r))return null;const G=$o({kind:"component",id:e});if(t&&!G)return null;if($.ok&&!r){const Te=g.at?` (${qe(g.at)})`:"",Co=G&&!$.stale||await E.confirm($.stale?`You left unsaved changes to the component "${e}"${Te}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to the component "${e}"${Te}. Restore them?`);if(!c())return null;Co&&(l.form={project:String(g.project??""),bundled:g.bundled===!0,source:String(g.source)},G&&!$.stale&&E.toast(`Your unsaved changes to the component "${e}" are back.`,{type:"info"}))}return g&&J(Ve(e)),N.set(e,l),ye(),l}catch(s){return c()&&o.compTab.peek()===e&&(o.compNote.value=`Could not open <dm-${e}>: ${s.message||"unknown error"}`),null}finally{je.delete(e)}})();return je.set(e,a),a},st=async e=>{if(!e||!le.has(e))return;const t=X({kind:"component",id:e});({ids:P}=Bo(P,t)),o.active.value=t,o.blockTab.value="",o.cssMode.peek()&&(o.cssMode.value=!1),o.viewMode.peek()==="preview"&&(o.viewMode.value="split"),o.compTab.value=e,te(),q(),z();let a=N.get(e);a||(o.compReady.value=!1,o.compReadOnly.value=!1,o.compState.value="",o.compNote.value=`Opening <dm-${e}>\u2026`,a=await go(e)),!(!a||!c()||o.compTab.peek()!==e)&&(xa(),q(),z(),ae[o.compPart.peek()]?.focus())},Js=(e,t)=>{const a=N.get(o.compTab.peek());if(!a||!o.compReady.peek()||a.readOnly)return;const s=ut(a.form.source);s[e]=t,a.form.source=os(s),clearTimeout(At),$t=a,At=setTimeout(()=>{$t=null,ye(),at(a)},300)};i(M.effect(()=>{const e=o.compProject.value,t=o.compBundled.value,a=N.get(o.compTab.peek());!a||!o.compReady.peek()||a.readOnly||a.form.project===e&&a.form.bundled===t||(a.form.project=e,a.form.bundled=t,ye())}));const Ee=O("#pe-comp-props");let ho=[];const yo=e=>{Ee.replaceChildren();const t=document.createElement("p");t.className="pe-dock-none",t.textContent=e,Ee.appendChild(t)};function ko(){const e=N.get(o.compTab.peek());if(!e){ho=[],yo("Open a component to try its props.");return}const{rows:t,error:a}=as(ut(e.form.source).props,e.previewProps);if(ho=t,a){yo("The <props> block is not valid JSON - fix it to see the props here.");return}if(!t.length){e.previewProps={},yo("No props declared. Add them in the <props> block.");return}e.previewProps=Object.fromEntries(t.map(s=>[s.name,s.value])),Ee.replaceChildren();for(const s of t){const r=document.createElement("div");r.className="pe-comp-prop",r.tabIndex=0,r.dataset.prop=s.name;const n=document.createElement("label");n.className="pe-comp-prop-label",n.textContent=s.label,n.title=`${s.name} (${s.type})`;const l=s.type==="array"||s.type==="object"?document.createElement("textarea"):document.createElement("input");s.type==="boolean"?(l.type="checkbox",l.className="form-check",l.checked=ss(s.type,s.value)):(l.tagName==="INPUT"?l.type=s.type==="number"?"number":"text":l.rows=2,l.className="form-input form-input--sm pe-comp-prop-input",l.value=ss(s.type,s.value)),l.setAttribute("aria-label",s.label);const g=()=>{e.previewProps[s.name]=pr(s.type,s.type==="boolean"?l.checked:l.value),Rt===e.name&&Pt({type:"update",payload:{props:e.previewProps}})};l.addEventListener("input",g),l.addEventListener("change",g);const $=document.createElement("button");$.type="button",$.className="pe-kebab",$.setAttribute("aria-label",`Actions for ${s.label}`),$.title="Actions",$.textContent="\u22EE",r.append(n,l,$),Ee.appendChild(r)}}const Ie=async(e,t)=>{try{await navigator.clipboard.writeText(e),E.toast(`${t} copied.`,{type:"success"})}catch{E.toast(e,{type:"info",duration:8e3})}},Ws=e=>{const t=N.get(o.compTab.peek()),a=ho.find(n=>n.name===e);if(!t||!a)return[];const s=lr(a.name,a.type,t.previewProps[a.name]),r=`dm-${t.name}`;return[{header:a.label},{label:"Reset to its default",icon:"rotate-ccw",action:()=>{delete t.previewProps[a.name];const n=as(ut(t.form.source).props,{}).rows.find(l=>l.name===a.name);t.previewProps[a.name]=n?n.value:null,ko(),Rt===t.name&&Pt({type:"update",payload:{props:t.previewProps}})}},{label:"Copy as an attribute",icon:"copy",disabled:!s,action:()=>Ie(s,"Attribute")},{label:"Copy the tag with this value",icon:"code",action:()=>Ie(`<${r}${s?` ${s}`:""}></${r}>`,"Tag")},{label:"Copy the prop name",icon:"copy",action:()=>Ie(a.name,"Name")},{type:"divider"},{label:"Edit in <props>",icon:"edit",action:()=>{o.compPart.value="props";const n=ae.props?.textarea;if(!n)return;const l=n.value.indexOf(`"${a.name}"`);n.focus(),l>=0&&n.setSelectionRange(l,l+a.name.length+2)}}]};{const e=E.contextMenu(Ee,{cursor:"glyph",match:".pe-comp-prop",inherit:!1,density:"compact",items:t=>Ws(t.dataset.prop)});i(()=>e?.destroy?.()),Ee.addEventListener("click",t=>{const a=t.target.closest(".pe-kebab");a&&Ce(a)}),Ee.addEventListener("keydown",t=>{const a=t.target.closest(".pe-comp-prop");a&&t.key==="F10"&&t.shiftKey&&(t.preventDefault(),Ce(a))})}let Fe=!1;const Oa=()=>zt()||"",Nt=async({project:e=Oa(),seed:t="",content:a=wr,css:s=""}={})=>{if(!Fe){Fe=!0;try{const n=await ls({kind:"block",projects:se,taken:[...be],project:e,name:t,check:Nn,register:i,create:async(l,g)=>{await h.blocks.put(l,Za({name:l,project:g,bundled:!1,content:a,css:s}))}});if(!c())return;if(!n){te();return}if(await Vo(),!c())return;pt("#/blocks"),E.toast(`Created the block "${n}".`,{type:"success"}),Be(n)}finally{Fe=!1}}},nt=async({name:e="",source:t=ir,project:a=Oa()}={})=>{if(!Fe){Fe=!0;try{const s=await ls({kind:"component",projects:se,taken:[...le],project:a,name:e,register:i,check:(r,n)=>dr(r,{isNew:!0,taken:n}),create:async(r,n)=>{await h.components.put(r,{source:t,bundled:!1,meta:n?{project:n}:{}})}});if(!c())return;if(!s){te();return}if(await _o(),!c())return;pt("#/components"),E.toast(`Created the component "${s}".`,{type:"success"}),st(s)}finally{Fe=!1}}},Ra=y.querySelector(".dm-plugin-body")||y,Ys=M.computed(()=>{o.blockRev.value,o.compRev.value,o.docRev.value,W(_()),o.base.value;const e=I(o.active.value),t=[...Re().map(s=>X({kind:"page",id:s.id})),...ge().map(s=>X({kind:"block",id:s})),...Ye().map(s=>X({kind:"component",id:s}))];let a=[];return e?.kind==="block"||e?.kind==="component"?a=[o.active.value]:e?.kind==="page"&&m&&(a=[o.active.value,...ge().filter(wt).map(s=>X({kind:"block",id:s}))]),sr(t,a)}),Gs={...p,...o,dirty:ys,pageDirty:gs,activeDirty:ks,cssUnsaved:hs,hasPage:ws,activeKind:Xo,canView:Zo,viewHref:Ss,problem:Es,dconfigNoteRows:Ms,statusText:Ts,saveAll:Ys,dockKind:Qo,blockTagText:Cs,compStateText:xs,compTagText:Ps,anySelected:Os,allSelected:Rs,someSelected:As},rt=(e,t)=>Ke(e,t),wo={save:()=>io(!1),saveAllOpen:()=>io(!0),discard:async()=>{if(await Ea(),!c())return;const e=I(o.active.peek());if(e?.kind==="block"){const t=L.get(e.id);if(!t||!de(t)||!await E.confirm(`Discard the changes to "${e.id}" you have not saved?`))return;t.form={...t.saved},J(He(e.id)),Sa(),Le()}else if(e?.kind==="component"){const t=N.get(e.id);if(!t||!he(t)||!await E.confirm(`Discard the changes to the component "${e.id}" you have not saved?`))return;t.form={...t.saved},t.state="",J(Ve(e.id)),xa(),ye()}else{if(!m||e?.kind==="page"&&e.id!==m.id||!(Wt()||yt())||!await E.confirm("Discard the changes to this page you have not saved?"))return;Ze(JSON.parse(o.base.peek())),p.cssOpened.peek()&&(p.css.value=ee.peek()),J(ze(m.id)),J(ft),Xe()}q(),z(),U()},newPage:()=>ue(""),newBlock:()=>Nt(),newComponent:()=>nt(),toggleSection:e=>{const t={here:"secHere",pages:"secPages",blocks:"secBlocks",components:"secComps"}[e];t&&(o[t].value=!o[t].peek(),rt(t,o[t].peek()))},toggleExplorer:()=>{o.explorerOn.value=!o.explorerOn.peek(),rt("explorer",o.explorerOn.peek())},toggleDock:()=>{o.dockOn.value=!o.dockOn.peek(),rt("dock",o.dockOn.peek()),o.dockOn.peek()&&o.dockTab.peek()==="history"&&!Qe&&pe()},showDock:e=>{o.dockTab.value=e,e==="history"&&m?.isEdit&&!Qe&&pe()},schemaChanged:()=>{o.seoSchemaNote.value=Po(p.seoSchema.peek()).error},insertSchemaExample:async e=>{const t=e?.target,a=t?.value||"";if(t&&(t.value=""),!a||p.seoSchema.peek().trim()&&!await E.confirm("Replace the structured data with the example?"))return;const s=p.urlPath.peek().trim()||m?.id||"/";p.seoSchema.value=fn(a,{title:p.seoTitle.peek().trim()||p.title.peek().trim(),description:p.seoDesc.peek().trim()||p.description.peek().trim(),url:`${location.origin}${s}`,siteName:Y?.title||Y?.seo?.defaultTitle||""}),o.seoSchemaNote.value=""},setBlockPart:e=>{o.blockPart.value=e==="css"?"css":"html",(e==="css"?me:oe)?.focus()},formatBlock:()=>{const e=o.blockPart.peek()==="css",t=e?me:oe;!t||!o.blockReady.peek()||Uo(_e(t.textarea,e?"css":"html"))},copyShortcode:async()=>{const e=o.blockTab.peek();e&&await Ie(In(e),"Shortcode")},setCompPart:e=>{o.compPart.value=qt.includes(e)?e:"template",ae[o.compPart.peek()]?.focus()},copyComponent:()=>{const e=N.get(o.compTab.peek());e&&nt({name:Lo(e.name,[...le]),source:e.form.source})},setMode:e=>{o.cssMode.peek()&&(o.cssMode.value=!1),o.viewMode.value=jo(e),rt("viewMode",o.viewMode.peek()),e!=="write"&&(U(!0),bo())},toggleFullscreen:()=>{o.fullscreen.value=!o.fullscreen.peek(),rt("fullscreen",o.fullscreen.peek())},toggleCss:()=>{o.blockTab.peek()||o.compTab.peek()||(o.cssMode.peek()?(o.cssMode.value=!1,o.viewMode.value=jo(Pe().viewMode),Q.refresh()):(ia(),o.cssMode.value=!0,o.viewMode.value="split"))},formatMarkdown:()=>{Jt(),Uo(_e(V,"md"))},toggleFold:()=>{o.folded.peek()?Q.unfoldAll():Q.foldAll(),o.folded.value=!o.folded.peek(),We.paint()},setWidth:e=>{o.liveWidth.value=e},refreshPreview:()=>{if(o.compTab.peek()){const e=N.get(o.compTab.peek());e&&at(e)}else U(!0)},share:async()=>{if(!m?.isEdit){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await yr(m.id)},toggleAll:e=>{const t=!!e?.target?.checked;o.versions.peek().forEach(a=>{a.checked.value=t})},deleteSelected:async()=>{const e=o.versions.peek().filter(a=>a.checked.peek()).map(a=>a.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await h.versions.bulkDelete(m.id,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await pe()}catch(a){E.toast("Delete failed: "+a.message,{type:"error"})}},saveNamed:async()=>{if(!m?.isEdit)return;const e=m;await kr(e.id)&&c()&&m===e&&await pe()},closeCompare:()=>{o.compare.value=null,Tt=null},restoreCompared:()=>ma(Tt)};Ge.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const Xs=M.applyBindings(Gs,Ra,{methods:wo});i(()=>Xs.dispose()),oe=zo(O('.pe-block-edit[data-part="html"]'),{ariaLabel:"Block template HTML",language:"html",onChange:e=>Ma("content",e)}),me=zo(O('.pe-block-edit[data-part="css"]'),{ariaLabel:"Block CSS",language:"css",placeholder:`/* Scoped to this block - e.g.
139
+ </span>`);Ze.prepend(zs),Ze.before(O(".pe-block-bar")),Ze.before(O(".pe-comp-bar"));const lo=e=>(Array.isArray(e)?e:[]).map(t=>({value:t,label:t})),io=O("#field-tags");let xt=null;xt=io?E.pillbox(io,{data:lo(Wt),value:[],creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{xt&&(p.tags.value=[...xt.getValue()])}}):null;const co=O("#field-admin-tags");let Pt=null;Pt=co?E.pillbox(co,{data:lo(T),value:[],creatable:!0,searchable:!0,placeholder:"Add admin tag\u2026",onChange:()=>{Pt&&(p.adminTags.value=[...Pt.getValue()])}}):null;const po=()=>{a.bodyRev.value=a.bodyRev.peek()+1},Le=()=>{a.blockRev.value=a.blockRev.peek()+1},ye=()=>{a.compRev.value=a.compRev.peek()+1},Qe=()=>{a.docRev.value=a.docRev.peek()+1},uo=()=>{p.cssOpened.peek()||(p.css.peek()||(p.css.value=ee.peek()),p.cssOpened.value=!0,dn(ve,{language:"css"}))},et=e=>{for(const t of Fr)p[t].value=e[t]==null?"":String(e[t]);for(const t of zr)p[t].value=!!e[t];p.tags.value=Array.isArray(e.tags)?e.tags:[],xt?.setValue(p.tags.peek()),Array.isArray(e.adminTags)&&(p.adminTags.value=e.adminTags),Pt?.setValue(p.adminTags.peek()),_a(O("#field-visibility"),p.visibility.peek()),Q.unfoldAll(),a.folded.value=!1,yt=!0,_.value=e.body==null?"":String(e.body),yt=!1,Q.refresh(),Ja(),po()},mo=(e,t)=>({urlPath:t||"",title:e?.title||"",description:e?.description||"",layout:Va(e?.layout||"default"),status:e?.status||"draft",sortOrder:String(e?.sortOrder??99),showInNav:!!e?.showInNav,sidebar:!!e?.sidebar,breadcrumbs:e?e.breadcrumbs!==!1:!0,bundled:!!e?.bundled,tags:Array.isArray(e?.tags)?e.tags:[],adminTags:Array.isArray(e?.adminTags)?e.adminTags:[],category:e?.category||"",visibility:typeof e?.visibility=="string"&&e.visibility?e.visibility:"public",project:In({isEdit:!!t,page:e,projects:se,fromHash:t?"":Vt()||"",urlPath:t||""}),theme:e?.theme||"",seoTitle:e?.seo?.title||"",seoDesc:e?.seo?.description||"",seoSchema:wn(e?.seo?.schema),dconfig:e?.dconfig?JSON.stringify(e.dconfig,null,2):"",body:e?.content||"",css:null}),fo=()=>{m&&(m.state=V(),m.at=Date.now())},bo=e=>{m!==e&&(fo(),m=e,et(e.state),a.base.value=e.base,a.curId.value=e.id,e.at=Date.now(),At(),Qe())},qs=()=>{fo(),m=null;const e=hs();et(e),a.base.value=W(e),a.curId.value=null,At(),Qe()};et(hs()),a.base.value=W(V());const J=e=>{try{S.remove?S.remove(e):S.set(e,null)}catch{}},vo=(e,t)=>({name:e.name,...e.form,base:e.base,at:t});u=()=>{try{for(const t of Ae())S.set(ze(t.id),Go(t===m?Za():t.state,t.base));wt()&&S.set(vt,p.css.peek());const e=new Date().toISOString();for(const t of ge()){const o=L.get(t);S.set(He(t),{form:{...o.form},base:o.base,at:e})}for(const t of Ye())S.set(_e(t),vo(N.get(t),e))}catch{}};const go=e=>{k()&&(u(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",go),l(()=>window.removeEventListener("beforeunload",go));let tt=!1,Ot=null;const At=()=>{tt=!1,Ot=null,a.versions.value=[],a.compare.value=null,a.histError.value="",a.dockOn.peek()&&a.dockTab.peek()==="history"&&(m?.isEdit?pe():a.dockTab.value="details")},pe=async()=>{const e=m;if(e?.isEdit){tt=!0,a.histLoading.value=!0,a.histError.value="";try{const t=(await g.versions.list(e.id)).versions||[];if(!i()||m!==e)return;a.versions.value=t.map(o=>({...o,when:qe(o.createdAt),author:o.author||"",label:o.label||"",who:[o.author,o.label].filter(Boolean).join(" - "),checked:M.observable(!1)}))}catch(t){m===e&&(a.histError.value="Failed to load versions: "+t.message)}finally{m===e&&(a.histLoading.value=!1)}}},na=e=>a.versions.peek().find(t=>t.filename===e),ra=async e=>{const t=m;try{const o=await g.versions.get(t.id,e.filename);if(!i()||m!==t)return;Ot=e.filename,a.compare.value={label:ps(e),old:o.content,current:kt()}}catch(o){E.toast("Failed to load version: "+o.message,{type:"error"})}},Hs=async()=>{const e=m;if(!e?.isEdit)return;const t=await g.pages.get(e.id).catch(()=>null);if(!i()||m!==e||!t)return;e.page=t;const o=mo(t,e.id);e.base=W(o),e.savedAdminTags=JSON.stringify(o.adminTags),et(o),a.base.value=e.base,At(),a.dockOn.peek()&&a.dockTab.peek()==="history"&&pe(),U()},ho=async e=>{if(!(!e||!await E.confirm("Restore this version? The current page content will be overwritten (a pre-restore snapshot will be saved automatically).")))try{await g.versions.restore(m.id,e),E.toast("Page restored.",{type:"success"}),await Hs()}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},_s=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await g.versions.delete(m.id,e.filename),E.toast("Version deleted.",{type:"success"}),await pe()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},Ne=new Map,Vs=e=>{if(!Ne.has(e)){const t=j(e);Ne.set(e,{id:e,kind:t.kind,kindText:t.kind==="page"?"Page":t.kind==="component"?"Component":"Block",label:M.observable(""),title:M.observable(""),active:M.observable(!1),dirty:M.observable(!1)})}return Ne.get(e)},yo=e=>{const t=e===m?p.title.peek():e?.loaded?e.state.title:D.find(o=>o.urlPath===e?.id)?.title;return String(t||"").trim()||e?.id||"New page"};let ko=!0,wo="";const Ks=()=>{if(ko)return;const e={ids:P,active:a.active.peek()},t=JSON.stringify(e);if(t!==wo){wo=t;try{S.set(vs,e)}catch{}}},q=()=>{const e=P.map(Vs);for(const t of e){const o=j(t.id);let s,r;if(o.kind==="page"){const c=K.get(o.id);s=yo(c),r=c?.loaded?Qt(c):xe(o),t.title.value=o.id?`${s} (${o.id})`:"New page"}else if(o.kind==="component"){const c=N.get(o.id);s=o.id,r=c?he(c):xe(o),t.title.value=`Component <dm-${o.id}>`}else{const c=L.get(o.id);s=o.id,r=c?de(c):Re.has(o.id)||xe(o),t.title.value=`Block ${o.id}`}t.label.peek()!==s&&(t.label.value=s),t.dirty.peek()!==r&&(t.dirty.value=r);const n=t.id===a.active.peek();t.active.peek()!==n&&(t.active.value=n)}e.map(t=>t.id).join("|")!==a.tabs.peek().map(t=>t.id).join("|")&&(a.tabs.value=e),Ks()};let ke=new Set;try{const e=S.get(gs);Array.isArray(e)&&(ke=new Set(e.filter(t=>typeof t=="string")))}catch{}const So=new Map,la=e=>{let t=So.get(e.key);return t?(t.label.peek()!==e.label&&(t.label.value=e.label),t.meta.peek()!==(e.meta||"")&&(t.meta.value=e.meta||""),t.status.peek()!==(e.status||"")&&(t.status.value=e.status||""),t.folder.peek()!==!!e.folder&&(t.folder.value=!!e.folder)):(t={...e,label:M.observable(e.label),meta:M.observable(e.meta||""),status:M.observable(e.status||""),folder:M.observable(!!e.folder),active:M.observable(!1),dirty:M.observable(!1),folded:M.observable(!1),title:""},So.set(e.key,t)),t.title=e.kind==="block"||e.kind==="component"?e.meta?`${e.id} - ${e.meta}`:e.id:e.kind==="folder"?e.meta:`${e.label} (${e.meta})`,t},ia=new Map,z=()=>{const e=a.filter.peek(),t=j(a.active.peek()),o=t?.kind==="page"?t.id:null,s=t?.kind==="block"?t.id:null,r=t?.kind==="component"?t.id:null,n=new Set(Ae().map(f=>f.id)),c=ir(D,{filter:e,folded:ke}).map(la);for(const f of c){const C=f.kind==="page"&&f.id===o,B=f.kind==="page"&&(n.has(f.id)||!K.get(f.id)?.loaded&&xe({kind:"page",id:f.id})),fe=ke.has(f.id);f.active.peek()!==C&&(f.active.value=C),f.dirty.peek()!==B&&(f.dirty.value=B),f.folded.peek()!==fe&&(f.folded.value=fe)}const h=tr(le,e).map(la),$=f=>{const C=L.get(f);return C?de(C):Re.has(f)||xe({kind:"block",id:f})};for(const f of h){const C=f.id===s,B=$(f.id);f.active.peek()!==C&&(f.active.value=C),f.dirty.peek()!==B&&(f.dirty.value=B)}const G=or(Je,e).map(la),Te=f=>{const C=N.get(f);return C?he(C):xe({kind:"component",id:f})};for(const f of G){const C=f.id===r,B=Te(f.id);f.active.peek()!==C&&(f.active.value=C),f.dirty.peek()!==B&&(f.dirty.value=B)}const Pa=m?Jn(Mt,Yt,be):[],Ko=(f,C,B)=>(ia.has(f)||ia.set(f,{key:f,id:C,label:B,kindText:"",title:"",active:M.observable(!1),dirty:M.observable(!1)}),ia.get(f)),Oa=Pa.map(f=>{const C=Ko(`block:${f.name}`,f.name,f.name);C.kindText=f.footer?"Footer":"Block",C.title=`${f.name} - ${f.footer?"in the footer, on every page":"a block on this page"}`;const B=f.name===s,fe=$(f.name);return C.active.peek()!==B&&(C.active.value=B),C.dirty.peek()!==fe&&(C.dirty.value=fe),C});for(const f of so){const C=Ko(`component:${f}`,f,f);C.kindText="Component",C.title=`<dm-${f}> - a component on this page`;const B=f===r,fe=Te(f);C.active.peek()!==B&&(C.active.value=B),C.dirty.peek()!==fe&&(C.dirty.value=fe),Oa.push(C)}const zt=(f,C)=>f.map(B=>B.key).join("|")===C.map(B=>B.key).join("|");zt(c,a.pageRows.peek())||(a.pageRows.value=c),zt(h,a.blockRows.peek())||(a.blockRows.value=h),zt(G,a.compRows.peek())||(a.compRows.value=G),zt(Oa,a.thisPage.peek())||(a.thisPage.value=Oa)},Eo=async()=>{const e=await g.pages.list().catch(()=>null);!i()||!Array.isArray(e)||(D=e,z(),q())},te=()=>{const e=j(a.active.peek()),t=e?ns(e):"/pages/new",o=window.location.hash.replace(/^#/,"");I.address=o.split("?")[0]===t?o:t,o.split("?")[0]!==t?history.replaceState({pe:1},"","#"+t):history.state?.pe||history.replaceState({pe:1},"",window.location.hash)},Us=async e=>{if(e.loading)return e.loading;e.loading=(async()=>{let t=null;if(e.isEdit){if(t=await g.pages.get(e.id).catch(()=>null),!i())return!1;if(!t)return E.toast(`Page ${e.id} not found.`,{type:"error"}),!1}e.page=t;const o=mo(t,e.id);e.state=o,e.base=W(o),e.savedAdminTags=JSON.stringify(o.adminTags),e.loaded=!0;let s=null;try{s=S.get(ze(e.id))}catch{}const r=Bn(s),n=Na({kind:"page",id:e.id});if(r){J(ze(e.id));const c=W(r.state)!==e.base,h=Dn(r,e.base),$={...o,...r.state,adminTags:o.adminTags};if(c&&n&&!h)e.state=$,E.toast(`Your unsaved changes to ${e.id||"the new page"} are back.`,{type:"info"});else if(c){const G=r.at?` (${qe(r.at)})`:"",Te=h?" The saved page has changed since - restoring puts your version back over it (the saved one stays in History).":"";await E.confirm(`You left unsaved changes to ${e.id||"a new page"}${G}. Restore them?${Te}`)&&i()&&(e.state=$)}}return!0})();try{return await e.loading}finally{e.loading=null}},Mo=e=>(K.has(e)||K.set(e,{kind:"page",id:e,isEdit:!!e,loaded:!1,page:null,state:null,base:"",savedAdminTags:"[]",at:0}),K.get(e)),ue=async e=>{const t=X({kind:"page",id:e});({ids:P}=Fa(P,t)),a.active.value=t,a.blockTab.value="",ha();const o=Mo(e);if(q(),!o.loaded){a.previewStatus.value=`Opening ${e||"a new page"}\u2026`;const s=await Us(o);if(!i()||!P.includes(t)||K.get(e)!==o)return;if(!s){To(t);return}if(a.active.peek()!==t){q(),z();return}}bo(o),te(),q(),z(),U(),Q.refresh(),_.focus()};w=async(e,t)=>{if(I.address=t,e.kind==="page"){ue(e.id);return}if(!e.id){(e.kind==="block"?jt:lt)({project:Vt(t)||""});return}const o=e.kind==="block"?be:ie;if(o.has(e.id)||await(e.kind==="block"?Ka:Ua)(),!!i()){if(!o.has(e.id)){E.toast(`There is no ${e.kind} called "${e.id}".`,{type:"warning"}),te(),z();return}e.kind==="block"?Be(e.id):rt(e.id)}};const To=e=>{const t=a.active.peek(),o=ar(P,e,t);P=o.ids,Ne.delete(e);const s=j(e),r=o.active!==t?j(o.active):null;if(s.kind==="page"){if(K.delete(s.id),m?.id===s.id){const n=[...K.values()].filter(c=>c.loaded&&c.id!==s.id).sort((c,h)=>h.at-c.at)[0];n?bo(n):qs()}}else s.kind==="component"?(N.delete(s.id),a.compTab.peek()===s.id&&ha()):(L.delete(s.id),a.blockTab.peek()===s.id&&(a.blockTab.value="",a.blockReady.value=!1));if(a.active.value=o.active,r?.kind==="page"){ue(r.id);return}if(r?.kind==="block"){Be(r.id);return}if(r?.kind==="component"){rt(r.id);return}te(),Qe(),Le(),ye(),q(),z(),U()},at=e=>{const t=j(e);if(!t||!P.includes(e))return;let o="";try{if(t.kind==="page"){const s=K.get(t.id);s&&Qt(s)&&(S.set(ze(s.id),Go(s===m?V():s.state,s.base)),La({kind:"page",id:s.id}),o=yo(s))}else if(t.kind==="component"){const s=N.get(t.id);s&&he(s)&&(S.set(_e(t.id),vo(s,new Date().toISOString())),La({kind:"component",id:t.id}),o=t.id)}else{const s=L.get(t.id);s&&de(s)&&(S.set(He(t.id),{form:{...s.form},base:s.base,at:new Date().toISOString()}),La({kind:"block",id:t.id}),o=t.id)}}catch{}To(e),o&&E.toast(`Changes to "${o}" kept as a draft - back when you open it again.`,{type:"info"})},Rt=e=>{const t=j(e);t&&(t.kind==="page"?ue(t.id):t.kind==="component"?rt(t.id):Be(t.id))},Co=new Set,Js=async e=>{const t=[];for(const o of e){const s=L.get(o);if(!s)continue;const r={...s.form};try{const n=os(r,s.block?.meta);await g.blocks.put(o,n),s.block={...s.block,meta:n.meta},s.saved=r,s.base=Ba(r),ht.set(o,new Date().toISOString()),i()&&J(He(o)),t.push(o)}catch(n){E.toast(`Block "${o}" not saved: ${n.message||"Unknown error"}`,{type:"error"})}}return t.forEach(o=>Co.add(o)),t.length&&(Le(),mt("#/blocks")),t},Ws=async e=>{const t=e===m?V():e.state,o=Aa(t.seoSchema).error;if(o)return E.toast(`${e.id||"The new page"} not saved - structured data: ${o}`,{type:"error"}),e===m&&(a.seoSchemaNote.value=o,a.dockOn.value=!0,a.dockTab.value="seo"),!1;const s=St(t,e,Et(t)),r=e.id;try{if(e.isEdit){const n={frontmatter:s,body:t.body};t.urlPath!==e.id&&(n.newUrlPath=t.urlPath),await g.pages.update(e.id,n)}else await g.pages.create({urlPath:t.urlPath,frontmatter:s,body:t.body})}catch(n){return E.toast(`${e.id||"The new page"} not saved: ${n.message||"Unknown error"}`,{type:"error"}),!1}if(!i())return!0;if(e.savedAdminTags=JSON.stringify(t.adminTags),e.base=W(t),e===m?a.base.value=e.base:e.state=t,J(ze(r)),!e.isEdit||t.urlPath!==e.id){K.delete(r),e.id=t.urlPath,e.isEdit=!0,K.set(e.id,e);const n=X({kind:"page",id:r}),c=X({kind:"page",id:e.id});Ne.delete(n),{ids:P}=pr(P,n,c,a.active.peek()),a.active.peek()===n&&(a.active.value=c),e===m&&(a.curId.value=e.id,te())}return!0},Ys=async e=>{const t=[];for(const o of e){const s=N.get(o);if(!s||s.readOnly)continue;if(It===s?(clearTimeout(Dt),It=null,ye(),nt(s)):s.state||nt(s),s.compiling&&await s.compiling,!i())return t;if(s.state!=="ok"){E.toast(`Component "${o}" not saved: ${s.state==="error"?"it does not compile - fix the errors first.":"it has not compiled yet - try again in a moment."}`,{type:"warning"});continue}const r={...s.form},n={...s.meta||{}};delete n.bundled,r.project?n.project=r.project:delete n.project;try{await g.components.put(o,{source:r.source,bundled:r.bundled,meta:n}),s.meta=n,s.saved=r,s.base=qa({name:o,...r});const c=Je.find(h=>h.name===o);c&&(c.bundled=r.bundled,c.meta=n),i()&&J(_e(o)),t.push(o)}catch(c){E.toast(`Component "${o}" not saved: ${c.message||"Unknown error"}`,{type:"error"})}}return t.length&&(ye(),mt("#/components")),t};let ca=!1;const da=async(e=!1)=>{if(a.saving.peek()||ca)return;ca=!0;let t,o,s=[];try{if(await Po(),!i())return;const n=j(a.active.peek());if(kr().formatOnSave&&ws(n),e)t=Ae(),o=ge(),s=Ye();else if(n?.kind==="block")t=[],o=L.has(n.id)&&de(L.get(n.id))?[n.id]:[];else if(n?.kind==="component")t=[],o=[],s=N.has(n.id)&&he(N.get(n.id))?[n.id]:[];else if(n?.kind==="page"&&m)o=ge().filter(Tt),t=!m.isEdit||Zt()||!o.length?[m]:[];else return;if(!t.length&&!o.length&&!s.length){E.toast("Nothing to save.",{type:"info"});return}for(const h of t){const $=$a(h===m?V():h.state);if($.length){E.toast(`${h.id||"New page"}: ${$[0].text}`,{type:"warning"}),h!==m&&await ue(h.id),a.dockOn.value=!0,a.dockTab.value=$[0].field==="dconfig"?"dconfig":"details",Ke("dock",!0);return}}const c=Xn(o,Object.fromEntries(o.map(h=>[h,ss(Ue.get(h),m?.id||"")])),Co);if(c&&(!await E.confirm(c)||!i()))return}finally{ca=!1}const r=t.length>0&&wt();a.saving.value=!0;try{let n=0;for(const $ of t)await Ws($)&&n++;if(!i())return;if(r){const $=p.css.peek();await g.settings.saveCustomCss($),ee.value=$,J(vt)}const c=await Js(o);if(!i())return;const h=await Ys(s);if((n||c.length||h.length)&&E.toast(Zn({pages:n,css:r,blocks:c,components:h}),{type:"success"}),Qe(),!i())return;q(),z(),n&&(mt("#/pages"),At(),Eo())}catch(n){E.toast(`Save failed: ${n.message||"Unknown error"}`,{type:"error"})}finally{a.saving.value=!1}};let ae=null,me=null;const De=new Map,Ie=new Map,xo=()=>{const e=L.get(a.blockTab.peek());!e||!ae||(a.blockReady.value=!1,ae.setValue(e.form.content),me.setValue(e.form.css),a.blockProject.value=e.form.project,_a(O("#block-project"),e.form.project),a.blockBundled.value=e.form.bundled,a.blockWhere.value=_n({usage:Ue.get(e.form.name)})||"Where it is used could not be read.",a.blockNote.value="",a.blockReady.value=!0,fa())},Gs=e=>{const t=L.get(e);if(!t)return{};const o=t.form.content;return Object.fromEntries(rs(ts(o),Ia(o),t.samples||{}).map(s=>[s.name,s.value]))};l(M.effect(()=>{const e=a.blockProject.value,t=a.blockBundled.value,o=L.get(a.blockTab.peek());!o||!a.blockReady.peek()||o.form.project===e&&o.form.bundled===t||(o.form.project=e,o.form.bundled=t,Le())}));const pa=async(e,{quiet:t=!1}={})=>{for(;De.has(e);)if(await De.get(e),L.has(e)||t)return L.get(e)||null;if(L.has(e))return L.get(e);const o=(async()=>{try{const s=await g.blocks.get(e);if(!i()||!t&&!P.includes(X({kind:"block",id:e})))return null;const r=as({...s,name:e}),n={block:s,saved:r,base:Ba(r),form:{...r},samples:{}};let c=null;try{c=S.get(He(e))}catch{}const h=Kn(c,n.base);if(t&&h?.stale)return null;if(Na({kind:"block",id:e}),h){const $=h.at?` (${qe(h.at)})`:"",G=!h.stale||await E.confirm(`You left unsaved changes to "${e}"${$}, but the block has been saved since. Restore your version anyway?`);if(!i())return null;G&&(n.form={...n.form,...h.form,name:e},h.stale||E.toast(`Your unsaved changes to "${e}" are back.`,{type:"info"}))}return c&&J(He(e)),L.set(e,n),Le(),n}catch(s){return i()&&a.blockTab.peek()===e&&(a.blockNote.value=`Could not open "${e}": ${s.message||"unknown error"}`),null}finally{De.delete(e)}})();return De.set(e,o),o},Po=async()=>{for(;De.size||Ie.size;)await Promise.all([...De.values(),...Ie.values()])},Be=async e=>{if(!e||!be.has(e))return;const t=X({kind:"block",id:e});({ids:P}=Fa(P,t)),a.active.value=t,ha(),a.cssMode.peek()&&(a.cssMode.value=!1),a.viewMode.peek()==="preview"&&(a.viewMode.value="split"),a.blockTab.peek()!==e&&(a.blockReady.value=!1),a.blockTab.value=e,a.blockUses.value=m&&Tt(e)?Qn(ss(Ue.get(e),m.id||"")):Wn(Ue.get(e)),te(),q(),z();let o=L.get(e);o||(a.blockReady.value=!1,a.blockNote.value=`Opening ${e}\u2026`,o=await pa(e)),!(!o||!i()||a.blockTab.peek()!==e)&&(xo(),q(),z(),ea=e,U(),(a.blockPart.peek()==="css"?me:ae)?.focus())};let ua=null;l(()=>clearTimeout(ua));const Oo=(e,t)=>{const o=L.get(a.blockTab.peek());!o||!a.blockReady.peek()||(o.form[e]=t,clearTimeout(ua),ua=setTimeout(()=>{Le(),e==="content"&&fa()},120),U())},we=O("#pe-block-samples"),Ao=e=>{we.replaceChildren();const t=document.createElement("p");t.className="pe-dock-none",t.textContent=e,we.appendChild(t)};let ot=[],ma=null;function fa(){const e=L.get(a.blockTab.peek());if(!e){ot=[],ma=null,Ao("Open a block to see its placeholders.");return}const t=rs(ts(e.form.content),Ia(e.form.content),e.samples);if(!(ma===e&&t.map(o=>o.name).join("|")===ot.join("|"))){if(ot=t.map(o=>o.name),ma=e,!t.length){Ao("No {{placeholders}} in the template yet.");return}we.replaceChildren();for(const o of t){const s=document.createElement("div");s.className="pe-comp-prop",s.tabIndex=0,s.dataset.sample=o.name;const r=document.createElement("label");r.className="pe-comp-prop-label",r.textContent=o.name,r.title=o.raw?`{{{${o.name}}}} - raw markup`:`{{${o.name}}}`;const n=document.createElement("input");n.type="text",n.className="form-input form-input--sm pe-comp-prop-input",n.value=o.value,n.setAttribute("aria-label",`Sample value for ${o.name}`),n.addEventListener("input",()=>{e.samples[o.name]=n.value,U()});const c=document.createElement("button");c.type="button",c.className="pe-kebab",c.setAttribute("aria-label",`Actions for ${o.name}`),c.title="Actions",c.textContent="\u22EE",s.append(r,n,c),we.appendChild(s)}}}const ba=(e,t)=>Ia(e.form.content).has(t)?`{{{${t}}}}`:`{{${t}}}`,Xs=e=>{const t=L.get(a.blockTab.peek());return!t||!ot.includes(e)?[]:[{header:ba(t,e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>{const o=ae?.textarea;o&&(a.blockPart.value="html",o.focus(),o.setRangeText(ba(t,e),o.selectionStart,o.selectionEnd,"end"),o.dispatchEvent(new Event("input",{bubbles:!0})))}},{label:"Copy the placeholder",icon:"copy",action:()=>je(ba(t,e),"Placeholder")},{type:"divider"},{label:"Reset the sample",icon:"rotate-ccw",action:()=>{delete t.samples[e],ot=[],fa(),U()}}]};{const e=E.contextMenu(we,{cursor:"glyph",match:".pe-comp-prop",inherit:!1,density:"compact",items:t=>Xs(t.dataset.sample)});l(()=>e?.destroy?.()),we.addEventListener("click",t=>{const o=t.target.closest(".pe-kebab");o&&Ce(o)}),we.addEventListener("keydown",t=>{const o=t.target.closest(".pe-comp-prop");o&&t.key==="F10"&&t.shiftKey&&(t.preventDefault(),Ce(o))})}const oe={},Se=O("#component-preview"),$t=e=>{try{Se?.contentWindow?.postMessage(e,"*")}catch{}};let va=Se.contentDocument?.readyState==="complete"&&/component-preview/.test(Se.contentDocument?.URL||""),Lt=!1,st=null;const Ro=()=>{va=!0,st&&i()&&($t({type:"mount",payload:st}),Lt=!0),st=null};Se.addEventListener("load",Ro),l(()=>Se.removeEventListener("load",Ro));const Zs=e=>{if(!va){st=e;return}if(!Lt){Lt=!0,$t({type:"mount",payload:e});return}st=e,va=!1,Lt=!1,Se.src=Se.src};let Nt="";const ga=(e=!1)=>{const t=N.get(a.compTab.peek());if(!(!t||t.state!=="ok"||!t.js)){if(a.viewMode.peek()==="write"){t.previewStale=!0;return}!e&&Nt===t.name&&!t.previewStale||(t.previewStale=!1,Nt=t.name,Zs({compiledJs:t.js,tagName:`dm-${t.name}`,props:t.previewProps}))}},nt=e=>{const t=++e.seq;return e.state="compiling",a.compTab.peek()===e.name&&(a.compState.value="compiling"),e.compiling=(async()=>{let o;try{o=await g.components.compile(e.name,e.form.source)}catch(s){o={errors:[{type:"request",message:s.message||"Compile request failed."}]}}if(!(t!==e.seq||!i())){if(o.errors?.length?(e.state="error",e.errors=o.errors.map(s=>`[${s.type}] ${s.message}`).join(`
140
+ `),e.js=null):(e.state="ok",e.errors="",e.js=o.js),a.compTab.peek()!==e.name){e.previewStale=!0;return}a.compState.value=e.state,a.compErrors.value=e.errors,e.state==="ok"&&(Sa(),ga(!0))}})().finally(()=>{e.compiling&&t===e.seq&&(e.compiling=null)}),e.compiling};let Dt=null,It=null;l(()=>clearTimeout(Dt));const $o=()=>{const e=N.get(a.compTab.peek());if(!e||!oe.template)return;a.compReady.value=!1;const t=ft(e.form.source);for(const o of Kt)oe[o].setValue(t[o]),oe[o].textarea.readOnly=e.readOnly;a.compReadOnly.value=e.readOnly,a.compProject.value=e.form.project,_a(O("#comp-project"),e.form.project),a.compBundled.value=e.form.bundled,a.compState.value=e.state,a.compErrors.value=e.errors,a.compNote.value="",a.compReady.value=!0,Sa(),e.state==="ok"?ga():e.state||nt(e)},ha=()=>{a.compTab.peek()&&(a.compTab.value="",a.compReady.value=!1)},Lo={page:"details",block:"details",component:"details"};let Bt="";l(M.effect(()=>{const e=to.value;e!==Bt&&(Bt&&(Lo[Bt]=a.dockTab.peek()),Bt=e,e&&(a.dockTab.value=Lo[e]||"details",e==="page"&&a.dockTab.peek()==="history"&&a.dockOn.peek()&&m?.isEdit&&!tt&&pe()))}));const ya=async(e,{quiet:t=!1}={})=>{if(Ie.has(e))return Ie.get(e);if(N.has(e))return N.get(e);const o=(async()=>{try{const s=await g.components.get(e);if(!i()||!P.includes(X({kind:"component",id:e})))return null;const r=s.readOnly===!0||s.origin==="plugin",n={project:s.meta?.project||"",bundled:!!s.bundled,source:ls(ft(s.source))},c={name:e,comp:s,saved:n,base:qa({name:e,...n}),form:{...n},meta:s.meta||{},readOnly:r,state:"",errors:"",js:null,seq:0,compiling:null,previewProps:{},previewStale:!1};let h=null;try{h=S.get(_e(e))}catch{}const $=gr(h,c.base);if(t&&(!$.ok||$.stale||r))return null;const G=Na({kind:"component",id:e});if(t&&!G)return null;if($.ok&&!r){const Te=h.at?` (${qe(h.at)})`:"",Pa=G&&!$.stale||await E.confirm($.stale?`You left unsaved changes to the component "${e}"${Te}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to the component "${e}"${Te}. Restore them?`);if(!i())return null;Pa&&(c.form={project:String(h.project??""),bundled:h.bundled===!0,source:String(h.source)},G&&!$.stale&&E.toast(`Your unsaved changes to the component "${e}" are back.`,{type:"info"}))}return h&&J(_e(e)),N.set(e,c),ye(),c}catch(s){return i()&&a.compTab.peek()===e&&(a.compNote.value=`Could not open <dm-${e}>: ${s.message||"unknown error"}`),null}finally{Ie.delete(e)}})();return Ie.set(e,o),o},rt=async e=>{if(!e||!ie.has(e))return;const t=X({kind:"component",id:e});({ids:P}=Fa(P,t)),a.active.value=t,a.blockTab.value="",a.cssMode.peek()&&(a.cssMode.value=!1),a.viewMode.peek()==="preview"&&(a.viewMode.value="split"),a.compTab.value=e,te(),q(),z();let o=N.get(e);o||(a.compReady.value=!1,a.compReadOnly.value=!1,a.compState.value="",a.compNote.value=`Opening <dm-${e}>\u2026`,o=await ya(e)),!(!o||!i()||a.compTab.peek()!==e)&&($o(),q(),z(),oe[a.compPart.peek()]?.focus())},Qs=(e,t)=>{const o=N.get(a.compTab.peek());if(!o||!a.compReady.peek()||o.readOnly)return;const s=ft(o.form.source);s[e]=t,o.form.source=ls(s),clearTimeout(Dt),It=o,Dt=setTimeout(()=>{It=null,ye(),nt(o)},300)};l(M.effect(()=>{const e=a.compProject.value,t=a.compBundled.value,o=N.get(a.compTab.peek());!o||!a.compReady.peek()||o.readOnly||o.form.project===e&&o.form.bundled===t||(o.form.project=e,o.form.bundled=t,ye())}));const Ee=O("#pe-comp-props");let ka=[];const wa=e=>{Ee.replaceChildren();const t=document.createElement("p");t.className="pe-dock-none",t.textContent=e,Ee.appendChild(t)};function Sa(){const e=N.get(a.compTab.peek());if(!e){ka=[],wa("Open a component to try its props.");return}const{rows:t,error:o}=is(ft(e.form.source).props,e.previewProps);if(ka=t,o){wa("The <props> block is not valid JSON - fix it to see the props here.");return}if(!t.length){e.previewProps={},wa("No props declared. Add them in the <props> block.");return}e.previewProps=Object.fromEntries(t.map(s=>[s.name,s.value])),Ee.replaceChildren();for(const s of t){const r=document.createElement("div");r.className="pe-comp-prop",r.tabIndex=0,r.dataset.prop=s.name;const n=document.createElement("label");n.className="pe-comp-prop-label",n.textContent=s.label,n.title=`${s.name} (${s.type})`;const c=s.type==="array"||s.type==="object"?document.createElement("textarea"):document.createElement("input");s.type==="boolean"?(c.type="checkbox",c.className="form-check",c.checked=cs(s.type,s.value)):(c.tagName==="INPUT"?c.type=s.type==="number"?"number":"text":c.rows=2,c.className="form-input form-input--sm pe-comp-prop-input",c.value=cs(s.type,s.value)),c.setAttribute("aria-label",s.label);const h=()=>{e.previewProps[s.name]=yr(s.type,s.type==="boolean"?c.checked:c.value),Nt===e.name&&$t({type:"update",payload:{props:e.previewProps}})};c.addEventListener("input",h),c.addEventListener("change",h);const $=document.createElement("button");$.type="button",$.className="pe-kebab",$.setAttribute("aria-label",`Actions for ${s.label}`),$.title="Actions",$.textContent="\u22EE",r.append(n,c,$),Ee.appendChild(r)}}const je=async(e,t)=>{try{await navigator.clipboard.writeText(e),E.toast(`${t} copied.`,{type:"success"})}catch{E.toast(e,{type:"info",duration:8e3})}},en=e=>{const t=N.get(a.compTab.peek()),o=ka.find(n=>n.name===e);if(!t||!o)return[];const s=vr(o.name,o.type,t.previewProps[o.name]),r=`dm-${t.name}`;return[{header:o.label},{label:"Reset to its default",icon:"rotate-ccw",action:()=>{delete t.previewProps[o.name];const n=is(ft(t.form.source).props,{}).rows.find(c=>c.name===o.name);t.previewProps[o.name]=n?n.value:null,Sa(),Nt===t.name&&$t({type:"update",payload:{props:t.previewProps}})}},{label:"Copy as an attribute",icon:"copy",disabled:!s,action:()=>je(s,"Attribute")},{label:"Copy the tag with this value",icon:"code",action:()=>je(`<${r}${s?` ${s}`:""}></${r}>`,"Tag")},{label:"Copy the prop name",icon:"copy",action:()=>je(o.name,"Name")},{type:"divider"},{label:"Edit in <props>",icon:"edit",action:()=>{a.compPart.value="props";const n=oe.props?.textarea;if(!n)return;const c=n.value.indexOf(`"${o.name}"`);n.focus(),c>=0&&n.setSelectionRange(c,c+o.name.length+2)}}]};{const e=E.contextMenu(Ee,{cursor:"glyph",match:".pe-comp-prop",inherit:!1,density:"compact",items:t=>en(t.dataset.prop)});l(()=>e?.destroy?.()),Ee.addEventListener("click",t=>{const o=t.target.closest(".pe-kebab");o&&Ce(o)}),Ee.addEventListener("keydown",t=>{const o=t.target.closest(".pe-comp-prop");o&&t.key==="F10"&&t.shiftKey&&(t.preventDefault(),Ce(o))})}let Fe=!1;const No=()=>Vt()||"",jt=async({project:e=No(),seed:t="",content:o=Or,css:s=""}={})=>{if(!Fe){Fe=!0;try{const n=await ms({kind:"block",projects:se,taken:[...be],project:e,name:t,check:Hn,register:l,create:async(c,h)=>{await g.blocks.put(c,os({name:c,project:h,bundled:!1,content:o,css:s}))}});if(!i())return;if(!n){te();return}if(await Ka(),!i())return;mt("#/blocks"),E.toast(`Created the block "${n}".`,{type:"success"}),Be(n)}finally{Fe=!1}}},lt=async({name:e="",source:t=br,project:o=No()}={})=>{if(!Fe){Fe=!0;try{const s=await ms({kind:"component",projects:se,taken:[...ie],project:o,name:e,register:l,check:(r,n)=>hr(r,{isNew:!0,taken:n}),create:async(r,n)=>{await g.components.put(r,{source:t,bundled:!1,meta:n?{project:n}:{}})}});if(!i())return;if(!s){te();return}if(await Ua(),!i())return;mt("#/components"),E.toast(`Created the component "${s}".`,{type:"success"}),rt(s)}finally{Fe=!1}}},Do=y.querySelector(".dm-plugin-body")||y,tn=M.computed(()=>{a.blockRev.value,a.compRev.value,a.docRev.value,W(V()),a.base.value;const e=j(a.active.value),t=[...Ae().map(s=>X({kind:"page",id:s.id})),...ge().map(s=>X({kind:"block",id:s})),...Ye().map(s=>X({kind:"component",id:s}))];let o=[];return e?.kind==="block"||e?.kind==="component"?o=[a.active.value]:e?.kind==="page"&&m&&(o=[a.active.value,...ge().filter(Tt).map(s=>X({kind:"block",id:s}))]),ur(t,o)}),an={...p,...a,dirty:Ms,pageDirty:Ss,activeDirty:Ts,cssUnsaved:Es,hasPage:Cs,activeKind:Qa,canView:eo,viewHref:xs,problem:Ps,dconfigNoteRows:Os,statusText:As,saveAll:tn,dockKind:to,blockTagText:Rs,compStateText:$s,compTagText:Ls,anySelected:Ns,allSelected:Ds,someSelected:Is},it=(e,t)=>Ke(e,t),Ea={scanImages:js,allowImageHost:Fs,reloadImagePolicy:()=>{u(),window.location.reload()},save:()=>da(!1),saveAllOpen:()=>da(!0),discard:async()=>{if(await Po(),!i())return;const e=j(a.active.peek());if(e?.kind==="block"){const t=L.get(e.id);if(!t||!de(t)||!await E.confirm(`Discard the changes to "${e.id}" you have not saved?`))return;t.form={...t.saved},J(He(e.id)),xo(),Le()}else if(e?.kind==="component"){const t=N.get(e.id);if(!t||!he(t)||!await E.confirm(`Discard the changes to the component "${e.id}" you have not saved?`))return;t.form={...t.saved},t.state="",J(_e(e.id)),$o(),ye()}else{if(!m||e?.kind==="page"&&e.id!==m.id||!(Zt()||wt())||!await E.confirm("Discard the changes to this page you have not saved?"))return;et(JSON.parse(a.base.peek())),p.cssOpened.peek()&&(p.css.value=ee.peek()),J(ze(m.id)),J(vt),Qe()}q(),z(),U()},newPage:()=>ue(""),newBlock:()=>jt(),newComponent:()=>lt(),toggleSection:e=>{const t={here:"secHere",pages:"secPages",blocks:"secBlocks",components:"secComps"}[e];t&&(a[t].value=!a[t].peek(),it(t,a[t].peek()))},toggleExplorer:()=>{a.explorerOn.value=!a.explorerOn.peek(),it("explorer",a.explorerOn.peek())},toggleDock:()=>{a.dockOn.value=!a.dockOn.peek(),it("dock",a.dockOn.peek()),a.dockOn.peek()&&a.dockTab.peek()==="history"&&!tt&&pe()},showDock:e=>{a.dockTab.value=e,e==="history"&&m?.isEdit&&!tt&&pe()},schemaChanged:()=>{a.seoSchemaNote.value=Aa(p.seoSchema.peek()).error},insertSchemaExample:async e=>{const t=e?.target,o=t?.value||"";if(t&&(t.value=""),!o||p.seoSchema.peek().trim()&&!await E.confirm("Replace the structured data with the example?"))return;const s=p.urlPath.peek().trim()||m?.id||"/";p.seoSchema.value=kn(o,{title:p.seoTitle.peek().trim()||p.title.peek().trim(),description:p.seoDesc.peek().trim()||p.description.peek().trim(),url:`${location.origin}${s}`,siteName:Y?.title||Y?.seo?.defaultTitle||""}),a.seoSchemaNote.value=""},setBlockPart:e=>{a.blockPart.value=e==="css"?"css":"html",(e==="css"?me:ae)?.focus()},formatBlock:()=>{const e=a.blockPart.peek()==="css",t=e?me:ae;!t||!a.blockReady.peek()||Wa(Ve(t.textarea,e?"css":"html"))},copyShortcode:async()=>{const e=a.blockTab.peek();e&&await je(Un(e),"Shortcode")},setCompPart:e=>{a.compPart.value=Kt.includes(e)?e:"template",oe[a.compPart.peek()]?.focus()},copyComponent:()=>{const e=N.get(a.compTab.peek());e&&lt({name:Da(e.name,[...ie]),source:e.form.source})},setMode:e=>{a.cssMode.peek()&&(a.cssMode.value=!1),a.viewMode.value=ja(e),it("viewMode",a.viewMode.peek()),e!=="write"&&(U(!0),ga())},toggleFullscreen:()=>{a.fullscreen.value=!a.fullscreen.peek(),it("fullscreen",a.fullscreen.peek())},toggleCss:()=>{a.blockTab.peek()||a.compTab.peek()||(a.cssMode.peek()?(a.cssMode.value=!1,a.viewMode.value=ja(Pe().viewMode),Q.refresh()):(uo(),a.cssMode.value=!0,a.viewMode.value="split"))},formatMarkdown:()=>{Xt(),Wa(Ve(_,"md"))},toggleFold:()=>{a.folded.peek()?Q.unfoldAll():Q.foldAll(),a.folded.value=!a.folded.peek(),We.paint()},setWidth:e=>{a.liveWidth.value=e},refreshPreview:()=>{if(a.compTab.peek()){const e=N.get(a.compTab.peek());e&&nt(e)}else U(!0)},share:async()=>{if(!m?.isEdit){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await xr(m.id)},toggleAll:e=>{const t=!!e?.target?.checked;a.versions.peek().forEach(o=>{o.checked.value=t})},deleteSelected:async()=>{const e=a.versions.peek().filter(o=>o.checked.peek()).map(o=>o.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await g.versions.bulkDelete(m.id,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await pe()}catch(o){E.toast("Delete failed: "+o.message,{type:"error"})}},saveNamed:async()=>{if(!m?.isEdit)return;const e=m;await Pr(e.id)&&i()&&m===e&&await pe()},closeCompare:()=>{a.compare.value=null,Ot=null},restoreCompared:()=>ho(Ot)};Ze.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const on=M.applyBindings(an,Do,{methods:Ea});l(()=>on.dispose()),ae=Ha(O('.pe-block-edit[data-part="html"]'),{ariaLabel:"Block template HTML",language:"html",onChange:e=>Oo("content",e)}),me=Ha(O('.pe-block-edit[data-part="css"]'),{ariaLabel:"Block CSS",language:"css",placeholder:`/* Scoped to this block - e.g.
141
141
  color: var(--dm-primary);
142
- h3 { font-size: 1.25rem; } */`,onChange:e=>Ma("css",e)}),i(()=>{oe.destroy(),me.destroy()});for(const e of qt)ae[e]=zo(O(`.pe-comp-edit[data-part="${e}"]`),{ariaLabel:`Component <${e}> block`,language:{template:"html",props:"json",script:"js",style:"css"}[e],onChange:t=>Js(e,t)});i(()=>{for(const e of qt)ae[e].destroy()});const Me=O(".pe-tabs-list"),Dt=e=>e.target.closest(".pe-tab")?.dataset.id||"",Aa=e=>{const t=Dt(e);t&&(e.target.closest(".pe-tab-close")?et(t):xt(t))},$a=e=>{e.button===1&&Dt(e)&&(e.preventDefault(),et(Dt(e)))},La=e=>{const t=Dt(e);!t||e.target.closest(".pe-tab-close")||(e.key==="Enter"||e.key===" "?(e.preventDefault(),xt(t)):e.key==="Delete"?(e.preventDefault(),et(t)):e.key==="F10"&&e.shiftKey&&(e.preventDefault(),Ce(e.target.closest(".pe-tab"))))};Me.addEventListener("click",Aa),Me.addEventListener("auxclick",$a),Me.addEventListener("keydown",La),i(()=>{Me.removeEventListener("click",Aa),Me.removeEventListener("auxclick",$a),Me.removeEventListener("keydown",La)});const So=e=>{for(const t of e)et(t)},Zs=async e=>{const t=I(e);if(t?.id)if(t.kind==="page")o.active.peek()!==e&&await ue(t.id),c()&&m?.id===t.id&&Ba();else if(t.kind==="block"){const s=(L.get(t.id)||await lo(t.id,{quiet:!0}))?.form||Xa(await h.blocks.get(t.id).catch(()=>null));c()&&Nt({seed:Lo(t.id,[...be]),content:s.content,css:s.css})}else{const s=(N.get(t.id)||await go(t.id,{quiet:!0}))?.form.source??(await h.components.get(t.id).catch(()=>null))?.source??"";c()&&nt({name:Lo(t.id,[...le]),source:s})}},Qs=e=>{const t=I(e);if(!t||!P.includes(e))return[];const a=P.indexOf(e),s=Ne.get(e)?.label.peek()||t.id,r=n=>{P=Zn(P,e,n),q()};return[{header:s},{label:"Close",icon:"x",action:()=>et(e)},{label:"Close others",icon:"x-circle",disabled:P.length<2,action:()=>So(Io(P,e,"others"))},{label:"Close to the right",icon:"chevrons-right",disabled:a>=P.length-1,action:()=>So(Io(P,e,"right"))},{label:"Close all",icon:"layers",action:()=>So(Io(P,e,"all"))},{type:"divider"},{label:"Move left",icon:"arrow-left",disabled:a===0,action:()=>r(-1)},{label:"Move right",icon:"arrow-right",disabled:a>=P.length-1,action:()=>r(1)},{type:"divider"},{label:t.kind==="page"?"Clone page\u2026":`Duplicate ${t.kind}\u2026`,icon:"copy",disabled:!t.id,action:()=>Zs(e)},{label:"Copy link",icon:"link",disabled:!t.id,action:()=>Ie(`${location.origin}${location.pathname}#${es(t)}`,"Link")}]};{const e=E.contextMenu(Me,{cursor:"glyph",match:".pe-tab",inherit:!1,density:"compact",items:t=>Qs(t.dataset.id)});i(()=>e?.destroy?.())}const Na=O(".pe-ex-scroll"),Da=e=>{const t=e.target.closest(".pe-ex-item"),a=t?.dataset.id||"";if(!a)return;const s=a.indexOf(":"),r=a.slice(0,s),n=a.slice(s+1),l=()=>{ke.has(n)?ke.delete(n):ke.add(n);try{S.set(us,[...ke])}catch{}z()};if(r==="folder"){l();return}if(r==="page"&&e.target.closest(".pe-ex-chev")&&t.classList.contains("is-folder")){l();return}r==="page"?ue(n):r==="block"?Be(n):r==="component"&&st(n)};Na.addEventListener("click",Da),i(()=>Na.removeEventListener("click",Da));let Eo=null;i(()=>clearTimeout(Eo)),V.addEventListener("input",()=>{Q.refresh(),ce.scrollTop=V.scrollTop,!gt&&(clearTimeout(Eo),Eo=setTimeout(ra,120),U())}),V.addEventListener("scroll",()=>{ce.scrollTop=V.scrollTop}),ve.addEventListener("scroll",()=>{ce.scrollTop=ve.scrollTop}),i(M.effect(()=>{for(const e of Object.keys(p))e!=="css"&&e!=="cssOpened"&&p[e].value;o.bodyRev.value,U()})),i(M.effect(()=>{o.bodyRev.value,o.docRev.value;const e=m?ht():"";kt=m?jn(e,bt.map(t=>t.name),bt.filter(t=>t.shortTag).map(t=>t.name)):[],ta=m?Xn(e,le):[],St(),z()})),i(M.effect(()=>{o.blockRev.value,o.compRev.value,o.docRev.value,o.active.value,o.filter.value,p.title.value,q(),z()})),i(M.effect(()=>{ce.hidden=!!(o.blockTab.value||o.compTab.value)}));let Mo=null;i(()=>clearTimeout(Mo)),i(M.effect(()=>{const e=p.css.value;o.cssMode.value&&(Ut.paint(),Lr(ce,e,ve),clearTimeout(Mo),Mo=setTimeout(oa,400))}));{const e=O(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:a=>{const s=oo(a.dataset.file);return s?[{header:rs(s)},{label:"Show beside the editor",icon:"columns",action:()=>ao(s)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>ma(s.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>Bs(s)}]:[]}});i(()=>t?.destroy?.()),e.addEventListener("click",a=>{const s=a.target.closest("tr[data-file]");if(!s||a.target.matches("input"))return;const r=a.target.closest(".pe-kebab");if(r){Ce(r);return}const n=oo(s.dataset.file);n&&ao(n)}),e.addEventListener("keydown",a=>{const s=a.target.closest("tr[data-file]");if(s){if(a.key==="F10"&&a.shiftKey)a.preventDefault(),Ce(s);else if(a.key==="Enter"&&a.target===s){const r=oo(s.dataset.file);r&&ao(r)}}})}Nr(O("#editor-body"),O("#editor-divider"),i),cn({image:e=>(o.cssMode.value=!1,Dr(e)),scribe:e=>un(e),card:e=>tn.open(e,{onMediaBrowse:()=>h.media.list().catch(()=>[])}),timeline:e=>on.open(e,{onPreview:async t=>{try{return await h.pages.preview(t)}catch{return null}},onListCollections:()=>h.collections.list().catch(()=>[]),onGetCollection:t=>h.collections.get(t).catch(()=>null)})}),to.onEffects(e=>rn.open(e)),to.onHelp(()=>Ha()),to.onTutorial(e=>mn(e));const ja=e=>{if(!c())return;const t=rr(e);t&&(e.preventDefault(),t==="save"?io(!1):t==="layout"?wo.setMode(Qn(o.viewMode.peek())):wo.toggleExplorer())};document.addEventListener("keydown",ja),i(()=>document.removeEventListener("keydown",ja));const it=y.querySelector(".dm-plugin-banner-name");i(M.effect(()=>{const e=o.curId.value,t=Xo.value,a=o.blockTab.value||o.compTab.value,s=t==="block"?"Blocks":t==="component"?"Components":"Pages",r=t==="block"||t==="component"?a:e===null?"Editor":Pn(!!e,p.title.value,e);if(!c()||(document.title=`${r} - Domma CMS`,!it))return;const n=it.querySelector(".dm-plugin-banner-parent"),l=it.lastChild;n&&l&&l.nodeType===3&&it.querySelector(".dm-plugin-banner-sep")?(n.textContent=s,l.textContent=r):it.textContent=`${s} \u203A ${r}`})),dt(d,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),dt(d,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>Ha()});const en=dt(d,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>{m?.isEdit&&window.open(p.urlPath.peek().trim()||m.id,"_blank","noopener")}}),Ba=()=>{if(!m?.isEdit||o.saving.peek()||!c())return;const e=m,t=_(),a=Ro({...t,urlPath:e.id});if(a.length){E.toast(a[0].text,{type:"warning"});return}const s=gn({page:e.page,frontmatter:{...Gt(t,e,Xt(t)),adminTags:t.adminTags},body:t.body,source:e.id,pages:D,onCloned:async r=>{c()&&(await ha(),c()&&ue(r))}});i(()=>s.close())},To=dt(d,{id:"page-editor-clone",icon:"copy",title:"Clone page",onClick:Ba});i(M.effect(()=>{const e=Zo.value;for(const t of[en,To])t&&(t.disabled=!e);To&&(To.title=e?"Clone page":"Open a saved page to clone it")})),dt(d,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{Yo=hr(Jo,Wo)}}),i(qa(Ra,{position:"bottom"})),Domma.icons.scan();let Ia={ids:[],active:""};try{Ia=or(S.get(ps))}catch{}const lt=Ua(b),ct=nr(lt,Ia,history.state?.pe===1);P=ct.ids.filter(e=>{const t=I(e);return t.kind==="page"||(t.kind==="component"?le:be).has(t.id)}),ba=!1;for(const e of P){const t=I(e);t.kind==="page"&&ya(t.id)}q(),z(),St();for(const e of Ae)lo(e,{quiet:!0}).then(()=>{c()&&(q(),z())});for(const e of P){const t=I(e);t.kind==="component"&&e!==ct.active&&xe(t)&&go(t.id,{quiet:!0}).then(()=>{c()&&(q(),z())})}const Fa=lt&&!lt.id&&lt.kind!=="page";ct.active&&P.includes(ct.active)?await xt(ct.active):P.length?await xt(P[0]):Fa||te(),Fa&&c()&&(await(lt.kind==="block"?Nt:nt)({project:zt(b)||""}),c()&&te())}};function qo(d,b){!d||typeof b!="string"||!b||[...d.options].some(v=>v.value===b)||(d.append(Object.assign(document.createElement("option"),{value:b,textContent:`${b} (custom)`})),d.value=b)}function Lr(d,b,v){d.replaceChildren(...String(b).split(`
143
- `).map((i,k)=>{const u=document.createElement("span");u.className="editor-line-number-row";const w=document.createElement("span");w.className="fold-toggle fold-toggle--empty";const y=document.createElement("span");return y.className="editor-line-num",y.textContent=String(k+1),u.append(w,y),u})),d.scrollTop=v.scrollTop}function Nr(d,b,v){if(!d||!b)return;const i=15,k=85,u=c=>Math.min(k,Math.max(i,c));let w=u(parseFloat(Pe().splitRatio)||50);const y=c=>{d.style.setProperty("--dm-editor-split",w+"%"),b.setAttribute("aria-valuenow",String(Math.round(w))),c&&Ke("splitRatio",w)};y(!1);const x=c=>{const A=d.getBoundingClientRect();A.width&&(w=u((c.clientX-A.left)/A.width*100)),y(!1)},j=()=>{document.removeEventListener("pointermove",x),document.removeEventListener("pointerup",j),d.classList.remove("is-resizing"),b.classList.remove("is-dragging"),y(!0)};v(j),b.addEventListener("pointerdown",c=>{window.matchMedia("(max-width: 768px)").matches||(c.preventDefault(),d.classList.add("is-resizing"),b.classList.add("is-dragging"),document.addEventListener("pointermove",x),document.addEventListener("pointerup",j))}),b.addEventListener("dblclick",()=>{w=50,y(!0)}),b.addEventListener("keydown",c=>{const A=c.shiftKey?10:2;let H=w;if(c.key==="ArrowLeft")H=w-A;else if(c.key==="ArrowRight")H=w+A;else if(c.key==="Home")H=i;else if(c.key==="End")H=k;else if(c.key==="Enter"||c.key===" ")H=50;else return;c.preventDefault(),w=u(H),y(!0)})}async function Dr(d){const b=d.selectionStart,v=d.selectionEnd,i=await vn({title:"Upload or insert an image"});if(!i||!d.isConnected)return;let k=i.split("/").pop().split("?")[0];try{k=decodeURIComponent(k)}catch{}const u=k.replace(/[\\[\]]/g,"\\$&"),w=i.replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/ /g,"%20");d.setSelectionRange(b,v),nn(d,`![${u}](${w})`),d.dispatchEvent(new Event("input",{bubbles:!0}))}function jr(d,b,v){const i=Object.entries(b||{});return d?(d.replaceChildren(...i.map(([k,u])=>new Option(u?.label||k,k))),i.some(([k])=>k===v)?v:i[0]?.[0]||v):v}async function Br(d,b){if(!d)return;let v=[];try{const x=await h.get("/collections/roles/entries?limit=100");v=Array.isArray(x)?x:x?.entries||[]}catch{}const i=v.map(x=>({name:x.data?.name,label:x.data?.label||x.data?.name,level:x.data?.level??99})).filter(x=>x.name&&x.level!==0).sort((x,j)=>j.level-x.level),k=(x,j)=>Object.assign(document.createElement("option"),{value:x,textContent:j}),u=()=>{typeof b=="string"&&b&&![...d.options].some(x=>x.value===b)&&d.append(k(b,`${b} (custom)`))};if(!i.length)return u();const w=document.createElement("optgroup");w.label="Role and above",i.forEach(x=>w.append(k(x.name,`${x.label} and above`)));const y=document.createElement("optgroup");y.label="Role only",i.forEach(x=>y.append(k(`=${x.name}`,`${x.label} only`))),d.replaceChildren(k("public","Public - everyone"),w,y,k("private","Private - super-admin only")),u()}
142
+ h3 { font-size: 1.25rem; } */`,onChange:e=>Oo("css",e)}),l(()=>{ae.destroy(),me.destroy()});for(const e of Kt)oe[e]=Ha(O(`.pe-comp-edit[data-part="${e}"]`),{ariaLabel:`Component <${e}> block`,language:{template:"html",props:"json",script:"js",style:"css"}[e],onChange:t=>Qs(e,t)});l(()=>{for(const e of Kt)oe[e].destroy()});const Me=O(".pe-tabs-list"),Ft=e=>e.target.closest(".pe-tab")?.dataset.id||"",Io=e=>{const t=Ft(e);t&&(e.target.closest(".pe-tab-close")?at(t):Rt(t))},Bo=e=>{e.button===1&&Ft(e)&&(e.preventDefault(),at(Ft(e)))},jo=e=>{const t=Ft(e);!t||e.target.closest(".pe-tab-close")||(e.key==="Enter"||e.key===" "?(e.preventDefault(),Rt(t)):e.key==="Delete"?(e.preventDefault(),at(t)):e.key==="F10"&&e.shiftKey&&(e.preventDefault(),Ce(e.target.closest(".pe-tab"))))};Me.addEventListener("click",Io),Me.addEventListener("auxclick",Bo),Me.addEventListener("keydown",jo),l(()=>{Me.removeEventListener("click",Io),Me.removeEventListener("auxclick",Bo),Me.removeEventListener("keydown",jo)});const Ma=e=>{for(const t of e)at(t)},sn=async e=>{const t=j(e);if(t?.id)if(t.kind==="page")a.active.peek()!==e&&await ue(t.id),i()&&m?.id===t.id&&Ho();else if(t.kind==="block"){const s=(L.get(t.id)||await pa(t.id,{quiet:!0}))?.form||as(await g.blocks.get(t.id).catch(()=>null));i()&&jt({seed:Da(t.id,[...be]),content:s.content,css:s.css})}else{const s=(N.get(t.id)||await ya(t.id,{quiet:!0}))?.form.source??(await g.components.get(t.id).catch(()=>null))?.source??"";i()&&lt({name:Da(t.id,[...ie]),source:s})}},nn=e=>{const t=j(e);if(!t||!P.includes(e))return[];const o=P.indexOf(e),s=Ne.get(e)?.label.peek()||t.id,r=n=>{P=rr(P,e,n),q()};return[{header:s},{label:"Close",icon:"x",action:()=>at(e)},{label:"Close others",icon:"x-circle",disabled:P.length<2,action:()=>Ma(za(P,e,"others"))},{label:"Close to the right",icon:"chevrons-right",disabled:o>=P.length-1,action:()=>Ma(za(P,e,"right"))},{label:"Close all",icon:"layers",action:()=>Ma(za(P,e,"all"))},{type:"divider"},{label:"Move left",icon:"arrow-left",disabled:o===0,action:()=>r(-1)},{label:"Move right",icon:"arrow-right",disabled:o>=P.length-1,action:()=>r(1)},{type:"divider"},{label:t.kind==="page"?"Clone page\u2026":`Duplicate ${t.kind}\u2026`,icon:"copy",disabled:!t.id,action:()=>sn(e)},{label:"Copy link",icon:"link",disabled:!t.id,action:()=>je(`${location.origin}${location.pathname}#${ns(t)}`,"Link")}]};{const e=E.contextMenu(Me,{cursor:"glyph",match:".pe-tab",inherit:!1,density:"compact",items:t=>nn(t.dataset.id)});l(()=>e?.destroy?.())}const Fo=O(".pe-ex-scroll"),zo=e=>{const t=e.target.closest(".pe-ex-item"),o=t?.dataset.id||"";if(!o)return;const s=o.indexOf(":"),r=o.slice(0,s),n=o.slice(s+1),c=()=>{ke.has(n)?ke.delete(n):ke.add(n);try{S.set(gs,[...ke])}catch{}z()};if(r==="folder"){c();return}if(r==="page"&&e.target.closest(".pe-ex-chev")&&t.classList.contains("is-folder")){c();return}r==="page"?ue(n):r==="block"?Be(n):r==="component"&&rt(n)};Fo.addEventListener("click",zo),l(()=>Fo.removeEventListener("click",zo));let Ta=null;l(()=>clearTimeout(Ta)),_.addEventListener("input",()=>{Q.refresh(),ce.scrollTop=_.scrollTop,!yt&&(clearTimeout(Ta),Ta=setTimeout(po,120),U())}),_.addEventListener("scroll",()=>{ce.scrollTop=_.scrollTop}),ve.addEventListener("scroll",()=>{ce.scrollTop=ve.scrollTop}),l(M.effect(()=>{for(const e of Object.keys(p))e!=="css"&&e!=="cssOpened"&&p[e].value;a.bodyRev.value,U()})),l(M.effect(()=>{a.bodyRev.value,a.docRev.value;const e=m?kt():"";Mt=m?Vn(e,gt.map(t=>t.name),gt.filter(t=>t.shortTag).map(t=>t.name)):[],so=m?nr(e,ie):[],Ge(),z()})),l(M.effect(()=>{a.blockRev.value,a.compRev.value,a.docRev.value,a.active.value,a.filter.value,p.title.value,q(),z()})),l(M.effect(()=>{ce.hidden=!!(a.blockTab.value||a.compTab.value)}));let Ca=null;l(()=>clearTimeout(Ca)),l(M.effect(()=>{const e=p.css.value;a.cssMode.value&&(Gt.paint(),qr(ce,e,ve),clearTimeout(Ca),Ca=setTimeout(ro,400))}));{const e=O(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:o=>{const s=na(o.dataset.file);return s?[{header:ps(s)},{label:"Show beside the editor",icon:"columns",action:()=>ra(s)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>ho(s.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>_s(s)}]:[]}});l(()=>t?.destroy?.()),e.addEventListener("click",o=>{const s=o.target.closest("tr[data-file]");if(!s||o.target.matches("input"))return;const r=o.target.closest(".pe-kebab");if(r){Ce(r);return}const n=na(s.dataset.file);n&&ra(n)}),e.addEventListener("keydown",o=>{const s=o.target.closest("tr[data-file]");if(s){if(o.key==="F10"&&o.shiftKey)o.preventDefault(),Ce(s);else if(o.key==="Enter"&&o.target===s){const r=na(s.dataset.file);r&&ra(r)}}})}Hr(O("#editor-body"),O("#editor-divider"),l),bn({image:e=>(a.cssMode.value=!1,_r(e)),scribe:e=>hn(e),card:e=>ln.open(e,{onMediaBrowse:()=>g.media.list().catch(()=>[])}),timeline:e=>cn.open(e,{onPreview:async t=>{try{return await g.pages.preview(t)}catch{return null}},onListCollections:()=>g.collections.list().catch(()=>[]),onGetCollection:t=>g.collections.get(t).catch(()=>null)})}),sa.onEffects(e=>mn.open(e)),sa.onHelp(()=>Jo()),sa.onTutorial(e=>yn(e));const qo=e=>{if(!i())return;const t=fr(e);t&&(e.preventDefault(),t==="save"?da(!1):t==="layout"?Ea.setMode(lr(a.viewMode.peek())):Ea.toggleExplorer())};document.addEventListener("keydown",qo),l(()=>document.removeEventListener("keydown",qo));const ct=y.querySelector(".dm-plugin-banner-name");l(M.effect(()=>{const e=a.curId.value,t=Qa.value,o=a.blockTab.value||a.compTab.value,s=t==="block"?"Blocks":t==="component"?"Components":"Pages",r=t==="block"||t==="component"?o:e===null?"Editor":Nn(!!e,p.title.value,e);if(!i()||(document.title=`${r} - Domma CMS`,!ct))return;const n=ct.querySelector(".dm-plugin-banner-parent"),c=ct.lastChild;n&&c&&c.nodeType===3&&ct.querySelector(".dm-plugin-banner-sep")?(n.textContent=s,c.textContent=r):ct.textContent=`${s} \u203A ${r}`})),ut(d,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),ut(d,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>Jo()});const rn=ut(d,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>{m?.isEdit&&window.open(p.urlPath.peek().trim()||m.id,"_blank","noopener")}}),Ho=()=>{if(!m?.isEdit||a.saving.peek()||!i())return;const e=m,t=V(),o=$a({...t,urlPath:e.id});if(o.length){E.toast(o[0].text,{type:"warning"});return}const s=En({page:e.page,frontmatter:{...St(t,e,Et(t)),adminTags:t.adminTags},body:t.body,source:e.id,pages:D,onCloned:async r=>{i()&&(await Eo(),i()&&ue(r))}});l(()=>s.close())},xa=ut(d,{id:"page-editor-clone",icon:"copy",title:"Clone page",onClick:Ho});l(M.effect(()=>{const e=eo.value;for(const t of[rn,xa])t&&(t.disabled=!e);xa&&(xa.title=e?"Clone page":"Open a saved page to clone it")})),ut(d,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{Xa=Cr(Ya,Ga)}}),l(Uo(Do,{position:"bottom"})),Domma.icons.scan();let _o={ids:[],active:""};try{_o=dr(S.get(vs))}catch{}const dt=Xo(b),pt=mr(dt,_o,history.state?.pe===1);P=pt.ids.filter(e=>{const t=j(e);return t.kind==="page"||(t.kind==="component"?ie:be).has(t.id)}),ko=!1;for(const e of P){const t=j(e);t.kind==="page"&&Mo(t.id)}q(),z(),Ge();for(const e of Re)pa(e,{quiet:!0}).then(()=>{i()&&(q(),z())});for(const e of P){const t=j(e);t.kind==="component"&&e!==pt.active&&xe(t)&&ya(t.id,{quiet:!0}).then(()=>{i()&&(q(),z())})}const Vo=dt&&!dt.id&&dt.kind!=="page";pt.active&&P.includes(pt.active)?await Rt(pt.active):P.length?await Rt(P[0]):Vo||te(),Vo&&i()&&(await(dt.kind==="block"?jt:lt)({project:Vt(b)||""}),i()&&te())}};function _a(d,b){!d||typeof b!="string"||!b||[...d.options].some(v=>v.value===b)||(d.append(Object.assign(document.createElement("option"),{value:b,textContent:`${b} (custom)`})),d.value=b)}function qr(d,b,v){d.replaceChildren(...String(b).split(`
143
+ `).map((l,k)=>{const u=document.createElement("span");u.className="editor-line-number-row";const w=document.createElement("span");w.className="fold-toggle fold-toggle--empty";const y=document.createElement("span");return y.className="editor-line-num",y.textContent=String(k+1),u.append(w,y),u})),d.scrollTop=v.scrollTop}function Hr(d,b,v){if(!d||!b)return;const l=15,k=85,u=i=>Math.min(k,Math.max(l,i));let w=u(parseFloat(Pe().splitRatio)||50);const y=i=>{d.style.setProperty("--dm-editor-split",w+"%"),b.setAttribute("aria-valuenow",String(Math.round(w))),i&&Ke("splitRatio",w)};y(!1);const x=i=>{const A=d.getBoundingClientRect();A.width&&(w=u((i.clientX-A.left)/A.width*100)),y(!1)},I=()=>{document.removeEventListener("pointermove",x),document.removeEventListener("pointerup",I),d.classList.remove("is-resizing"),b.classList.remove("is-dragging"),y(!0)};v(I),b.addEventListener("pointerdown",i=>{window.matchMedia("(max-width: 768px)").matches||(i.preventDefault(),d.classList.add("is-resizing"),b.classList.add("is-dragging"),document.addEventListener("pointermove",x),document.addEventListener("pointerup",I))}),b.addEventListener("dblclick",()=>{w=50,y(!0)}),b.addEventListener("keydown",i=>{const A=i.shiftKey?10:2;let H=w;if(i.key==="ArrowLeft")H=w-A;else if(i.key==="ArrowRight")H=w+A;else if(i.key==="Home")H=l;else if(i.key==="End")H=k;else if(i.key==="Enter"||i.key===" ")H=50;else return;i.preventDefault(),w=u(H),y(!0)})}async function _r(d){const b=d.selectionStart,v=d.selectionEnd,l=await Sn({title:"Upload or insert an image"});if(!l||!d.isConnected)return;let k=l.split("/").pop().split("?")[0];try{k=decodeURIComponent(k)}catch{}const u=k.replace(/[\\[\]]/g,"\\$&"),w=l.replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/ /g,"%20");d.setSelectionRange(b,v),un(d,`![${u}](${w})`),d.dispatchEvent(new Event("input",{bubbles:!0}))}function Vr(d,b,v){const l=Object.entries(b||{});return d?(d.replaceChildren(...l.map(([k,u])=>new Option(u?.label||k,k))),l.some(([k])=>k===v)?v:l[0]?.[0]||v):v}async function Kr(d,b){if(!d)return;let v=[];try{const x=await g.get("/collections/roles/entries?limit=100");v=Array.isArray(x)?x:x?.entries||[]}catch{}const l=v.map(x=>({name:x.data?.name,label:x.data?.label||x.data?.name,level:x.data?.level??99})).filter(x=>x.name&&x.level!==0).sort((x,I)=>I.level-x.level),k=(x,I)=>Object.assign(document.createElement("option"),{value:x,textContent:I}),u=()=>{typeof b=="string"&&b&&![...d.options].some(x=>x.value===b)&&d.append(k(b,`${b} (custom)`))};if(!l.length)return u();const w=document.createElement("optgroup");w.label="Role and above",l.forEach(x=>w.append(k(x.name,`${x.label} and above`)));const y=document.createElement("optgroup");y.label="Role only",l.forEach(x=>y.append(k(`=${x.name}`,`${x.label} only`))),d.replaceChildren(k("public","Public - everyone"),w,y,k("private","Private - super-admin only")),u()}
@@ -0,0 +1,11 @@
1
+ # External images in the page editor
2
+
3
+ Open **External images** above the preview to see image domains used by the current page. Preview renders update this list automatically. **Scan images** renders the current unsaved page and checks it even when the preview is hidden.
4
+
5
+ The scan includes image elements, responsive image candidates, SVG images, video posters and URLs in inline styles and style blocks. Server-rendered blocks and shortcodes are included. Images added later by JavaScript or referenced only in linked stylesheets are outside this scan.
6
+
7
+ For a blocked domain, choose **Allow images from this domain**. This requires permission to update site settings and allows HTTPS images from that exact hostname across the site. Subdomains require their own entries. HTTP addresses, custom ports and wildcard rules cannot be approved here.
8
+
9
+ The site uses the updated policy on its next response, without a server restart. An already-open editor keeps its previous browser policy: use **Reload editor** to refresh previews. Unsaved work is retained through the editor's draft recovery.
10
+
11
+ The allowlist is stored in `config/server.json` under `csp.imgHosts`. For StartupBase badges, use `statics.startupbase.io`; for Fazier badges, use `fazier.com`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.32",
3
+ "version": "1.10.34",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -0,0 +1,32 @@
1
+ /** The page editor can inspect image permissions; only settings editors may grant them. */
2
+ import {getConfig, saveConfig} from '../../config.js';
3
+ import {authenticate, requirePermission} from '../../middleware/auth.js';
4
+ import {getPermissionsFor} from '../../services/roles.js';
5
+ import {getEffectiveRoles} from '../../services/userRoles.js';
6
+ import {normaliseImageHost} from '../../services/embedCsp.js';
7
+
8
+ function imageHosts() {
9
+ const hosts = getConfig('server').csp?.imgHosts;
10
+ return [...new Set((Array.isArray(hosts) ? hosts : []).map(normaliseImageHost).filter(Boolean))];
11
+ }
12
+
13
+ export async function imageHostRoutes(fastify) {
14
+ fastify.get('/pages/_image-hosts', {
15
+ preHandler: [authenticate, requirePermission('pages', 'read')]
16
+ }, async request => ({
17
+ hosts: imageHosts(),
18
+ canAllow: getEffectiveRoles(request.user).some(role => getPermissionsFor('settings', 'update').includes(role))
19
+ }));
20
+
21
+ fastify.post('/pages/_image-hosts', {
22
+ preHandler: [authenticate, requirePermission('settings', 'update')]
23
+ }, async (request, reply) => {
24
+ const host = normaliseImageHost(request.body?.host);
25
+ if (!host) return reply.code(400).send({error: 'Enter a hostname such as fazier.com, without a scheme, port, path or wildcard.'});
26
+ // Add one host and preserve every other setting. No awaits between read and write.
27
+ const server = getConfig('server');
28
+ const hosts = [...new Set([...imageHosts(), host])];
29
+ saveConfig('server', {...server, csp: {...server.csp, imgHosts: hosts}});
30
+ return {hosts};
31
+ });
32
+ }
@@ -28,6 +28,7 @@ import {authenticate, requirePermission} from '../../middleware/auth.js';
28
28
  import {canSeeArtefact, canSeePage} from '../../services/projects.js';
29
29
  import {getConfig, saveConfig} from '../../config.js';
30
30
  import {relinkItems, relinkMenus} from '../../services/menus.js';
31
+ import {imageHostRoutes} from './image-hosts.js';
31
32
  import {ago, clip, plural} from '../../services/sidebarBadges.js';
32
33
  import {dommaCatalogue} from '../../services/dommaCatalogue.js';
33
34
 
@@ -69,6 +70,7 @@ export function pagesBadge(pages, now = Date.now()) {
69
70
  }
70
71
 
71
72
  export async function pagesRoutes(fastify) {
73
+ await imageHostRoutes(fastify);
72
74
  const canRead = {preHandler: [authenticate, requirePermission('pages', 'read')]};
73
75
  const canCreate = {preHandler: [authenticate, requirePermission('pages', 'create')]};
74
76
  const canUpdate = {preHandler: [authenticate, requirePermission('pages', 'update')]};
package/server/server.js CHANGED
@@ -131,7 +131,7 @@ await app.register(helmet, {
131
131
  scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", 'https://cdn.jsdelivr.net', gaSources('script')],
132
132
  styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
133
133
  // config/server.json csp.imgHosts: other sites a page may show images from (services/embedCsp.js).
134
- imgSrc: [...imageSources(serverConfig.csp?.imgHosts), gaSources('img')],
134
+ imgSrc: [() => imageSources(getConfig('server').csp?.imgHosts).join(' '), gaSources('img')],
135
135
  fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
136
136
  // config/server.json csp.connectHosts: other sites a page may read from (services/embedCsp.js).
137
137
  connectSrc: [...connectSources(serverConfig.csp?.connectHosts), gaSources('connect')],
@@ -57,15 +57,21 @@ export const ALWAYS_IMAGED = ["'self'", 'data:', 'blob:'];
57
57
  /**
58
58
  * The img-src: the site, data: and blob:, and every host in config/server.json `csp.imgHosts` - for a page
59
59
  * that shows an image hosted elsewhere (a badge, a partner logo). Bare hostnames only, served over https;
60
- * anything else is dropped. Read at boot, like connect-src.
60
+ * anything else is dropped. Evaluated per response so editor approvals apply immediately.
61
61
  *
62
62
  * @param {string[]} imgHosts - config/server.json csp.imgHosts
63
63
  * @returns {string[]} img-src sources
64
64
  */
65
65
  export function imageSources(imgHosts = []) {
66
66
  const extra = (Array.isArray(imgHosts) ? imgHosts : [])
67
- .map(h => String(h).trim().toLowerCase())
68
- .filter(h => HOST_RE.test(h))
67
+ .map(normaliseImageHost).filter(Boolean)
69
68
  .map(h => `https://${h}`);
70
69
  return [...new Set([...ALWAYS_IMAGED, ...extra])];
71
70
  }
71
+
72
+ /** Accept exact HTTPS hostnames, never URLs, ports or wildcard directives. */
73
+ export function normaliseImageHost(value) {
74
+ if (typeof value !== 'string') return null;
75
+ const host = value.trim().toLowerCase();
76
+ return HOST_RE.test(host) ? host : null;
77
+ }
@@ -98,14 +98,21 @@ const _registeredSidebarItems = [];
98
98
  * Unknown plugins remain directly in Tools until they declare their own group.
99
99
  */
100
100
  const TOOL_GROUPS = new Map(Object.entries({
101
- Productivity: ['#/notes', '#/todo', '#/calculator', '#/plugins/calendar', '#/plugins/waypoint-pro'],
101
+ Productivity: ['#/notes', '#/todo', '#/calculator', '#/plugins/calendar', '#/plugins/waypoint-pro', '#/plugins/sheets'],
102
102
  Communication: ['#/contacts', '#/plugins/contacts-pro', '#/plugins/mail-reader', '#/plugins/email-pro', '#/plugins/messages'],
103
- Marketing: ['#/analytics', '#/seo', '#/plugins/analytics-pro/dashboard', '#/plugins/social', '#/plugins/feedback', '#/plugins/surveys'],
104
- Publishing: ['#/plugins/blog', '#/plugins/blog-pro', '#/plugins/docs', '#/plugins/theme-roller'],
105
- Business: ['#/plugins/shopping-cart', '#/plugins/booking', '#/plugins/invoice', '#/job-board', '#/plugins/garage'],
106
- Maintenance: ['#/plugins/security', '#/plugins/security-pro', '#/plugins/security-pro/my-2fa', '#/plugins/backup-pro', '#/plugins/data-transfer', '#/wordpress-import']
103
+ Marketing: ['#/analytics', '#/seo', '#/plugins/analytics-pro/dashboard', '#/plugins/social', '#/plugins/feedback', '#/plugins/surveys', '#/plugins/campaigns-pro', '#/plugins/ecards'],
104
+ Publishing: ['#/plugins/blog', '#/plugins/blog-pro', '#/plugins/docs', '#/plugins/theme-roller', '#/plugins/wiki', '#/plugins/wiki-pro', '#/plugins/scraper', '#/plugins/scraper-pro'],
105
+ Business: ['#/plugins/shopping-cart', '#/plugins/booking', '#/plugins/booking-pro', '#/plugins/invoice', '#/job-board', '#/plugins/garage', '#/plugins/takeaway', '#/plugins/support', '#/plugins/onboard-pro'],
106
+ Maintenance: ['#/plugins/security', '#/plugins/security-pro', '#/plugins/security-pro/my-2fa', '#/plugins/backup-pro', '#/plugins/data-transfer', '#/wordpress-import', '#/plugins/site-manager/backups'],
107
+ Manage: ['#/plugins/site-manager/dashboard', '#/plugins/site-manager/server', '#/plugins/site-manager/injector-library'],
108
+ Games: ['#/plugins/crossword', '#/plugins/daily-word', '#/plugins/quiz', '#/plugins/word-search']
107
109
  }).flatMap(([group, urls]) => urls.map(url => [url, group])));
108
110
 
111
+ /** Default category for a known Tool URL, also used when upgrading saved menus. */
112
+ export function defaultToolGroup(url) {
113
+ return TOOL_GROUPS.get(url);
114
+ }
115
+
109
116
  /**
110
117
  * Register a Tool's sidebar entry. Entries are placed into the rendered admin
111
118
  * sidebar at render time - they are NOT persisted into menu JSON - so plugins
@@ -130,7 +137,7 @@ export function registerSidebarItem(opts) {
130
137
  }
131
138
  const {folder: itemFolder, group: itemGroup, ...item} = opts.item;
132
139
  const folder = opts.folder || itemFolder || DEFAULT_TOOL_FOLDER;
133
- const defaultGroup = folder === DEFAULT_TOOL_FOLDER && !opts.parent ? TOOL_GROUPS.get(item.url) : undefined;
140
+ const defaultGroup = folder === DEFAULT_TOOL_FOLDER && !opts.parent ? defaultToolGroup(item.url) : undefined;
134
141
  const group = [opts.group, itemGroup, defaultGroup].find(g => typeof g === 'string' && g.trim());
135
142
  _registeredSidebarItems.push({
136
143
  parent: opts.parent || null,
@@ -15,6 +15,7 @@ import fs from 'fs/promises';
15
15
  import path from 'path';
16
16
  import {fileURLToPath} from 'url';
17
17
  import {invalidateMenuIndex} from './menus.js';
18
+ import {defaultToolGroup} from './hooks.js';
18
19
 
19
20
  const __filename = fileURLToPath(import.meta.url);
20
21
  const DEFAULT_CONFIG_DIR = path.resolve(path.dirname(__filename), '..', '..', 'config');
@@ -482,6 +483,38 @@ export async function ensureFolderKeys(opts = {}) {
482
483
  changed = true;
483
484
  }
484
485
 
486
+ // Categorise only loose, known Tools once. Custom submenus and entries
487
+ // outside Tools keep their placement; later admin rearrangements win.
488
+ if (!menu.meta?.purposeToolGroups && items.length) {
489
+ const find = (nodes, test) => {
490
+ for (const node of nodes || []) {
491
+ if (test(node)) return node;
492
+ const nested = find(node?.items, test);
493
+ if (nested) return nested;
494
+ }
495
+ return null;
496
+ };
497
+ const tools = find(items, n => isFolder(n) && n.key === 'tools');
498
+ if (tools && Array.isArray(tools.items)) {
499
+ const loose = tools.items.filter(n => n?.url && defaultToolGroup(n.url));
500
+ for (const node of loose) {
501
+ const name = defaultToolGroup(node.url);
502
+ const key = `tools.${name.toLowerCase()}`;
503
+ let group = find(items, n => isFolder(n) && n.key === key)
504
+ || tools.items.find(n => isFolder(n) && !n.key && String(n.text).toLowerCase() === name.toLowerCase());
505
+ if (!group) {
506
+ group = {text: name, key, icon: 'folder', items: []};
507
+ tools.items.push(group);
508
+ }
509
+ tools.items = tools.items.filter(n => n !== node);
510
+ if (!Array.isArray(group.items)) group.items = [];
511
+ group.items.push(node);
512
+ }
513
+ }
514
+ menu.meta = {...(menu.meta || {}), purposeToolGroups: true};
515
+ changed = true;
516
+ }
517
+
485
518
  if (!changed) return {updated: false, reason: 'already up to date'};
486
519
 
487
520
  menu.meta = {...(menu.meta || {}), updatedAt: new Date().toISOString()};