domma-cms 0.96.0 → 0.98.0
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/admin/css/admin.css +1 -1
- package/admin/js/lib/menu-drag.js +1 -0
- package/admin/js/lib/sidebar-arrange.js +7 -0
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/templates/collection-editor.html +4 -3
- package/admin/js/templates/context-menu-editor.html +3 -0
- package/admin/js/templates/docs/usage-navigation.html +22 -0
- package/admin/js/views/collection-editor.js +6 -6
- package/admin/js/views/context-menu-editor.js +5 -4
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/menu-editor.js +12 -12
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{api as g}from"../api.js";import{addBannerAction as
|
|
2
|
-
<div class="cex-row" data-key="{{key}}" tabindex="0"
|
|
3
|
-
<span class="cex-grip" title="Drag to reorder" aria-hidden="true"
|
|
1
|
+
import{api as g}from"../api.js";import{addBannerAction as de}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{onceClosed as lt,openMenuAt as Fe}from"../lib/tool-kit.js";import{FIELD_TYPES as st,OPERATIONS as it,OPTION_TYPES as rt,accessOptions as ct,checkForm as dt,draftOf as ut,duplicateField as pt,errorCount as B,fieldFromPanel as Ne,fieldProblem as ft,fieldRow as mt,formFromCollection as De,formFromDraft as vt,moveField as Le,nameFromLabel as ht,newField as bt,panelForm as gt,payloadFromForm as yt,roleLadder as wt,slugify as X,snapshot as _,storageChange as Te}from"../lib/collection-editor-arrange.js";import{fmtDateTime as xt}from"../lib/dates.js";import{rowPlaceKeeper as kt}from"../lib/menu-drag.js";const Et=/^\/collections\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,St=s=>`collection-editor.draft.${s||"new"}`,Mt=`
|
|
2
|
+
<div class="cex-row" data-key="{{key}}" tabindex="0" data-bind-class="rowCls">
|
|
3
|
+
<span class="cex-grip" title="Drag to reorder" aria-hidden="true"></span>
|
|
4
4
|
<span class="cex-main"><strong data-bind-text="label"></strong><small data-bind-text="meta"></small><small class="cex-problem" data-bind-hidden="!problem" data-bind-text="problem"></small></span>
|
|
5
5
|
<span class="cex-chip is-ok" data-bind-hidden="!required">Required</span>
|
|
6
6
|
<span class="cex-chip" data-bind-hidden="!hidden">Hidden</span>
|
|
7
7
|
<button type="button" class="cex-kebab" aria-label="Actions for this field" title="Actions">\u22EE</button>
|
|
8
|
-
</div>`,
|
|
8
|
+
</div>`,Z=s=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function $t(s){const d=document.createElement("template");return d.innerHTML=s.trim(),d.content.firstElementChild}function Pe(s,d){const i=s.querySelector(".dm-plugin-banner-name");if(!i)return;const C=document.createElement("span");C.className="dm-plugin-banner-parent",C.textContent="Collections";const w=document.createElement("span");w.className="dm-plugin-banner-sep",w.setAttribute("aria-hidden","true"),w.textContent="\u203A",i.replaceChildren(C,w,document.createTextNode(d))}let y=null,qe=!1;const ee=s=>String(s||"").split("?")[0];function Ct(){qe||!window.R?.use||(qe=!0,R.use((s,d,i)=>{if(!(y&&Et.test(d?.path||"")&&ee(s?.path)!==ee(d?.path))||!y.dirty())return i();const w=`#${d.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(c=>{c?i():history.replaceState(null,"",w)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(y&&ee(s?.path)!==y.path){const d=y;y=null,d.dispose()}}))}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(s){if(Ct(),y){const e=y;y=null,e.dispose()}const d=s.get(0),i=d.querySelector(".cex")||d,C=ee(location.hash.replace(/^#/,""))||"/collections/new",w=C.match(/^\/collections\/edit\/([^/?#]+)/),c=w?decodeURIComponent(w[1]):null,D=!c,ue=[],h=e=>(ue.push(e),e);let pe=!1;const te=()=>!pe&&i.isConnected,fe=E.loader(d,{type:"dots"});let O=null,ne=[],L=null,me=[],u=0,T=[];try{[O,ne,L,me,T]=await Promise.all([c?g.collections.get(c):Promise.resolve(null),g.projects.list().catch(()=>[]),g.collections.proStatus().catch(()=>null),D?g.collections.list().then(e=>(e||[]).map(t=>t.slug)).catch(()=>[]):Promise.resolve([]),g.collections.listEntries("roles",{limit:1e3}).then(wt).catch(()=>[])]),c&&(u=(await g.collections.listEntries(c,{limit:1}).catch(()=>null))?.total??0)}catch(e){fe.destroy(),E.toast(e?.status===404?"Collection not found.":`Could not load the collection: ${e.message||e}`,{type:"error"}),R.navigate("/collections");return}if(fe.destroy(),!i.isConnected)return;if(c&&!O){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const ve=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",f=De(O,{project:ve,defaultRole:T.at(-1)?.name||""}),x=Object.fromEntries(f.fields.map(e=>[e._key,e.name])),Ie=O?.meta||{};let K=f.storageAdapter;const Be=new Set(["public","token",...T.map(e=>e.name)]),he=(e,t)=>{if(!T.length)return"";const o=[...new Set(e.filter(a=>a&&!Be.has(a)))];return o.length?`Not a role on this site: ${o.join(", ")}. Until you pick another, ${t}.`:""},n={};for(const[e,t]of Object.entries(f))n[e]=M.observable(t);const k=M.observable(D),U=M.observable(!1),H=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),be=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),N=M.observable(_(f)),P=M.computed(()=>_(be())!==N.value);let W=!D;h(M.effect(()=>{const e=n.title.value;W||(n.slug.value=X(e))}));const m=M.computed(()=>dt(be(),{isNew:k.value,taken:me,original:x})),ge=M.observable([]);h(M.effect(()=>{const e=m.value.fields||{},t=n.layout.value==="grid";ge.value=n.fields.value.map(o=>mt(o,e,{grid:t}))}));const _e=M.computed(()=>new Set(n.fields.value.map(e=>e.name))),Ke={...n,isNew:k,saving:U,dirty:P,rows:ge,errTitle:M.computed(()=>m.value.title||""),errSlug:M.computed(()=>m.value.slug||""),errColumns:M.computed(()=>m.value.columns||""),errExport:M.computed(()=>m.value.export||""),problemCount:M.computed(()=>B(m.value)),problemText:M.computed(()=>{const e=B(m.value);return e?`${e} thing${e===1?"":"s"} to fix before it can be saved`:""}),settingsBad:M.computed(()=>!!(m.value.title||m.value.slug||m.value.columns)),fieldsBad:M.computed(()=>!!m.value.fields),exportBad:M.computed(()=>!!m.value.export),fieldCount:M.computed(()=>n.fields.value.length),slugHint:M.computed(()=>k.value?`Will be created as "${X(n.slug.value||n.title.value)||"\u2026"}".`:"Cannot be changed once the collection exists."),apiPath:M.computed(()=>`/api/collections/${X(n.slug.value||n.title.value)||"<slug>"}/public`),unknownReadFields:M.computed(()=>{const e=String(n.readFields.value||"").split(",").map(t=>t.trim()).filter(t=>t&&!_e.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),missingApiRoles:M.computed(()=>he(it.filter(e=>n[`${e}Enabled`].value).map(e=>n[`${e}Access`].value),"only the level-0 role passes")),missingExportRole:M.computed(()=>he(n.exportEnabled.value?[n.exportAccess.value]:[],"only the level-0 role is offered export")),showStorage:M.observable(!!L?.pro&&c!=="roles"),storageNote:M.computed(()=>{const e=Te(K,n.storageAdapter.value);return!e||k.value?"":u?`Switching ${e}: Save asks whether to move the ${u} existing ${u===1?"entry":"entries"} across. Export them first if they matter.`:`Switching ${e}. There are no entries to move.`})},ye=i.querySelector("#collection-project");for(const e of ne){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,ye.appendChild(t)}if(f.project&&!ne.some(e=>e.slug===f.project)){const e=document.createElement("option");e.value=f.project,e.textContent=`${f.project} (not found)`,ye.appendChild(e)}const Ue=[...i.querySelectorAll('.cex-op select[data-model$="Access.value"]')],we=e=>{for(const t of Ue){const o=t.getAttribute("data-model").split(".")[0],a=e[o];t.replaceChildren(...ct(T,a,{token:t.id!=="export-access"}).map(r=>new Option(r.label,r.value))),t.value=a}};we(f);const He=i.querySelector("#storage-connection"),We=Array.isArray(L?.connections)&&L.connections.length?L.connections:["default"];for(const e of new Set([...We,f.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,He.appendChild(t)}const Y=e=>n.fields.peek().findIndex(t=>t._key===e),j=e=>{n.fields.value=e};function z(e,t){const o=Y(e);o<0||(j(Le(n.fields.peek(),o,o+t)),requestAnimationFrame(()=>i.querySelector(`.cex-row[data-key="${e}"]`)?.focus()))}async function oe(e){const t=Y(e);if(t<0)return;const o=n.fields.peek()[t];!k.peek()&&u&&x[e]&&!await E.confirm(`Remove the field "${Z(o.label||o.name)}"? The ${u} existing ${u===1?"entry keeps its":"entries keep their"} value for it, but it is no longer shown or edited. Nothing is lost until you Save - Discard puts it back.`)||j(n.fields.peek().filter(a=>a._key!==e))}function G(e,{isAdded:t=!1}={}){const o=Y(e);if(o<0)return;const a=n.fields.peek()[o],r=gt(a),v={};for(const[l,$]of Object.entries(r))v[l]=M.observable($);const p=JSON.stringify(r),J=()=>Object.fromEntries(Object.keys(v).map(l=>[l,v[l].peek()])),$e=()=>JSON.stringify(J())!==p;let Ce=!!a.name;const Xe=M.effect(()=>{const l=v.label.value;Ce||(v.name.value=ht(l))}),Ze=new Set(n.fields.peek().filter(l=>l._key!==e).map(l=>l.name)),Oe=M.computed(()=>{const l=Ne(a,Object.fromEntries(Object.keys(v).map($=>[$,v[$].value])));return l.name&&Ze.has(l.name)?`"${l.name}" is used by another field.`:ft(l,x[e])}),et=M.computed(()=>!k.peek()&&u&&x[e]&&v.name.value.trim()!==x[e]?`Entries keep their value under "${x[e]}" - renaming the field does not move it.`:""),tt={...v,problem:Oe,renamed:et,isOptions:M.computed(()=>rt.has(v.type.value)),isGrid:M.computed(()=>n.layout.value==="grid")},b=E.slideover({title:t?"New field":`Field: ${Z(a.label||a.name||"Untitled")}`,size:"md",position:"right"}),F=$t(`
|
|
9
9
|
<div class="cex-panel">
|
|
10
10
|
<div class="cex-grid">
|
|
11
11
|
<div>
|
|
@@ -20,7 +20,7 @@ import{api as g}from"../api.js";import{addBannerAction as ue}from"../lib/plugin-
|
|
|
20
20
|
<div>
|
|
21
21
|
<label class="form-label" for="cex-f-type">Type</label>
|
|
22
22
|
<select id="cex-f-type" class="form-input" data-model="type.value">
|
|
23
|
-
${
|
|
23
|
+
${st.map(l=>`<option value="${l.value}">${l.label}</option>`).join("")}
|
|
24
24
|
</select>
|
|
25
25
|
</div>
|
|
26
26
|
<div class="cex-setting">
|
|
@@ -61,4 +61,4 @@ import{api as g}from"../api.js";import{addBannerAction as ue}from"../lib/plugin-
|
|
|
61
61
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
62
62
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="done" data-bind-disabled="problem.value"><span data-icon="check" data-icon-size="13"></span> Done</button>
|
|
63
63
|
</div>
|
|
64
|
-
</div>`);let
|
|
64
|
+
</div>`);let Q=!1,re=!1;const nt=b.close.bind(b);function je(){if(Oe.peek())return;const l=Ne(a,J());j(n.fields.peek().map($=>$._key===e?l:$)),Q=!0,b.close()}b.element.appendChild(F);const ot=Ae(F),at=M.applyBindings(tt,F,{methods:{done:je,cancel:()=>b.close(),remove:async()=>{Q=!0,b.close(),await oe(e)}}}),Re=F.querySelector("#cex-f-name");Re.addEventListener("input",()=>{Ce=!!Re.value}),F.addEventListener("keydown",l=>{(l.ctrlKey||l.metaKey)&&l.key==="Enter"&&(l.preventDefault(),je())}),lt(b,()=>{Xe(),at.dispose(),ot(),se.delete(b)}),b.close=async l=>{if(!Q&&l!==!0&&$e()){if(re)return;re=!0;const ce=await E.confirm("Discard the changes to this field?");if(re=!1,!ce)return}Q=!0;const $=nt();return t&&!$e()&&!a.name&&j(n.fields.peek().filter(ce=>ce._key!==e)),$},se.add(b),I.scan(F),b.open(),F.querySelector("#cex-f-label")?.focus()}function Ye(){const e=bt();j([...n.fields.peek(),e]),Ve.activate?.(1),G(e._key,{isAdded:!0})}function ze(e){const t=Y(e);if(t<0)return[];const o=n.fields.peek()[t],a=n.fields.peek().length;return[{header:o.label||o.name||"Field"},{label:"Edit\u2026",icon:"edit",action:()=>G(e)},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>z(e,-1)},{label:"Move down",icon:"arrow-down",disabled:t===a-1,action:()=>z(e,1)},{label:"Duplicate",icon:"copy",action:()=>j(pt(n.fields.peek(),t))},{type:"divider"},{label:"Remove\u2026",icon:"trash",danger:!0,action:()=>oe(e)}]}async function ae(){if(U.peek())return;const e=m.peek();if(B(e)){E.toast(`Not saved - ${B(e)} thing${B(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=yt(H(),{isNew:k.peek(),meta:Ie});U.value=!0;try{if(k.peek()){const a=await g.collections.create(t);N.value=_(H()),ie(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(a.slug)}`);return}const o=Te(K,t.storage.adapter);if(o&&u){if(await E.confirm(`You changed the storage (${o}). Move the ${u} existing ${u===1?"entry":"entries"} to the new storage?`)){const r=await g.collections.migrateStorage(c,t.storage);E.toast(`Moved ${r.migrated} of ${r.total} entries.`,{type:"success"})}else if(!await E.confirm(`Switch without moving them? The ${u} ${u===1?"entry stays":"entries stay"} in ${Z(K)} storage and this collection will show none until they are moved.`))return}if(await g.collections.update(c,t),!te())return;K=t.storage.adapter,o&&(u=(await g.collections.listEntries(c,{limit:1}).catch(()=>null))?.total??u);for(const a of n.fields.peek())x[a._key]=a.name;N.value=_(H()),ie(),Pe(d,t.title),E.toast("Collection saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{U.value=!1}}async function Ge(){if(!P.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=De(O,{project:ve});for(const t of e.fields)x[t._key]=t.name;for(const[t,o]of Object.entries(e))n[t].value=o;N.value=_(e),W=!k.peek()}const q=i.querySelector(".cex-list");q.innerHTML=Mt;const Ve=E.tabs(i.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Je=M.applyBindings(Ke,i,{methods:{save:()=>ae(),discard:()=>Ge(),addField:()=>Ye()}});h(()=>Je.dispose()),h(Ae(i,{position:"bottom"}));const le=i.querySelector("#field-slug"),xe=()=>{W=!!le.value};le.addEventListener("input",xe),h(()=>le.removeEventListener("input",xe));const se=new Set;h(()=>[...se].forEach(e=>{try{e.close(!0)}catch{}}));const Qe=E.contextMenu(q,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>ze(e.dataset.key)});h(()=>Qe?.destroy?.());const ke={click:e=>{const t=e.target.closest(".cex-row");if(!t)return;const o=e.target.closest(".cex-kebab");if(o){Fe(o);return}G(t.dataset.key)},keydown:e=>{const t=e.target.closest(".cex-row");!t||e.target!==t||(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Fe(t)):e.key==="Enter"?G(t.dataset.key):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),z(t.dataset.key,-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),z(t.dataset.key,1)):e.key==="Delete"&&(e.preventDefault(),oe(t.dataset.key)))}};for(const[e,t]of Object.entries(ke))q.addEventListener(e,t);if(h(()=>{for(const[e,t]of Object.entries(ke))q.removeEventListener(e,t)}),typeof E.sortable=="function"){const e=kt(),t=E.sortable(q,{items:".cex-row",key:"data-key",keyboard:!1,onStart:e.start,onEnd:({item:o})=>e.restore(o),onSort:({item:o,from:a,to:r})=>{e.restore(o),j(Le(n.fields.peek(),a,r))}});h(()=>t.destroy())}Pe(d,O?.title||"New collection"),de(s,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),c&&de(s,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(c)}/entries`)}),de(s,{id:"collection-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>ae()});const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!te()||(e.preventDefault(),e.stopImmediatePropagation(),ae())};document.addEventListener("keydown",Ee,!0),h(()=>document.removeEventListener("keydown",Ee,!0));const V=St(c);function ie(){try{S.remove?S.remove(V):S.set(V,null)}catch{}}const Se=()=>{if(P.peek())try{S.set(V,{form:ut(H()),base:N.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{P.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),h(()=>window.removeEventListener("beforeunload",Me)),y={path:C,dirty:()=>P.peek(),dispose(){pe=!0,Se(),ue.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let A=null;try{A=S.get(V)}catch{}if(A?.form){ie();const e=A.base&&A.base!==N.peek(),t=A.at?` (${xt(A.at)})`:"",o=c?`changes to "${Z(O?.title||c)}"`:"a new collection",a=e?`You left unsaved ${o}${t}, but the saved collection has changed since. Restore your version anyway?`:`You left unsaved ${o}${t}. Restore them?`;if(await E.confirm(a)&&te()){const r=vt(A.form,f);D||(r.slug=f.slug);const v=Object.fromEntries(f.fields.filter(p=>p.id).map(p=>[p.id,p.name]));for(const p of r.fields)p.id&&v[p.id]!==void 0&&(x[p._key]=v[p.id]);we(r);for(const[p,J]of Object.entries(r))n[p]&&(n[p].value=J);D&&(W=!!r.slug&&r.slug!==X(r.title))}}}};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin-chrome.js";import{helpPopovers as qe}from"../lib/help-popover.js";import{onceClosed as
|
|
1
|
+
import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin-chrome.js";import{helpPopovers as qe}from"../lib/help-popover.js";import{onceClosed as it,openMenuAt as Ne}from"../lib/tool-kit.js";import{makeIconInput as rt}from"/admin/js/lib/shortcode-modal.js";import{makePageInput as ct}from"/admin/js/lib/page-picker.js";import{ACCENTS as lt,ACTION_ARG as H,ACTION_GROUPS as dt,EDITABLE_TYPES as ut,NEEDS_TARGET as mt,SELF_DISABLING as pt,checkMenu as vt,duplicateItem as ft,formFromMenu as Re,insertItem as ht,itemForm as bt,itemFromForm as Fe,itemRow as yt,moveItem as Pe,newItem as gt,previewItems as xt,previewable as Ke,recordFromForm as kt,removeItem as Y,slugify as wt,snapshot as ue,splitList as Q,themeFromForm as Et}from"../lib/context-menu-editor-arrange.js";import{fmtDateTime as St}from"../lib/dates.js";import{rowPlaceKeeper as Mt}from"../lib/menu-drag.js";const At=/^\/context-menus\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,F=l=>`context-menu-editor.draft.${l||"new"}`,Tt=`
|
|
2
2
|
<div class="cmx-row" data-index="{{index}}" tabindex="0" data-bind-class="cls">
|
|
3
|
+
<span class="cmx-grip" title="Drag to reorder" aria-hidden="true"></span>
|
|
3
4
|
<span class="cmx-icon" aria-hidden="true" data-name="{{icon}}"></span>
|
|
4
5
|
<span class="cmx-type">{{typeLabel}}</span>
|
|
5
6
|
<span class="cmx-main"><strong>{{text}}</strong><small data-bind-hidden="!does">{{does}}</small><small data-bind-hidden="!note">{{note}}</small><small class="cmx-bad" data-bind-hidden="!problem">{{problem}}</small></span>
|
|
@@ -9,7 +10,7 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
|
|
|
9
10
|
<button type="button" class="cmx-down" data-bind-disabled="last" aria-label="Move down" title="Move down (Alt+\u2193)">\u2193</button>
|
|
10
11
|
</span>
|
|
11
12
|
<button type="button" class="cmx-kebab" aria-label="Actions for {{text}}" title="Actions">\u22EE</button>
|
|
12
|
-
</div>`,
|
|
13
|
+
</div>`,It='<span class="cmx-chip" data-bind-class="cls">{{text}}</span>',Lt="<small>{{text}}</small>";function C(l){return String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function Ct(l){const u=document.createElement("template");return u.innerHTML=l.trim(),u.content.firstElementChild}let X=null;function me(){if(X){try{X.destroy()}catch{}X=null}const l=document.getElementById("ctx-preview");if(l&&E.contextMenu&&E.contextMenu.registry)for(const u of E.contextMenu.registry(l))try{u.destroy()}catch{}}function pe(l,u){const c=l.querySelector(".dm-plugin-banner-name");if(!c)return;const L=c.querySelector(".dm-plugin-banner-sep");if(L){for(;L.nextSibling;)L.nextSibling.remove();c.appendChild(document.createTextNode(u))}}let w=null,He=!1;const Z=l=>String(l||"").split("?")[0].replace(/\/$/,"");function Dt(){He||!window.R?.use||(He=!0,R.use((l,u,c)=>!(At.test(u?.path||"")&&Z(l?.path)!==Z(u?.path))||!w?.dirty()?c():E.confirm("This context menu has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(A=>{A?c():history.replaceState(null,"",`#${u.path}`)})),M.subscribe("router:afterChange",({to:l}={})=>{if(w&&Z(l?.path)!==w.path){const u=w;w=null,u.dispose()}}))}export const contextMenuEditorView={templateUrl:"/admin/js/templates/context-menu-editor.html",async onMount(l){if(Dt(),w){const e=w;w=null,e.dispose()}me();const u=[],c=e=>(u.push(e),e);let L=!1;const A=l.get(0),m=A.querySelector(".cmx")||A,B=()=>!L&&m.isConnected,ve=location.hash.match(/^#\/context-menus\/edit\/([^?#]+)/),p=ve?decodeURIComponent(ve[1]):null,h=!p,Be=Z(location.hash.replace(/^#/,""));let x=null;if(!h){const e=E.loader(A,{type:"dots"});try{x=await de.contextMenus.get(p)}catch(n){e.destroy(),E.toast(`Could not load "${p}": ${n.message||n}`,{type:"error"});return}if(e.destroy(),!m.isConnected)return}const D=Re(x),r={};for(const[e,n]of Object.entries(D))r[e]=M.observable(n);const _=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].peek()])),fe=()=>Object.fromEntries(Object.keys(r).map(e=>[e,r[e].value])),s=M.observable(Array.isArray(x?.items)?x.items:[]),U=M.observable(ue(D,s.peek())),z=M.computed(()=>ue(fe(),s.value)!==U.value),G=M.observable(!1),V=M.observable(!1),P=M.observable([]),O=M.computed(()=>vt(fe(),s.value,{isNew:h})),_e=M.computed(()=>{const e=O.value.errors;if(V.value)return e;const n={};for(const t of Object.keys(e))t!=="items"&&String(r[t]?.value??"").trim()&&(n[t]=e[t]);return n}),he=M.computed(()=>{const e=V.value?O.value.errors:{};return{binding:!!(e.name||e.slug||e.selector),items:!!e.items||O.value.itemErrors.some(Boolean),appearance:!!(e.opacity||e.accent)}});let ee=!h;c(M.effect(()=>{const e=r.name.value;ee||(r.slug.value=wt(e))}));const te=M.observable([]);c(M.effect(()=>{const e=s.value,n=O.value.itemErrors;te.value=e.map((t,i)=>({...yt(t,i,e.length,n[i]||""),key:`${i}:${JSON.stringify(t)}`}))}));const b=m.querySelector("#ctx-items");c(M.effect(()=>{te.value,requestAnimationFrame(()=>{b?.isConnected&&(b.querySelectorAll(".cmx-icon[data-name]").forEach(e=>{const n=e.dataset.name;if(!n||e.dataset.drawn===n)return;e.dataset.drawn=n;const t=document.createElement("span");t.setAttribute("data-icon",n),t.setAttribute("data-icon-size","14"),e.replaceChildren(t)}),I.scan(b))})}));const Ue=M.computed(()=>{const e=[];e.push(r.enabled.value?{text:"On",cls:"is-ok"}:{text:"Off",cls:"is-warn"});const n=Q(r.pages.value).length,t=Q(r.projects.value).length;e.push({text:n||t?`${n+t} scope rule${n+t===1?"":"s"}`:"Site-wide",cls:""});const i=s.value.filter(a=>!["divider","header"].includes(a?.type||"item")).length;return e.push({text:`${i} item${i===1?"":"s"} to click`,cls:i?"":"is-danger"}),r.selector.value.trim()&&e.push({text:r.selector.value.trim(),cls:""}),e});let ne=null;function ze(){if(!B())return;me();const e=s.peek();if(!Ke(e))return;const n=Et(_());X=E.contextMenu("#ctx-preview",{cursor:"glyph",items:xt(e),inherit:!1,accent:n.accent,transition:n.transition,density:n.density,radius:n.radius,shadow:n.shadow,opacity:n.opacity})}c(M.effect(()=>{s.value,r.accent.value,r.transition.value,r.density.value,r.radius.value,r.shadow.value,r.opacity.value,clearTimeout(ne),ne=setTimeout(ze,120)})),c(()=>{clearTimeout(ne),me()});const Ge=M.computed(()=>Ke(s.value)?"Right-click here to try the menu":"Add an item to try the menu here"),ae=e=>be(e),k=e=>{s.value=e};function $(e){requestAnimationFrame(()=>b?.querySelector(`.cmx-row[data-index="${e}"]`)?.focus())}function j(e,n){n<0||n>=s.peek().length||(k(Pe(s.peek(),e,n)),$(n))}function q(e,n=s.peek().length){const t=gt(e);k(ht(s.peek(),n,t)),e!=="divider"&&be(n,{isAdd:!0})}function Ve(e){const n=s.peek(),t=n[e];if(!t)return[];const i=t.type||"item";return[{header:i==="divider"?"Divider":t.text||"(no text)"},{label:i==="divider"?"Change\u2026":"Edit\u2026",icon:"edit",action:()=>ae(e)},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:e===0,action:()=>j(e,e-1)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:e===n.length-1,action:()=>j(e,e+1)},{label:"Duplicate",icon:"copy",action:()=>{k(ft(s.peek(),e)),$(e+1)}},{label:"Add below",icon:"plus",submenu:[{label:"Item",action:()=>q("item",e+1)},{label:"Heading",action:()=>q("header",e+1)},{label:"Divider",action:()=>q("divider",e+1)}]},{type:"divider"},{label:"Remove",icon:"trash",danger:!0,action:()=>{k(Y(s.peek(),e)),$(Math.max(0,e-1))}}]}let v=null;function be(e,{isAdd:n=!1}={}){const t=s.peek()[e];if(!t)return;if(v)try{v.so.close(!0)}catch{}const i=bt(t),a={};for(const[o,d]of Object.entries(i))a[o]=M.observable(d);const J=()=>Object.fromEntries(Object.keys(a).map(o=>[o,a[o].peek()])),Ze=JSON.stringify(i),se=()=>JSON.stringify(J())!==Ze,Ae=!ut.includes(i.type),ie=Array.isArray(t.submenu)&&t.submenu.length,Te={...a,locked:Ae,notLocked:!Ae,hasSub:ie,subText:ie?`Opens a submenu of ${t.submenu.length} - kept as it is (edit it in the file).`:"",isItem:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"),hasText:M.computed(()=>a.type.value!=="divider"),showAction:M.computed(()=>a.type.value!=="divider"&&a.type.value!=="header"&&!ie),argLabel:M.computed(()=>H[a.action.value]?.label||""),note:M.computed(()=>pt[a.action.value]||mt[a.action.value]||""),problem:M.computed(()=>{const o=Fe(Object.fromEntries(Object.keys(a).map(d=>[d,a[d].value])),t);return o.type==="divider"?"":String(o.text||"").trim()?H[o.action?.type]?.required&&!String(o.action?.[H[o.action.type].key]||"").trim()?`Needs a ${H[o.action.type].label.toLowerCase()}.`:"":"Needs text."})},g=E.slideover({title:n?"New item":i.type==="divider"?"Divider":`Edit ${C(i.text||"item")}`,size:"sm",position:"right"}),et=dt.map(o=>`<optgroup label="${C(o.label)}">${Object.entries(o.actions).map(([d,W])=>`<option value="${C(d)}">${C(W)}</option>`).join("")}</optgroup>`).join(""),T=Ct(`
|
|
13
14
|
<div class="cmx-form">
|
|
14
15
|
<div data-bind-hidden="locked">
|
|
15
16
|
<label class="form-label" for="cmx-i-type">Type
|
|
@@ -31,7 +32,7 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
|
|
|
31
32
|
<div data-bind-hidden="!showAction.value">
|
|
32
33
|
<label class="form-label" for="cmx-i-action">Does
|
|
33
34
|
<span data-help-title="What it does" data-help="A fixed set of actions, on purpose: nothing typed here runs as script on your pages. Some grey themselves out when they do not apply to the click - the note under the choice says when."></span></label>
|
|
34
|
-
<select id="cmx-i-action" class="form-select" data-model="action.value">${
|
|
35
|
+
<select id="cmx-i-action" class="form-select" data-model="action.value">${et}</select>
|
|
35
36
|
<small class="cmx-hint" data-bind-hidden="!note.value" data-bind-text="note.value"></small>
|
|
36
37
|
</div>
|
|
37
38
|
<div data-bind-hidden="!argLabel.value || !showAction.value">
|
|
@@ -51,4 +52,4 @@ import{api as de}from"../api.js";import{addBannerAction as je}from"../lib/plugin
|
|
|
51
52
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
52
53
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="done"><span data-icon="check" data-icon-size="13"></span> Done</button>
|
|
53
54
|
</div>
|
|
54
|
-
</div>`),
|
|
55
|
+
</div>`),N=rt("edit",i.icon);N.input.id="cmx-i-icon",N.input.classList.add("form-input");const Ie=()=>{a.icon.value=N.input.value};N.input.addEventListener("input",Ie),T.querySelector(".cmx-icon-mount").appendChild(N.el);const re=T.querySelector(".cmx-arg-mount");let f=null;const ce=()=>{a.arg.value=f.value};function Le(){f?.removeEventListener("input",ce),re.replaceChildren();const o=H[a.action.peek()];if(!o){f=null;return}if(o.key==="url"){const d=ct(o.placeholder,a.arg.peek());f=d.input,re.appendChild(d.el)}else f=document.createElement("input"),f.type="text",f.className="form-input",f.placeholder=o.placeholder,f.value=a.arg.peek(),re.appendChild(f);f.id="cmx-i-arg",f.addEventListener("input",ce)}let Ce=a.action.peek();const tt=M.effect(()=>{const o=a.action.value;o!==Ce&&(Ce=o,a.arg.value="",Le())});Le();const De=()=>{const o=Fe(J(),t),d=[...s.peek()],W=d.indexOf(t);W!==-1&&(d[W]=o,k(d))};let K=!1,le=!1;const Oe={done:()=>{De(),K=!0,g.close(),$(e)},cancel:()=>g.close(),remove:()=>{const o=s.peek().indexOf(t);o!==-1&&k(Y(s.peek(),o)),K=!0,g.close()}};g.element.appendChild(T);const nt=qe(T),at=M.applyBindings(Te,T,{methods:Oe}),ot=o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),Oe.done())};T.addEventListener("keydown",ot);const $e={so:g,dirty:se,apply:De};v=$e,it(g,()=>{tt(),at.dispose(),nt(),N.input.removeEventListener("input",Ie),f?.removeEventListener("input",ce),v===$e&&(v=null)});const st=g.close.bind(g);g.close=async o=>{if(!K&&o!==!0&&se()){if(le)return;le=!0;const d=await E.confirm("Discard the changes to this item?");if(le=!1,!d)return}if(!K&&n&&o!==!0&&!se()){const d=s.peek().indexOf(t);d!==-1&&k(Y(s.peek(),d))}return K=!0,st()},I.scan(T),g.open(),requestAnimationFrame(()=>T.querySelector(i.type==="divider"?"#cmx-i-type":"#cmx-i-text")?.focus())}c(()=>{if(v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}});async function oe(){if(G.peek())return;V.value=!0,P.value=[];const e=O.peek();if(!e.ok){const t=Object.keys(e.errors).length;E.toast(`Not saved yet - ${t===1?"one thing needs":`${t} things need`} fixing (marked in red).`,{type:"warning"});const i=he.peek(),a=i.binding?0:i.items?1:i.appearance?2:-1;a>=0&&Ye?.activate?.(a);return}G.value=!0;const n=kt(_(),s.peek());try{if(h)await de.contextMenus.create(n);else{const t=await de.contextMenus.update(p,n);t?.items&&(s.value=t.items)}if(!B())return;U.value=ue(_(),s.peek());try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:"#/context-menus"}})),h?(E.toast("Context menu created.",{type:"success"}),location.hash="#/context-menus"):(E.toast("Context menu saved.",{type:"success"}),pe(A,r.name.peek()||p))}catch(t){P.value=String(t.message||t).split("; ").map((i,a)=>({key:String(a),text:i})),m.querySelector(".cmx-alert")?.scrollIntoView({behavior:"smooth",block:"nearest"})}finally{G.value=!1}}function Je(){E.confirm("Throw away your unsaved changes to this menu?").then(e=>{if(!e||!B())return;const n=Re(x);for(const[t,i]of Object.entries(n))r[t].value=i;s.value=Array.isArray(x?.items)?x.items:[],P.value=[]})}m.querySelector("#ctx-items").innerHTML=Tt,m.querySelector(".cmx-chips").innerHTML=It,m.querySelector(".cmx-server-errors").innerHTML=Lt;const ye=m.querySelector("#ctx-accent");if(D.accent&&!lt.includes(D.accent)&&ye){const e=document.createElement("option");e.value=D.accent,e.textContent=`Custom (${D.accent})`,ye.appendChild(e)}const We={...r,items:s,rows:te,chips:Ue,dirty:z,saving:G,err:_e,tabBad:he,serverErrors:P,isNew:h,notNew:!h,previewText:Ge,serverError:M.computed(()=>P.value.length>0),saveText:M.computed(()=>h?"Create menu":"Save"),problemText:M.computed(()=>V.value&&!O.value.ok?" - some need fixing before they can be saved":""),scopeText:M.computed(()=>{const e=Q(r.pages.value),n=Q(r.projects.value);return!e.length&&!n.length?"Site-wide: every page carries this menu.":`Only ${[e.length&&`pages matching ${e.join(", ")}`,n.length&&`pages in ${n.join(", ")}`].filter(Boolean).join(", or ")}.`})},Ye=E.tabs(m.querySelector("#ctx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(We,m,{methods:{save:()=>oe(),discard:()=>Je(),addItem:()=>q("item"),addHeader:()=>q("header"),addDivider:()=>q("divider")}});c(()=>Qe.dispose()),c(qe(m,{position:"bottom"}));const ge=m.querySelector("#ctx-slug"),xe=()=>{ee=!0};ge?.addEventListener("input",xe),c(()=>ge?.removeEventListener("input",xe));const Xe=E.contextMenu(b,{cursor:"glyph",match:".cmx-row",inherit:!1,density:"compact",items:e=>Ve(Number(e.dataset.index))});c(()=>Xe?.destroy?.());const ke=e=>{const n=e.target.closest(".cmx-row");if(!n)return;const t=Number(n.dataset.index);if(e.target.closest(".cmx-kebab")){Ne(e.target.closest(".cmx-kebab"));return}if(e.target.closest(".cmx-up")){j(t,t-1);return}if(e.target.closest(".cmx-down")){j(t,t+1);return}ae(t)},we=e=>{const n=e.target.closest(".cmx-row");if(!n||e.target!==n)return;const t=Number(n.dataset.index);e.key==="F10"&&e.shiftKey?(e.preventDefault(),Ne(n)):e.key==="Enter"?(e.preventDefault(),ae(t)):e.altKey&&e.key==="ArrowUp"?(e.preventDefault(),j(t,t-1)):e.altKey&&e.key==="ArrowDown"?(e.preventDefault(),j(t,t+1)):e.key==="Delete"&&(e.preventDefault(),k(Y(s.peek(),t)),$(Math.max(0,t-1)))};if(b.addEventListener("click",ke),b.addEventListener("keydown",we),c(()=>{b.removeEventListener("click",ke),b.removeEventListener("keydown",we)}),typeof E.sortable=="function"){const e=Mt(),n=E.sortable(b,{items:".cmx-row",key:null,keyboard:!1,onStart:e.start,onEnd:({item:t})=>e.restore(t),onSort:({item:t,from:i,to:a})=>{e.restore(t),k(Pe(s.peek(),i,a)),$(a)}});c(()=>n.destroy())}h?pe(A,"New menu"):pe(A,x?.name||p),je(l,{id:"ctx-editor-back",icon:"arrow-left",title:"All context menus",onClick:()=>{location.hash="#/context-menus"}}),je(l,{id:"ctx-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>oe()});const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||v||(e.preventDefault(),e.stopImmediatePropagation(),oe())};document.addEventListener("keydown",Ee,!0),c(()=>document.removeEventListener("keydown",Ee,!0));const Se=()=>{if(z.peek())try{S.set(F(p),{form:_(),items:s.peek(),base:U.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{v?.dirty()&&v.apply(),z.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),c(()=>window.removeEventListener("beforeunload",Me)),w={path:Be,dirty:()=>z.peek()||!!v?.dirty(),dispose(){if(L=!0,v){const e=v;e.dirty()&&e.apply();try{e.so.close(!0)}catch{}}Se(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let y=null;try{y=S.get(F(p))}catch{}if(y?.form){try{S.remove?S.remove(F(p)):S.set(F(p),null)}catch{}const e=y.base&&y.base!==U.peek(),n=y.at?` (${St(y.at)})`:"",t=h?"the new context menu you started":`"${C(x?.name||p)}"`,i=e?`You left unsaved changes to ${t}${n}, but it has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${n}. Restore them?`;if(await E.confirm(i)&&B()){for(const[a,J]of Object.entries(y.form))r[a]&&a!==(h?"":"slug")&&(r[a].value=J);h&&y.form.slug&&(ee=!0),Array.isArray(y.items)&&(s.value=y.items)}}}};
|