domma-cms 1.6.50 → 1.6.51

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.
@@ -134,6 +134,13 @@
134
134
  <small class="form-hint">Comma-separated keywords for filtering and search.</small>
135
135
  </div>
136
136
  </div>
137
+ <div class="row mb-3">
138
+ <div class="col">
139
+ <label class="form-label">Admin tags <span data-help="Labels that link pages together in the Pages list - pick one with the list's cog to show only those pages. Seen only here in the admin, never on the site; the Pages list's right-click menu adds and removes them too."></span></label>
140
+ <input id="field-admin-tags" type="text" class="form-input" />
141
+ <small class="form-hint">Only seen in the admin - for grouping pages in the Pages list.</small>
142
+ </div>
143
+ </div>
137
144
  <div class="row">
138
145
  <div class="col-auto">
139
146
  <label class="form-check-label">
@@ -1,4 +1,4 @@
1
- import{api as g}from"../api.js";import{CardBuilder as lt}from"../lib/card-builder.js";import{TimelineBuilder as ct}from"../lib/timeline-builder.js";import{attachEditorKeybindings as dt,createToolbar as pt,insertAtCursor as ut}from"../lib/markdown-toolbar.js";import{EffectsBuilder as vt}from"../lib/effects-builder.js";import{attachContextMenu as ft}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as mt}from"../lib/shortcode-insert.js";import{populateThemeSelect as bt}from"../lib/themes.js";import{createFoldingManager as gt}from"../lib/editor-folding.js";import{openScribeComposer as ht}from"../lib/scribe-composer.js";import{openCrudTutorial as yt}from"../lib/crud-tutorial.js";import{addBannerAction as $}from"../lib/plugin-chrome.js";import{helpPopovers as je}from"../lib/help-popover.js";import{onceClosed as he,openMenuAt as qe}from"../lib/tool-kit.js";import{pickMedia as wt}from"../lib/media-picker.js";import{openPageClone as kt}from"../lib/page-clone.js";import{openEditorReference as Ve}from"../lib/page-editor-reference.js";import{cleanType as G,DEFAULT_TYPE as Y,EDITOR_FONTS as Et,fontHref as Mt,isDefaultType as St,LINE_HEIGHTS as Ct,SIZES as Tt,typeVars as He}from"../lib/editor-type.js";import{getProjectFromHash as xt}from"../lib/project-context.js";import{SCREEN as Pt,bannerSection as Lt,draftIsStale as Dt,draftKey as At,initialProject as Ot,makeDraft as Rt,problems as ye,readDraft as Ft,snapshot as z}from"../lib/page-editor-arrange.js";import{fmtDateTime as X}from"../lib/dates.js";function Z(a){const r=document.createElement("template");return r.innerHTML=a.trim(),r.content.firstElementChild}const Nt=X,Ke=a=>X(a.createdAt)+(a.label?` - ${a.label}`:""),Bt=`
1
+ import{api as b}from"../api.js";import{CardBuilder as vt}from"../lib/card-builder.js";import{TimelineBuilder as ft}from"../lib/timeline-builder.js";import{attachEditorKeybindings as mt,createToolbar as gt,insertAtCursor as bt}from"../lib/markdown-toolbar.js";import{EffectsBuilder as ht}from"../lib/effects-builder.js";import{attachContextMenu as yt}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as wt}from"../lib/shortcode-insert.js";import{populateThemeSelect as kt}from"../lib/themes.js";import{createFoldingManager as Et}from"../lib/editor-folding.js";import{openScribeComposer as Mt}from"../lib/scribe-composer.js";import{openCrudTutorial as St}from"../lib/crud-tutorial.js";import{addBannerAction as $}from"../lib/plugin-chrome.js";import{helpPopovers as Ke}from"../lib/help-popover.js";import{onceClosed as ye,openMenuAt as Ue}from"../lib/tool-kit.js";import{pickMedia as Tt}from"../lib/media-picker.js";import{openPageClone as Ct}from"../lib/page-clone.js";import{openEditorReference as _e}from"../lib/page-editor-reference.js";import{cleanType as X,DEFAULT_TYPE as Z,EDITOR_FONTS as xt,fontHref as Pt,isDefaultType as Lt,LINE_HEIGHTS as At,SIZES as Ot,typeVars as Je}from"../lib/editor-type.js";import{getProjectFromHash as Dt}from"../lib/project-context.js";import{SCREEN as Rt,bannerSection as Nt,draftIsStale as Ft,draftKey as It,initialProject as Bt,makeDraft as $t,problems as we,readDraft as zt,snapshot as z}from"../lib/page-editor-arrange.js";import{fmtDateTime as Q}from"../lib/dates.js";function ee(s){const r=document.createElement("template");return r.innerHTML=s.trim(),r.content.firstElementChild}const jt=Q,We=s=>Q(s.createdAt)+(s.label?` - ${s.label}`:""),Vt=`
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 g}from"../api.js";import{CardBuilder as lt}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 Ue(a){const r=Mt(a);if(!r||document.querySelector(`link[data-pe-font="${CSS.escape(a)}"]`))return;const l=Object.assign(document.createElement("link"),{rel:"stylesheet",href:r});l.dataset.peFont=a,document.head.appendChild(l)}const It=`
33
+ </style>`;function Ge(s){const r=Pt(s);if(!r||document.querySelector(`link[data-pe-font="${CSS.escape(s)}"]`))return;const l=Object.assign(document.createElement("link"),{rel:"stylesheet",href:r});l.dataset.peFont=s,document.head.appendChild(l)}const qt=`
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>
@@ -61,17 +61,17 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
61
61
  .pe-settings-sample { white-space: pre-wrap; padding: .75rem 1rem; border-radius: var(--dm-radius-md); border: 1px solid var(--dm-border);
62
62
  background: var(--dm-background-alt); font-family: var(--pe-font); font-size: var(--pe-size); line-height: var(--pe-line); }
63
63
  .pe-settings-actions { display: flex; gap: .5rem; border-top: 1px solid var(--dm-border); padding-top: .75rem; }
64
- </style>`;function $t(a){const r=E.slideover({title:"Editor settings",size:"sm",position:"right"}),l=Z(`<div>${It}</div>`),s=G(a.peek()),i={font:M.observable(s.font),size:M.observable(String(s.size)),line:M.observable(String(s.line)),wrap:M.observable(s.wrap)},f=M.computed(()=>G({font:i.font.value,size:i.size.value,line:i.line.value,wrap:i.wrap.value}));i.isDefault=M.computed(()=>St(f.value));const p=l.querySelector("#pe-set-font");for(const[v,C]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const N=Et.filter(b=>b.kind===v),q=C?Object.assign(document.createElement("optgroup"),{label:C}):p.querySelector("optgroup:last-of-type");q.append(...N.map(b=>new Option(b.label,b.id))),C&&p.appendChild(q)}l.querySelector("#pe-set-size").append(...Tt.map(v=>new Option(`${v}px`,String(v)))),l.querySelector("#pe-set-line").append(...Ct.map(v=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[v]||String(v),String(v))));const c=l.querySelector(".pe-settings-sample"),u=M.effect(()=>{const v=f.value;Ue(v.font);for(const[C,N]of Object.entries(He(v)))c.style.setProperty(C,N);c.style.whiteSpace=v.wrap?"pre-wrap":"pre",c.style.overflowX=v.wrap?"":"auto",JSON.stringify(v)!==JSON.stringify(G(a.peek()))&&(a.value=v,Q("type",v))});i.reset=()=>{i.font.value=Y.font,i.size.value=String(Y.size),i.line.value=String(Y.line),i.wrap.value=Y.wrap},i.done=()=>r.close(),r.element.appendChild(l);const T=M.applyBindings(i,l),d=je(l);return he(r,()=>{T.dispose(),d(),u?.()}),Domma.icons?.scan?.(l),r.open(),r}async function zt(a){const r=E.slideover({title:"Share preview link",size:"md",position:"right"}),l=Z(`<div>${Bt}</div>`),s={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},i=async()=>{try{const p=await g.pages.previewLinks.list(a);s.links.value=p.filter(c=>!c.revokedAt&&!c.expired).map(c=>({id:c.id,title:c.label||"Untitled link",meta:`Expires ${Nt(c.expiresAt)} - ${c.useCount===1?"1 view":`${c.useCount||0} views`}`})),s.listError.value=""}catch(p){s.listError.value="Could not load existing links: "+(p.message||"unknown error")}finally{s.loaded.value=!0}};s.create=async()=>{s.creating.value=!0;try{const p=await g.pages.previewLinks.create(a,s.expiry.peek(),s.note.peek().trim());s.url.value=p.url,s.note.value="",E.toast("Share link created.",{type:"success"}),await i(),l.querySelector(".pe-share-url")?.select()}catch(p){E.toast("Could not create link: "+(p.message||"unknown error"),{type:"error"})}finally{s.creating.value=!1}},s.copy=async()=>{try{await navigator.clipboard.writeText(s.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{l.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},s.revoke=async p=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await g.pages.previewLinks.revoke(p),E.toast("Link revoked.",{type:"success"}),await i()}catch(c){E.toast("Could not revoke: "+(c.message||"unknown error"),{type:"error"})}};const f=M.applyBindings(s,l);he(r,()=>f.dispose()),r.element.appendChild(l),r.open(),await i()}function jt(a){return new Promise(r=>{const l=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),s=Z(`
64
+ </style>`;function Ht(s){const r=E.slideover({title:"Editor settings",size:"sm",position:"right"}),l=ee(`<div>${qt}</div>`),o=X(s.peek()),i={font:M.observable(o.font),size:M.observable(String(o.size)),line:M.observable(String(o.line)),wrap:M.observable(o.wrap)},f=M.computed(()=>X({font:i.font.value,size:i.size.value,line:i.line.value,wrap:i.wrap.value}));i.isDefault=M.computed(()=>Lt(f.value));const p=l.querySelector("#pe-set-font");for(const[v,T]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const F=xt.filter(m=>m.kind===v),V=T?Object.assign(document.createElement("optgroup"),{label:T}):p.querySelector("optgroup:last-of-type");V.append(...F.map(m=>new Option(m.label,m.id))),T&&p.appendChild(V)}l.querySelector("#pe-set-size").append(...Ot.map(v=>new Option(`${v}px`,String(v)))),l.querySelector("#pe-set-line").append(...At.map(v=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[v]||String(v),String(v))));const c=l.querySelector(".pe-settings-sample"),u=M.effect(()=>{const v=f.value;Ge(v.font);for(const[T,F]of Object.entries(Je(v)))c.style.setProperty(T,F);c.style.whiteSpace=v.wrap?"pre-wrap":"pre",c.style.overflowX=v.wrap?"":"auto",JSON.stringify(v)!==JSON.stringify(X(s.peek()))&&(s.value=v,te("type",v))});i.reset=()=>{i.font.value=Z.font,i.size.value=String(Z.size),i.line.value=String(Z.line),i.wrap.value=Z.wrap},i.done=()=>r.close(),r.element.appendChild(l);const C=M.applyBindings(i,l),d=Ke(l);return ye(r,()=>{C.dispose(),d(),u?.()}),Domma.icons?.scan?.(l),r.open(),r}async function Kt(s){const r=E.slideover({title:"Share preview link",size:"md",position:"right"}),l=ee(`<div>${Vt}</div>`),o={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},i=async()=>{try{const p=await b.pages.previewLinks.list(s);o.links.value=p.filter(c=>!c.revokedAt&&!c.expired).map(c=>({id:c.id,title:c.label||"Untitled link",meta:`Expires ${jt(c.expiresAt)} - ${c.useCount===1?"1 view":`${c.useCount||0} views`}`})),o.listError.value=""}catch(p){o.listError.value="Could not load existing links: "+(p.message||"unknown error")}finally{o.loaded.value=!0}};o.create=async()=>{o.creating.value=!0;try{const p=await b.pages.previewLinks.create(s,o.expiry.peek(),o.note.peek().trim());o.url.value=p.url,o.note.value="",E.toast("Share link created.",{type:"success"}),await i(),l.querySelector(".pe-share-url")?.select()}catch(p){E.toast("Could not create link: "+(p.message||"unknown error"),{type:"error"})}finally{o.creating.value=!1}},o.copy=async()=>{try{await navigator.clipboard.writeText(o.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{l.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},o.revoke=async p=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await b.pages.previewLinks.revoke(p),E.toast("Link revoked.",{type:"success"}),await i()}catch(c){E.toast("Could not revoke: "+(c.message||"unknown error"),{type:"error"})}};const f=M.applyBindings(o,l);ye(r,()=>f.dispose()),r.element.appendChild(l),r.open(),await i()}function Ut(s){return new Promise(r=>{const l=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),o=ee(`
65
65
  <div style="padding:1rem">
66
66
  <label class="form-label" for="pe-version-label">Version Label</label>
67
67
  <input id="pe-version-label" type="text" class="form-input" style="margin-bottom:.75rem"
68
68
  placeholder="e.g. Initial design, Before restructure" data-model="label.value" data-on-keydown="key">
69
69
  <button type="button" class="btn btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save Version</button>
70
- </div>`);let i=!1;const f={label:M.observable(""),saving:M.observable(!1)};f.save=async()=>{f.saving.value=!0;try{await g.versions.create(a,f.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),i=!0,l.close()}catch(c){E.toast("Failed to save version: "+c.message,{type:"error"})}finally{f.saving.value=!1}},f.key=c=>{c.key==="Enter"&&(c.preventDefault(),f.save())};const p=M.applyBindings(f,s);he(l,()=>{p.dispose(),r(i)}),l.element.appendChild(s),l.open(),setTimeout(()=>s.querySelector("input")?.focus(),100)})}let w=null,_e=!1;function qt(){_e||!window.R?.use||(_e=!0,R.use((a,r,l)=>{const s=r?.path||"",i=a?.path||"",f=Pt.test(s)&&i!==s;return f&&w?.path===s&&w.cloneTarget===i?(w.cloneTarget=null,w.keepDraft(),l()):!f||!w||w.path!==s||!w.dirty()?l():E.confirm("You have unsaved changes to this page. Leave anyway? They are kept as a draft for your next visit.").then(p=>{p?(w?.keepDraft(),l()):history.replaceState(null,"","#"+s)})}),M.subscribe("router:afterChange",({to:a}={})=>{if(w&&(a?.path||"")!==w.path){const r=w;w=null,r.dispose()}}))}function Vt(a){return a.split(`
70
+ </div>`);let i=!1;const f={label:M.observable(""),saving:M.observable(!1)};f.save=async()=>{f.saving.value=!0;try{await b.versions.create(s,f.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),i=!0,l.close()}catch(c){E.toast("Failed to save version: "+c.message,{type:"error"})}finally{f.saving.value=!1}},f.key=c=>{c.key==="Enter"&&(c.preventDefault(),f.save())};const p=M.applyBindings(f,o);ye(l,()=>{p.dispose(),r(i)}),l.element.appendChild(o),l.open(),setTimeout(()=>o.querySelector("input")?.focus(),100)})}let w=null,Ye=!1;function _t(){Ye||!window.R?.use||(Ye=!0,R.use((s,r,l)=>{const o=r?.path||"",i=s?.path||"",f=Rt.test(o)&&i!==o;return f&&w?.path===o&&w.cloneTarget===i?(w.cloneTarget=null,w.keepDraft(),l()):!f||!w||w.path!==o||!w.dirty()?l():E.confirm("You have unsaved changes to this page. Leave anyway? They are kept as a draft for your next visit.").then(p=>{p?(w?.keepDraft(),l()):history.replaceState(null,"","#"+o)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(w&&(s?.path||"")!==w.path){const r=w;w=null,r.dispose()}}))}function Jt(s){return s.split(`
71
71
  `).map(r=>r.trimEnd()).join(`
72
72
  `).replace(/\n{3,}/g,`
73
73
 
74
- `)}const We="editor_prefs";function j(){return S.get(We)||{viewMode:"split",fullscreen:!1}}function Q(a,r){const l=j();l[a]=r,S.set(We,l)}const Ht=["edit","details","seo","dconfig","live","history"],Je=["split","write","preview"],Kt=a=>a.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function Ge(a,r,l=()=>{}){let s=0;try{s=a.contentWindow.scrollY||0}catch{}a.addEventListener("load",()=>{try{a.contentWindow.scrollTo(0,s)}catch{}l()},{once:!0}),a.srcdoc=r}export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(a){if(qt(),w){const e=w;w=null,e.dispose()}const r=window.location.hash.replace(/^#/,""),l=[],s=e=>(l.push(e),e);let i=()=>!1,f=()=>{};const p={path:r,dirty:()=>i(),keepDraft:()=>f(),dispose:()=>{l.splice(0).forEach(e=>{try{e()}catch{}})}};w=p;const c=a.get(0),u=()=>w===p&&c.isConnected,T=window.location.hash.match(/#\/pages\/edit(\/[^?]*)/),d=T?T[1]:null,v=!!d,C=E.loader(c,{type:"dots"}),[N,q,b,ee,we,Ye]=await Promise.all([g.layouts.get().catch(()=>({})),g.settings.get().catch(()=>({})),v?g.pages.get(d).catch(()=>null):null,v?g.pages.list().catch(()=>[]):[],g.projects.list().catch(()=>[]),g.pages.tags().catch(()=>[])]);if(!u()){C.destroy();return}if(C.destroy(),v&&!b){E.toast("Page not found.",{type:"error"}),R.navigate("/pages");return}const y=e=>c.querySelector(e);if(bt(y("#field-theme"),{includeDefault:!0}),await Gt(y("#field-visibility"),b?.visibility),!u())return;const Xe=Jt(y("#field-layout"),N,b?.layout||"default");for(const e of we)y("#field-project").append(new Option(e.name||e.slug,e.slug));const ke=[...Array.isArray(ee)?ee:[]].sort((e,t)=>(e.title||e.urlPath).localeCompare(t.title||t.urlPath));for(const e of ke)y("#quick-switch").append(new Option(`${e.title||"Untitled"} (${e.urlPath})`,e.urlPath,!1,e.urlPath===d));const V=j(),o={urlPath:M.observable(d||""),title:M.observable(b?.title||""),description:M.observable(b?.description||""),layout:M.observable(Xe),status:M.observable(b?.status||"draft"),sortOrder:M.observable(String(b?.sortOrder??99)),showInNav:M.observable(!!b?.showInNav),sidebar:M.observable(!!b?.sidebar),breadcrumbs:M.observable(b?b.breadcrumbs!==!1:!0),bundled:M.observable(!!b?.bundled),tags:M.observable(Array.isArray(b?.tags)?b.tags:[]),category:M.observable(b?.category||""),visibility:M.observable(typeof b?.visibility=="string"&&b.visibility?b.visibility:"public"),project:M.observable(Ot({isEdit:v,page:b,projects:we,fromHash:xt()||"",urlPath:d||""})),theme:M.observable(b?.theme||""),seoTitle:M.observable(b?.seo?.title||""),seoDesc:M.observable(b?.seo?.description||""),dconfig:M.observable(b?.dconfig?JSON.stringify(b.dconfig,null,2):""),css:M.observable(""),cssOpened:M.observable(!1)},n={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),tab:M.observable("edit"),viewMode:M.observable(Je.includes(V.viewMode)?V.viewMode:"split"),fullscreen:M.observable(!!V.fullscreen),cssMode:M.observable(!1),folded:M.observable(!1),liveStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null)},k=y("#markdown-editor"),B=y("#css-editor");k.value=b?.content||"";const L=document.createElement("div");L.className="editor-line-numbers editor-line-numbers--foldable",k.parentElement.insertBefore(L,k);const x=gt(k,L,{onFoldChange:()=>{}});let O=!1;x.setGuard(e=>{O=e});const I=()=>x.getUnfoldedContent(),Ee=M.observable(G(V.type));s(M.effect(()=>{const e=Ee.value,t=y("#editor-body");if(t){Ue(e.font);for(const[m,h]of Object.entries(He(e)))t.style.setProperty(m,h);t.classList.toggle("pe-nowrap",!e.wrap),k.wrap=e.wrap?"soft":"off",B.wrap=e.wrap?"soft":"off"}}));let Me=null;s(()=>{try{Me?.close()}catch{}});const D=()=>(n.bodyRev.value,{urlPath:o.urlPath.value.trim(),title:o.title.value,description:o.description.value,layout:o.layout.value,status:o.status.value,sortOrder:o.sortOrder.value,showInNav:o.showInNav.value,sidebar:o.sidebar.value,breadcrumbs:o.breadcrumbs.value,bundled:o.bundled.value,tags:o.tags.value,category:o.category.value,visibility:o.visibility.value,project:o.project.value,theme:o.theme.value,seoTitle:o.seoTitle.value,seoDesc:o.seoDesc.value,dconfig:o.dconfig.value,body:I(),css:o.cssOpened.value?o.css.value:null}),Ze=M.computed(()=>z(D())!==n.base.value),Qe=M.computed(()=>ye({urlPath:o.urlPath.value,dconfig:o.dconfig.value})[0]?.text||""),et=M.computed(()=>n.versions.value.some(e=>e.checked.value)),tt=M.computed(()=>n.versions.value.length>0&&n.versions.value.every(e=>e.checked.value)),at=M.computed(()=>{const e=n.versions.value.filter(t=>t.checked.value).length;return e>0&&e<n.versions.value.length});i=()=>z(D())!==n.base.peek();const H=e=>({title:o.title.peek().trim()||"Untitled",description:o.description.peek().trim(),layout:o.layout.peek(),status:o.status.peek(),sortOrder:parseInt(String(o.sortOrder.peek()),10)||99,showInNav:o.showInNav.peek(),sidebar:o.sidebar.peek(),...o.breadcrumbs.peek()?{}:{breadcrumbs:!1},...o.bundled.peek()?{bundled:!0}:{},tags:o.tags.peek(),category:o.category.peek().trim()||null,visibility:o.visibility.peek()||"public",project:o.project.peek()||"",seo:{title:o.seoTitle.peek().trim(),description:o.seoDesc.peek().trim()},dconfig:e,...o.theme.peek()?{theme:o.theme.peek()}:{}}),te=()=>{const e=o.dconfig.peek().trim();if(!e)return null;try{return JSON.parse(e)}catch{return null}},Se=()=>o.urlPath.peek().trim()||d||"/",K=y("#markdown-preview");let Ce="";g.settings.getCustomCss().then(({css:e})=>{Ce=e||""}).catch(()=>{});const Te=()=>{const e=K?.contentDocument;if(!e?.head)return;const t=o.cssOpened.peek()?o.css.peek():"";if(!t)return;let m=e.getElementById("preview-custom-css");m||(m=e.createElement("style"),m.id="preview-custom-css",e.head.appendChild(m)),m.textContent=t};let ae=null;const U=()=>{clearTimeout(ae),ae=setTimeout(async()=>{if(!(!K||n.viewMode.peek()==="write"))try{const{html:e}=await g.pages.previewFull(Se(),H(te()),I());u()&&Ge(K,Kt(e),Te)}catch(e){K.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(e?.message||"unknown error").replace(/[<&]/g,t=>t==="<"?"&lt;":"&amp;")+"</body>"}},500)},xe=y("#live-preview-frame");let se=!0,oe=null,ne=!1;const re=async()=>{if(!(ne||!xe)){ne=!0,n.liveStatus.value="Rendering\u2026";try{const{html:e}=await g.pages.previewFull(Se(),H(te()),I());if(!u())return;Ge(xe,e),se=!1,n.liveStatus.value="Showing unsaved changes"}catch(e){n.liveStatus.value="Preview failed - "+(e?.message||"unknown error")}finally{ne=!1}}},Pe=()=>{se=!0,n.tab.peek()==="live"&&(clearTimeout(oe),oe=setTimeout(re,600))};s(()=>{clearTimeout(ae),clearTimeout(oe)});const ie=pt(a.find("#markdown-editor"),a.find("#editor-toolbar"),{spacerDefault:q?.layoutOptions?.spacerSize??40});ft(k);const le=c.querySelector(".editor-toolbar-right");le.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}')`)}),y("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const st=Z(`
74
+ `)}const Xe="editor_prefs";function j(){return S.get(Xe)||{viewMode:"split",fullscreen:!1}}function te(s,r){const l=j();l[s]=r,S.set(Xe,l)}const Wt=["edit","details","seo","dconfig","live","history"],Ze=["split","write","preview"],Gt=s=>s.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function Qe(s,r,l=()=>{}){let o=0;try{o=s.contentWindow.scrollY||0}catch{}s.addEventListener("load",()=>{try{s.contentWindow.scrollTo(0,o)}catch{}l()},{once:!0}),s.srcdoc=r}export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(s){if(_t(),w){const e=w;w=null,e.dispose()}const r=window.location.hash.replace(/^#/,""),l=[],o=e=>(l.push(e),e);let i=()=>!1,f=()=>{};const p={path:r,dirty:()=>i(),keepDraft:()=>f(),dispose:()=>{l.splice(0).forEach(e=>{try{e()}catch{}})}};w=p;const c=s.get(0),u=()=>w===p&&c.isConnected,C=window.location.hash.match(/#\/pages\/edit(\/[^?]*)/),d=C?C[1]:null,v=!!d,T=E.loader(c,{type:"dots"}),[F,V,m,ae,ke,et,tt]=await Promise.all([b.layouts.get().catch(()=>({})),b.settings.get().catch(()=>({})),v?b.pages.get(d).catch(()=>null):null,v?b.pages.list().catch(()=>[]):[],b.projects.list().catch(()=>[]),b.pages.tags().catch(()=>[]),b.listTags.list("pages").then(e=>e.map(t=>t.name)).catch(()=>[])]);if(!u()){T.destroy();return}if(T.destroy(),v&&!m){E.toast("Page not found.",{type:"error"}),R.navigate("/pages");return}const y=e=>c.querySelector(e);if(kt(y("#field-theme"),{includeDefault:!0}),await ea(y("#field-visibility"),m?.visibility),!u())return;const at=Qt(y("#field-layout"),F,m?.layout||"default");for(const e of ke)y("#field-project").append(new Option(e.name||e.slug,e.slug));const Ee=[...Array.isArray(ae)?ae:[]].sort((e,t)=>(e.title||e.urlPath).localeCompare(t.title||t.urlPath));for(const e of Ee)y("#quick-switch").append(new Option(`${e.title||"Untitled"} (${e.urlPath})`,e.urlPath,!1,e.urlPath===d));const q=j(),a={urlPath:M.observable(d||""),title:M.observable(m?.title||""),description:M.observable(m?.description||""),layout:M.observable(at),status:M.observable(m?.status||"draft"),sortOrder:M.observable(String(m?.sortOrder??99)),showInNav:M.observable(!!m?.showInNav),sidebar:M.observable(!!m?.sidebar),breadcrumbs:M.observable(m?m.breadcrumbs!==!1:!0),bundled:M.observable(!!m?.bundled),tags:M.observable(Array.isArray(m?.tags)?m.tags:[]),adminTags:M.observable(Array.isArray(m?.adminTags)?m.adminTags:[]),category:M.observable(m?.category||""),visibility:M.observable(typeof m?.visibility=="string"&&m.visibility?m.visibility:"public"),project:M.observable(Bt({isEdit:v,page:m,projects:ke,fromHash:Dt()||"",urlPath:d||""})),theme:M.observable(m?.theme||""),seoTitle:M.observable(m?.seo?.title||""),seoDesc:M.observable(m?.seo?.description||""),dconfig:M.observable(m?.dconfig?JSON.stringify(m.dconfig,null,2):""),css:M.observable(""),cssOpened:M.observable(!1)},n={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),tab:M.observable("edit"),viewMode:M.observable(Ze.includes(q.viewMode)?q.viewMode:"split"),fullscreen:M.observable(!!q.fullscreen),cssMode:M.observable(!1),folded:M.observable(!1),liveStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null)},k=y("#markdown-editor"),I=y("#css-editor");k.value=m?.content||"";const L=document.createElement("div");L.className="editor-line-numbers editor-line-numbers--foldable",k.parentElement.insertBefore(L,k);const x=Et(k,L,{onFoldChange:()=>{}});let D=!1;x.setGuard(e=>{D=e});const B=()=>x.getUnfoldedContent(),Me=M.observable(X(q.type));o(M.effect(()=>{const e=Me.value,t=y("#editor-body");if(t){Ge(e.font);for(const[g,h]of Object.entries(Je(e)))t.style.setProperty(g,h);t.classList.toggle("pe-nowrap",!e.wrap),k.wrap=e.wrap?"soft":"off",I.wrap=e.wrap?"soft":"off"}}));let Se=null;o(()=>{try{Se?.close()}catch{}});const A=()=>(n.bodyRev.value,{urlPath:a.urlPath.value.trim(),title:a.title.value,description:a.description.value,layout:a.layout.value,status:a.status.value,sortOrder:a.sortOrder.value,showInNav:a.showInNav.value,sidebar:a.sidebar.value,breadcrumbs:a.breadcrumbs.value,bundled:a.bundled.value,tags:a.tags.value,adminTags:a.adminTags.value,category:a.category.value,visibility:a.visibility.value,project:a.project.value,theme:a.theme.value,seoTitle:a.seoTitle.value,seoDesc:a.seoDesc.value,dconfig:a.dconfig.value,body:B(),css:a.cssOpened.value?a.css.value:null}),st=M.computed(()=>z(A())!==n.base.value),ot=M.computed(()=>we({urlPath:a.urlPath.value,dconfig:a.dconfig.value})[0]?.text||""),nt=M.computed(()=>n.versions.value.some(e=>e.checked.value)),rt=M.computed(()=>n.versions.value.length>0&&n.versions.value.every(e=>e.checked.value)),it=M.computed(()=>{const e=n.versions.value.filter(t=>t.checked.value).length;return e>0&&e<n.versions.value.length});i=()=>z(A())!==n.base.peek();let Te=JSON.stringify(a.adminTags.peek());const H=e=>({title:a.title.peek().trim()||"Untitled",description:a.description.peek().trim(),layout:a.layout.peek(),status:a.status.peek(),sortOrder:parseInt(String(a.sortOrder.peek()),10)||99,showInNav:a.showInNav.peek(),sidebar:a.sidebar.peek(),...a.breadcrumbs.peek()?{}:{breadcrumbs:!1},...a.bundled.peek()?{bundled:!0}:{},tags:a.tags.peek(),...v&&JSON.stringify(a.adminTags.peek())===Te?{}:{adminTags:a.adminTags.peek()},category:a.category.peek().trim()||null,visibility:a.visibility.peek()||"public",project:a.project.peek()||"",seo:{title:a.seoTitle.peek().trim(),description:a.seoDesc.peek().trim()},dconfig:e,...a.theme.peek()?{theme:a.theme.peek()}:{}}),se=()=>{const e=a.dconfig.peek().trim();if(!e)return null;try{return JSON.parse(e)}catch{return null}},Ce=()=>a.urlPath.peek().trim()||d||"/",K=y("#markdown-preview");let xe="";b.settings.getCustomCss().then(({css:e})=>{xe=e||""}).catch(()=>{});const Pe=()=>{const e=K?.contentDocument;if(!e?.head)return;const t=a.cssOpened.peek()?a.css.peek():"";if(!t)return;let g=e.getElementById("preview-custom-css");g||(g=e.createElement("style"),g.id="preview-custom-css",e.head.appendChild(g)),g.textContent=t};let oe=null;const U=()=>{clearTimeout(oe),oe=setTimeout(async()=>{if(!(!K||n.viewMode.peek()==="write"))try{const{html:e}=await b.pages.previewFull(Ce(),H(se()),B());u()&&Qe(K,Gt(e),Pe)}catch(e){K.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(e?.message||"unknown error").replace(/[<&]/g,t=>t==="<"?"&lt;":"&amp;")+"</body>"}},500)},Le=y("#live-preview-frame");let ne=!0,re=null,ie=!1;const le=async()=>{if(!(ie||!Le)){ie=!0,n.liveStatus.value="Rendering\u2026";try{const{html:e}=await b.pages.previewFull(Ce(),H(se()),B());if(!u())return;Qe(Le,e),ne=!1,n.liveStatus.value="Showing unsaved changes"}catch(e){n.liveStatus.value="Preview failed - "+(e?.message||"unknown error")}finally{ie=!1}}},Ae=()=>{ne=!0,n.tab.peek()==="live"&&(clearTimeout(re),re=setTimeout(le,600))};o(()=>{clearTimeout(oe),clearTimeout(re)});const ce=gt(s.find("#markdown-editor"),s.find("#editor-toolbar"),{spacerDefault:V?.layoutOptions?.spacerSize??40});yt(k);const de=c.querySelector(".editor-toolbar-right");de.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}')`)}),y("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const lt=ee(`
75
75
  <span class="pe-toolbar-extras" style="display:contents">
76
76
  <button type="button" class="editor-view-btn" data-tip="Edit Custom CSS" data-on-click="toggleCss"
77
77
  data-bind-class="cssMode.value && 'active'"><span>CSS</span></button>
@@ -83,5 +83,5 @@ The quick brown fox - 0O 1lI {} [] () =&gt; ==
83
83
  <span class="pe-fold-icon" hidden data-bind-hidden="!folded.value"><span data-icon="chevrons-right" data-icon-size="10"></span></span>
84
84
  </button>
85
85
  <span class="editor-toolbar-sep"></span>
86
- </span>`);le.prepend(st);const Le=y("#field-tags"),ce=Le?E.pillbox(Le,{data:Ye,value:o.tags.peek(),creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{o.tags.value=ce?[...ce.getValue()]:[]}}):null;let de=!1,pe=null;const _=async()=>{de=!0,n.histLoading.value=!0,n.histError.value="";try{const e=(await g.versions.list(d)).versions||[];if(!u())return;n.versions.value=e.map(t=>({...t,when:X(t.createdAt),author:t.author||"",label:t.label||"",checked:M.observable(!1)}))}catch(e){n.histError.value="Failed to load versions: "+e.message}finally{n.histLoading.value=!1}},ue=e=>n.versions.peek().find(t=>t.filename===e),ve=async e=>{try{const t=await g.versions.get(d,e.filename);pe=e.filename,n.compare.value={label:Ke(e),old:t.content,current:I()}}catch(t){E.toast("Failed to load version: "+t.message,{type:"error"})}},De=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(d,e),E.toast("Page restored. Reloading editor\u2026",{type:"success"}),R.navigate("/pages"),setTimeout(()=>R.navigate(`/pages/edit${d}`),300)}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},ot=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await g.versions.delete(d,e.filename),E.toast("Version deleted.",{type:"success"}),await _()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},Ae=()=>{n.bodyRev.value=n.bodyRev.peek()+1},Oe=()=>{o.cssOpened.peek()||(o.css.value=Ce,o.cssOpened.value=!0,dt(B))},Re=e=>{for(const t of["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","dconfig"])o[t].value=e[t]==null?"":String(e[t]);for(const t of["showInNav","sidebar","breadcrumbs","bundled"])o[t].value=!!e[t];o.tags.value=Array.isArray(e.tags)?e.tags:[],ce?.setValue(o.tags.peek()),x.unfoldAll(),O=!0,k.value=e.body==null?"":String(e.body),O=!1,x.refresh(),e.css!=null&&(Oe(),o.css.value=String(e.css)),Ae()};n.base.value=z(D());const W=At(d||"");f=()=>{if(i())try{S.set(W,Rt(D(),n.base.peek()))}catch{}};const fe=()=>{try{S.remove?S.remove(W):S.set(W,null)}catch{}},Fe=e=>{i()&&(f(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Fe),s(()=>window.removeEventListener("beforeunload",Fe));const Ne=async()=>{if(n.saving.peek())return;const e=D(),t=ye(e);if(t.length){E.toast(t[0].text,{type:"warning"}),c.querySelectorAll("#editor-meta-tabs .tab-item")[Ht.indexOf(t[0].field==="dconfig"?"dconfig":"details")]?.click();return}const m=(e.dconfig||"").trim(),h=H(m?JSON.parse(m):null),P=o.cssOpened.peek();n.saving.value=!0;try{if(v){const A={frontmatter:h,body:e.body};e.urlPath!==d&&(A.newUrlPath=e.urlPath),await g.pages.update(d,A)}else await g.pages.create({urlPath:e.urlPath,frontmatter:h,body:e.body});P&&await g.settings.saveCustomCss(o.css.peek()),E.toast(P?"Page and CSS saved.":"Page saved.",{type:"success"}),fe(),n.base.value=z(D()),v?e.urlPath!==d&&R.navigate(`/pages/edit${e.urlPath}`):R.navigate("/pages")}catch(A){E.toast(`Save failed: ${A.message||"Unknown error"}`,{type:"error"})}finally{n.saving.value=!1}},Be=c.querySelector(".dm-plugin-body")||c,nt={...o,...n,dirty:Ze,problem:Qe,anySelected:et,allSelected:tt,someSelected:at,isEdit:v,savedPath:d||"",hasPages:ke.length>0},rt={save:()=>Ne(),discard:async()=>{!i()||!await E.confirm("Discard the changes you have not saved?")||(Re(JSON.parse(n.base.peek())),fe())},switchPage:e=>{const t=e?.target?.value;t&&R.navigate("/pages/edit"+t)},showTab:e=>{n.tab.value=e,e==="live"&&se&&re(),e==="history"&&v&&!de&&_()},setMode:e=>{n.cssMode.peek()&&(n.cssMode.value=!1),n.viewMode.value=e,Q("viewMode",e),e!=="write"&&U()},toggleFullscreen:()=>{n.fullscreen.value=!n.fullscreen.peek(),Q("fullscreen",n.fullscreen.peek())},toggleCss:()=>{n.cssMode.peek()?(n.cssMode.value=!1,n.viewMode.value=Je.includes(j().viewMode)?j().viewMode:"split",x.refresh()):(Oe(),n.cssMode.value=!0,n.viewMode.value="split")},formatMarkdown:()=>{const e=k.selectionStart,t=Vt(I());x.unfoldAll(),O=!0,k.value=t,O=!1,k.selectionStart=k.selectionEnd=Math.min(e,t.length),k.dispatchEvent(new Event("input")),x.refresh(),E.toast("Markdown formatted.",{type:"success"})},toggleFold:()=>{n.folded.peek()?x.unfoldAll():x.foldAll(),n.folded.value=!n.folded.peek()},setWidth:e=>{n.liveWidth.value=e;const t=y("#live-preview-stage");t&&(t.scrollTop=0)},refreshLive:()=>re(),share:async()=>{if(!v){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await zt(d)},toggleAll:e=>{const t=!!e?.target?.checked;n.versions.peek().forEach(m=>{m.checked.value=t})},deleteSelected:async()=>{const e=n.versions.peek().filter(m=>m.checked.peek()).map(m=>m.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await g.versions.bulkDelete(d,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await _()}catch(m){E.toast("Delete failed: "+m.message,{type:"error"})}},saveNamed:async()=>{await jt(d)&&u()&&(n.tab.peek()==="history"?await _():de=!1)},closeCompare:()=>{n.compare.value=null,pe=null},restoreCompared:()=>De(pe)};le.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const it=M.applyBindings(nt,Be,{methods:rt});s(()=>it.dispose()),E.tabs(y("#editor-meta-tabs"));let me=null;s(()=>clearTimeout(me)),k.addEventListener("input",()=>{x.refresh(),L.scrollTop=k.scrollTop,!O&&(clearTimeout(me),me=setTimeout(Ae,120),U(),Pe())}),k.addEventListener("scroll",()=>{L.scrollTop=k.scrollTop}),B.addEventListener("scroll",()=>{L.scrollTop=B.scrollTop});let Ie=!0;s(M.effect(()=>{for(const e of Object.keys(o))e!=="css"&&e!=="cssOpened"&&o[e].value;if(n.bodyRev.value,Ie){Ie=!1,U();return}U(),Pe()}));let be=null;if(s(()=>clearTimeout(be)),s(M.effect(()=>{const e=o.css.value;n.cssMode.value&&(Ut(L,e,B),clearTimeout(be),be=setTimeout(Te,400))})),v){const e=y(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:m=>{const h=ue(m.dataset.file);return h?[{header:Ke(h)},{label:"Compare with the editor",icon:"columns",action:()=>ve(h)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>De(h.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>ot(h)}]:[]}});s(()=>t?.destroy?.()),e.addEventListener("click",m=>{const h=m.target.closest("tr[data-file]");if(!h||m.target.matches("input"))return;const P=m.target.closest(".pe-kebab");if(P){qe(P);return}const A=ue(h.dataset.file);A&&ve(A)}),e.addEventListener("keydown",m=>{const h=m.target.closest("tr[data-file]");if(h){if(m.key==="F10"&&m.shiftKey)m.preventDefault(),qe(h);else if(m.key==="Enter"&&m.target===h){const P=ue(h.dataset.file);P&&ve(P)}}})}_t(y("#editor-body"),y("#editor-divider"),s),mt({image:e=>(n.cssMode.value=!1,Wt(e)),scribe:e=>ht(e),card:e=>lt.open(e,{onMediaBrowse:()=>g.media.list().catch(()=>[])}),timeline:e=>ct.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)})}),ie.onEffects(e=>vt.open(e)),ie.onHelp(()=>Ve()),ie.onTutorial(e=>yt(e));const $e=e=>{(e.ctrlKey||e.metaKey)&&!e.shiftKey&&!e.altKey&&e.key.toLowerCase()==="s"&&u()&&(e.preventDefault(),Ne())};document.addEventListener("keydown",$e),s(()=>document.removeEventListener("keydown",$e));const J=c.querySelector(".dm-plugin-banner-name");s(M.effect(()=>{const e=Lt(v,o.title.value,d);if(!u()||(document.title=`${e} - Domma CMS`,!J))return;const t=J.lastChild;t&&t.nodeType===3&&J.querySelector(".dm-plugin-banner-sep")?t.textContent=e:J.textContent=`Pages \u203A ${e}`})),$(a,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),$(a,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>Ve()}),v&&$(a,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>window.open(o.urlPath.peek()||d,"_blank","noopener")});const ge=$(a,{id:"page-editor-clone",icon:"copy",title:"Clone page",onClick:()=>{if(!v||n.saving.peek()||!u())return;const e=D(),t=ye({...e,urlPath:d});if(t.length){E.toast(t[0].text,{type:"warning"});return}const m=kt({page:b,frontmatter:H(te()),body:e.body,source:d,pages:ee,onCloned:h=>{u()&&(p.cloneTarget=`/pages/edit${h}`,R.navigate(p.cloneTarget))}});s(()=>m.close())}});ge&&(ge.disabled=!v,v||(ge.title="Save the page before cloning it")),$(a,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{Me=$t(Ee)}}),s(je(Be,{position:"bottom"})),Domma.icons.scan();let ze=null;try{ze=S.get(W)}catch{}const F=Ft(ze);if(F&&(fe(),z(F.state)!==n.base.peek())){const e=F.at?` (${X(F.at)})`:"",t=Dt(F,n.base.peek())?" 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 this page${e}. Restore them?${t}`)&&u()&&Re(F.state)}}};function Ut(a,r,l){a.replaceChildren(...String(r).split(`
87
- `).map((s,i)=>{const f=document.createElement("span");f.className="editor-line-number-row";const p=document.createElement("span");p.className="fold-toggle fold-toggle--empty";const c=document.createElement("span");return c.className="editor-line-num",c.textContent=String(i+1),f.append(p,c),f})),a.scrollTop=l.scrollTop}function _t(a,r,l){if(!a||!r)return;const s=15,i=85,f=d=>Math.min(i,Math.max(s,d));let p=f(parseFloat(j().splitRatio)||50);const c=d=>{a.style.setProperty("--dm-editor-split",p+"%"),r.setAttribute("aria-valuenow",String(Math.round(p))),d&&Q("splitRatio",p)};c(!1);const u=d=>{const v=a.getBoundingClientRect();v.width&&(p=f((d.clientX-v.left)/v.width*100)),c(!1)},T=()=>{document.removeEventListener("pointermove",u),document.removeEventListener("pointerup",T),a.classList.remove("is-resizing"),r.classList.remove("is-dragging"),c(!0)};l(T),r.addEventListener("pointerdown",d=>{window.matchMedia("(max-width: 768px)").matches||(d.preventDefault(),a.classList.add("is-resizing"),r.classList.add("is-dragging"),document.addEventListener("pointermove",u),document.addEventListener("pointerup",T))}),r.addEventListener("dblclick",()=>{p=50,c(!0)}),r.addEventListener("keydown",d=>{const v=d.shiftKey?10:2;let C=p;if(d.key==="ArrowLeft")C=p-v;else if(d.key==="ArrowRight")C=p+v;else if(d.key==="Home")C=s;else if(d.key==="End")C=i;else if(d.key==="Enter"||d.key===" ")C=50;else return;d.preventDefault(),p=f(C),c(!0)})}async function Wt(a){const r=a.selectionStart,l=a.selectionEnd,s=await wt({title:"Upload or insert an image"});if(!s||!a.isConnected)return;let i=s.split("/").pop().split("?")[0];try{i=decodeURIComponent(i)}catch{}const f=i.replace(/[\\[\]]/g,"\\$&"),p=s.replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/ /g,"%20");a.setSelectionRange(r,l),ut(a,`![${f}](${p})`),a.dispatchEvent(new Event("input",{bubbles:!0}))}function Jt(a,r,l){const s=Object.entries(r||{});return a?(a.replaceChildren(...s.map(([i,f])=>new Option(f?.label||i,i))),s.some(([i])=>i===l)?l:s[0]?.[0]||l):l}async function Gt(a,r){if(!a)return;let l=[];try{const u=await g.get("/collections/roles/entries?limit=100");l=Array.isArray(u)?u:u?.entries||[]}catch{}const s=l.map(u=>({name:u.data?.name,label:u.data?.label||u.data?.name,level:u.data?.level??99})).filter(u=>u.name&&u.level!==0).sort((u,T)=>T.level-u.level),i=(u,T)=>Object.assign(document.createElement("option"),{value:u,textContent:T}),f=()=>{typeof r=="string"&&r&&![...a.options].some(u=>u.value===r)&&a.append(i(r,`${r} (custom)`))};if(!s.length)return f();const p=document.createElement("optgroup");p.label="Role and above",s.forEach(u=>p.append(i(u.name,`${u.label} and above`)));const c=document.createElement("optgroup");c.label="Role only",s.forEach(u=>c.append(i(`=${u.name}`,`${u.label} only`))),a.replaceChildren(i("public","Public - everyone"),p,c,i("private","Private - super-admin only")),f()}
86
+ </span>`);de.prepend(lt);const Oe=e=>(Array.isArray(e)?e:[]).map(t=>({value:t,label:t})),De=y("#field-tags");let _=null;_=De?E.pillbox(De,{data:Oe(et),value:a.tags.peek(),creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{_&&(a.tags.value=[..._.getValue()])}}):null;const Re=y("#field-admin-tags");let J=null;J=Re?E.pillbox(Re,{data:Oe(tt),value:a.adminTags.peek(),creatable:!0,searchable:!0,placeholder:"Add admin tag\u2026",onChange:()=>{J&&(a.adminTags.value=[...J.getValue()])}}):null;let pe=!1,ue=null;const W=async()=>{pe=!0,n.histLoading.value=!0,n.histError.value="";try{const e=(await b.versions.list(d)).versions||[];if(!u())return;n.versions.value=e.map(t=>({...t,when:Q(t.createdAt),author:t.author||"",label:t.label||"",checked:M.observable(!1)}))}catch(e){n.histError.value="Failed to load versions: "+e.message}finally{n.histLoading.value=!1}},ve=e=>n.versions.peek().find(t=>t.filename===e),fe=async e=>{try{const t=await b.versions.get(d,e.filename);ue=e.filename,n.compare.value={label:We(e),old:t.content,current:B()}}catch(t){E.toast("Failed to load version: "+t.message,{type:"error"})}},Ne=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 b.versions.restore(d,e),E.toast("Page restored. Reloading editor\u2026",{type:"success"}),R.navigate("/pages"),setTimeout(()=>R.navigate(`/pages/edit${d}`),300)}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},ct=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await b.versions.delete(d,e.filename),E.toast("Version deleted.",{type:"success"}),await W()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},Fe=()=>{n.bodyRev.value=n.bodyRev.peek()+1},Ie=()=>{a.cssOpened.peek()||(a.css.value=xe,a.cssOpened.value=!0,mt(I))},Be=e=>{for(const t of["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","dconfig"])a[t].value=e[t]==null?"":String(e[t]);for(const t of["showInNav","sidebar","breadcrumbs","bundled"])a[t].value=!!e[t];a.tags.value=Array.isArray(e.tags)?e.tags:[],_?.setValue(a.tags.peek()),Array.isArray(e.adminTags)&&(a.adminTags.value=e.adminTags),J?.setValue(a.adminTags.peek()),x.unfoldAll(),D=!0,k.value=e.body==null?"":String(e.body),D=!1,x.refresh(),e.css!=null&&(Ie(),a.css.value=String(e.css)),Fe()};n.base.value=z(A());const G=It(d||"");f=()=>{if(i())try{S.set(G,$t(A(),n.base.peek()))}catch{}};const me=()=>{try{S.remove?S.remove(G):S.set(G,null)}catch{}},$e=e=>{i()&&(f(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",$e),o(()=>window.removeEventListener("beforeunload",$e));const ze=async()=>{if(n.saving.peek())return;const e=A(),t=we(e);if(t.length){E.toast(t[0].text,{type:"warning"}),c.querySelectorAll("#editor-meta-tabs .tab-item")[Wt.indexOf(t[0].field==="dconfig"?"dconfig":"details")]?.click();return}const g=(e.dconfig||"").trim(),h=H(g?JSON.parse(g):null),P=a.cssOpened.peek();n.saving.value=!0;try{if(v){const O={frontmatter:h,body:e.body};e.urlPath!==d&&(O.newUrlPath=e.urlPath),await b.pages.update(d,O)}else await b.pages.create({urlPath:e.urlPath,frontmatter:h,body:e.body});P&&await b.settings.saveCustomCss(a.css.peek()),E.toast(P?"Page and CSS saved.":"Page saved.",{type:"success"}),me(),Te=JSON.stringify(a.adminTags.peek()),n.base.value=z(A()),v?e.urlPath!==d&&R.navigate(`/pages/edit${e.urlPath}`):R.navigate("/pages")}catch(O){E.toast(`Save failed: ${O.message||"Unknown error"}`,{type:"error"})}finally{n.saving.value=!1}},je=c.querySelector(".dm-plugin-body")||c,dt={...a,...n,dirty:st,problem:ot,anySelected:nt,allSelected:rt,someSelected:it,isEdit:v,savedPath:d||"",hasPages:Ee.length>0},pt={save:()=>ze(),discard:async()=>{!i()||!await E.confirm("Discard the changes you have not saved?")||(Be(JSON.parse(n.base.peek())),me())},switchPage:e=>{const t=e?.target?.value;t&&R.navigate("/pages/edit"+t)},showTab:e=>{n.tab.value=e,e==="live"&&ne&&le(),e==="history"&&v&&!pe&&W()},setMode:e=>{n.cssMode.peek()&&(n.cssMode.value=!1),n.viewMode.value=e,te("viewMode",e),e!=="write"&&U()},toggleFullscreen:()=>{n.fullscreen.value=!n.fullscreen.peek(),te("fullscreen",n.fullscreen.peek())},toggleCss:()=>{n.cssMode.peek()?(n.cssMode.value=!1,n.viewMode.value=Ze.includes(j().viewMode)?j().viewMode:"split",x.refresh()):(Ie(),n.cssMode.value=!0,n.viewMode.value="split")},formatMarkdown:()=>{const e=k.selectionStart,t=Jt(B());x.unfoldAll(),D=!0,k.value=t,D=!1,k.selectionStart=k.selectionEnd=Math.min(e,t.length),k.dispatchEvent(new Event("input")),x.refresh(),E.toast("Markdown formatted.",{type:"success"})},toggleFold:()=>{n.folded.peek()?x.unfoldAll():x.foldAll(),n.folded.value=!n.folded.peek()},setWidth:e=>{n.liveWidth.value=e;const t=y("#live-preview-stage");t&&(t.scrollTop=0)},refreshLive:()=>le(),share:async()=>{if(!v){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await Kt(d)},toggleAll:e=>{const t=!!e?.target?.checked;n.versions.peek().forEach(g=>{g.checked.value=t})},deleteSelected:async()=>{const e=n.versions.peek().filter(g=>g.checked.peek()).map(g=>g.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await b.versions.bulkDelete(d,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await W()}catch(g){E.toast("Delete failed: "+g.message,{type:"error"})}},saveNamed:async()=>{await Ut(d)&&u()&&(n.tab.peek()==="history"?await W():pe=!1)},closeCompare:()=>{n.compare.value=null,ue=null},restoreCompared:()=>Ne(ue)};de.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const ut=M.applyBindings(dt,je,{methods:pt});o(()=>ut.dispose()),E.tabs(y("#editor-meta-tabs"));let ge=null;o(()=>clearTimeout(ge)),k.addEventListener("input",()=>{x.refresh(),L.scrollTop=k.scrollTop,!D&&(clearTimeout(ge),ge=setTimeout(Fe,120),U(),Ae())}),k.addEventListener("scroll",()=>{L.scrollTop=k.scrollTop}),I.addEventListener("scroll",()=>{L.scrollTop=I.scrollTop});let Ve=!0;o(M.effect(()=>{for(const e of Object.keys(a))e!=="css"&&e!=="cssOpened"&&a[e].value;if(n.bodyRev.value,Ve){Ve=!1,U();return}U(),Ae()}));let be=null;if(o(()=>clearTimeout(be)),o(M.effect(()=>{const e=a.css.value;n.cssMode.value&&(Yt(L,e,I),clearTimeout(be),be=setTimeout(Pe,400))})),v){const e=y(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:g=>{const h=ve(g.dataset.file);return h?[{header:We(h)},{label:"Compare with the editor",icon:"columns",action:()=>fe(h)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>Ne(h.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>ct(h)}]:[]}});o(()=>t?.destroy?.()),e.addEventListener("click",g=>{const h=g.target.closest("tr[data-file]");if(!h||g.target.matches("input"))return;const P=g.target.closest(".pe-kebab");if(P){Ue(P);return}const O=ve(h.dataset.file);O&&fe(O)}),e.addEventListener("keydown",g=>{const h=g.target.closest("tr[data-file]");if(h){if(g.key==="F10"&&g.shiftKey)g.preventDefault(),Ue(h);else if(g.key==="Enter"&&g.target===h){const P=ve(h.dataset.file);P&&fe(P)}}})}Xt(y("#editor-body"),y("#editor-divider"),o),wt({image:e=>(n.cssMode.value=!1,Zt(e)),scribe:e=>Mt(e),card:e=>vt.open(e,{onMediaBrowse:()=>b.media.list().catch(()=>[])}),timeline:e=>ft.open(e,{onPreview:async t=>{try{return await b.pages.preview(t)}catch{return null}},onListCollections:()=>b.collections.list().catch(()=>[]),onGetCollection:t=>b.collections.get(t).catch(()=>null)})}),ce.onEffects(e=>ht.open(e)),ce.onHelp(()=>_e()),ce.onTutorial(e=>St(e));const qe=e=>{(e.ctrlKey||e.metaKey)&&!e.shiftKey&&!e.altKey&&e.key.toLowerCase()==="s"&&u()&&(e.preventDefault(),ze())};document.addEventListener("keydown",qe),o(()=>document.removeEventListener("keydown",qe));const Y=c.querySelector(".dm-plugin-banner-name");o(M.effect(()=>{const e=Nt(v,a.title.value,d);if(!u()||(document.title=`${e} - Domma CMS`,!Y))return;const t=Y.lastChild;t&&t.nodeType===3&&Y.querySelector(".dm-plugin-banner-sep")?t.textContent=e:Y.textContent=`Pages \u203A ${e}`})),$(s,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),$(s,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>_e()}),v&&$(s,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>window.open(a.urlPath.peek()||d,"_blank","noopener")});const he=$(s,{id:"page-editor-clone",icon:"copy",title:"Clone page",onClick:()=>{if(!v||n.saving.peek()||!u())return;const e=A(),t=we({...e,urlPath:d});if(t.length){E.toast(t[0].text,{type:"warning"});return}const g=Ct({page:m,frontmatter:{...H(se()),adminTags:a.adminTags.peek()},body:e.body,source:d,pages:ae,onCloned:h=>{u()&&(p.cloneTarget=`/pages/edit${h}`,R.navigate(p.cloneTarget))}});o(()=>g.close())}});he&&(he.disabled=!v,v||(he.title="Save the page before cloning it")),$(s,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{Se=Ht(Me)}}),o(Ke(je,{position:"bottom"})),Domma.icons.scan();let He=null;try{He=S.get(G)}catch{}const N=zt(He);if(N&&(me(),z(N.state)!==n.base.peek())){const e=N.at?` (${Q(N.at)})`:"",t=Ft(N,n.base.peek())?" 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 this page${e}. Restore them?${t}`)&&u()&&Be(N.state)}}};function Yt(s,r,l){s.replaceChildren(...String(r).split(`
87
+ `).map((o,i)=>{const f=document.createElement("span");f.className="editor-line-number-row";const p=document.createElement("span");p.className="fold-toggle fold-toggle--empty";const c=document.createElement("span");return c.className="editor-line-num",c.textContent=String(i+1),f.append(p,c),f})),s.scrollTop=l.scrollTop}function Xt(s,r,l){if(!s||!r)return;const o=15,i=85,f=d=>Math.min(i,Math.max(o,d));let p=f(parseFloat(j().splitRatio)||50);const c=d=>{s.style.setProperty("--dm-editor-split",p+"%"),r.setAttribute("aria-valuenow",String(Math.round(p))),d&&te("splitRatio",p)};c(!1);const u=d=>{const v=s.getBoundingClientRect();v.width&&(p=f((d.clientX-v.left)/v.width*100)),c(!1)},C=()=>{document.removeEventListener("pointermove",u),document.removeEventListener("pointerup",C),s.classList.remove("is-resizing"),r.classList.remove("is-dragging"),c(!0)};l(C),r.addEventListener("pointerdown",d=>{window.matchMedia("(max-width: 768px)").matches||(d.preventDefault(),s.classList.add("is-resizing"),r.classList.add("is-dragging"),document.addEventListener("pointermove",u),document.addEventListener("pointerup",C))}),r.addEventListener("dblclick",()=>{p=50,c(!0)}),r.addEventListener("keydown",d=>{const v=d.shiftKey?10:2;let T=p;if(d.key==="ArrowLeft")T=p-v;else if(d.key==="ArrowRight")T=p+v;else if(d.key==="Home")T=o;else if(d.key==="End")T=i;else if(d.key==="Enter"||d.key===" ")T=50;else return;d.preventDefault(),p=f(T),c(!0)})}async function Zt(s){const r=s.selectionStart,l=s.selectionEnd,o=await Tt({title:"Upload or insert an image"});if(!o||!s.isConnected)return;let i=o.split("/").pop().split("?")[0];try{i=decodeURIComponent(i)}catch{}const f=i.replace(/[\\[\]]/g,"\\$&"),p=o.replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/ /g,"%20");s.setSelectionRange(r,l),bt(s,`![${f}](${p})`),s.dispatchEvent(new Event("input",{bubbles:!0}))}function Qt(s,r,l){const o=Object.entries(r||{});return s?(s.replaceChildren(...o.map(([i,f])=>new Option(f?.label||i,i))),o.some(([i])=>i===l)?l:o[0]?.[0]||l):l}async function ea(s,r){if(!s)return;let l=[];try{const u=await b.get("/collections/roles/entries?limit=100");l=Array.isArray(u)?u:u?.entries||[]}catch{}const o=l.map(u=>({name:u.data?.name,label:u.data?.label||u.data?.name,level:u.data?.level??99})).filter(u=>u.name&&u.level!==0).sort((u,C)=>C.level-u.level),i=(u,C)=>Object.assign(document.createElement("option"),{value:u,textContent:C}),f=()=>{typeof r=="string"&&r&&![...s.options].some(u=>u.value===r)&&s.append(i(r,`${r} (custom)`))};if(!o.length)return f();const p=document.createElement("optgroup");p.label="Role and above",o.forEach(u=>p.append(i(u.name,`${u.label} and above`)));const c=document.createElement("optgroup");c.label="Role only",o.forEach(u=>c.append(i(`=${u.name}`,`${u.label} only`))),s.replaceChildren(i("public","Public - everyone"),p,c,i("private","Private - super-admin only")),f()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.6.50",
3
+ "version": "1.6.51",
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",
@@ -142,6 +142,21 @@ function cleanTags(tags) {
142
142
  )];
143
143
  }
144
144
 
145
+ /**
146
+ * The admin-only list tags (services/listTags.js) as the page editor sends
147
+ * them: the same cleaning as listTags' (case-insensitive duplicates go), and
148
+ * an empty list leaves no key behind.
149
+ *
150
+ * @param {object} meta - frontmatter, changed in place
151
+ */
152
+ function cleanAdminTags(meta) {
153
+ if (!('adminTags' in meta)) return;
154
+ const seen = new Set();
155
+ const list = cleanTags(meta.adminTags).filter(t => !seen.has(t.toLowerCase()) && seen.add(t.toLowerCase()));
156
+ if (list.length) meta.adminTags = list;
157
+ else delete meta.adminTags;
158
+ }
159
+
145
160
  export async function createPage(urlPath, frontmatter, body) {
146
161
  const filePath = urlPathToFilePath(urlPath);
147
162
  await fs.mkdir(path.dirname(filePath), { recursive: true });
@@ -165,6 +180,7 @@ export async function createPage(urlPath, frontmatter, body) {
165
180
  updatedAt: now
166
181
  };
167
182
  if (meta.project === '' || meta.project === undefined) delete meta.project;
183
+ cleanAdminTags(meta);
168
184
 
169
185
  await fs.writeFile(filePath, serialiseMarkdown(meta, body || ''), 'utf8');
170
186
  const page = await readPageFile(filePath);
@@ -198,6 +214,7 @@ export async function updatePage(urlPath, frontmatter, body, {author} = {}) {
198
214
  // Only when tags are part of this save: a caller that sends no tags is not
199
215
  // asking for the page's tags to be emptied.
200
216
  if ('tags' in meta) meta.tags = cleanTags(meta.tags);
217
+ cleanAdminTags(meta);
201
218
 
202
219
  // `seo` is merged, not replaced: the page editor sends only title and
203
220
  // description, and a save from it must not wipe what the SEO Tool set