domma-cms 0.92.1 → 0.94.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/CLAUDE.md +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
package/admin/js/views/media.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as z}from"../api.js";import{addBannerAction as Be}from"../lib/plugin-chrome.js";import{helpPopovers as $e}from"../lib/help-popover.js";import{onceClosed as Oe,openMenuAt as Me,settingsStore as He,sidebarCount as ee,viewLifetime as Ke}from"../lib/tool-kit.js";import{openImageEditor as _e}from"../lib/image-editor.js";import{KIND_LABEL as De,arrangeMedia as je,clashes as Ve,formatBytes as Le,isEditable as te,isPreviewable as Ye,kindCounts as Ge,kindOf as Je,markdownFor as ae,mediaTile as Qe,normalisePrefs as We,safeName as B}from"../lib/media-arrange.js";import{fmtDateTime as Xe}from"../lib/dates.js";const Ae=/^#\/media\/?(\?.*)?$/,ne="#/media",O="media.draft",Se=He("media.prefs",We),Ze=`
|
|
2
2
|
<div class="mdx-tile" data-name="{{name}}" tabindex="0" title="{{name}}">
|
|
3
3
|
<div class="mdx-thumb">
|
|
4
4
|
<img data-bind-hidden="!preview" data-bind-src="thumb" alt="" loading="lazy" decoding="async">
|
|
@@ -7,7 +7,7 @@ import{api as R}from"../api.js";import{addBannerAction as Be}from"../lib/plugin-
|
|
|
7
7
|
<span class="mdx-chip">{{kindLabel}}</span>
|
|
8
8
|
<button type="button" class="mdx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
9
9
|
<div class="mdx-info"><span class="mdx-name">{{name}}</span><span class="mdx-meta">{{meta}}</span></div>
|
|
10
|
-
</div>`,
|
|
10
|
+
</div>`,R=u=>String(u??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function et(u){const c=document.createElement("template");return c.innerHTML=u.trim(),c.content.firstElementChild}const l=Ke(Ae);async function H(u,c){try{await navigator.clipboard.writeText(u),E.toast(`${c} copied.`,{type:"success"})}catch{E.toast("Copy failed - the browser refused the clipboard.",{type:"error"})}}export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(u){l.reset();const c=u.get(0).querySelector(".mdx")||u.get(0),b=()=>Ae.test(location.hash)&&c.isConnected,se=Se.read(),p=M.observable([]),oe=M.observable(!0),K=M.observable(""),y=M.observable(se.kind),_=M.observable(se.sort),ie=M.observable(""),D=M.observable(""),q=M.observable(!1);let j=String(Date.now());l.add(M.effect(()=>Se.write({kind:y.value,sort:_.value})));const U=M.observable([]);l.add(M.effect(()=>{const e=Date.now();U.value=je(p.value,{kind:y.value,query:ie.value,sort:_.value}).map(t=>Qe(t,{now:e,stamp:j}))}));const re=M.computed(()=>Ge(p.value));l.add(M.effect(()=>{y.value!=="all"&&!re.value[y.value]&&p.value.length&&(y.value="all")}));const Ce=M.computed(()=>{const e=p.value,t=U.value.length,s=Le(e.reduce((n,i)=>n+(Number(i.size)||0),0));return t===e.length?`${e.length} file${e.length===1?"":"s"} \xB7 ${s}`:`${t} of ${e.length} \xB7 ${s} in all`}),Te=M.computed(()=>p.value.length?"No files match.":"Nothing uploaded yet - drop files here, or Upload in the banner."),L=e=>p.peek().find(t=>t.name===e);async function A(){try{const e=await z.media.list();if(!b())return;j=String(Date.now()),p.value=Array.isArray(e)?e:[],K.value=""}catch(e){K.value=`Could not load the media: ${e.message||e}`}finally{oe.value=!1}}async function le(e){const t=[...e||[]].filter(o=>o&&o.size!==void 0);if(!t.length)return;if(D.peek()){E.toast("An upload is still running.",{type:"warning"});return}const s=Ve(t.map(o=>o.name),p.peek().map(o=>o.name));if(s.length){const o=s.slice(0,5).map(w=>`"${R(w)}"`).join(", ")+(s.length>5?` and ${s.length-5} more`:"");if(!await E.confirm(`${s.length===1?"A file":`${s.length} files`} with the same name ${s.length===1?"is":"are"} already here: ${o}. Replace ${s.length===1?"it":"them"}?`))return}const n=[];let i=0;try{for(const o of t){D.value=`Uploading ${i+1} of ${t.length} - ${o.name}`;const w=new FormData;w.append("file",o);try{await z.media.upload(w),i++}catch(J){n.push(`${o.name}: ${J.message||"failed"}`)}}}finally{D.value=""}i&&E.toast(`Uploaded ${i} file${i===1?"":"s"}.`,{type:"success"}),n.length&&E.toast(`Not uploaded - ${n.join("; ")}`,{type:"error",duration:9e3}),ee(ne),b()&&await A()}async function V(e){if(!await E.confirm(`Delete "${R(e.name)}"? Pages that show it will show a broken image or link. This cannot be undone.`))return!1;try{return await z.media.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),ee(ne),b()&&await A(),!0}catch(t){return E.toast(`Not deleted: ${t.message||t}`,{type:"error"}),!1}}async function de(e){const t=await _e(e);return t&&b()&&(ee(ne),await A()),t}const Y=e=>window.location.origin+e.url;let h=null,ce=!1;const G=new Set;function k(e,{focusRename:t=!1,draftName:s=null}={}){const n=L(e);if(!n){E.toast(`"${e}" is not here any more.`,{type:"warning"});return}const i=M.observable(s??n.name),o=M.observable(!1),w=M.computed(()=>{const a=i.value.trim();if(!a)return"A file needs a name.";const r=B(a);return r!==n.name&&L(r)?`"${r}" is already here.`:""}),J=M.computed(()=>{const a=i.value.trim(),r=B(a);return a&&r!==a?`Saved as "${r}" - only letters, numbers, dots, hyphens and underscores are kept.`:""}),ge=()=>i.peek().trim()!==n.name,we={newName:i,saving:o,renameErr:w,renameHint:J,cannotRename:M.computed(()=>o.value||!!w.value||B(i.value.trim())===n.name),editable:te(n.name)},m=E.slideover({title:R(n.name),size:"md",position:"right"}),d=et(`
|
|
11
11
|
<div class="mdx-panel">
|
|
12
12
|
<div class="mdx-preview"></div>
|
|
13
13
|
<dl class="mdx-facts"></dl>
|
|
@@ -37,4 +37,4 @@ import{api as R}from"../api.js";import{addBannerAction as Be}from"../lib/plugin-
|
|
|
37
37
|
<a class="btn btn-sm btn-ghost mdx-open" target="_blank" rel="noopener"><span data-icon="external-link" data-icon-size="13"></span> Open</a>
|
|
38
38
|
<button type="button" class="btn btn-sm btn-outline" data-on-click="edit" data-bind-hidden="!editable"><span data-icon="edit" data-icon-size="13"></span> Edit image</button>
|
|
39
39
|
</div>
|
|
40
|
-
</div>`),
|
|
40
|
+
</div>`),Q=d.querySelector(".mdx-preview"),$=Je(n.name);if(Ye(n.name)){const a=document.createElement("img");a.src=`${n.url}?t=${encodeURIComponent(j)}`,a.alt=n.name,Q.appendChild(a)}else if($==="video"||$==="audio"){const a=document.createElement($);a.src=n.url,a.controls=!0,a.preload="metadata",Q.appendChild(a)}else{const a=document.createElement("span");a.setAttribute("data-icon",$==="document"?"file-text":"paperclip"),a.setAttribute("data-icon-size","40"),Q.appendChild(a)}const qe=d.querySelector(".mdx-facts"),P=(a,r)=>{const xe=document.createElement("dt");xe.textContent=a;const Z=document.createElement("dd");return Z.textContent=r,qe.append(xe,Z),Z};if(P("Type",`${De[$]==="Images"?"Image":De[$].replace(/s$/,"")} (${(n.name.split(".").pop()||"").toUpperCase()})`),P("Size",Le(n.size)),P("Uploaded",n.createdAt?Xe(n.createdAt):"-"),te(n.name)){const a=P("Dimensions","\u2026");z.media.info(n.name).then(r=>{a.textContent=r?.width?`${r.width} \xD7 ${r.height} px`:"-"}).catch(()=>{a.textContent="-"})}d.querySelector(".mdx-url").value=Y(n),d.querySelector(".mdx-md").value=ae(n),d.querySelector(".mdx-open").href=n.url;let N=!1;async function ke(){if(!we.cannotRename.peek()){o.value=!0;try{const a=await z.media.rename(n.name,i.peek().trim());E.toast(`Renamed to "${a?.name||B(i.peek().trim())}".`,{type:"success"}),N=!0,m.close(),b()&&await A()}catch(a){E.toast(`Not renamed: ${a.message||a}`,{type:"error"})}finally{o.value=!1}}}m.element.appendChild(d);const Ie=$e(d),Ue=M.applyBindings(we,d,{methods:{rename:ke,copyUrl:()=>H(Y(n),"Address"),copyMd:()=>H(ae(n),"Markdown"),async del(){await V(n)&&(N=!0,m.close())},async edit(){N=!0,m.close(),await de(n)}}}),W=d.querySelector("#mdx-rename"),Fe=a=>{a.key==="Enter"&&a.target===W&&(a.preventDefault(),ke())};d.addEventListener("keydown",Fe);const Ee={name:n.name,dirty:ge,newName:()=>i.peek()};h=Ee,G.add(m),Oe(m,()=>{Ue.dispose(),Ie(),d.querySelectorAll("video, audio").forEach(a=>{try{a.pause()}catch{}}),G.delete(m),h===Ee&&(h=null)});const Pe=m.close.bind(m);let X=!1;m.close=async a=>{if(!N&&!ce&&a!==!0&&ge()){if(X)return;X=!0;const r=await E.confirm("Discard the new name you typed?");if(X=!1,!r)return}return N=!0,Pe()},I.scan(d),m.open(),(t||s)&&(W.focus(),W.select())}function Ne(e){const t=L(e);return t?[{header:t.name},{label:"Details\u2026",icon:"info",action:()=>k(t.name)},{label:"Copy address",icon:"link",action:()=>H(Y(t),"Address")},{label:"Copy for a page",icon:"copy",action:()=>H(ae(t),"Markdown")},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(t.url,"_blank","noopener")},{type:"divider"},{label:"Rename\u2026",icon:"type",action:()=>k(t.name,{focusRename:!0})},...te(t.name)?[{label:"Edit image\u2026",icon:"edit",action:()=>de(t)}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>V(t)}]:[]}const f=c.querySelector(".mdx-grid");f.innerHTML=Ze;const x=e=>()=>{y.value=e},ze=M.applyBindings({files:p,loading:oe,loadError:K,kind:y,sort:_,query:ie,tiles:U,counts:re,totalText:Ce,emptyText:Te,busyText:D,dragging:q},c,{methods:{kindAll:x("all"),kindImage:x("image"),kindVideo:x("video"),kindAudio:x("audio"),kindDocument:x("document"),kindOther:x("other")}});l.add(()=>ze.dispose()),l.add($e(c,{position:"bottom"})),l.add(M.effect(()=>{U.value,requestAnimationFrame(()=>{f.isConnected&&I.scan(f)})}));const Re=E.contextMenu(f,{cursor:"glyph",match:".mdx-tile",inherit:!1,density:"compact",items:e=>Ne(e.dataset.name)});l.add(()=>Re?.destroy?.());const me=e=>{const t=e.target.closest(".mdx-tile");if(!t)return;const s=e.target.closest(".mdx-kebab");if(s){Me(s);return}k(t.dataset.name)},ue=e=>{const t=e.target.closest(".mdx-tile");if(!t||e.target!==t)return;const s=L(t.dataset.name);s&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Me(t)):e.key==="Enter"?k(s.name):e.key==="F2"?(e.preventDefault(),k(s.name,{focusRename:!0})):e.key==="Delete"&&(e.preventDefault(),V(s)))};f.addEventListener("click",me),f.addEventListener("keydown",ue),l.add(()=>{f.removeEventListener("click",me),f.removeEventListener("keydown",ue)});const C=c.querySelector(".mdx-file-input"),pe=()=>{const e=[...C.files];C.value="",le(e)};C.addEventListener("change",pe),l.add(()=>C.removeEventListener("change",pe)),Be(u,{id:"media-upload",icon:"upload",title:"Upload files",onClick:()=>C.click()});const F=e=>[...e.dataTransfer?.types||[]].includes("Files");let T=0;const fe=e=>{F(e)&&(e.preventDefault(),T++,q.value=!0)},ve=e=>{F(e)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")},be=e=>{F(e)&&(T=Math.max(0,T-1),T||(q.value=!1))},ye=e=>{F(e)&&(e.preventDefault(),T=0,q.value=!1,le(e.dataTransfer.files))},v=u.get(0);v.addEventListener("dragenter",fe),v.addEventListener("dragover",ve),v.addEventListener("dragleave",be),v.addEventListener("drop",ye),l.add(()=>{v.removeEventListener("dragenter",fe),v.removeEventListener("dragover",ve),v.removeEventListener("dragleave",be),v.removeEventListener("drop",ye)});const he=e=>{(D.peek()||h?.dirty())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",he),l.add(()=>window.removeEventListener("beforeunload",he)),l.add(()=>{if(h?.dirty())try{S.set(O,{name:h.name,newName:h.newName(),at:new Date().toISOString()})}catch{}ce=!0,[...G].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(c),await A(),!b())return;let g=null;try{g=S.get(O)}catch{}if(g?.name){try{S.remove?S.remove(O):S.set(O,null)}catch{}L(g.name)&&await E.confirm(`You were renaming "${R(g.name)}" to "${R(g.newName)}" and left before saving. Pick up where you left off?`)&&b()&&k(g.name,{draftName:g.newName})}}};
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import{api as T}from"../api.js";import{colourToCss as
|
|
1
|
+
import{api as T}from"../api.js";import{colourToCss as kt,HIGHLIGHT_HOVER as nt,HIGHLIGHT_ACTIVE as ot,HIGHLIGHT_HOVER_LABELS as at,HIGHLIGHT_ACTIVE_LABELS as it}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{openIconPicker as St}from"../lib/card-builder.js";import{applyDrop as $t}from"../lib/menu-tree.js";import{placeTools as At,ensureLockedTools as xt,isLockedUrl as Et,TOOLS_FOLDER_KEY as Lt}from"../lib/sidebar-grouping.js";import{flatten as _t,nest as Ht,uid as xe}from"../lib/menu-rows.js";import{addBannerAction as Fe}from"../lib/plugin-chrome.js";import{helpPopovers as Ct}from"../lib/help-popover.js";import{onceClosed as It,openMenuAt as ze,sidebarCount as Tt}from"../lib/tool-kit.js";import{addChild as Dt,bindingFrom as Rt,duplicateRow as jt,indentRow as Nt,insertBelow as Ot,isStructural as oe,menuFromForm as Pt,menuProblems as st,moveRow as lt,outdentRow as Bt,siblingsOf as Ft,snapshot as Ee}from"../lib/menu-editor-arrange.js";import{fmtDateTime as zt}from"../lib/dates.js";const Mt=760,qt="admin-sidebar",Wt=/^\/menus\/(new|edit\/[^?#]+)(\?.*)?$/;function w(t){return String(t||"").replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<")}const Le=t=>String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Kt(t,r){let l=0,i=r.find(o=>o.id===t);for(;i&&i.parentId;)l++,i=r.find(o=>o.id===i.parentId);return l}function rt(t,r){const{top:l,height:i}=t.getBoundingClientRect(),o=i?(r-l)/i:.5;return o<.25?"above":o>.75?"below":"into"}const ct=["me-drop-above","me-drop-below","me-drop-into"];function Me(t){t.querySelectorAll(".menu-tree-row").forEach(r=>r.classList.remove(...ct))}function Ut(t,r,l){let i=null;t.querySelectorAll(".menu-tree-row").forEach(o=>{const f=o.querySelector(".menu-tree-grip");o.draggable=!1,f&&(f.addEventListener("mousedown",()=>{o.draggable=!0}),f.addEventListener("mouseup",()=>{o.draggable=!1})),o.addEventListener("dragstart",h=>{i=o.dataset.id,o.style.opacity="0.45",h.dataTransfer.effectAllowed="move";try{h.dataTransfer.setData("text/plain",i)}catch{}}),o.addEventListener("dragend",()=>{o.style.opacity="",o.draggable=!1,i=null,Me(t)}),o.addEventListener("dragover",h=>{if(!i||i===o.dataset.id)return;h.preventDefault(),h.dataTransfer.dropEffect="move";const d=rt(o,h.clientY);Me(t),o.classList.add(`me-drop-${d}`)}),o.addEventListener("dragleave",()=>o.classList.remove(...ct)),o.addEventListener("drop",h=>{h.preventDefault();const d=o.dataset.id,C=rt(o,h.clientY);if(Me(t),!i)return;const $=$t(r(),i,d,C);if($){const j=r().find(A=>A.id===i);j&&(j.touched=!0)}i=null,$&&l()})})}function dt(t){const r=new Set,l=new Set,i=o=>{for(const f of t.filter(h=>h.parentId===o))!f.type&&Et(f.url)&&!l.has(f.url)&&(l.add(f.url),r.add(f.id)),i(f.id)};return i(null),r}const qe='<button type="button" class="btn btn-sm btn-ghost mex-kebab" aria-label="Actions" title="Actions (right-click, or Shift+F10)">\u22EE</button>';function Gt(t,r,l=!1){const i=l?dt(r):new Set,o=[];function f(h){for(const d of r.filter(C=>C.parentId===h)){const $=`style="margin-left:${Kt(d.id,r)*28}px"`;if(d.type==="separator"){o.push(`
|
|
2
2
|
<div class="menu-tree-row menu-tree-row--separator" data-id="${d.id}" tabindex="0" ${$}>
|
|
3
3
|
<span class="menu-tree-grip" data-icon="move" title="Drag to move"></span>
|
|
4
4
|
<span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
|
|
5
|
-
${
|
|
6
|
-
</div>`);continue}if(d.type==="spacer"){const
|
|
5
|
+
${qe}
|
|
6
|
+
</div>`);continue}if(d.type==="spacer"){const ee=["xs","sm","md","lg","xl","flex"].map(P=>`<option value="${P}"${(d.size||"md")===P?" selected":""}>${P==="flex"?"flexible":P}</option>`).join("");o.push(`
|
|
7
7
|
<div class="menu-tree-row menu-tree-row--spacer" data-id="${d.id}" tabindex="0" ${$}>
|
|
8
8
|
<span class="menu-tree-grip" data-icon="move" title="Drag to move"></span>
|
|
9
9
|
<span class="me-spacer-line">
|
|
10
10
|
<span class="me-spacer-label">Spacer</span>
|
|
11
|
-
<select class="form-input me-spacer-size" title="Spacer size - flexible fills the leftover room" aria-label="Spacer size">${
|
|
11
|
+
<select class="form-input me-spacer-size" title="Spacer size - flexible fills the leftover room" aria-label="Spacer size">${ee}</select>
|
|
12
12
|
</span>
|
|
13
|
-
${
|
|
14
|
-
</div>`);continue}const
|
|
13
|
+
${qe}
|
|
14
|
+
</div>`);continue}const j=d.badge&&(d.badge.text||d.badge.countFrom)?`<span class="dm-menu-badge">${w(String(d.badge.text||"#"))}</span>`:"",A=d.tool,g=A&&(A.persisted||d.touched),W=A?A.orphan?' <span class="badge badge-secondary me-tool-badge" title="A placeholder for a Tool that is not enabled - it returns here when it is">Not enabled</span>':` <span class="badge badge-secondary me-tool-badge" title="${g?"A Tool, placed here by this menu":"A Tool in its default place - move or edit it to place it here"}">${w(A.source||"Tool")}${g?"":" \xB7 default"}</span>`:"",ae=i.has(d.id),Q=kt(d.colour),K=Q?` style="color:${w(Q)}"`:"",ie=ae?'<button type="button" class="btn btn-sm btn-ghost" disabled title="Always shown - it can be moved, not hidden" aria-label="Always shown"><span data-icon="lock"></span></button>':`<button type="button" class="btn btn-sm btn-ghost me-hidden ${d.hidden?"active":""}" title="${d.hidden?"Hidden - click to show":"Hide"}" aria-label="${d.hidden?"Show":"Hide"}" aria-pressed="${d.hidden?"true":"false"}"><span data-icon="${d.hidden?"eye-off":"eye"}"></span></button>`;o.push(`
|
|
15
15
|
<div class="menu-tree-row menu-tree-row--slim${d.hidden?" menu-tree-row--hidden":""}${A?" menu-tree-row--tool":""}${A&&!g?" menu-tree-row--default":""}" data-id="${d.id}" tabindex="0" ${$}>
|
|
16
16
|
<span class="menu-tree-grip" data-icon="move" title="Drag to move"></span>
|
|
17
|
-
<span class="me-row-label${!d.text&&!A?" mex-untitled":""}"${
|
|
17
|
+
<span class="me-row-label${!d.text&&!A?" mex-untitled":""}"${K}>${w(d.text||A?.label||"(no label - click to add one)")}${j}${W}</span>
|
|
18
18
|
<span class="me-row-url">${w(d.url||"")}</span>
|
|
19
|
-
${
|
|
19
|
+
${ie}
|
|
20
20
|
<button type="button" class="btn btn-sm btn-ghost me-edit" title="Edit" aria-label="Edit"><span data-icon="edit"></span></button>
|
|
21
|
-
${
|
|
22
|
-
</div>`),f(d.id)}}f(null),t.innerHTML=o.join("")||'<p class="mex-note">No items yet - Add item, or right-click an item for more.</p>',I.scan(t)}function k(t,r={},...l){const i=document.createElement(t);Object.assign(i,r);for(const o of l)o!=null&&i.append(o);return i}function j(t,r,l){const i=k("label",{className:"form-field"},k("span",{className:"form-label",textContent:t}),r);return l&&i.append(k("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Vt(t,r){const l=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(r||[]).map(o=>[o.name,`${o.label||o.name} and above`]),...(r||[]).filter(o=>o.level!==0).map(o=>[`=${o.name}`,`${o.label||o.name} only`]),["private","Private - super-admin only"]];t&&!l.some(([o])=>o===t)&&l.push([t,`${t} (custom)`]);const i=Q(l,t||"");return i.name="visibility",i}function Q(t,r){const l=k("select",{className:"form-select"});return t.forEach(([i,o])=>l.add(new Option(o,i))),l.value=r??"",l}function He(t,r){const l=typeof r=="string"&&r.startsWith("#"),i=Q([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":r||""),o=k("input",{type:"color",className:"form-input me-colour-hex",value:l?r:"#000000"});o.style.display=l?"":"none",i.addEventListener("change",()=>{o.style.display=i.value==="__custom__"?"":"none"});const f=j(t,i);return f.append(o),f._read=()=>i.value==="__custom__"?o.value:i.value,f}function Yt(t,r){const l=k("input",{className:"form-input me-icon",name:"icon",value:r||""}),i=k("span",{className:"me-icon-preview"});r&&i.setAttribute("data-icon",r);const o=k("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),f=h=>{h?i.setAttribute("data-icon",h):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return o.addEventListener("click",()=>$t(h=>{l.value=h,f(h)})),l.addEventListener("input",()=>f(l.value.trim())),j(t,k("div",{className:"me-icon-field"},l,o,i))}function Ce(...t){return k("div",{className:"me-field-grid"},...t)}async function Xt(t,r,l,i,o){const f=t.text||t.tool?.label||"",h=c=>k("h4",{className:"me-form-section",textContent:c}),d=j("Label",k("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),C=k("input",{className:"form-input",name:"url",value:t.url||""}),$=j("URL",C,t.tool||t.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(t.tool||t.locked)&&(C.readOnly=!0);const N=Yt("Icon",t.icon||""),A=Vt(t.visibility||"",i),g=j("Visibility",A,t.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');t.locked&&(A.disabled=!0);const K=Q([["","- no permission gate -"],...(r||[]).map(c=>[c.key,c.label||c.key])],t.permission||"");K.name="permission";const ie=j("Permission",K,t.tool||t.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(t.tool||t.locked)&&(K.disabled=!0);const ee=j("Badge text",k("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),U=Q([["","- none -"],...(l||[]).map(c=>[c.slug,c.title||c.slug])],t.badge?.countFrom||"");U.name="badgeCount";const se=j("Badge count from",U,"Show a live count of this collection\u2019s entries as the badge."),te=He("Badge colour",t.badge?.variant||""),B=Q([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");B.name="renderAs";const Ie=j("Render as",B,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),G=j("Pill style",Q([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));G.querySelector("select").name="pillStyle";const pe=He("Pill colour",t.pill?.variant||""),le=k("div",{className:"me-pill-wrap"},Ce(G,pe));le.style.display=t.pill?"":"none",B.addEventListener("change",()=>{le.style.display=B.value==="pill"?"":"none"});const p=He("Text colour",t.colour||""),O=Q([["","- use the menu default -"],...ot.map(c=>[c,it[c]||c])],t.highlight?.hover||"");O.name="hlHover";const re=j("Hover effect",O,"Overrides the menu-wide hover effect for this item only."),fe=Q([["","- use the menu default -"],...at.map(c=>[c,st[c]||c])],t.highlight?.active||"");fe.name="hlActive";const ye=j("When selected",fe,"How this item is marked when it points at the page being viewed."),F=He("Highlight colour",t.highlight?.accent||""),x=k("button",{className:"btn btn-primary",textContent:"Apply",title:"Apply to the item (Ctrl+Enter) - Save the menu to keep it"}),z=k("div",{className:"me-item-form"},h("Link"),d,$,N,h("Access"),Ce(g,ie),h("Badge"),Ce(ee,te),se,h("Appearance"),Ie,le,p,h("Highlighting"),Ce(re,ye),F,x),y=E.slideover({title:f?`Edit ${_e(f)}`:"Edit menu item",size:"lg",position:"right"});y.element.appendChild(z);const X=()=>JSON.stringify([...z.querySelectorAll("input, select, textarea")].map(c=>c.type==="checkbox"?c.checked:c.value)),ne=X();let V=!1,ce=!1;Tt(y);const we=y.close.bind(y);y.close=async c=>{if(!V&&c!==!0&&X()!==ne){if(ce)return;ce=!0;const Y=await E.confirm("Discard the changes to this item?");if(ce=!1,!Y)return}return V=!0,we()},z.addEventListener("keydown",c=>{(c.ctrlKey||c.metaKey)&&c.key==="Enter"&&(c.preventDefault(),x.click())}),y.open();const he=y.element.closest(".dm-slideover")||y.element.querySelector(".dm-slideover")||y.element;he.style.setProperty("width",zt+"px","important"),he.style.setProperty("max-width","95vw","important"),Domma.icons.scan(z);const L=c=>z.querySelector(`[name="${c}"]`)?.value||"";x.addEventListener("click",()=>{t.text=L("text"),t.icon=L("icon"),t.locked||(t.visibility=L("visibility")),!t.tool&&!t.locked?(t.url=L("url"),t.permission=L("permission")):t.touched=!0;const c=L("badgeText"),Y=L("badgeCount"),oe=te._read();if(t.badge=c||Y?{...c&&{text:c},...Y&&{countFrom:Y},...oe&&{variant:oe}}:null,L("renderAs")==="pill"){const ve=pe._read();t.pill={style:L("pillStyle")||"filled",...ve&&{variant:ve}}}else t.pill=null;t.colour=p._read();const de=L("hlHover"),ue=L("hlActive"),me=F._read();t.highlight=de||ue||me?{...de&&{hover:de},...ue&&{active:ue},...me&&{accent:me}}:null,V=!0,y.close(!0),o()})}let W=null,mt=!1;function Jt(){mt||!window.R?.use||(mt=!0,R.use((t,r,l)=>{const i=r?.path||"";return!(Wt.test(i)&&(t?.path||"")!==i)||!W?.dirty()?l():E.confirm(`You have unsaved changes to the menu "${_e(W.name())}". Leave anyway? They are kept as a draft for your next visit.`).then(f=>{f?l():history.replaceState(null,"",`#${i}`)})}),M.subscribe("router:afterChange",({to:t}={})=>{if(W&&(t?.path||"")!==W.path){const r=W;W=null,r.dispose()}}))}function pt(t,r){const l=t.querySelector(".dm-plugin-banner-name"),i=l?.querySelector(".dm-plugin-banner-sep");if(!(!l||!i)){for(;i.nextSibling;)i.nextSibling.remove();l.appendChild(document.createTextNode(r))}}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){if(Jt(),W){const e=W;W=null,e.dispose()}const r=[],l=e=>(r.push(e),e);let i=!1;const o=t.get(0).querySelector(".mex")||t.get(0),f=()=>!i&&o.isConnected,h=location.hash.replace(/^#/,""),d=location.hash.match(/^#\/menus\/edit\/([^?#]+)/),C=d?decodeURIComponent(d[1]):null,$=!C,N=`menu-editor.draft.${C||"new"}`,A=E.loader(t.get(0),{type:"dots"});let g,K,ie,ee,U,se,te;try{if($)g={slug:"",name:"",description:"",items:[]};else try{g=await T.menus.get(C)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}[K,ie,ee,U,se,te]=await Promise.all([T.projects.list().catch(()=>[]),H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),T.collections.list().catch(()=>[]),T.get("/collections/roles/entries?limit=100").catch(()=>null),T.menuLocations.registry().catch(()=>[]),T.pages.list().catch(()=>[])])}finally{A.destroy()}if(!o.isConnected)return;const Ie=(Array.isArray(U)?U:U?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,n)=>n.level-e.level),G=!$&&C===qt;let pe=[];if(G&&(pe=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])),!o.isConnected))return;const le=e=>Ht(G?Et(xt(e||[],pe,{forEditor:!0}),{leaveInHiddenFolders:!0}):e);let p=le(g.items);const O=M.observable(!1),re=M.observable(!1),fe=M.observable(""),ye=M.observable(0);let F="";const x=o.querySelector("#me-items-list");function z(){Gt(x,p,G),Ut(x,()=>p,()=>{z(),q()}),ye.value=p.filter(e=>!ae(e)).length}const y=()=>{z(),q()},X=()=>G?ut(p):new Set,ne=e=>p.find(n=>n.id===e);function V(e){requestAnimationFrame(()=>x.querySelector(`.menu-tree-row[data-id="${CSS.escape(e)}"]`)?.focus())}function ce(e){const n=u=>p.find(m=>!m.type&&!m.url&&!m.tool&&(m.key||"").toLowerCase()===u)||p.find(m=>!m.type&&!m.url&&!m.tool&&!m.key&&(m.text||"").toLowerCase()===u),s=n(String(e.tool.folder||"").toLowerCase())||n(_t);e.parentId=s?s.id:null,p.splice(p.indexOf(e),1),p.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}const we=e=>!X().has(e.id)&&!(e.tool&&!e.tool.orphan);async function he(e){const n=ne(e);if(!n||!we(n))return;const s=b=>{let Z=b;for(;Z;){if(Z.id===e)return!0;Z=p.find(P=>P.id===Z.parentId)}return!1},u=p.filter(b=>b.id!==e&&s(b));if(u.length&&!await E.confirm(`Remove "${_e(n.text||n.type||"this item")}" and the ${u.length} item${u.length===1?"":"s"} beneath it?`))return;const m=X(),v=u.filter(b=>b.tool),_=u.filter(b=>!b.tool&&m.has(b.id));p=p.filter(b=>!s(b)||v.includes(b)||_.includes(b));for(const b of _)b.parentId=n.parentId??null;for(const b of v)b.tool.orphan?b.parentId=n.parentId??null:ce(b);y()}function L(e){const n=ne(e);!n||ae(n)||X().has(e)||(n.hidden=!n.hidden,n.touched=!0,y())}function c(e){const n=ne(e);!n||ae(n)||(n.locked=G&&X().has(n.id),Xt(n,ie.resources||[],ee,Ie,()=>{y(),V(e)}))}function Y(e){const n={id:Ee(),parentId:null,...e==="separator"?{type:e}:e==="spacer"?{type:e,size:"md"}:{text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}};p.push(n),y(),ae(n)?V(n.id):c(n.id)}const oe=e=>n=>{e(n)&&(y(),V(n))},de=oe(e=>rt(p,e,-1)),ue=oe(e=>rt(p,e,1)),me=oe(e=>Ot(p,e)),ve=oe(e=>Ft(p,e));function ft(e){const n=ne(e);if(!n)return[];const s=ae(n),u=X().has(e),m=Mt(p,e),v=m.findIndex(P=>P.id===e),_=n.tool,b=_&&(_.persisted||n.touched),Z=P=>()=>{const be=Pt(p,e,P,Ee());y(),be&&!ae(be)?c(be.id):be&&V(be.id)};return[{header:s?n.type==="separator"?"Separator":"Spacer":n.text||_?.label||"(untitled)"},...s?[]:[{label:"Edit\u2026",icon:"edit",action:()=>c(e)}],{type:"divider"},{label:"Add item below",icon:"plus",action:Z("item")},{label:"Add sub-item",icon:"corner-down-right",disabled:s,action:()=>{const P=Rt(p,e,Ee());y(),P&&c(P.id)}},{label:"Add separator below",icon:"minus",action:Z("separator")},{label:"Add spacer below",icon:"move",action:Z("spacer")},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:v<=0,action:()=>de(e)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:v<0||v>=m.length-1,action:()=>ue(e)},{label:"Indent",icon:"chevron-right",shortcut:"Alt+\u2192",disabled:v<=0||ae(m[v-1]),action:()=>me(e)},{label:"Outdent",icon:"chevron-left",shortcut:"Alt+\u2190",disabled:n.parentId==null,action:()=>ve(e)},{type:"divider"},...s?[]:[{label:n.hidden?"Show":"Hide",icon:n.hidden?"eye":"eye-off",disabled:u,action:()=>L(e)}],{label:"Duplicate",icon:"copy",disabled:!!_,action:()=>{const P=Nt(p,e,Ee);y(),P&&V(P.id)}},..._&&!_.orphan&&b?[{label:"Back to its default place",icon:"rotate-ccw",action:()=>{ce(n),y()}}]:[],{label:u?"Remove - always in the sidebar":_&&!_.orphan?"Remove - a Tool is hidden, not removed":"Remove\u2026",icon:"trash",danger:!0,disabled:!we(n),action:()=>he(e)}]}const ht=E.contextMenu(x,{cursor:"glyph",match:".menu-tree-row",inherit:!1,density:"compact",items:e=>ft(e.dataset.id)});l(()=>ht?.destroy?.());const Ke=e=>{const n=e.target.closest(".menu-tree-row");if(!n)return;const s=n.dataset.id;if(e.target.closest(".mex-kebab")){ze(e.target.closest(".mex-kebab"));return}if(e.target.closest(".me-hidden")){L(s);return}if(e.target.closest(".me-edit")){c(s);return}e.target.closest("select, .menu-tree-grip, button")||c(s)},Ue=e=>{const n=e.target.closest(".menu-tree-row");if(!n||e.target!==n){n&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ze(n));return}const s=n.dataset.id;if(e.key==="F10"&&e.shiftKey)e.preventDefault(),ze(n);else if(e.key==="Enter")e.preventDefault(),c(s);else if(e.key==="Delete")e.preventDefault(),he(s);else if(e.altKey&&e.key==="ArrowUp")e.preventDefault(),de(s);else if(e.altKey&&e.key==="ArrowDown")e.preventDefault(),ue(s);else if(e.altKey&&e.key==="ArrowRight")e.preventDefault(),me(s);else if(e.altKey&&e.key==="ArrowLeft")e.preventDefault(),ve(s);else if(e.key==="ArrowDown"||e.key==="ArrowUp"){const u=[...x.querySelectorAll(".menu-tree-row")],m=u.indexOf(n)+(e.key==="ArrowDown"?1:-1);u[m]&&(e.preventDefault(),u[m].focus())}},Ge=e=>{if(!e.target.matches(".me-spacer-size"))return;const n=ne(e.target.closest(".menu-tree-row")?.dataset.id);n&&(n.size=e.target.value,q())};x.addEventListener("click",Ke),x.addEventListener("keydown",Ue),x.addEventListener("change",Ge),l(()=>{x.removeEventListener("click",Ke),x.removeEventListener("keydown",Ue),x.removeEventListener("change",Ge)});const a=e=>t.find(e),ke=a("#me-float-accent"),Te=a("#me-float-accent-hex"),De=()=>{Te.get(0).style.display=ke.val()==="__custom__"?"":"none"},vt=()=>ke.val()==="__custom__"?Te.val():ke.val(),Se=a("#me-highlight-accent"),Re=a("#me-highlight-accent-hex"),Ve=()=>{Re.get(0).style.display=Se.val()==="__custom__"?"":"none"},gt=()=>Se.val()==="__custom__"?Re.val():Se.val(),Ye=()=>{const e=a("#me-position").val()==="floating";a(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&De();const n=a("#me-orientation").val()==="vertical";a(".dm-vertical-field").each(function(){this.style.display=n?"":"none"});const s=!!(a("#me-float-collapsible").get(0)||{}).checked;a(".dm-float-collapsed-field").each(function(){this.style.display=e&&s?"":"none"})},Xe=(e,n,s,u)=>{const m=a(e).get(0);m&&(m.innerHTML="",m.add(new Option(u,"")),n.forEach(v=>m.add(new Option(s[v]||v,v))))};Xe("#me-highlight-hover",ot,it,"- theme default -"),Xe("#me-highlight-active",at,st,"- theme default -");const je=a("#me-bind-slot"),Ne=a("#me-bind-projects"),$e=a("#me-bind-pages");je.html('<option value="">- not bound -</option>'+(se||[]).map(e=>`<option value="${w(e.slot)}">${w(e.label||e.slot)} (${w(e.slot)})</option>`).join("")),Ne.html(K.map(e=>`<option value="${w(e.slug)}">${w(e.name||e.slug)}</option>`).join("")),a("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+te.map(e=>{const n=e.urlPath||e.url||"";return n?`<option value="${w(n)}">${w(e.title||n)} - ${w(n)}</option>`:""}).join(""));const Je=()=>jt({slot:je.val(),projects:Array.from(Ne.get(0).selectedOptions).map(e=>e.value),pagesText:$e.val()});function Oe(){const e=Je(),n=a("#me-bind-summary");if(!e){n.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const s=[];e.projects.length&&s.push(`pages in ${e.projects.map(v=>`<code>${w(v)}</code>`).join(", ")}`),e.pages.length&&s.push(`pages matching ${e.pages.map(v=>`<code>${w(v)}</code>`).join(", ")}`);const u=e.pages.filter(v=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(v)),m=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(v=>`<code>${w(v)}</code>`).join(", ")}</div>`:"";n.html(s.length?`Takes over the <code>${w(e.slot)}</code> slot on ${s.join(" and ")}.${m}`:`<span class="text-warning">Slot <code>${w(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}const{getProjectFromHash:bt}=await import("../lib/project-context.js");if(!f())return;const yt=$?bt():null;a("#me-project").html('<option value="">- none -</option>'+K.map(e=>`<option value="${w(e.slug)}">${w(e.name||e.slug)}</option>`).join(""));function Pe(e){p=le(e.items),a("#me-variant").val(e.variant||""),a("#me-position").val(e.position||"");const n=a("#me-appearOnHover").get(0);n&&(n.checked=e.appearOnHover===!0),a("#me-float-anchor").val(e.float?.anchor||"TL"),a("#me-float-offsetX").val(e.float?.offsetX??""),a("#me-float-offsetY").val(e.float?.offsetY??""),a("#me-float-width").val(e.float?.width??""),a("#me-float-radius").val(e.float?.radius??""),a("#me-float-shadow").val(e.float?.shadow||""),a("#me-float-opacity").val(e.float?.opacity??""),a("#me-float-title").val(e.float?.title||"");for(const[v,_]of[["#me-float-collapsible",e.float?.collapsible],["#me-float-collapsed",e.float?.collapsed],["#me-float-movable",e.float?.movable],["#me-float-resizable",e.float?.resizable]]){const b=a(v).get(0);b&&(b.checked=_===!0)}const s=e.float?.accent||"";ke.val(s.startsWith("#")?"__custom__":s),s.startsWith("#")&&Te.val(s),De(),a("#me-orientation").val(e.orientation||""),a("#me-align").val(e.align||""),a("#me-side").val(e.side||"left"),a("#me-highlight-hover").val(e.highlight?.hover||""),a("#me-highlight-active").val(e.highlight?.active||"");const u=e.highlight?.accent||"";Se.val(u.startsWith("#")?"__custom__":u),u.startsWith("#")&&Re.val(u),Ve(),a("#me-fontFamily").val(e.style?.fontFamily||""),a("#me-fontSize").val(e.style?.fontSize||""),a("#me-fontWeight").val(e.style?.fontWeight||""),a("#me-letterSpacing").val(e.style?.letterSpacing||""),a("#me-iconSize").val(e.style?.iconSize||""),je.val(e.binding?.slot||"");const m=new Set(e.binding?.projects||[]);Array.from(Ne.get(0).options).forEach(v=>{v.selected=m.has(v.value)}),$e.val((e.binding?.pages||[]).join(`
|
|
23
|
-
`)),a("#me-slug").val(e.slug||""),a("#me-name").val(e.name||""),a("#me-description").val(e.description||""),a("#me-project").val(e.meta?.project||
|
|
24
|
-
`).map(s=>s.trim()).filter(Boolean);n.includes(e)||n.push(e)
|
|
25
|
-
`)),
|
|
21
|
+
${qe}
|
|
22
|
+
</div>`),f(d.id)}}f(null),t.innerHTML=o.join("")||'<p class="mex-note">No items yet - Add item, or right-click an item for more.</p>',I.scan(t)}function k(t,r={},...l){const i=document.createElement(t);Object.assign(i,r);for(const o of l)o!=null&&i.append(o);return i}function D(t,r,l){const i=k("label",{className:"form-field"},k("span",{className:"form-label",textContent:t}),r);return l&&i.append(k("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Vt(t,r){const l=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(r||[]).map(o=>[o.name,`${o.label||o.name} and above`]),...(r||[]).filter(o=>o.level!==0).map(o=>[`=${o.name}`,`${o.label||o.name} only`]),["private","Private - super-admin only"]];t&&!l.some(([o])=>o===t)&&l.push([t,`${t} (custom)`]);const i=Z(l,t||"");return i.name="visibility",i}function Z(t,r){const l=k("select",{className:"form-select"});return t.forEach(([i,o])=>l.add(new Option(o,i))),l.value=r??"",l}function _e(t,r){const l=typeof r=="string"&&r.startsWith("#"),i=Z([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":r||""),o=k("input",{type:"color",className:"form-input me-colour-hex",value:l?r:"#000000"});o.style.display=l?"":"none",i.addEventListener("change",()=>{o.style.display=i.value==="__custom__"?"":"none"});const f=D(t,i);return f.append(o),f._read=()=>i.value==="__custom__"?o.value:i.value,f}function Yt(t,r){const l=k("input",{className:"form-input me-icon",name:"icon",value:r||""}),i=k("span",{className:"me-icon-preview"});r&&i.setAttribute("data-icon",r);const o=k("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),f=h=>{h?i.setAttribute("data-icon",h):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return o.addEventListener("click",()=>St(h=>{l.value=h,f(h)})),l.addEventListener("input",()=>f(l.value.trim())),D(t,k("div",{className:"me-icon-field"},l,o,i))}function He(...t){return k("div",{className:"me-field-grid"},...t)}async function Xt(t,r,l,i,o){const f=t.text||t.tool?.label||"",h=c=>k("h4",{className:"me-form-section",textContent:c}),d=D("Label",k("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),C=k("input",{className:"form-input",name:"url",value:t.url||""}),$=D("URL",C,t.tool||t.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(t.tool||t.locked)&&(C.readOnly=!0);const j=Yt("Icon",t.icon||""),A=Vt(t.visibility||"",i),g=D("Visibility",A,t.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');t.locked&&(A.disabled=!0);const W=Z([["","- no permission gate -"],...(r||[]).map(c=>[c.key,c.label||c.key])],t.permission||"");W.name="permission";const ae=D("Permission",W,t.tool||t.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(t.tool||t.locked)&&(W.disabled=!0);const Q=D("Badge text",k("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),K=Z([["","- none -"],...(l||[]).map(c=>[c.slug,c.title||c.slug])],t.badge?.countFrom||"");K.name="badgeCount";const ie=D("Badge count from",K,"Show a live count of this collection\u2019s entries as the badge."),ee=_e("Badge colour",t.badge?.variant||""),P=Z([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");P.name="renderAs";const Ce=D("Render as",P,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),U=D("Pill style",Z([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));U.querySelector("select").name="pillStyle";const me=_e("Pill colour",t.pill?.variant||""),se=k("div",{className:"me-pill-wrap"},He(U,me));se.style.display=t.pill?"":"none",P.addEventListener("change",()=>{se.style.display=P.value==="pill"?"":"none"});const p=_e("Text colour",t.colour||""),N=Z([["","- use the menu default -"],...nt.map(c=>[c,at[c]||c])],t.highlight?.hover||"");N.name="hlHover";const le=D("Hover effect",N,"Overrides the menu-wide hover effect for this item only."),pe=Z([["","- use the menu default -"],...ot.map(c=>[c,it[c]||c])],t.highlight?.active||"");pe.name="hlActive";const be=D("When selected",pe,"How this item is marked when it points at the page being viewed."),B=_e("Highlight colour",t.highlight?.accent||""),x=k("button",{className:"btn btn-primary",textContent:"Apply",title:"Apply to the item (Ctrl+Enter) - Save the menu to keep it"}),F=k("div",{className:"me-item-form"},h("Link"),d,$,j,h("Access"),He(g,ae),h("Badge"),He(Q,ee),ie,h("Appearance"),Ce,se,p,h("Highlighting"),He(le,be),B,x),y=E.slideover({title:f?`Edit ${Le(f)}`:"Edit menu item",size:"lg",position:"right"});y.element.appendChild(F);const Y=()=>JSON.stringify([...F.querySelectorAll("input, select, textarea")].map(c=>c.type==="checkbox"?c.checked:c.value)),te=Y();let G=!1,re=!1;It(y);const ye=y.close.bind(y);y.close=async c=>{if(!G&&c!==!0&&Y()!==te){if(re)return;re=!0;const V=await E.confirm("Discard the changes to this item?");if(re=!1,!V)return}return G=!0,ye()},F.addEventListener("keydown",c=>{(c.ctrlKey||c.metaKey)&&c.key==="Enter"&&(c.preventDefault(),x.click())}),y.open();const fe=y.element.closest(".dm-slideover")||y.element.querySelector(".dm-slideover")||y.element;fe.style.setProperty("width",Mt+"px","important"),fe.style.setProperty("max-width","95vw","important"),Domma.icons.scan(F);const L=c=>F.querySelector(`[name="${c}"]`)?.value||"";x.addEventListener("click",()=>{t.text=L("text"),t.icon=L("icon"),t.locked||(t.visibility=L("visibility")),!t.tool&&!t.locked?(t.url=L("url"),t.permission=L("permission")):t.touched=!0;const c=L("badgeText"),V=L("badgeCount"),ne=ee._read();if(t.badge=c||V?{...c&&{text:c},...V&&{countFrom:V},...ne&&{variant:ne}}:null,L("renderAs")==="pill"){const he=me._read();t.pill={style:L("pillStyle")||"filled",...he&&{variant:he}}}else t.pill=null;t.colour=p._read();const ce=L("hlHover"),de=L("hlActive"),ue=B._read();t.highlight=ce||de||ue?{...ce&&{hover:ce},...de&&{active:de},...ue&&{accent:ue}}:null,G=!0,y.close(!0),o()})}let q=null,ut=!1;function Jt(){ut||!window.R?.use||(ut=!0,R.use((t,r,l)=>{const i=r?.path||"";return!(Wt.test(i)&&(t?.path||"")!==i)||!q?.dirty()?l():E.confirm(`You have unsaved changes to the menu "${Le(q.name())}". Leave anyway? They are kept as a draft for your next visit.`).then(f=>{f?l():history.replaceState(null,"",`#${i}`)})}),M.subscribe("router:afterChange",({to:t}={})=>{if(q&&(t?.path||"")!==q.path){const r=q;q=null,r.dispose()}}))}function mt(t,r){const l=t.querySelector(".dm-plugin-banner-name"),i=l?.querySelector(".dm-plugin-banner-sep");if(!(!l||!i)){for(;i.nextSibling;)i.nextSibling.remove();l.appendChild(document.createTextNode(r))}}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){if(Jt(),q){const e=q;q=null,e.dispose()}const r=[],l=e=>(r.push(e),e);let i=!1;const o=t.get(0).querySelector(".mex")||t.get(0),f=()=>!i&&o.isConnected,h=location.hash.replace(/^#/,""),d=location.hash.match(/^#\/menus\/edit\/([^?#]+)/),C=d?decodeURIComponent(d[1]):null,$=!C,j=`menu-editor.draft.${C||"new"}`,A=E.loader(t.get(0),{type:"dots"});let g,W,ae,Q,K,ie,ee;try{if($)g={slug:"",name:"",description:"",items:[]};else try{g=await T.menus.get(C)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}[W,ae,Q,K,ie,ee]=await Promise.all([T.projects.list().catch(()=>[]),H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),T.collections.list().catch(()=>[]),T.get("/collections/roles/entries?limit=100").catch(()=>null),T.menuLocations.registry().catch(()=>[]),T.pages.list().catch(()=>[])])}finally{A.destroy()}if(!o.isConnected)return;const Ce=(Array.isArray(K)?K:K?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,n)=>n.level-e.level),U=!$&&C===qt;let me=[];if(U&&(me=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])),!o.isConnected))return;const se=e=>_t(U?xt(At(e||[],me,{forEditor:!0}),{leaveInHiddenFolders:!0}):e);let p=se(g.items);const N=M.observable(!1),le=M.observable(!1),pe=M.observable(""),be=M.observable(0);let B="";const x=o.querySelector("#me-items-list");function F(){Gt(x,p,U),Ut(x,()=>p,()=>{F(),z()}),be.value=p.filter(e=>!oe(e)).length}const y=()=>{F(),z()},Y=()=>U?dt(p):new Set,te=e=>p.find(n=>n.id===e);function G(e){requestAnimationFrame(()=>x.querySelector(`.menu-tree-row[data-id="${CSS.escape(e)}"]`)?.focus())}function re(e){const n=u=>p.find(m=>!m.type&&!m.url&&!m.tool&&(m.key||"").toLowerCase()===u)||p.find(m=>!m.type&&!m.url&&!m.tool&&!m.key&&(m.text||"").toLowerCase()===u),s=n(String(e.tool.folder||"").toLowerCase())||n(Lt);e.parentId=s?s.id:null,p.splice(p.indexOf(e),1),p.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}const ye=e=>!Y().has(e.id)&&!(e.tool&&!e.tool.orphan);async function fe(e){const n=te(e);if(!n||!ye(n))return;const s=b=>{let J=b;for(;J;){if(J.id===e)return!0;J=p.find(O=>O.id===J.parentId)}return!1},u=p.filter(b=>b.id!==e&&s(b));if(u.length&&!await E.confirm(`Remove "${Le(n.text||n.type||"this item")}" and the ${u.length} item${u.length===1?"":"s"} beneath it?`))return;const m=Y(),v=u.filter(b=>b.tool),_=u.filter(b=>!b.tool&&m.has(b.id));p=p.filter(b=>!s(b)||v.includes(b)||_.includes(b));for(const b of _)b.parentId=n.parentId??null;for(const b of v)b.tool.orphan?b.parentId=n.parentId??null:re(b);y()}function L(e){const n=te(e);!n||oe(n)||Y().has(e)||(n.hidden=!n.hidden,n.touched=!0,y())}function c(e){const n=te(e);!n||oe(n)||(n.locked=U&&Y().has(n.id),Xt(n,ae.resources||[],Q,Ce,()=>{y(),G(e)}))}function V(e){const n={id:xe(),parentId:null,...e==="separator"?{type:e}:e==="spacer"?{type:e,size:"md"}:{text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}};p.push(n),y(),oe(n)?G(n.id):c(n.id)}const ne=e=>n=>{e(n)&&(y(),G(n))},ce=ne(e=>lt(p,e,-1)),de=ne(e=>lt(p,e,1)),ue=ne(e=>Nt(p,e)),he=ne(e=>Bt(p,e));function pt(e){const n=te(e);if(!n)return[];const s=oe(n),u=Y().has(e),m=Ft(p,e),v=m.findIndex(O=>O.id===e),_=n.tool,b=_&&(_.persisted||n.touched),J=O=>()=>{const ge=Ot(p,e,O,xe());y(),ge&&!oe(ge)?c(ge.id):ge&&G(ge.id)};return[{header:s?n.type==="separator"?"Separator":"Spacer":n.text||_?.label||"(untitled)"},...s?[]:[{label:"Edit\u2026",icon:"edit",action:()=>c(e)}],{type:"divider"},{label:"Add item below",icon:"plus",action:J("item")},{label:"Add sub-item",icon:"corner-down-right",disabled:s,action:()=>{const O=Dt(p,e,xe());y(),O&&c(O.id)}},{label:"Add separator below",icon:"minus",action:J("separator")},{label:"Add spacer below",icon:"move",action:J("spacer")},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:v<=0,action:()=>ce(e)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:v<0||v>=m.length-1,action:()=>de(e)},{label:"Indent",icon:"chevron-right",shortcut:"Alt+\u2192",disabled:v<=0||oe(m[v-1]),action:()=>ue(e)},{label:"Outdent",icon:"chevron-left",shortcut:"Alt+\u2190",disabled:n.parentId==null,action:()=>he(e)},{type:"divider"},...s?[]:[{label:n.hidden?"Show":"Hide",icon:n.hidden?"eye":"eye-off",disabled:u,action:()=>L(e)}],{label:"Duplicate",icon:"copy",disabled:!!_,action:()=>{const O=jt(p,e,xe);y(),O&&G(O.id)}},..._&&!_.orphan&&b?[{label:"Back to its default place",icon:"rotate-ccw",action:()=>{re(n),y()}}]:[],{label:u?"Remove - always in the sidebar":_&&!_.orphan?"Remove - a Tool is hidden, not removed":"Remove\u2026",icon:"trash",danger:!0,disabled:!ye(n),action:()=>fe(e)}]}const ft=E.contextMenu(x,{cursor:"glyph",match:".menu-tree-row",inherit:!1,density:"compact",items:e=>pt(e.dataset.id)});l(()=>ft?.destroy?.());const We=e=>{const n=e.target.closest(".menu-tree-row");if(!n)return;const s=n.dataset.id;if(e.target.closest(".mex-kebab")){ze(e.target.closest(".mex-kebab"));return}if(e.target.closest(".me-hidden")){L(s);return}if(e.target.closest(".me-edit")){c(s);return}e.target.closest("select, .menu-tree-grip, button")||c(s)},Ke=e=>{const n=e.target.closest(".menu-tree-row");if(!n||e.target!==n){n&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ze(n));return}const s=n.dataset.id;if(e.key==="F10"&&e.shiftKey)e.preventDefault(),ze(n);else if(e.key==="Enter")e.preventDefault(),c(s);else if(e.key==="Delete")e.preventDefault(),fe(s);else if(e.altKey&&e.key==="ArrowUp")e.preventDefault(),ce(s);else if(e.altKey&&e.key==="ArrowDown")e.preventDefault(),de(s);else if(e.altKey&&e.key==="ArrowRight")e.preventDefault(),ue(s);else if(e.altKey&&e.key==="ArrowLeft")e.preventDefault(),he(s);else if(e.key==="ArrowDown"||e.key==="ArrowUp"){const u=[...x.querySelectorAll(".menu-tree-row")],m=u.indexOf(n)+(e.key==="ArrowDown"?1:-1);u[m]&&(e.preventDefault(),u[m].focus())}},Ue=e=>{if(!e.target.matches(".me-spacer-size"))return;const n=te(e.target.closest(".menu-tree-row")?.dataset.id);n&&(n.size=e.target.value,z())};x.addEventListener("click",We),x.addEventListener("keydown",Ke),x.addEventListener("change",Ue),l(()=>{x.removeEventListener("click",We),x.removeEventListener("keydown",Ke),x.removeEventListener("change",Ue)});const a=e=>t.find(e),we=a("#me-float-accent"),Ie=a("#me-float-accent-hex"),Te=()=>{Ie.get(0).style.display=we.val()==="__custom__"?"":"none"},ht=()=>we.val()==="__custom__"?Ie.val():we.val(),ke=a("#me-highlight-accent"),De=a("#me-highlight-accent-hex"),Ge=()=>{De.get(0).style.display=ke.val()==="__custom__"?"":"none"},vt=()=>ke.val()==="__custom__"?De.val():ke.val(),Ve=()=>{const e=a("#me-position").val()==="floating";a(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&Te();const n=a("#me-orientation").val()==="vertical";a(".dm-vertical-field").each(function(){this.style.display=n?"":"none"});const s=!!(a("#me-float-collapsible").get(0)||{}).checked;a(".dm-float-collapsed-field").each(function(){this.style.display=e&&s?"":"none"})},Ye=(e,n,s,u)=>{const m=a(e).get(0);m&&(m.innerHTML="",m.add(new Option(u,"")),n.forEach(v=>m.add(new Option(s[v]||v,v))))};Ye("#me-highlight-hover",nt,at,"- theme default -"),Ye("#me-highlight-active",ot,it,"- theme default -");const Re=a("#me-bind-slot"),je=a("#me-bind-projects"),Se=a("#me-bind-pages");Re.html('<option value="">- not bound -</option>'+(ie||[]).map(e=>`<option value="${w(e.slot)}">${w(e.label||e.slot)} (${w(e.slot)})</option>`).join("")),je.html(W.map(e=>`<option value="${w(e.slug)}">${w(e.name||e.slug)}</option>`).join("")),a("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+ee.map(e=>{const n=e.urlPath||e.url||"";return n?`<option value="${w(n)}">${w(e.title||n)} - ${w(n)}</option>`:""}).join(""));const Xe=()=>Rt({slot:Re.val(),projects:Array.from(je.get(0).selectedOptions).map(e=>e.value),pagesText:Se.val()});function Ne(){const e=Xe(),n=a("#me-bind-summary");if(!e){n.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const s=[];e.projects.length&&s.push(`pages in ${e.projects.map(v=>`<code>${w(v)}</code>`).join(", ")}`),e.pages.length&&s.push(`pages matching ${e.pages.map(v=>`<code>${w(v)}</code>`).join(", ")}`);const u=e.pages.filter(v=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(v)),m=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(v=>`<code>${w(v)}</code>`).join(", ")}</div>`:"";n.html(s.length?`Takes over the <code>${w(e.slot)}</code> slot on ${s.join(" and ")}.${m}`:`<span class="text-warning">Slot <code>${w(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}const{getProjectFromHash:gt}=await import("../lib/project-context.js");if(!f())return;const bt=$?gt():null;a("#me-project").html('<option value="">- none -</option>'+W.map(e=>`<option value="${w(e.slug)}">${w(e.name||e.slug)}</option>`).join(""));function Oe(e){p=se(e.items),a("#me-variant").val(e.variant||""),a("#me-position").val(e.position||"");const n=a("#me-appearOnHover").get(0);n&&(n.checked=e.appearOnHover===!0),a("#me-float-anchor").val(e.float?.anchor||"TL"),a("#me-float-offsetX").val(e.float?.offsetX??""),a("#me-float-offsetY").val(e.float?.offsetY??""),a("#me-float-width").val(e.float?.width??""),a("#me-float-radius").val(e.float?.radius??""),a("#me-float-shadow").val(e.float?.shadow||""),a("#me-float-opacity").val(e.float?.opacity??""),a("#me-float-title").val(e.float?.title||"");for(const[v,_]of[["#me-float-collapsible",e.float?.collapsible],["#me-float-collapsed",e.float?.collapsed],["#me-float-movable",e.float?.movable],["#me-float-resizable",e.float?.resizable]]){const b=a(v).get(0);b&&(b.checked=_===!0)}const s=e.float?.accent||"";we.val(s.startsWith("#")?"__custom__":s),s.startsWith("#")&&Ie.val(s),Te(),a("#me-orientation").val(e.orientation||""),a("#me-align").val(e.align||""),a("#me-side").val(e.side||"left"),a("#me-highlight-hover").val(e.highlight?.hover||""),a("#me-highlight-active").val(e.highlight?.active||"");const u=e.highlight?.accent||"";ke.val(u.startsWith("#")?"__custom__":u),u.startsWith("#")&&De.val(u),Ge(),a("#me-fontFamily").val(e.style?.fontFamily||""),a("#me-fontSize").val(e.style?.fontSize||""),a("#me-fontWeight").val(e.style?.fontWeight||""),a("#me-letterSpacing").val(e.style?.letterSpacing||""),a("#me-iconSize").val(e.style?.iconSize||""),Re.val(e.binding?.slot||"");const m=new Set(e.binding?.projects||[]);Array.from(je.get(0).options).forEach(v=>{v.selected=m.has(v.value)}),Se.val((e.binding?.pages||[]).join(`
|
|
23
|
+
`)),a("#me-slug").val(e.slug||""),a("#me-name").val(e.name||""),a("#me-description").val(e.description||""),a("#me-project").val(e.meta?.project||bt||""),Ve(),Ne(),F()}const ve=e=>!!(a(e).get(0)||{}).checked;function $e(){const e={variant:a("#me-variant").val(),position:a("#me-position").val(),appearOnHover:ve("#me-appearOnHover"),floatAnchor:a("#me-float-anchor").val(),floatOffsetX:a("#me-float-offsetX").val(),floatOffsetY:a("#me-float-offsetY").val(),floatWidth:a("#me-float-width").val(),floatRadius:a("#me-float-radius").val(),floatShadow:a("#me-float-shadow").val(),floatAccent:ht(),floatOpacity:a("#me-float-opacity").val(),floatTitle:a("#me-float-title").val(),floatCollapsible:ve("#me-float-collapsible"),floatCollapsed:ve("#me-float-collapsed"),floatMovable:ve("#me-float-movable"),floatResizable:ve("#me-float-resizable"),highlightHover:a("#me-highlight-hover").val(),highlightActive:a("#me-highlight-active").val(),highlightAccent:vt(),orientation:a("#me-orientation").val(),align:a("#me-align").val(),side:a("#me-side").val(),fontFamily:a("#me-fontFamily").val(),fontSize:a("#me-fontSize").val(),fontWeight:a("#me-fontWeight").val(),letterSpacing:a("#me-letterSpacing").val(),iconSize:a("#me-iconSize").val()},n={slug:a("#me-slug").val(),name:a("#me-name").val(),description:a("#me-description").val(),project:a("#me-project").val()};return Pt(e,n,Xe(),Ht(p),g.meta||{})}let Je=!1,Pe=null;function z(){if(!B)return;const e=$e();Pe=e,N.value=Ee(e)!==B,pe.value=N.peek()||Je?st(e).join(" "):""}const Ae=e=>{e.target.closest("#me-items-list")||(e.target.matches("#me-position, #me-orientation, #me-float-collapsible")&&Ve(),e.target.matches("#me-float-accent")&&Te(),e.target.matches("#me-highlight-accent")&&Ge(),e.target.closest("#me-binding")&&Ne(),z())};o.addEventListener("input",Ae),o.addEventListener("change",Ae),l(()=>{o.removeEventListener("input",Ae),o.removeEventListener("change",Ae)}),a("#me-bind-page-add").on("click",()=>{const e=a("#me-bind-page-pick").val();if(!e)return;const n=Se.val().split(`
|
|
24
|
+
`).map(s=>s.trim()).filter(Boolean);n.includes(e)||n.push(e),Se.val(n.join(`
|
|
25
|
+
`)),Ne(),z()}),Oe(g),B=Ee($e()),z();const Ze=()=>{try{S.remove?S.remove(j):S.set(j,null)}catch{}};async function Be(){if(le.peek())return;const e=$e(),n=st(e);if(n.length){Je=!0,z(),E.toast(n.join(" "),{type:"warning",duration:7e3});return}le.value=!0;try{if($)await T.menus.create(e);else if(e.slug!==g.slug){await T.menus.create(e);const s=await T.menuLocations.get();let u=!1;for(const[m,v]of Object.entries(s))v===g.slug&&(s[m]=e.slug,u=!0);u&&await T.menuLocations.save(s),await T.menus.remove(g.slug)}else await T.menus.update(g.slug,e);if(B=Ee(e),N.value=!1,Ze(),Tt("#/menus"),E.toast("Saved.",{type:"success"}),$||e.slug!==g.slug)location.hash="#/menus/edit/"+encodeURIComponent(e.slug);else{g={...g,...e};for(const s of p)s.tool&&s.touched&&(s.tool={...s.tool,persisted:!0});F(),B=Ee($e()),z(),mt(t.get(0),e.name||e.slug)}if(U)try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(s){E.toast(`Save failed: ${s.message||s}`,{type:"error"})}finally{le.value=!1}}async function yt(){!N.peek()||!await E.confirm("Throw away the changes you have not saved?")||(Oe(g),z())}E.tabs(a("#me-tabs").get(0));const wt=M.applyBindings({dirty:N,saving:le,problems:pe,itemCount:be},o,{methods:{save:()=>Be(),discard:()=>yt(),addItem:()=>V("item"),addSeparator:()=>V("separator"),addSpacer:()=>V("spacer")}});l(()=>wt.dispose()),l(Ct(o,{position:"bottom"})),mt(t.get(0),$?"New menu":g.name||g.slug),Fe(t,{id:"menu-editor-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),Fe(t,{id:"menu-editor-locations",icon:"map-pin",title:"Menu locations",onClick:()=>{location.hash="#/menu-locations"}}),Fe(t,{id:"menu-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Be()});const Qe=e=>{if(!(!(e.ctrlKey||e.metaKey)||e.key!=="s")){if(e.preventDefault(),e.stopImmediatePropagation(),document.querySelector(".me-item-form")?.offsetParent){E.toast("Apply the item first (Ctrl+Enter), then Save.",{type:"info"});return}Be()}};document.addEventListener("keydown",Qe,!0),l(()=>document.removeEventListener("keydown",Qe,!0));const et=()=>{if(!(!N.peek()||!Pe))try{S.set(j,{menu:Pe,base:B,at:new Date().toISOString()})}catch{}},tt=e=>{N.peek()&&(et(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",tt),l(()=>window.removeEventListener("beforeunload",tt)),q={path:h,dirty:()=>N.peek(),name:()=>a("#me-name").val()||g.name||g.slug||"new menu",dispose(){i=!0,et(),r.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(o);let X=null;try{X=S.get(j)}catch{}if(X?.menu){Ze();const e=X.base&&X.base!==B,n=X.at?` (${zt(X.at)})`:"",s=$?"the new menu you started":`the menu "${Le(g.name||g.slug)}"`,u=e?`You left unsaved changes to ${s}${n}, but the saved menu has changed since. Restore your version anyway?`:`You left unsaved changes to ${s}${n}. Restore them?`;await E.confirm(u)&&f()&&(Oe({...X.menu,meta:{...g.meta||{},...X.menu.meta||{}}}),z())}}};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as g}from"../api.js";import{addBannerAction as N}from"../lib/plugin-chrome.js";import{helpPopovers as
|
|
1
|
+
import{api as g}from"../api.js";import{addBannerAction as N}from"../lib/plugin-chrome.js";import{helpPopovers as ee}from"../lib/help-popover.js";import{openMenuAt as V,sidebarCount as te}from"../lib/tool-kit.js";import{depthOfItems as ne,locationRows as ae,locationsPayload as se,locationsSnapshot as w}from"../lib/menu-editor-arrange.js";import{fmtDateTime as oe}from"../lib/dates.js";const K=/^\/menu-locations\/?(\?.*)?$/,v="menu-locations.draft",le=`
|
|
2
2
|
<div class="mlx-row" data-slot="{{slot}}" tabindex="0" data-bind-class="(changed ? 'is-changed ' : '') + (tooDeep || missing ? 'is-bad' : '')">
|
|
3
3
|
<span class="mlx-main">
|
|
4
4
|
<strong>{{label}}</strong>
|
|
@@ -13,4 +13,4 @@ import{api as g}from"../api.js";import{addBannerAction as N}from"../lib/plugin-c
|
|
|
13
13
|
</span>
|
|
14
14
|
<select class="form-input mlx-select" aria-label="Menu for {{label}}"></select>
|
|
15
15
|
<button type="button" class="mlx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
|
|
16
|
-
</div>`,ie=o=>String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");let u=null,W=!1;function re(){W||!window.R?.use||(W=!0,R.use((o,p,s)=>!(
|
|
16
|
+
</div>`,ie=o=>String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");let u=null,W=!1;function re(){W||!window.R?.use||(W=!0,R.use((o,p,s)=>!(K.test(p?.path||"")&&!K.test(o?.path||""))||!u?.dirty()?s():E.confirm("You have unsaved menu locations. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?s():history.replaceState(null,"","#/menu-locations")})),M.subscribe("router:afterChange",({to:o}={})=>{if(u&&!K.test(o?.path||"")){const p=u;u=null,p.dispose()}}))}export const menuLocationsView={templateUrl:"/admin/js/templates/menu-locations.html",async onMount(o){if(re(),u){const e=u;u=null,e.dispose()}const p=[],s=e=>(p.push(e),e);let k=!1;const d=o.get(0).querySelector(".mlx")||o.get(0),f=()=>!k&&d.isConnected,x=M.observable([]),L=M.observable([]),O=M.observable({}),l=M.observable({}),a=M.observable({}),C=M.observable(!0),$=M.observable(""),y=M.observable(!1),h=M.computed(()=>w(a.value)!==w(l.value)),b=M.observable([]);s(M.effect(()=>{b.value=ae(x.value,a.value,l.value,L.value,O.value)}));const H=M.computed(()=>{const e=b.value;return`${e.filter(n=>!n.empty).length} of ${e.length} slot${e.length===1?"":"s"} have a menu`});async function _(){try{const[e,t,n]=await Promise.all([g.menuLocations.registry(),g.menuLocations.get().catch(()=>({})),g.menus.list().catch(()=>[])]),r=await Promise.all((n||[]).map(c=>g.menus.get(c.slug).catch(()=>null)));if(!f())return;x.value=Array.isArray(e)?e:[],L.value=n||[],O.value=Object.fromEntries(r.filter(Boolean).map(c=>[c.slug,ne(c.items||[])])),l.value={...t||{}},a.value={...t||{}},$.value=""}catch(e){$.value=`Could not load the locations: ${e.message||e}`}finally{C.value=!1}}const A=(e,t)=>{a.value={...a.peek(),[e]:t||""}};async function D(){if(!(y.peek()||C.peek())){y.value=!0;try{const e=se(a.peek(),x.peek());if(await g.menuLocations.save(e),!f())return;l.value={...e},a.value={...e};try{S.remove?S.remove(v):S.set(v,null)}catch{}te("#/menus"),E.toast("Locations saved.",{type:"success"})}catch(e){E.toast(`Save failed: ${e.message||e}`,{type:"error"})}finally{y.value=!1}}}async function z(){!h.peek()||!await E.confirm("Throw away the changes you have not saved?")||(a.value={...l.peek()})}const i=d.querySelector(".mlx-list");i.innerHTML=le;const G=M.applyBindings({rows:b,loading:C,loadError:$,saving:y,dirty:h,summary:H},d,{methods:{save:()=>D(),discard:()=>z()}});s(()=>G.dispose()),s(ee(d,{position:"bottom"}));const J=()=>{const e=[["","- none -"],...L.peek().map(t=>[t.slug,t.name||t.slug])];for(const t of i.querySelectorAll(".mlx-select")){const n=t.closest(".mlx-row")?.dataset.slot;if(!t.options.length)for(const[c,Z]of e)t.add(new Option(Z,c));const r=a.peek()[n]||"";r&&![...t.options].some(c=>c.value===r)&&t.add(new Option(`${r} (missing)`,r)),t.value!==r&&(t.value=r)}};s(M.effect(()=>{b.value,requestAnimationFrame(()=>{f()&&J()})}));const P=e=>{e.target.matches(".mlx-select")&&A(e.target.closest(".mlx-row").dataset.slot,e.target.value)},T=e=>{const t=a.peek()[e];t&&(location.hash=`#/menus/edit/${encodeURIComponent(t)}`)},q=(e,t)=>navigator.clipboard?.writeText(e).then(()=>E.toast(`${t} copied.`,{type:"success"}),()=>E.toast(e,{type:"info",duration:8e3})),Q=e=>{const t=b.peek().find(n=>n.slot===e);return t?[{header:t.label},{label:t.menu?`Edit "${t.menuName}"`:"Edit its menu",icon:"edit",disabled:!t.menu||t.missing,action:()=>T(e)},{label:"Clear this slot",icon:"x-circle",disabled:!t.menu,action:()=>A(e,"")},{label:"Undo the change",icon:"rotate-ccw",disabled:!t.changed,action:()=>A(e,l.peek()[e]||"")},{type:"divider"},{label:"Copy slot name",icon:"copy",action:()=>q(e,"Slot name")},{label:"Copy [menu location] shortcode",icon:"code",action:()=>q(`[menu location="${e}" /]`,"Shortcode")}]:[]},X=E.contextMenu(i,{cursor:"glyph",match:".mlx-row",inherit:!1,density:"compact",items:e=>Q(e.dataset.slot)});s(()=>X?.destroy?.());const U=e=>{const t=e.target.closest(".mlx-row");if(!t)return;const n=e.target.closest(".mlx-kebab");if(n){V(n);return}e.target.closest("select")||T(t.dataset.slot)},Y=e=>{const t=e.target.closest(".mlx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&T(t.dataset.slot))};i.addEventListener("change",P),i.addEventListener("click",U),i.addEventListener("keydown",Y),s(()=>{i.removeEventListener("change",P),i.removeEventListener("click",U),i.removeEventListener("keydown",Y)}),N(o,{id:"menu-locations-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),N(o,{id:"menu-locations-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>D()});const B=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),D())};document.addEventListener("keydown",B,!0),s(()=>document.removeEventListener("keydown",B,!0));const F=()=>{if(h.peek())try{S.set(v,{map:a.peek(),base:w(l.peek()),at:new Date().toISOString()})}catch{}},j=e=>{h.peek()&&(F(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",j),s(()=>window.removeEventListener("beforeunload",j)),u={dirty:()=>h.peek(),dispose(){k=!0,F(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(d),await _(),!f())return;let m=null;try{m=S.get(v)}catch{}if(m?.map){try{S.remove?S.remove(v):S.set(v,null)}catch{}const e=m.base&&m.base!==w(l.peek()),t=m.at?` (${oe(m.at)})`:"",n=e?`You left unsaved menu locations${t}, but the saved locations have changed since. Restore your version anyway?`:`You left unsaved menu locations${t}. Restore them?`;await E.confirm(ie(n))&&f()&&(a.value={...l.peek(),...m.map})}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as z,apiRequest as Se,getToken as Le,refreshAccessToken as Te}from"../api.js";import{avatarUrl as F,showTopbarAvatar as ve}from"../lib/avatar.js";import{addBannerAction as Ae}from"../lib/plugin-chrome.js";import{helpPopovers as me}from"../lib/help-popover.js";import{accountFacts as fe,buildPayload as _e,checkForm as he,draftOf as De,fieldKind as W,fieldOptions as G,formFromMe as J,initials as je,snapshot as K}from"../lib/my-profile-arrange.js";import{fmtDate as Q,fmtDateTime as Ce}from"../lib/dates.js";const X=/^\/my-profile\/?(\?.*)?$/,w=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");let $=null,ye=!1;function Pe(){ye||!window.R?.use||(ye=!0,R.use((a,s,l)=>!(X.test(s?.path||"")&&!X.test(a?.path||""))||!$?.dirty()?l():E.confirm("You have unsaved changes to your profile. Leave anyway? They are kept for your next visit - except a new password, which you would type again.").then(c=>{c?l():history.replaceState(null,"","#/my-profile")})),M.subscribe("router:afterChange",({to:a}={})=>{if($&&!X.test(a?.path||"")){const s=$;$=null,s.dispose()}}))}function Ne(a,s){const l=W(a.type),m=`mpx-pf-${s}`,c=a.required?' <span class="text-danger">*</span>':"",u=a.help||a.description?` <span data-help="${w(a.help||a.description)}"></span>`:"",d=`<label class="form-label" for="${m}">${w(a.label||a.name)}${c}${u}</label>`,g=`<small class="mpx-err" data-bind-hidden="!pe_${s}.value" data-bind-text="pe_${s}.value"></small>`,o=a.placeholder?` placeholder="${w(a.placeholder)}"`:"";switch(l){case"textarea":return`<div class="mpx-wide">${d}<textarea id="${m}" class="form-input" rows="3"${o} data-model="pf_${s}.value"></textarea>${g}</div>`;case"select":return`<div>${d}<select id="${m}" class="form-input" data-model="pf_${s}.value"><option value="">-</option>${G(a).map(p=>`<option value="${w(p.value)}">${w(p.label)}</option>`).join("")}</select>${g}</div>`;case"checkbox":return`<div><label class="mpx-check"><input id="${m}" type="checkbox" data-model="pf_${s}.value"> ${w(a.label||a.name)}${c}</label>${u}${g}</div>`;case"multi":return`<div class="mpx-wide">${d}<div data-multi="${s}">${G(a).map((p,f)=>`<label class="mpx-check"><input type="checkbox" data-opt="${w(p.value)}" data-bind-checked="po_${s}_${f}.value"> ${w(p.label)}</label>`).join("")}</div>${g}</div>`;default:return`<div>${d}<input id="${m}" type="${l}" class="form-input"${o} data-model="pf_${s}.value">${g}</div>`}}export const myProfileView={templateUrl:"/admin/js/templates/my-profile.html",async onMount(a){if(Pe(),$){const e=$;$=null,e.dispose()}const s=[],l=e=>(s.push(e),e);let m=!1;const c=a.get(0).querySelector(".mpx")||a.get(0),u=()=>!m&&c.isConnected,d=M.observable(!0),g=M.observable(""),o=M.observable(null),p=M.observable(""),f=M.observable(""),x=M.observable(""),D=M.observable(""),ge=M.observable(!1),P=M.observable(!1),Z=M.observable(!1);let v=[];const k=new Map,ee=M.observable(0),h=M.observable(J({},[])),b=M.observable(!1),N=M.observable(!1),be=()=>(ee.value,Object.fromEntries([...k].map(([e,t])=>[e,t.value]))),we=()=>Object.fromEntries([...k].map(([e,t])=>[e,t.peek()])),te=()=>({name:p.value,email:f.value,password:x.value,confirm:D.value,profile:be()}),ae=()=>({name:p.peek(),email:f.peek(),password:x.peek(),confirm:D.peek(),profile:we()}),U=M.computed(()=>he(te(),v,{saved:h.value})),L=M.computed(()=>!d.value&&K(te())!==K(h.value)),$e={loading:d,loadError:g,name:p,email:f,password:x,confirm:D,showPw:ge,hasProfile:Z,err:U,dirty:L,cannotSave:M.computed(()=>P.value||!L.value||Object.keys(U.value).length>0),emailChanged:M.computed(()=>f.value.trim()!==""&&f.value.trim()!==h.value.email),pwMatch:M.computed(()=>x.value.length>=8&&x.value===D.value),avatar:M.computed(()=>je(p.value||o.value?.name)),avatarUrl:M.computed(()=>F(o.value)),avatarAction:M.computed(()=>b.value?"Uploading\u2026":F(o.value)?"Change picture":"Add a picture"),uploading:b,dropping:N,shownName:M.computed(()=>p.value.trim()||o.value?.name||""),shownEmail:M.computed(()=>f.value.trim()||o.value?.email||""),rolesText:M.computed(()=>fe(o.value).roles.join(" + ")),accessText:M.computed(()=>fe(o.value).access),factsText:M.computed(()=>{const e=o.value;return e?[e.createdAt&&`Member since ${Q(e.createdAt)}`,e.lastLogin&&`last signed in ${Q(e.lastLogin)}`,e.updatedAt&&`changed ${Q(e.updatedAt)}`,e.isActive===!1&&"account disabled"].filter(Boolean).join(" \xB7 "):""})},O=()=>`my-profile.draft.${o.peek()?.id||"me"}`;function q(e){p.value=e.name,f.value=e.email,x.value="",D.value="";for(const[t,r]of k)t in(e.profile||{})&&(r.value=e.profile[t])}async function Y(){if(P.peek()||d.peek())return;const e=ae();if(Object.keys(he(e,v,{saved:h.peek()})).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const t=_e(h.peek(),e,v);if(!t){E.toast("Nothing has changed.",{type:"info"});return}P.value=!0;try{const r=await z.auth.updateMe(t.payload);if(!u())return;o.value=r;const y=J(r,v);h.value=y,q(y),H();try{S.set("auth_user",{...S.get("auth_user")||{},name:r.name,email:r.email})}catch{}const n=document.getElementById("topbar-user-name");n&&r.name&&(n.textContent=r.name);const i={name:"your name",email:"your email",password:"your password",profile:"your profile"};E.toast(`Saved ${t.parts.map(_=>i[_]).join(", ").replace(/, ([^,]*)$/," and $1")}.`,{type:"success"})}catch(r){E.toast(`Not saved: ${r.message||r}`,{type:"error"})}finally{P.value=!1}}async function ke(){L.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!u()||(q(h.peek()),H()))}function oe(){if(!L.peek())return;const e=De(ae());if(K({...e,password:"",confirm:""})!==K(h.peek()))try{S.set(O(),{form:e,at:new Date().toISOString()})}catch{}}function H(){try{S.remove?S.remove(O()):S.set(O(),null)}catch{}}const j=c.querySelector(".mpx-avatar-file");async function re(e){if(!(!e||b.peek())){if(!/^image\//.test(e.type)&&!/\.(heic|heif|avif)$/i.test(e.name)){E.toast("Choose an image file.",{type:"warning"});return}if(e.size>8*1024*1024){E.toast("That image is too large - at most 8 MB.",{type:"warning"});return}b.value=!0;try{const t=n=>{const i=new FormData;return i.append("file",e,e.name),fetch("/api/auth/me/avatar",{method:"POST",body:i,headers:n?{Authorization:`Bearer ${n}`}:{}})};let r=await t(Le());r.status===401&&(r=await t(await Te()));const y=await r.json().catch(()=>({}));if(!r.ok)throw new Error(y.error||`HTTP ${r.status}`);if(!u())return;o.value={...o.peek(),avatar:y.avatar},ve(F(y)),E.toast("Your picture is saved.",{type:"success"})}catch(t){E.toast(`Not saved: ${t.message||t}`,{type:"error"})}finally{b.value=!1,j.value=""}}}async function Ee(){if(!(!F(o.peek())||b.peek())&&!(!await E.confirm("Remove your picture? Your initials show instead.")||!u())){b.value=!0;try{if(await Se("/auth/me/avatar",{method:"DELETE"}),!u())return;o.value={...o.peek(),avatar:""},ve(""),E.toast("Picture removed.",{type:"success"})}catch(e){E.toast(`Not removed: ${e.message||e}`,{type:"error"})}finally{b.value=!1}}}const ne=()=>re(j.files?.[0]);j.addEventListener("change",ne),l(()=>j.removeEventListener("change",ne));const T=c.querySelector(".mpx-avatar"),se=e=>{[...e.dataTransfer?.types||[]].includes("Files")&&(e.preventDefault(),N.value=!0)},le=()=>{N.value=!1},ie=e=>{e.preventDefault(),N.value=!1,re(e.dataTransfer?.files?.[0])};T.addEventListener("dragover",se),T.addEventListener("dragleave",le),T.addEventListener("drop",ie),l(()=>{T.removeEventListener("dragover",se),T.removeEventListener("dragleave",le),T.removeEventListener("drop",ie)});const Me=M.applyBindings($e,c,{methods:{save:Y,discard:ke,removeAvatar:Ee,pickAvatar:()=>j.click()}});l(()=>Me.dispose()),l(me(c,{position:"bottom"})),Ae(a,{id:"my-profile-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Y()});const ce=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!u()||(e.preventDefault(),e.stopImmediatePropagation(),Y())};document.addEventListener("keydown",ce,!0),l(()=>document.removeEventListener("keydown",ce,!0));const ue=e=>{L.peek()&&(oe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ue),l(()=>window.removeEventListener("beforeunload",ue)),$={dirty:()=>L.peek(),dispose(){m=!0,oe(),s.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(c);let pe=null;try{const[e,t]=await Promise.all([z.auth.me(),z.collections.get("user-profiles").catch(()=>null)]);if(!u())return;o.value=e,pe=t}catch(e){if(!u())return;g.value=`Could not load your account: ${e.message||e}`,d.value=!1;return}v=(pe?.fields||[]).filter(e=>e&&e.name);const C=J(o.peek(),v),V=v.map((e,t)=>({f:e,i:t})).filter(({f:e})=>W(e.type)!=="hidden");if(v.forEach((e,t)=>k.set(e.name,M.observable(C.profile[e.name]))),V.length){const e=c.querySelector(".mpx-profile");e.innerHTML=V.map(({f:n,i})=>Ne(n,i)).join("");const t={};v.forEach((n,i)=>{t[`pf_${i}`]=k.get(n.name),t[`pe_${i}`]=M.computed(()=>U.value[`profile.${n.name}`]||""),W(n.type)==="multi"&&G(n).forEach((_,B)=>{t[`po_${i}_${B}`]=M.computed(()=>(k.get(n.name).value||[]).includes(_.value))})});const r=M.applyBindings(t,e);l(()=>r.dispose());const y=n=>{const i=n.target.closest("[data-multi]");if(!i||!n.target.matches("input[data-opt]"))return;const _=k.get(v[Number(i.dataset.multi)].name),B=n.target.dataset.opt,de=_.peek()||[];_.value=n.target.checked?[...new Set([...de,B])]:de.filter(xe=>xe!==B)};e.addEventListener("change",y),l(()=>e.removeEventListener("change",y)),l(me(e,{position:"bottom"})),I.scan(e)}Z.value=V.length>0,h.value=C,q(C),ee.value++,d.value=!1;let A=null;try{A=S.get(O())}catch{}if(A?.form){H();const e=A.at?` (${Ce(A.at)})`:"";await E.confirm(`You left unsaved changes to your profile${e}. Restore them?`)&&u()&&q({...C,...A.form,profile:{...C.profile,...A.form.profile||{}}})}}};
|