domma-cms 0.97.0 → 0.99.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/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -1
- package/admin/js/lib/collection-editor-arrange.js +2 -2
- package/admin/js/lib/menu-drag.js +1 -1
- package/admin/js/templates/collection-editor.html +7 -3
- package/admin/js/templates/context-menu-editor.html +3 -0
- package/admin/js/templates/docs/api-forms.html +5 -0
- package/admin/js/templates/docs/tutorial-forms.html +3 -0
- package/admin/js/templates/docs/usage-collections.html +4 -1
- package/admin/js/templates/form-editor.html +4 -0
- package/admin/js/views/collection-editor.js +22 -6
- package/admin/js/views/collection-entries.js +5 -5
- package/admin/js/views/context-menu-editor.js +5 -4
- package/admin/js/views/form-editor.js +6 -6
- package/package.json +2 -2
- package/public/js/form-logic-engine.js +1 -1
- package/server/templates/page.html +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 Ie}from"../lib/help-popover.js";import{onceClosed as st,openMenuAt as Re}from"../lib/tool-kit.js";import{ADDON_TYPES as it,FIELD_TYPES as rt,OPERATIONS as ct,OPTION_TYPES as dt,accessOptions as pt,checkForm as ut,draftOf as ft,duplicateField as mt,errorCount as q,fieldFromPanel as Te,fieldProblem as ht,fieldRow as vt,formFromCollection as Fe,formFromDraft as bt,moveField as De,nameFromLabel as gt,newField as yt,panelForm as xt,payloadFromForm as wt,roleLadder as kt,slugify as X,snapshot as _,storageChange as Ne}from"../lib/collection-editor-arrange.js";import{fmtDateTime as Et}from"../lib/dates.js";import{rowPlaceKeeper as St}from"../lib/menu-drag.js";import{openIconPicker as Pe}from"../lib/card-builder.js";const Mt=/^\/collections\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,$t=s=>`collection-editor.draft.${s||"new"}`,Ct=`
|
|
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 Ot(s){const p=document.createElement("template");return p.innerHTML=s.trim(),p.content.firstElementChild}function Be(s,p){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 x=document.createElement("span");x.className="dm-plugin-banner-sep",x.setAttribute("aria-hidden","true"),x.textContent="\u203A",i.replaceChildren(C,x,document.createTextNode(p))}let y=null,Le=!1;const ee=s=>String(s||"").split("?")[0];function jt(){Le||!window.R?.use||(Le=!0,R.use((s,p,i)=>{if(!(y&&Mt.test(p?.path||"")&&ee(s?.path)!==ee(p?.path))||!y.dirty())return i();const x=`#${p.path}`;return E.confirm("This collection has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?i():history.replaceState(null,"",x)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(y&&ee(s?.path)!==y.path){const p=y;y=null,p.dispose()}}))}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(s){if(jt(),y){const e=y;y=null,e.dispose()}const p=s.get(0),i=p.querySelector(".cex")||p,C=ee(location.hash.replace(/^#/,""))||"/collections/new",x=C.match(/^\/collections\/edit\/([^/?#]+)/),d=x?decodeURIComponent(x[1]):null,D=!d,pe=[],v=e=>(pe.push(e),e);let ue=!1;const te=()=>!ue&&i.isConnected,fe=E.loader(p,{type:"dots"});let O=null,ne=[],N=null,me=[],u=0,P=[];try{[O,ne,N,me,P]=await Promise.all([d?g.collections.get(d):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(kt).catch(()=>[])]),d&&(u=(await g.collections.listEntries(d,{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(d&&!O){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const he=new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",m=Fe(O,{project:he,defaultRole:P.at(-1)?.name||""}),w=Object.fromEntries(m.fields.map(e=>[e._key,e.name])),qe=O?.meta||{};let K=m.storageAdapter;const _e=new Set(["public","token",...P.map(e=>e.name)]),ve=(e,t)=>{if(!P.length)return"";const o=[...new Set(e.filter(l=>l&&!_e.has(l)))];return o.length?`Not a role on this site: ${o.join(", ")}. Until you pick another, ${t}.`:""},n={};for(const[e,t]of Object.entries(m))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])),F=M.observable(_(m)),B=M.computed(()=>_(be())!==F.value);let W=!D;v(M.effect(()=>{const e=n.title.value;W||(n.slug.value=X(e))}));const h=M.computed(()=>ut(be(),{isNew:k.value,taken:me,original:w})),ge=M.observable([]);v(M.effect(()=>{const e=h.value.fields||{},t=n.layout.value==="grid";ge.value=n.fields.value.map(o=>vt(o,e,{grid:t}))}));const Ke=M.computed(()=>new Set(n.fields.value.map(e=>e.name))),Ue={...n,isNew:k,saving:U,dirty:B,rows:ge,errTitle:M.computed(()=>h.value.title||""),errSlug:M.computed(()=>h.value.slug||""),errColumns:M.computed(()=>h.value.columns||""),errExport:M.computed(()=>h.value.export||""),problemCount:M.computed(()=>q(h.value)),problemText:M.computed(()=>{const e=q(h.value);return e?`${e} thing${e===1?"":"s"} to fix before it can be saved`:""}),settingsBad:M.computed(()=>!!(h.value.title||h.value.slug||h.value.columns)),fieldsBad:M.computed(()=>!!h.value.fields),exportBad:M.computed(()=>!!h.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&&!Ke.value.has(t));return e.length?`Not a field of this collection: ${e.join(", ")}`:""}),missingApiRoles:M.computed(()=>ve(ct.filter(e=>n[`${e}Enabled`].value).map(e=>n[`${e}Access`].value),"only the level-0 role passes")),missingExportRole:M.computed(()=>ve(n.exportEnabled.value?[n.exportAccess.value]:[],"only the level-0 role is offered export")),showStorage:M.observable(!!N?.pro&&d!=="roles"),storageNote:M.computed(()=>{const e=Ne(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(m.project&&!ne.some(e=>e.slug===m.project)){const e=document.createElement("option");e.value=m.project,e.textContent=`${m.project} (not found)`,ye.appendChild(e)}const He=[...i.querySelectorAll('.cex-op select[data-model$="Access.value"]')],xe=e=>{for(const t of He){const o=t.getAttribute("data-model").split(".")[0],l=e[o];t.replaceChildren(...pt(P,l,{token:t.id!=="export-access"}).map(r=>new Option(r.label,r.value))),t.value=l}};xe(m);const We=i.querySelector("#storage-connection"),Ye=Array.isArray(N?.connections)&&N.connections.length?N.connections:["default"];for(const e of new Set([...Ye,m.storageConnection])){const t=document.createElement("option");t.value=e,t.textContent=e,We.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(De(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&&w[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(l=>l._key!==e))}function G(e,{isAdded:t=!1}={}){const o=Y(e);if(o<0)return;const l=n.fields.peek()[o],r=xt(l),c={};for(const[a,$]of Object.entries(r))c[a]=M.observable($);const f=JSON.stringify(r),V=()=>Object.fromEntries(Object.keys(c).map(a=>[a,c[a].peek()])),$e=()=>JSON.stringify(V())!==f;let Ce=!!l.name;const Ze=M.effect(()=>{const a=c.label.value;Ce||(c.name.value=gt(a))}),et=new Set(n.fields.peek().filter(a=>a._key!==e).map(a=>a.name)),Oe=M.computed(()=>{const a=Te(l,Object.fromEntries(Object.keys(c).map($=>[$,c[$].value])));return a.name&&et.has(a.name)?`"${a.name}" is used by another field.`:ht(a,w[e])}),tt=M.computed(()=>!k.peek()&&u&&w[e]&&c.name.value.trim()!==w[e]?`Entries keep their value under "${w[e]}" - renaming the field does not move it.`:""),nt={...c,problem:Oe,renamed:tt,isOptions:M.computed(()=>dt.has(c.type.value)),canAddon:M.computed(()=>it.has(c.type.value)),isGrid:M.computed(()=>n.layout.value==="grid")},b=E.slideover({title:t?"New field":`Field: ${Z(l.label||l.name||"Untitled")}`,size:"md",position:"right"}),T=Ot(`
|
|
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
|
+
${rt.map(a=>`<option value="${a.value}">${a.label}</option>`).join("")}
|
|
24
24
|
</select>
|
|
25
25
|
</div>
|
|
26
26
|
<div class="cex-setting">
|
|
@@ -42,6 +42,22 @@ import{api as g}from"../api.js";import{addBannerAction as ue}from"../lib/plugin-
|
|
|
42
42
|
<span data-help-title="Options" data-help="One per line, as value: Label - the value is what is stored, the label what is shown. A line with no colon uses the same text for both."></span></label>
|
|
43
43
|
<textarea id="cex-f-options" class="form-input font-mono" rows="5" placeholder="s: Small m: Medium" data-model="options.value"></textarea>
|
|
44
44
|
</div>
|
|
45
|
+
<div class="cex-wide cex-addons" data-bind-hidden="!canAddon.value">
|
|
46
|
+
<span class="form-label">Joined to the input
|
|
47
|
+
<span data-help-title="Icon or text beside the input" data-help="An icon or a short text joined to the start or end of the input in the entry form - a magnifier before a search box, \xA3 before a price, kg after a weight. Pick an icon, or type text; an icon wins if both are set."></span></span>
|
|
48
|
+
<div class="cex-addon-row">
|
|
49
|
+
<span class="cex-addon-side">Before</span>
|
|
50
|
+
<input id="cex-f-prefix-icon" type="text" class="form-input" maxlength="60" placeholder="Icon" aria-label="Icon before the input" data-model="prefixIcon.value">
|
|
51
|
+
<button type="button" class="btn btn-sm btn-ghost" title="Browse icons" aria-label="Browse icons for before the input" data-on-click="pickPrefix"><span data-icon="grid" data-icon-size="14"></span></button>
|
|
52
|
+
<input type="text" class="form-input" maxlength="24" placeholder="or text, e.g. \xA3" aria-label="Text before the input" data-model="prefixText.value">
|
|
53
|
+
</div>
|
|
54
|
+
<div class="cex-addon-row">
|
|
55
|
+
<span class="cex-addon-side">After</span>
|
|
56
|
+
<input id="cex-f-suffix-icon" type="text" class="form-input" maxlength="60" placeholder="Icon" aria-label="Icon after the input" data-model="suffixIcon.value">
|
|
57
|
+
<button type="button" class="btn btn-sm btn-ghost" title="Browse icons" aria-label="Browse icons for after the input" data-on-click="pickSuffix"><span data-icon="grid" data-icon-size="14"></span></button>
|
|
58
|
+
<input type="text" class="form-input" maxlength="24" placeholder="or text, e.g. kg" aria-label="Text after the input" data-model="suffixText.value">
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
45
61
|
<div data-bind-hidden="!isGrid.value">
|
|
46
62
|
<label class="form-label" for="cex-f-span">Column span
|
|
47
63
|
<span data-help="How many of the grid's columns the field takes, in the entry form."></span></label>
|
|
@@ -61,4 +77,4 @@ import{api as g}from"../api.js";import{addBannerAction as ue}from"../lib/plugin-
|
|
|
61
77
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
62
78
|
<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
79
|
</div>
|
|
64
|
-
</div>`);let
|
|
80
|
+
</div>`);let Q=!1,re=!1;const ot=b.close.bind(b);function je(){if(Oe.peek())return;const a=Te(l,V());j(n.fields.peek().map($=>$._key===e?a:$)),Q=!0,b.close()}b.element.appendChild(T);const at=Ie(T),lt=M.applyBindings(nt,T,{methods:{done:je,cancel:()=>b.close(),pickPrefix:()=>Pe(a=>{c.prefixIcon.value=a,c.prefixText.value=""}),pickSuffix:()=>Pe(a=>{c.suffixIcon.value=a,c.suffixText.value=""}),remove:async()=>{Q=!0,b.close(),await oe(e)}}}),Ae=T.querySelector("#cex-f-name");Ae.addEventListener("input",()=>{Ce=!!Ae.value}),T.addEventListener("keydown",a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),je())}),st(b,()=>{Ze(),lt.dispose(),at(),se.delete(b)}),b.close=async a=>{if(!Q&&a!==!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 $=ot();return t&&!$e()&&!l.name&&j(n.fields.peek().filter(ce=>ce._key!==e)),$},se.add(b),I.scan(T),b.open(),T.querySelector("#cex-f-label")?.focus()}function ze(){const e=yt();j([...n.fields.peek(),e]),Ve.activate?.(1),G(e._key,{isAdded:!0})}function Ge(e){const t=Y(e);if(t<0)return[];const o=n.fields.peek()[t],l=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===l-1,action:()=>z(e,1)},{label:"Duplicate",icon:"copy",action:()=>j(mt(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=h.peek();if(q(e)){E.toast(`Not saved - ${q(e)} thing${q(e)===1?"":"s"} to fix first (marked on the tabs).`,{type:"warning"});return}const t=wt(H(),{isNew:k.peek(),meta:qe});U.value=!0;try{if(k.peek()){const l=await g.collections.create(t);F.value=_(H()),ie(),E.toast(`Created "${t.title}".`,{type:"success"}),R.navigate(`/collections/edit/${encodeURIComponent(l.slug)}`);return}const o=Ne(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(d,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(d,t),!te())return;K=t.storage.adapter,o&&(u=(await g.collections.listEntries(d,{limit:1}).catch(()=>null))?.total??u);for(const l of n.fields.peek())w[l._key]=l.name;F.value=_(H()),ie(),Be(p,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 Je(){if(!B.peek()||!await E.confirm("Discard the changes you have not saved?"))return;const e=Fe(O,{project:he});for(const t of e.fields)w[t._key]=t.name;for(const[t,o]of Object.entries(e))n[t].value=o;F.value=_(e),W=!k.peek()}const L=i.querySelector(".cex-list");L.innerHTML=Ct;const Ve=E.tabs(i.querySelector(".cex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings(Ue,i,{methods:{save:()=>ae(),discard:()=>Je(),addField:()=>ze()}});v(()=>Qe.dispose()),v(Ie(i,{position:"bottom"}));const le=i.querySelector("#field-slug"),we=()=>{W=!!le.value};le.addEventListener("input",we),v(()=>le.removeEventListener("input",we));const se=new Set;v(()=>[...se].forEach(e=>{try{e.close(!0)}catch{}}));const Xe=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:e=>Ge(e.dataset.key)});v(()=>Xe?.destroy?.());const ke={click:e=>{const t=e.target.closest(".cex-row");if(!t)return;const o=e.target.closest(".cex-kebab");if(o){Re(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(),Re(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))L.addEventListener(e,t);if(v(()=>{for(const[e,t]of Object.entries(ke))L.removeEventListener(e,t)}),typeof E.sortable=="function"){const e=St(),t=E.sortable(L,{items:".cex-row",key:"data-key",keyboard:!1,onStart:e.start,onEnd:({item:o})=>e.restore(o),onSort:({item:o,from:l,to:r})=>{e.restore(o),j(De(n.fields.peek(),l,r))}});v(()=>t.destroy())}Be(p,O?.title||"New collection"),de(s,{id:"collection-editor-back",icon:"arrow-left",title:"All collections",onClick:()=>R.navigate("/collections")}),d&&de(s,{id:"collection-editor-entries",icon:"list",title:"Open entries",onClick:()=>R.navigate(`/collections/${encodeURIComponent(d)}/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),v(()=>document.removeEventListener("keydown",Ee,!0));const J=$t(d);function ie(){try{S.remove?S.remove(J):S.set(J,null)}catch{}}const Se=()=>{if(B.peek())try{S.set(J,{form:ft(H()),base:F.peek(),at:new Date().toISOString()})}catch{}},Me=e=>{B.peek()&&(Se(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Me),v(()=>window.removeEventListener("beforeunload",Me)),y={path:C,dirty:()=>B.peek(),dispose(){ue=!0,Se(),pe.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let A=null;try{A=S.get(J)}catch{}if(A?.form){ie();const e=A.base&&A.base!==F.peek(),t=A.at?` (${Et(A.at)})`:"",o=d?`changes to "${Z(O?.title||d)}"`:"a new collection",l=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(l)&&te()){const r=bt(A.form,m);D||(r.slug=m.slug);const c=Object.fromEntries(m.fields.filter(f=>f.id).map(f=>[f.id,f.name]));for(const f of r.fields)f.id&&c[f.id]!==void 0&&(w[f._key]=c[f.id]);xe(r);for(const[f,V]of Object.entries(r))n[f]&&(n[f].value=V);D&&(W=!!r.slug&&r.slug!==X(r.title))}}}};
|
|
@@ -4,7 +4,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
4
4
|
${Array.from({length:m},(v,a)=>`<td data-col="${a}" title="{{t${a}}}">{{c${a}}}</td>`).join("")}
|
|
5
5
|
<td class="cex-when" title="{{whenFull}}">{{when}}</td>
|
|
6
6
|
<td class="cex-tools"><button type="button" class="cex-run" data-bind-hidden="!runs" title="Run one of this collection's actions">\u26A1 {{runs}}</button><button type="button" class="cex-kebab" aria-label="Actions for this entry" title="Actions">\u22EE</button></td>
|
|
7
|
-
</tr>`}function wt(m,h){const v={};return m.forEach(a=>{const
|
|
7
|
+
</tr>`}function wt(m,h){const v={};return m.forEach(a=>{const r={};a.fullWidth?r.span=h:a.span>1&&(r.span=a.span),a.placeholder&&(r.placeholder=a.placeholder),a.helper&&(r.hint=a.helper),a.prefix&&(r.prefix=a.prefix),a.suffix&&(r.suffix=a.suffix);const d=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;v[a.name]={type:d,label:a.label,required:a.required,options:a.options,...Object.keys(r).length?{formConfig:r}:{}}}),v}function ce(m,h){const v={};for(const a of h){const r=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!r.length)continue;const d=r[0];d.type==="radio"?v[a.name]=r.find(p=>p.checked)?.value??"":d.type==="checkbox"&&r.length>1?v[a.name]=r.filter(p=>p.checked).map(p=>p.value):d.type==="checkbox"?v[a.name]=d.checked:d.tagName==="SELECT"&&d.multiple?v[a.name]=[...d.selectedOptions].map(p=>p.value):v[a.name]=gt(a,d.value)}return v}function Fe(m,h,v){for(const a of h){if(!(a.name in v))continue;const r=v[a.name],d=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!d.length)continue;const p=d[0];p.type==="radio"?d.forEach(c=>{c.checked=c.value===String(r)}):p.type==="checkbox"&&d.length>1?d.forEach(c=>{c.checked=Array.isArray(r)&&r.includes(c.value)}):p.type==="checkbox"?p.checked=r===!0:p.tagName==="SELECT"&&p.multiple?[...p.options].forEach(c=>{c.selected=Array.isArray(r)&&r.includes(c.value)}):p.value=r??"",d.forEach(c=>c.dispatchEvent(new Event("input",{bubbles:!0})))}}const Pe=dt(Be);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(m){Pe.reset();const h=m.get(0).querySelector(".cex")||m.get(0),v=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=v?decodeURIComponent(v[1]):null,r=()=>Be.test(location.hash)&&h.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),d=e=>(Pe.add(e),e);if(!a){E.toast("No collection selected.",{type:"error"});return}const p=M.observable(null),c=M.observable([]),x=M.observable(0),Y=M.observable(!0),_=M.observable(""),_e=M.computed(()=>p.value!==null),le=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),y=M.observable([]),q=M.observable(X),J=M.observable([]),Q=M.observable({}),A=M.observable(!1);let ee=1;const ie=()=>p.peek()?.fields||[],re=M.computed(()=>(p.value?.fields||[]).slice(0,ut)),V=e=>c.peek().find(t=>t.id===e),ye=e=>J.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),T=M.observable([]),ve=M.observable([]);d(M.effect(()=>{const e=pt(c.value,{filters:w.value,query:le.value,sort:Z.value});T.value=e})),d(M.effect(()=>{const e=re.value,t=new Set(y.value);Q.value,J.value,ve.value=T.value.slice(0,q.value).map(n=>{const s={id:n.id,sel:t.has(n.id),rowCls:t.has(n.id)?"is-selected":""};e.forEach((i,u)=>{const f=he(i,n.data?.[i.name]);s[`c${u}`]=Ue(f),s[`t${u}`]=je(f)});const o=n.meta?.createdAt;s.when=o?Et(o):"",s.whenFull=o?`Created ${B(o)}${n.meta?.updatedAt?` \xB7 changed ${B(n.meta.updatedAt)}`:""}`:"";const l=ye(n.id).length;return s.runs=l?String(l):"",s})}));const Je=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:vt(e)}))),Ve=M.computed(()=>y.value.length),He={schema:p,loaded:c,loading:Y,loadError:_,ready:_e,query:le,rows:ve,chips:Je,busy:A,selectedCount:Ve,countLine:M.computed(()=>mt(T.value.length,c.value.length,x.value)),selectedText:M.computed(()=>`${y.value.length} selected`),canClear:M.computed(()=>ft(a)),introText:M.computed(()=>{const e=p.value;return e?`${e.description?`${e.description} - `:""}${(e.fields||[]).length} field${(e.fields||[]).length===1?"":"s"}${e.storage?.adapter==="mongodb"?", on MongoDB":""}.`:""}),emptyText:M.computed(()=>c.value.length?"No entries match the search or filters.":"No entries yet - Add entry in the banner makes one, or Import brings many."),moreText:M.computed(()=>{const e=T.value.length-q.value;if(e>0)return`Show ${Math.min(X,e)} more (${e} not shown)`;const t=x.value-c.value.length;return t>0?`Load the next ${Math.min(se,t)} from the server`:""})};async function H({reset:e=!0}={}){Y.value=!0;try{const t=e?Math.max(se,c.peek().length):se;e&&(ee=1);const n=await g.collections.listEntries(a,{limit:t,page:ee});if(!r())return;const s=n?.entries||(Array.isArray(n)?n:[]);c.value=e?s:[...c.peek(),...s.filter(l=>!V(l.id))],x.value=Number(n?.total)||c.peek().length,e&&(ee=Math.max(1,Math.ceil(c.peek().length/se)));const o=new Set(c.peek().map(l=>l.id));y.value=y.peek().filter(l=>o.has(l)),_.value="",await We()}catch(t){_.value=`Could not load the entries: ${t.message||t}`}finally{Y.value=!1}}async function Ke(){ee+=1,await H({reset:!1})}async function Ge(){try{if((await it("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await g.actions.forCollection(a);J.value=Array.isArray(t)?t:[]}catch{J.value=[]}}async function We(){const e=J.peek().filter(s=>s.access?.rowLevel);if(!e.length||!c.peek().length)return;const t=c.peek().map(s=>s.id),n={...Q.peek()};await Promise.all(e.map(async s=>{try{n[s.slug]=new Set((await g.actions.checkAccess(s.slug,t)).allowed)}catch{n[s.slug]=null}})),Q.value=n}const K=e=>{const t=ie()[0];return(t?Ue(he(t,e?.data?.[t.name]),40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function be(e){if(await E.confirm(`Delete \u201C${D(K(e))}\u201D? This cannot be undone.`))try{await g.collections.deleteEntry(a,e.id),c.value=c.peek().filter(t=>t.id!==e.id),x.value=Math.max(0,x.peek()-1),y.value=y.peek().filter(t=>t!==e.id),j("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function Xe(){const e=[...y.peek()];if(!e.length||!await E.confirm(`Delete ${e.length} entr${e.length===1?"y":"ies"}? This cannot be undone.`))return;A.value=!0;let t=0;const n=[];for(const s of e)try{await g.collections.deleteEntry(a,s),t++}catch(o){n.push(o.message||String(o))}A.value=!1,y.value=[],j("#/collections"),E.toast(n.length?`Deleted ${t}; ${n.length} refused (${n[0]}).`:`Deleted ${t} entr${t===1?"y":"ies"}.`,{type:n.length?"warning":"success",duration:n.length?7e3:4e3}),r()&&await H()}async function Ye(){const e=p.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${D(e?.title||a)}\u201D? This cannot be undone.`)){A.value=!0;try{await g.collections.clearEntries(a),c.value=[],x.value=0,y.value=[],j("#/collections"),E.toast("All entries deleted.",{type:"success"})}catch(t){E.toast(`Not cleared: ${t.message||t}`,{type:"error"})}finally{A.value=!1}}}async function Ze(e){try{const t=await g.collections.createEntry(a,{...e.data||{}});c.value=[t,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Duplicated - the copy is at the top.",{type:"success"})}catch(t){E.toast(`Not duplicated: ${t.message||t}`,{type:"error"})}}async function Qe(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(D(n)))){A.value=!0;try{const s=await g.actions.execute(e.slug,t.id);if(s?.success){const o=s.stepsCompleted??0;E.toast(`${e.trigger?.label||e.title||"Action"} done (${o} step${o===1?"":"s"}).`,{type:"success"}),r()&&await H()}else E.toast(s?.results?.find(o=>!o.success)?.error||"The action failed.",{type:"error"})}catch(s){E.toast(s.message||"The action failed.",{type:"error"})}finally{A.value=!1}}}async function ge(e){try{await navigator.clipboard.writeText(e),E.toast("Copied.",{type:"success",duration:1500})}catch{E.toast("Copy failed - your browser refused the clipboard.",{type:"error"})}}function de(e){const t=y.peek();y.value=t.includes(e)?t.filter(n=>n!==e):[...t,e]}function Ee(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],q.value=X}const ue=new Set;let N=null,ke=!1;function xe(e,t,n){const s=e.close.bind(e);let o=!1,l=!1;e.close=async i=>{if(!o&&i!==!0&&!ke&&t()){if(l)return;l=!0;const u=await E.confirm(n);if(l=!1,!u)return}return o=!0,s()}}function te(e,t=()=>{}){ue.add(e),rt(e,()=>{ue.delete(e),t()})}function O(e,t=null){const n=p.peek(),s=ie(),o=!!e,l=E.slideover({title:o?`Edit ${D(K(e))}`:"New entry",size:"md",position:"right"}),i=oe(`
|
|
8
8
|
<div class="cex-form">
|
|
9
9
|
<div class="cex-fields"></div>
|
|
10
10
|
<small class="cex-hint cex-when-line"></small>
|
|
@@ -15,7 +15,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
15
15
|
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
16
16
|
<button type="button" class="btn btn-sm btn-primary" data-act="save"><span data-icon="check" data-icon-size="13"></span> <span class="cex-save-text"></span></button>
|
|
17
17
|
</div>
|
|
18
|
-
</div>`);i.querySelector(".cex-save-text").textContent=o?"Save":"Add entry";const
|
|
18
|
+
</div>`);i.querySelector(".cex-save-text").textContent=o?"Save":"Add entry";const u=i.querySelector(".cex-fields");F.create(wt(s,n?.columns||2),e?.data||{},{layout:n?.layout||"stacked",columns:n?.columns||2,showSubmitButton:!1}).renderTo(u),s.forEach(k=>{if(k.type!=="date"||!k.name)return;const U=u.querySelector(`[name="${CSS.escape(k.name)}"]`);U&&U.type!=="date"&&(U.type="date")}),e?.data&&Fe(u,s,e.data),o&&e.meta?.createdAt&&(i.querySelector(".cex-when-line").textContent=`Created ${B(e.meta.createdAt)}${e.meta.updatedAt?`, last changed ${B(e.meta.updatedAt)}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),l.element.appendChild(i);const f=ze(ce(u,s));t&&Fe(u,s,t);const b=()=>ze(ce(u,s))!==f,lt=i.querySelector(".cex-dirty"),fe=()=>{lt.hidden=!b()};u.addEventListener("input",fe),u.addEventListener("change",fe),fe();let me=!1;async function Ne(){if(me)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const k=ce(u,s);me=!0;const U=i.querySelector('[data-act="save"]');U.disabled=!0;try{if(o){const W=await g.collections.updateEntry(a,e.id,k);c.value=c.peek().map(De=>De.id===e.id?W:De),E.toast("Entry saved.",{type:"success"})}else{const W=await g.collections.createEntry(a,k);c.value=[W,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}l.close(!0)}catch(W){E.toast(W.message||"Not saved.",{type:"error"})}finally{me=!1,U.disabled=!1}}i.querySelector('[data-act="save"]').addEventListener("click",Ne),i.querySelector('[data-act="cancel"]').addEventListener("click",()=>l.close()),i.addEventListener("keydown",k=>{(k.ctrlKey||k.metaKey)&&(k.key==="Enter"||k.key==="s")&&(k.preventDefault(),Ne())});const Ie={id:e?.id??null,dirty:b,values:()=>ht(ce(u,s),s)};N=Ie,te(l,()=>{N===Ie&&(N=null)}),xe(l,b,"Discard the changes you have not saved?"),I.scan(i),l.open(),u.querySelector("input, select, textarea")?.focus()}function et(e){const t=E.slideover({title:D(K(e)),size:"md",position:"right"}),n=oe(`
|
|
19
19
|
<div class="cex-form">
|
|
20
20
|
<dl class="cex-detail"></dl>
|
|
21
21
|
<div class="cex-meta"></div>
|
|
@@ -24,7 +24,7 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
24
24
|
<span class="cex-spacer"></span>
|
|
25
25
|
<button type="button" class="btn btn-sm btn-primary" data-act="edit"><span data-icon="edit" data-icon-size="13"></span> Edit</button>
|
|
26
26
|
</div>
|
|
27
|
-
</div>`),s=n.querySelector(".cex-detail");for(const i of ie()){const
|
|
27
|
+
</div>`),s=n.querySelector(".cex-detail");for(const i of ie()){const u=je(he(i,e.data?.[i.name])),f=document.createElement("dt");f.textContent=i.label||i.name;const b=document.createElement("dd");b.textContent=u===""?"-":u,s.append(f,b)}const o=n.querySelector(".cex-meta"),l=i=>{const u=document.createElement("span");u.textContent=i,o.appendChild(u)};l(`Id: ${e.id}`),e.meta?.createdAt&&l(`Created: ${B(e.meta.createdAt)}`),e.meta?.updatedAt&&l(`Changed: ${B(e.meta.updatedAt)}`),e.meta?.source&&l(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>ge(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),O(e)}),t.element.appendChild(n),te(t),I.scan(n),t.open()}function tt(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=oe(`
|
|
28
28
|
<div class="cex-form">
|
|
29
29
|
<p class="cex-hint">A JSON array of entries, each with a "data" object of field values - what Export gives you.
|
|
30
30
|
<span data-help="Each entry is added as new, with a new id and today's date (the Data Transfer plugin from the Marketplace keeps ids and dates). Entries are checked like any other save - one that fails, or points a reference at an entry that does not exist, is skipped and reported. Values are stored as given, including fields the collection does not have."></span></p>
|
|
@@ -40,11 +40,11 @@ import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}fro
|
|
|
40
40
|
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
41
41
|
<button type="button" class="btn btn-sm btn-primary" data-act="import"><span data-icon="upload" data-icon-size="13"></span> Import</button>
|
|
42
42
|
</div>
|
|
43
|
-
</div>`),n=t.querySelector("textarea"),s=t.querySelector(".cex-err"),o=t.querySelector(".cex-ok"),l=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>l.click()),l.addEventListener("change",async()=>{const
|
|
43
|
+
</div>`),n=t.querySelector("textarea"),s=t.querySelector(".cex-err"),o=t.querySelector(".cex-ok"),l=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>l.click()),l.addEventListener("change",async()=>{const u=l.files?.[0];l.value="",u&&(n.value=await u.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let i=!1;t.querySelector('[data-act="import"]').addEventListener("click",async()=>{s.hidden=!0,o.hidden=!0;let u;try{if(u=JSON.parse(n.value),!Array.isArray(u))throw new Error("it must be a JSON array")}catch(b){s.textContent=`Not valid: ${b.message}`,s.hidden=!1;return}const f=t.querySelector('[data-act="import"]');f.disabled=!0;try{const b=await g.collections.import(a,u);o.textContent=`Imported ${b.imported}; skipped ${b.skipped}.`,o.hidden=!1,i=!0,j("#/collections"),r()&&await H(),setTimeout(()=>e.close(!0),1200)}catch(b){s.textContent=b.message||"Import failed.",s.hidden=!1}finally{f.disabled=!1}}),e.element.appendChild(t),te(e),xe(e,()=>!i&&n.value.trim()!=="","Discard the entries you pasted?"),Oe(t),I.scan(t),e.open(),n.focus()}function nt(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=oe(`
|
|
44
44
|
<div class="cex-form">
|
|
45
45
|
<p class="cex-hint">Every entry in the collection - not only the ones shown.</p>
|
|
46
46
|
<div class="cex-export">
|
|
47
47
|
<button type="button" class="btn btn-ghost" data-fmt="json"><span data-icon="download" data-icon-size="14"></span> Export as JSON</button>
|
|
48
48
|
<button type="button" class="btn btn-ghost" data-fmt="csv"><span data-icon="download" data-icon-size="14"></span> Export as CSV</button>
|
|
49
49
|
</div>
|
|
50
|
-
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const s=n.dataset.fmt;try{const o=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${s}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!o.ok)throw new Error(`${o.status}`);const l=await o.blob(),i=document.createElement("a");i.href=URL.createObjectURL(l),i.download=`${a}-entries.${s}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(o){E.toast(`Export failed (${o.message}).`,{type:"error"})}})),e.element.appendChild(t),te(e),I.scan(t),e.open()}function at(e,t){const n=V(e.dataset.id);if(!n)return[];const s=t?.event?.target?.closest?.("td[data-col]"),o=s?re.peek()[Number(s.dataset.col)]:null,l=ye(n.id),i=y.peek().includes(n.id),
|
|
50
|
+
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const s=n.dataset.fmt;try{const o=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${s}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!o.ok)throw new Error(`${o.status}`);const l=await o.blob(),i=document.createElement("a");i.href=URL.createObjectURL(l),i.download=`${a}-entries.${s}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(o){E.toast(`Export failed (${o.message}).`,{type:"error"})}})),e.element.appendChild(t),te(e),I.scan(t),e.open()}function at(e,t){const n=V(e.dataset.id);if(!n)return[];const s=t?.event?.target?.closest?.("td[data-col]"),o=s?re.peek()[Number(s.dataset.col)]:null,l=ye(n.id),i=y.peek().includes(n.id),u=o?yt(o,n.data?.[o.name]):[];return[{header:K(n)},{label:"Edit\u2026",icon:"edit",action:()=>O(n)},{label:"See all of it\u2026",icon:"eye",action:()=>et(n)},{label:"Duplicate",icon:"copy",action:()=>Ze(n)},{label:"Copy id",icon:"hash",action:()=>ge(n.id)},{label:i?"Deselect":"Select",icon:"check",action:()=>de(n.id)},...l.length?[{type:"divider"},...l.map(f=>({label:`${f.trigger?.label||f.title||f.slug}\u2026`,icon:f.trigger?.icon||"zap",disabled:A.peek(),action:()=>Qe(f,n)}))]:[],...o?[{type:"divider"},{label:`Filter by ${o.label||o.name}`,icon:"filter",submenu:[...u.map(f=>({label:f.label,action:()=>Ee(o,f.op,f.value)})),{label:`${o.label||o.name} contains\u2026`,action:async()=>{const f=await E.prompt(`Show entries whose ${D(o.label||o.name)} contains`,{inputPlaceholder:"text"});f&&String(f).trim()&&Ee(o,"contains",String(f).trim())}}]}]:[],...w.peek().length?[{label:`Clear filters (${w.peek().length})`,icon:"x",action:()=>{w.value=[]}}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>be(n)}]}const L=h.querySelector(".cex-rows"),G=h.querySelector(".cex-head"),pe=h.querySelector(".cex-chips-list");pe.innerHTML=kt;const st=E.loader(h,{type:"dots"});try{p.value=await g.collections.get(a)}catch(e){_.value=`Could not load the collection: ${e.message||e}`}finally{st.destroy()}if(!r())return;if(!p.peek()&&!_.peek()){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const we=re.peek();L.innerHTML=xt(we.length),G.replaceChildren();const ne=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),G.appendChild(n),n},$=document.createElement("input");$.type="checkbox",$.title="Select every entry shown",$.setAttribute("aria-label","Select every entry shown"),ne("cex-check",$);const Se=(e,t)=>{const n=document.createElement("button");return n.type="button",n.dataset.sort=e,n.append(document.createTextNode(t),Object.assign(document.createElement("span"),{className:"cex-sort"})),n};we.forEach(e=>ne("",Se(e.name,e.label||e.name))),ne("cex-when",Se("created","Created")),ne("cex-tools"),d(M.effect(()=>{const e=Z.value;G.querySelectorAll("[data-sort]").forEach(t=>{t.querySelector(".cex-sort").textContent=t.dataset.sort===e.key?e.dir==="asc"?"\u25B2":"\u25BC":"",t.parentElement.setAttribute("aria-sort",t.dataset.sort===e.key?e.dir==="asc"?"ascending":"descending":"none")})})),d(M.effect(()=>{const e=T.value.map(n=>n.id),t=new Set(y.value);$.checked=e.length>0&&e.every(n=>t.has(n)),$.indeterminate=!$.checked&&e.some(n=>t.has(n))}));const Ce=e=>{const t=e.target.closest("[data-sort]");if(!t)return;const n=Z.peek();Z.value={key:t.dataset.sort,dir:n.key===t.dataset.sort&&n.dir==="asc"?"desc":"asc"}};G.addEventListener("click",Ce),$.addEventListener("change",()=>{const e=T.peek().map(t=>t.id);y.value=$.checked?[...new Set([...y.peek(),...e])]:y.peek().filter(t=>!e.includes(t))}),d(()=>G.removeEventListener("click",Ce));const ot=M.applyBindings(He,h,{methods:{clearSelection:()=>{y.value=[]},deleteSelected:()=>Xe(),clearAll:()=>Ye(),clearFilters:()=>{w.value=[]},showMore:()=>{T.peek().length>q.peek()?q.value=q.peek()+X:Ke()}}});d(()=>ot.dispose()),d(Oe(h,{position:"bottom"})),d(M.effect(()=>{le.value,w.value,q.value=X}));const $e=e=>{const t=e.target.closest("button[data-idx]");if(!t)return;const n=Number(t.dataset.idx);w.value=w.peek().filter((s,o)=>o!==n)};pe.addEventListener("click",$e),d(()=>pe.removeEventListener("click",$e));const ct=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>at(e,t)});d(()=>ct?.destroy?.());const Ae=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){Re(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=V(t.dataset.id);n&&O(n)},Me=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&de(t.dataset.id)},Le=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=V(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Re(t)):e.key==="Enter"?(e.preventDefault(),O(n)):e.key===" "?(e.preventDefault(),de(n.id)):e.key==="Delete"&&(e.preventDefault(),be(n)))};L.addEventListener("click",Ae),L.addEventListener("change",Me),L.addEventListener("keydown",Le),d(()=>{L.removeEventListener("click",Ae),L.removeEventListener("change",Me),L.removeEventListener("keydown",Le)});const qe=p.peek(),ae=m.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(ae&&qe?.title){for(;ae.nextSibling;)ae.nextSibling.remove();ae.after(document.createTextNode(qe.title))}z(m,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),z(m,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),bt(a)&&z(m,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),z(m,{id:"entries-export",icon:"download",title:"Export",onClick:()=>nt()}),z(m,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>tt()}),z(m,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>O(null)});const Te=e=>{N?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Te),d(()=>window.removeEventListener("beforeunload",Te)),d(()=>{if(N?.dirty())try{S.set(P(a),{id:N.id,values:N.values(),at:new Date().toISOString()})}catch{}ke=!0,[...ue].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(h),!p.peek()){Y.value=!1;return}if(await Ge(),!r()||(await H(),!r()))return;let C=null;try{C=S.get(P(a))}catch{}if(C?.values){try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}let e=C.id?V(C.id):null;if(C.id&&!e&&(e=await g.collections.getEntry(a,C.id).catch(()=>null)),!r()||C.id&&!e)return;const t=e?`your changes to \u201C${D(K(e))}\u201D`:"a new entry",n=e?.meta?.updatedAt&&C.at&&e.meta.updatedAt>C.at?" It has been changed since - check before saving.":"";await E.confirm(`You left ${t} unsaved.${n} Pick up where you left off?`)&&r()&&O(e,C.values)}}};
|
|
@@ -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)}}}};
|