domma-cms 1.6.58 → 1.6.60
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/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/draft-owner.js +1 -0
- package/admin/js/views/user-editor.js +1 -1
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/compose-window.js +6 -0
- package/plugins/_lib/admin/mail/font-controls.js +375 -0
- package/plugins/_lib/admin/mail/fonts.js +279 -0
- package/plugins/_lib/admin/mail/keyboard.js +49 -0
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +4 -2
- package/plugins/_lib/admin/mail/section-kit.js +4 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools CSS v0.52.
|
|
2
|
+
* Domma Tools CSS v0.52.3
|
|
3
3
|
* Dynamic Object Manipulation & Modeling API
|
|
4
4
|
* (c) 2026 Darryl Waterhouse & DCBW-IT
|
|
5
|
-
* Built: 2026-10-
|
|
6
|
-
* Commit:
|
|
5
|
+
* Built: 2026-10-15T16:39:54.461Z
|
|
6
|
+
* Commit: e7ca3a1
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/*!
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Domma Tools v0.52.
|
|
2
|
+
* Domma Tools v0.52.3
|
|
3
3
|
* Developer tools: Theme Roller & Page Roller
|
|
4
4
|
* (c) 2026 Darryl Waterhouse & DCBW-IT
|
|
5
|
-
* Built: 2026-10-
|
|
6
|
-
* Commit:
|
|
5
|
+
* Built: 2026-10-15T16:39:42.640Z
|
|
6
|
+
* Commit: e7ca3a1
|
|
7
7
|
*
|
|
8
8
|
* Requires: domma.min.js
|
|
9
9
|
*/
|
package/admin/js/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{loginHashFor as $}from"./lib/login-arrange.js";const m="/api";function a(){return S.get("auth_token")}function u(){return S.get("auth_refresh_token")}function f(e){S.set("auth_token",e)}function y(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function g(){const e=u();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw y(),sendToLogin({expired:!0}),new Error("Token refresh failed");const{token:n}=await o.json();return f(n),n}async function t(e,o={}){let n=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:r(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),d=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(d.validation=i.validation),d.status=s.status,d.body=i,d}return s.status===204?null:s.json()}async function O(e,o){const n=a(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),verifyLogin:e=>t("/auth/login/verify",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),checkReset:(e,o)=>t("/auth/reset-password/check",{method:"POST",body:JSON.stringify({token:e,...o&&{password:o}})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},listTags:{list:e=>t(`/list-tags/${e}`,{method:"GET"}).then(o=>o.tags||[]),add:(e,o)=>t(`/list-tags/${e}`,{method:"POST",body:JSON.stringify({name:o})}),changeItem:(e,o,n)=>t(`/list-tags/${e}/item`,{method:"PUT",body:JSON.stringify({id:o,...n})}).then(r=>r.tags||[]),rename:(e,o,n)=>t(`/list-tags/${e}/rename`,{method:"POST",body:JSON.stringify({from:o,to:n})}),remove:(e,o)=>t(`/list-tags/${e}/delete`,{method:"POST",body:JSON.stringify({name:o})})},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:e=>t(`/theme/admin${e?`?id=${encodeURIComponent(e)}`:""}`,{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"}),removeContents:(e,o)=>t(`/projects/${encodeURIComponent(e)}/remove-contents`,{method:"POST",body:JSON.stringify({confirm:o})})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>O("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"}),passwordReset:(e,o)=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"POST",body:JSON.stringify({via:o})}),cancelPasswordReset:e=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"DELETE"})},tools:{list:()=>t("/tools",{method:"GET"}),update:(e,o)=>t(`/tools/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!d.ok)throw new Error(c.error||`Install failed (${d.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&f(e),o&&S.set("auth_refresh_token",o),n&&S.set("auth_user",n)}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=$(location.hash,{expired:e}))}export function logout(){!T&&window.M?.subscribe&&(T=!0,M.subscribe("router:afterChange",({to:e}={})=>{/^\/login(\?|$)/.test(e?.path||"")||(p=!1)})),p=!0,R.navigate("/login")}export function takePendingLogout(){return p?(p=!1,b(),!0):!1}function b(){const e=u();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),y()}export{t as apiRequest,g as refreshAccessToken,a as getToken,y as clearAuth};
|
|
1
|
+
import{claimDrafts as $}from"./lib/draft-owner.js";import{loginHashFor as O}from"./lib/login-arrange.js";const m="/api";function a(){return S.get("auth_token")}function u(){return S.get("auth_refresh_token")}function y(e){S.set("auth_token",e)}function f(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function g(){const e=u();if(!e)throw new Error("No refresh token");const o=await fetch(`${m}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw f(),sendToLogin({expired:!0}),new Error("Token refresh failed");const{token:n}=await o.json();return y(n),n}async function t(e,o={}){let n=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let s=await fetch(`${m}${e}`,{...o,headers:r(n)});if(s.status===401&&u())try{n=await g(),s=await fetch(`${m}${e}`,{...o,headers:r(n)})}catch{return}if(!s.ok){const i=await s.json().catch(()=>({error:"Request failed"})),d=new Error(i.error||i.message||`HTTP ${s.status}`);throw Array.isArray(i.validation)&&(d.validation=i.validation),d.status=s.status,d.body=i,d}return s.status===204?null:s.json()}async function b(e,o){const n=a(),r=n?{Authorization:`Bearer ${n}`}:{},s=await fetch(`${m}${e}`,{method:"POST",headers:r,body:o});if(!s.ok){const i=await s.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${s.status}`)}return s.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),verifyLogin:e=>t("/auth/login/verify",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),checkReset:(e,o)=>t("/auth/reset-password/check",{method:"POST",body:JSON.stringify({token:e,...o&&{password:o}})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),previewFull:(e,o,n)=>t("/pages/preview/full",{method:"POST",body:JSON.stringify({urlPath:e,frontmatter:o,body:n})}),previewLinks:{list:e=>t(`/pages/preview-links${e?`?urlPath=${encodeURIComponent(e)}`:""}`,{method:"GET"}).then(o=>o.links||[]),create:(e,o,n)=>t("/pages/preview-links",{method:"POST",body:JSON.stringify({urlPath:e,expiresIn:o,label:n})}),revoke:e=>t(`/pages/preview-links/${e}`,{method:"DELETE"})},tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},listTags:{list:e=>t(`/list-tags/${e}`,{method:"GET"}).then(o=>o.tags||[]),add:(e,o)=>t(`/list-tags/${e}`,{method:"POST",body:JSON.stringify({name:o})}),changeItem:(e,o,n)=>t(`/list-tags/${e}/item`,{method:"PUT",body:JSON.stringify({id:o,...n})}).then(r=>r.tags||[]),rename:(e,o,n)=>t(`/list-tags/${e}/rename`,{method:"POST",body:JSON.stringify({from:o,to:n})}),remove:(e,o)=>t(`/list-tags/${e}/delete`,{method:"POST",body:JSON.stringify({name:o})})},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},theme:{get:()=>t("/theme",{method:"GET"}),save:e=>t("/theme",{method:"PUT",body:JSON.stringify(e)}),catalog:()=>t("/theme/catalog",{method:"GET"}),admin:e=>t(`/theme/admin${e?`?id=${encodeURIComponent(e)}`:""}`,{method:"GET"}),themeTokens:e=>t(`/theme/themes/${encodeURIComponent(e)}`,{method:"GET"}),compile:e=>t("/theme/compile",{method:"POST",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},menus:{list:()=>t("/menus",{method:"GET"}),get:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"})},contextMenus:{list:()=>t("/context-menus",{method:"GET"}),get:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/context-menus",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/context-menus/${encodeURIComponent(e)}`,{method:"DELETE"}),duplicate:e=>t(`/context-menus/${encodeURIComponent(e)}/duplicate`,{method:"POST"}),actionTypes:()=>t("/context-menus/action-types",{method:"GET"})},menuLocations:{get:()=>t("/menu-locations",{method:"GET"}),save:e=>t("/menu-locations",{method:"PUT",body:JSON.stringify(e)}),registry:()=>t("/menu-locations/registry",{method:"GET"})},projects:{list:()=>t("/projects",{method:"GET"}),listWithCounts:()=>t("/projects?counts=1",{method:"GET"}),get:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/projects",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/projects/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),artefacts:e=>t(`/projects/${encodeURIComponent(e)}/artefacts`,{method:"GET"}),untagAll:e=>t(`/projects/${encodeURIComponent(e)}/untag-all`,{method:"POST"}),removeContents:(e,o)=>t(`/projects/${encodeURIComponent(e)}/remove-contents`,{method:"POST",body:JSON.stringify({confirm:o})})},apiTokens:{list:()=>t("/api-tokens",{method:"GET"}),create:e=>t("/api-tokens",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),revoke:e=>t(`/api-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})},apiEndpoints:{list:()=>t("/api-endpoints",{method:"GET"}),get:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"GET"}),create:e=>t("/api-endpoints",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/api-endpoints/${encodeURIComponent(e)}`,{method:"DELETE"})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>b("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"}),passwordReset:(e,o)=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"POST",body:JSON.stringify({via:o})}),cancelPasswordReset:e=>t(`/users/${encodeURIComponent(e)}/password-reset`,{method:"DELETE"})},tools:{list:()=>t("/tools",{method:"GET"}),update:(e,o)=>t(`/tools/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)})},plugins:{list:()=>t("/plugins",{method:"GET"}),scaffold:e=>t("/plugins/scaffold",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),files:e=>t(`/plugins/${e}/files`,{method:"GET"}),readFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"GET"}),writeFile:(e,o,n)=>t(`/plugins/${e}/file`,{method:"PUT",body:JSON.stringify({path:o,content:n})}),deleteFile:(e,o)=>t(`/plugins/${e}/file?path=${encodeURIComponent(o)}`,{method:"DELETE"}),restart:()=>t("/plugins/restart",{method:"POST"}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"}),async exportBundle(e,{includeData:o=!1}={}){const n=a(),r=o?"?includeData=1":"",s=await fetch(`${m}/plugins/${encodeURIComponent(e)}/export${r}`,{headers:n?{Authorization:`Bearer ${n}`}:{}});if(!s.ok){const E=await s.json().catch(()=>({}));throw new Error(E.error||`Export failed (${s.status})`)}const i=s.headers.get("Content-Disposition")||"",d=/filename="([^"]+)"/.exec(i)?.[1],c=await s.blob(),h=URL.createObjectURL(c),l=document.createElement("a");l.href=h,l.download=d||`${e}.dcmsplugin`,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(h)},async installFile(e,{confirmUnsigned:o=!1,confirmUpgrade:n=!1,confirmDowngrade:r=!1}={}){const s=new FormData;o&&s.append("confirmUnsigned","true"),n&&s.append("confirmUpgrade","true"),r&&s.append("confirmDowngrade","true"),s.append("file",e);const i=a(),d=await fetch(`${m}/plugins/install-upload`,{method:"POST",headers:i?{Authorization:`Bearer ${i}`}:{},body:s}),c=await d.json().catch(()=>({}));if(d.status===409){const h=new Error(c.error||"Plugin already installed");throw h.code="CONFLICT",h.name=c.name,h.installedVersion=c.installedVersion,h.incomingVersion=c.incomingVersion,h}if(!d.ok)throw new Error(c.error||`Install failed (${d.status})`);return c}},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o,{update:n=!1}={})=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o,update:n})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${n?"?"+n:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,n)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:n})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/collections/${e}/public${n?"?"+n:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const n=new URLSearchParams(o).toString();return t(`/views/${e}/execute${n?"?"+n:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Block already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),n=await fetch(`${m}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!n.ok){const d=await n.json().catch(()=>({}));throw new Error(d.error||`Export failed (${n.status})`)}const r=await n.blob(),s=URL.createObjectURL(r),i=document.createElement("a");i.href=s,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(s)},async importBundle(e,{overwrite:o=!1}={}){const n=await fetch(`${m}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(n.status===409){const r=await n.json().catch(()=>({})),s=new Error(r.error||"Component already exists");throw s.code="CONFLICT",s.name=r.name,s}if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`Import failed (${n.status})`)}return n.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:n}){e&&y(e),o&&S.set("auth_refresh_token",o),n&&($(n.id),S.set("auth_user",n))}let p=!1,T=!1;export function sendToLogin({expired:e=!1}={}){/^#\/(login|reset-password)(\?|\/|$)/.test(location.hash)||(location.hash=O(location.hash,{expired:e}))}export function logout(){!T&&window.M?.subscribe&&(T=!0,M.subscribe("router:afterChange",({to:e}={})=>{/^\/login(\?|$)/.test(e?.path||"")||(p=!1)})),p=!0,R.navigate("/login")}export function takePendingLogout(){return p?(p=!1,P(),!0):!1}function P(){const e=u();e&&fetch(`${m}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),f()}export{t as apiRequest,g as refreshAccessToken,a as getToken,f as clearAuth};
|
package/admin/js/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{views as K}from"./views/index.js";import{withPluginChrome as F}from"./lib/plugin-chrome.js";import{api as c,getUser as O,isAuthenticated as m,logout as J,sendToLogin as Y}from"./api.js";import{isAuthScreen as b,loginHashFor as Q}from"./lib/login-arrange.js";import{installHttpInterceptor as X}from"./http-interceptor.js";import{installResizableSlideovers as Z}from"./lib/slideover-resizable.js";import{installActivityThrobber as tt}from"./lib/activity-throbber.js";import{installUpdateNotice as et}from"./lib/update-notice.js";import{isPathAllowed as it,redirectFor as ot}from"./lib/admin-scope.js";import{deliverDesktop as at,desktopActive as st,notificationSettings as nt}from"./lib/desktop-notify.js";import{avatarUrl as lt}from"./lib/avatar.js";import{installCalculator as rt}from"./lib/calculator.js";import{takeOverviewPass as ct,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as mt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as pt}from"./lib/router-catch-up.js";$(()=>{X(),rt(),et(),tt(),Z(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,r="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||ot(p,t);return i||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"&&!ct()?y[r]||r:null)}const y={},N={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Y();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),r=t.adminHome||"/";const i=t.permissions||[];return w=it(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,r="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,r="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),nt.read().signInToasts)try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}L()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const x="cms_card_states",k=S.get(x)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(x,k))}),mt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...K},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(G))l[t]&&(l[t]=F(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=_(l.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);l[e]=F(a[o.exportName],(t.meta||{})[e])}catch{}l.dashboard=_(l.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(N[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function L(){const t=O();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=lt(t);$("#topbar-user").html(`
|
|
1
|
+
import{views as K}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as J,sendToLogin as Y}from"./api.js";import{claimDrafts as Q}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as X}from"./lib/login-arrange.js";import{installHttpInterceptor as Z}from"./http-interceptor.js";import{installResizableSlideovers as tt}from"./lib/slideover-resizable.js";import{installActivityThrobber as et}from"./lib/activity-throbber.js";import{installUpdateNotice as it}from"./lib/update-notice.js";import{isPathAllowed as ot,redirectFor as at}from"./lib/admin-scope.js";import{deliverDesktop as st,desktopActive as nt,notificationSettings as lt}from"./lib/desktop-notify.js";import{avatarUrl as rt}from"./lib/avatar.js";import{installCalculator as ct}from"./lib/calculator.js";import{takeOverviewPass as mt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as pt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as dt}from"./lib/router-catch-up.js";$(()=>{Z(),ct(),it(),et(),tt(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,r="/",w=!1,g=null;function D(t){if(N[t])return E.toast(`${N[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||at(p,t);return i||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"&&!mt()?y[r]||r:null)}const y={},N={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Y();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),r=t.adminHome||"/";const i=t.permissions||[];return w=ot(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,r="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let P=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(P){A=!0;return}P=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{P=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,r="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),lt.read().signInToasts)try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&k[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),pt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const l={...K},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,i]of Object.entries(G))l[t]&&(l[t]=O(l[t],{plugin:t,licence:"Built in",...i}));l.dashboard=_(l.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);l[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}l.dashboard=_(l.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(N[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=rt(t);$("#topbar-user").html(`
|
|
2
2
|
${o?`<img id="topbar-avatar" class="topbar-avatar" src="${d(o)}" alt="">`:""}
|
|
3
3
|
<span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
|
|
4
4
|
<span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${e}</span>
|
|
@@ -20,4 +20,4 @@ import{views as K}from"./views/index.js";import{withPluginChrome as F}from"./lib
|
|
|
20
20
|
<span data-icon="log-out"></span>
|
|
21
21
|
<span>Sign out</span>
|
|
22
22
|
</a>
|
|
23
|
-
`);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),J()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),
|
|
23
|
+
`);const a=$("#topbar-bell").get(0);a&&(a.hidden=!0);const n=$("#topbar-settings").get(0);n&&(n.hidden=!w),$("#topbar-logout-btn").on("click",s=>{s.preventDefault(),J()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),F(),u||(u=setInterval(F,6e4))}function d(t){return String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}let u=null;async function F(){if(!(!m()||g===!1))try{const{count:t}=await c.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const i=$("#topbar-bell-badge");t>0?i.text(t>99?"99+":String(t)).removeAttr("hidden"):i.attr("hidden","").text(""),t>0&&nt()&&st(await c.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!m()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash.startsWith("#/login")||(window.location.hash=X(window.location.hash));else if(Q(x()?.id),await U(),x()){const a=await f(),n=(window.location.hash||"#/").slice(1)||"/",s=D(n);s&&(window.location.hash=`#${s}`),await j(a),B()}const t=R._extractParams.bind(R);R._extractParams=function(o,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),o.endsWith("/*")){const s=o.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(o,a)};const i=R._renderNotFound.bind(R);let e="";R._renderNotFound=async function(o){return m()&&e!==o&&(e=o,await U(),R._matchRoute(o).route)?(e="",R._handleRouteChange()):(e="",m()&&$("#admin-sidebar").get(0)?.style.display==="none"?(E.toast("That link goes nowhere - here is your home page.",{type:"warning"}),R.navigate("/")):i(o))},dt(R),R.init({container:"#view-container",routes:I,views:l,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}})})()});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LAST_USER_KEY="auth_last_user";const i=[/^auth_/,/^dm\.slideover\.width\./];export function isDraftKey(t){return typeof t!="string"||!t||t===LAST_USER_KEY||i.some(r=>r.test(t))?!1:/draft/i.test(t)}export function isScopedTo(t,r){return r?t.split(/[.:/@]/).includes(String(r)):!1}const f=[/^user-editor\.draft\.[^.]+$/];export function draftKeysToDrop(t,r,n){return!r||!n||String(r)===String(n)?[]:(t||[]).filter(e=>isDraftKey(e)&&(f.some(o=>o.test(e))||!(isScopedTo(e,r)||isScopedTo(e,n))))}export function claimDrafts(t){if(!t||typeof S>"u")return[];let r=[];try{const n=S.get(LAST_USER_KEY),e=typeof S.keys=="function"?S.keys():[];r=draftKeysToDrop(e,t,n);for(const o of r)S.remove(o);n!==String(t)&&S.set(LAST_USER_KEY,String(t))}catch{}return r}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as _,getUser as Ve}from"../api.js";import{addBannerAction as Te}from"../lib/plugin-chrome.js";import{resetPassword as Ge}from"../lib/password-reset-actions.js";import{helpPopovers as Je}from"../lib/help-popover.js";import{sidebarCount as Re}from"../lib/tool-kit.js";import{roleIndex as We,initials as Xe}from"../lib/users-arrange.js";import{fieldKind as ce,fieldOptions as ue}from"../lib/my-profile-arrange.js";import{buildUserPayload as ze,checkUserForm as Ne,draftOf as Qe,editRights as Ze,emailConflict as et,formFromUser as de,roleChoices as tt,snapshot as se}from"../lib/user-editor-arrange.js";import{fmtDate as pe,fmtDateTime as at}from"../lib/dates.js";const st=/^\/users\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,oe=a=>String(a||"").split("?")[0].replace(/\/$/,""),p=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),ot=`<button type="button" class="uex-pick-chip" data-role="{{name}}" data-bind-class="on && 'is-on'" data-bind-disabled="disabled" title="{{title}}" data-bind-aria-pressed="on ? 'true' : 'false'">{{label}}</button>`,nt=`<button type="button" class="uex-pick-chip" data-project="{{slug}}" data-bind-class="on && 'is-on'" data-bind-aria-pressed="on ? 'true' : 'false'" title="{{title}}">{{name}}</button>`;let y=null,Oe=!1;function lt(){Oe||!window.R?.use||(Oe=!0,R.use((a,l,v)=>{const c=oe(l?.path);return!(st.test(l?.path||"")&&oe(a?.path)!==c)||!y?.dirty()||y.path!==c?v():E.confirm("You have unsaved changes to this user. Leave anyway? They are kept for your next visit - except a password, which is typed again.").then(A=>{A?v():history.replaceState(null,"",`#${c}`)})}),M.subscribe("router:afterChange",({to:a}={})=>{if(y&&oe(a?.path)!==y.path){const l=y;y=null,l.dispose()}}))}function rt(a,l){const v=ce(a.type),c=`uex-pf-${l}`,s=a.required?' <span class="text-danger">*</span>':"",A=a.help||a.description?` <span data-help="${p(a.help||a.description)}"></span>`:"",T=`<label class="form-label" for="${c}">${p(a.label||a.name)}${s}${A}</label>`,i=`<small class="uex-err" data-bind-hidden="!pe_${l}.value" data-bind-text="pe_${l}.value"></small>`,K=a.placeholder?` placeholder="${p(a.placeholder)}"`:"";switch(v){case"textarea":return`<div class="uex-wide">${T}<textarea id="${c}" class="form-input" rows="3"${K} data-model="pf_${l}.value"></textarea>${i}</div>`;case"select":return`<div>${T}<select id="${c}" class="form-input" data-model="pf_${l}.value"><option value="">-</option>${ue(a).map(m=>`<option value="${p(m.value)}">${p(m.label)}</option>`).join("")}</select>${i}</div>`;case"checkbox":return`<div><label class="uex-check"><input id="${c}" type="checkbox" data-model="pf_${l}.value"> ${p(a.label||a.name)}${s}</label>${A}${i}</div>`;case"multi":return`<div class="uex-wide">${T}<div data-multi="${l}">${ue(a).map((m,u)=>`<label class="uex-check"><input type="checkbox" data-opt="${p(m.value)}" data-bind-checked="po_${l}_${u}.value"> ${p(m.label)}</label>`).join("")}</div>${i}</div>`;default:return`<div>${T}<input id="${c}" type="${v}" class="form-input"${K} data-model="pf_${l}.value">${i}</div>`}}function ve(a,l){const v=a.querySelector(".dm-plugin-banner-name");if(!v)return;const c=v.querySelector(".dm-plugin-banner-parent")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-parent",textContent:"Users"}),s=v.querySelector(".dm-plugin-banner-sep")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-sep",textContent:"\u203A"});s.setAttribute("aria-hidden","true"),v.replaceChildren(c,s,document.createTextNode(l))}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(a){if(lt(),y){const e=y;y=null,e.dispose()}const l=oe(location.hash.slice(1)),v=l.match(/^\/users\/edit\/([^/?#]+)/),c=v?decodeURIComponent(v[1]):null,s=!c,A=s&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",T=[],i=e=>(T.push(e),e);let K=!1;const m=a.get(0),u=m.querySelector(".uex")||m,N=()=>!K&&u.isConnected,B=M.observable(!0),me=M.observable(""),Y=M.observable(!1),f=M.observable(null),q=M.observable(""),w=M.observable(""),J=M.observable(""),_e=M.observable(!1),j=M.observable("user"),b=M.observable([]),W=M.observable(""),C=M.observable([]),X=M.observable(!0),U=M.observable(""),V=M.observable(""),fe=M.observable(!1),he=M.observable(!1),be=M.observable([]),ge=M.observable([]),$=M.observable({self:!1,canEdit:!0,reason:""}),D=M.observable([]);let x={};const ke=M.observable([]);let g=[];const O=new Map,ye=M.observable(0),k=M.observable(de(null,[])),Be=()=>(ye.value,Object.fromEntries([...O].map(([e,t])=>[e,t.value]))),qe=()=>Object.fromEntries([...O].map(([e,t])=>[e,t.peek()])),we=()=>({name:q.value,email:w.value,password:J.value,role:j.value,additionalRoles:b.value,ownedByProject:W.value,projects:C.value,isActive:X.value,profile:Be()}),ne=()=>({name:q.peek(),email:w.peek(),password:J.peek(),role:j.peek(),additionalRoles:b.peek(),ownedByProject:W.peek(),projects:C.peek(),isActive:X.peek(),profile:qe()}),z=M.computed(()=>Ne(we(),{isNew:s,fields:g,saved:s?void 0:k.value,choices:D.value})),H=M.computed(()=>!B.value&&$.value.canEdit&&se(we())!==se(k.value)),F=M.computed(()=>!$.value.canEdit);i(M.effect(()=>{const e=new Set(b.value);be.value=D.value.filter(t=>t.name!==j.value).map(t=>({name:t.name,label:t.label,on:e.has(t.name),disabled:!t.assignable&&!e.has(t.name),title:t.assignable||e.has(t.name)?`Level ${t.level}`:t.reason}))})),i(M.effect(()=>{const e=new Set(C.value);ge.value=ke.value.map(t=>({slug:t.slug,name:t.name||t.slug,on:e.has(t.slug),title:t.slug}))}));const le={loading:B,loadError:me,saving:Y,name:q,email:w,password:J,showPw:_e,role:j,ownedByProject:W,isActive:X,hasProfile:fe,hasProjects:he,extraChips:be,projectChips:ge,err:z,dirty:H,locked:F,isSelf:M.computed(()=>$.value.self),lockedReason:M.computed(()=>$.value.reason),emailErr:M.computed(()=>z.value.email||(U.value&&w.value.trim().toLowerCase()===V.value?U.value:"")),cannotSave:M.computed(()=>Y.value||F.value||!H.value||Object.keys(z.value).length>0||!!(U.value&&w.value.trim().toLowerCase()===V.value)),saveText:s?"Create user":"Save",dirtyText:s?"This user has not been created yet.":"You have unsaved changes.",pwLabel:M.computed(()=>s?"Password":"New password"),pwPlaceholder:M.computed(()=>s?"At least 8 characters":"Leave empty to keep it"),avatar:M.computed(()=>Xe({name:q.value,email:w.value})),shownName:M.computed(()=>q.value.trim()||(s?"New user":f.value?.name||"")),shownEmail:M.computed(()=>w.value.trim()||""),roleText:M.computed(()=>[j.value,...b.value.filter(t=>t!==j.value)].filter(Boolean).map(t=>x[t]?.label||t).join(" + ")||"-"),accessHint:M.computed(()=>C.value.length?`Sees only ${C.value.length===1?"that project":"those projects"}, plus Core.`:"Nothing ticked: no restriction - sees every project."),factsText:M.computed(()=>{const e=f.value;return s||!e?"Not created yet":[e.createdAt&&`Added ${pe(e.createdAt)}`,e.lastLogin?`last signed in ${pe(e.lastLogin)}`:"never signed in",e.updatedAt&&`changed ${pe(e.updatedAt)}`].filter(Boolean).join(" \xB7 ")})},Q=`user-editor.draft.${c||"new"}`;function Z(e){q.value=e.name,w.value=e.email,J.value="",j.value=e.role,b.value=[...e.additionalRoles||[]],W.value=e.ownedByProject||"",C.value=[...e.projects||[]],X.value=e.isActive!==!1;for(const[t,o]of O)t in(e.profile||{})&&(o.value=e.profile[t])}function Ie(e,t){const o=et(e);return o?(U.value=o,V.value=String(t||"").trim().toLowerCase(),u.querySelector("#uex-email")?.focus(),!0):!1}async function re(){if(Y.peek()||B.peek()||F.peek())return;const e=ne(),t=Ne(e,{isNew:s,fields:g,saved:s?void 0:k.peek(),choices:D.peek()});if(Object.keys(t).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const o=ze(k.peek(),e,{isNew:s,fields:g,meta:f.peek()?.meta||{}});if(!o){E.toast("Nothing has changed.",{type:"info"});return}if(!(!s&&$.peek().self&&o.update.role&&(x[o.update.role]?.level??99)>(x[k.peek().role]?.level??99)&&!await E.confirm(`Give yourself the less senior role "${p(x[o.update.role]?.label||o.update.role)}"? You will not be able to raise it again yourself.`))){Y.value=!0;try{if(s){const d=await _.users.create(o.create);if(!N())return;ee(),k.value=ne(),E.toast(`Created ${d?.name||e.name.trim()}.`,{type:"success"}),Re("#/users"),location.hash=d?.id?`#/users/edit/${encodeURIComponent(d.id)}`:"#/users";return}const n=await _.users.update(c,o.update);if(!N())return;f.value=n;const r=de(n,g);if(k.value=r,Z(r),ee(),ve(m,n.name||n.email||"Edit"),$.peek().self){try{S.set("auth_user",{...S.get("auth_user")||{},name:n.name,email:n.email})}catch{}const d=document.getElementById("topbar-user-name");d&&n.name&&(d.textContent=n.name)}Re("#/users"),E.toast("Saved.",{type:"success"})}catch(n){Ie(n,e.email)||E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{Y.value=!1}}}async function Ue(){H.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!N()||(Z(k.peek()),ee()))}function $e(){if(!H.peek())return;const e=Qe(ne());if(se({...e,password:""})!==se(k.peek()))try{S.set(Q,{form:e,at:new Date().toISOString(),basedOn:f.peek()?.updatedAt||null})}catch{}}function ee(){try{S.remove?S.remove(Q):S.set(Q,null)}catch{}}u.querySelector(".uex-roles").innerHTML=ot,u.querySelector(".uex-projects").innerHTML=nt;const Me=e=>{if(F.peek())return;const t=e.target.closest(".uex-pick-chip");if(!(!t||t.disabled)){if(t.dataset.role){const o=t.dataset.role,n=b.peek();b.value=n.includes(o)?n.filter(r=>r!==o):[...n,o]}else if(t.dataset.project){const o=t.dataset.project,n=C.peek();C.value=n.includes(o)?n.filter(r=>r!==o):[...n,o]}}};u.addEventListener("click",Me),i(()=>u.removeEventListener("click",Me)),i(M.effect(()=>{U.peek()&&w.value.trim().toLowerCase()!==V.peek()&&(U.value="",V.value="")})),i(M.effect(()=>{const e=j.value;b.peek().includes(e)&&(b.value=b.peek().filter(t=>t!==e))})),Te(a,{id:"user-editor-back",icon:"arrow-left",title:"Back to users",onClick:()=>{location.hash="#/users"}}),Te(a,{id:"user-editor-save",icon:"save",title:s?"Create user (Ctrl+S)":"Save (Ctrl+S)",onClick:()=>re()}),ve(m,s?"New user":"Edit");const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!N()||(e.preventDefault(),e.stopImmediatePropagation(),re())};document.addEventListener("keydown",Ee,!0),i(()=>document.removeEventListener("keydown",Ee,!0));const xe=e=>{H.peek()&&($e(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",xe),i(()=>window.removeEventListener("beforeunload",xe)),y={path:l,dirty:()=>H.peek(),dispose(){K=!0,$e(),T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}};let h=null,Se=null,je=null,L=[];try{[h,je,Se,L]=await Promise.all([s?Promise.resolve(null):_.users.get(c),_.collections.listEntries("roles",{limit:1e3}).catch(()=>null),_.collections.get("user-profiles").catch(()=>null),_.projects.list().catch(()=>[])])}catch(e){if(!N())return;me.value=e?.status===404?"There is no such user - it may have been deleted.":`Could not load this user: ${e.message||e}`,B.value=!1,I.scan(u);return}if(!N())return;L=Array.isArray(L)?L:[],ke.value=L,x=We(je?.entries||[]),Object.keys(x).length||(x={admin:{label:"Admin",level:1},user:{label:"User",level:2}});const Ce=Ve()||{};$.value=Ze(Ce,h,x),D.value=tt(x,Ce,h),f.value=h;const De=u.querySelector(".uex-role-select");De.innerHTML=D.peek().map(e=>`<option value="${p(e.name)}"${e.assignable?"":" disabled"}>${p(e.label)}${e.assignable?"":" - only a more senior role can give it"}</option>`).join("");const He=u.querySelector(".uex-owner-select");He.innerHTML=`<option value="">(none)</option>${L.map(e=>`<option value="${p(e.slug)}">${p(e.name||e.slug)}</option>`).join("")}`,he.value=L.length>0;const Le=D.peek().filter(e=>e.assignable).map(e=>e.name),Fe=Le.includes("user")?"user":Le.at(-1)||"user";g=(Se?.fields||[]).filter(e=>e&&e.name);const G=de(h,g,{project:L.some(e=>e.slug===A)?A:"",defaultRole:Fe});g.forEach(e=>O.set(e.name,M.observable(G.profile[e.name])));const Pe=async e=>{const t=f.peek();if(t&&await Ge(t,e))try{f.value={...t,...await _.users.get(t.id)}}catch{}};le.canReset=M.computed(()=>!s&&!B.value&&$.value.canEdit&&!$.value.self&&f.value?.isActive!==!1),le.resetOpen=M.computed(()=>!!f.value?.resetLinkExpires&&Date.parse(f.value.resetLinkExpires)>Date.now());const Ke=M.applyBindings(le,u,{methods:{save:re,discard:Ue,resetEmail:()=>Pe("email"),resetLink:()=>Pe("link")}});i(()=>Ke.dispose());const ie=g.map((e,t)=>({f:e,i:t})).filter(({f:e})=>ce(e.type)!=="hidden");if(ie.length){const e=u.querySelector(".uex-profile");e.innerHTML=ie.map(({f:r,i:d})=>rt(r,d)).join("");const t={};g.forEach((r,d)=>{t[`pf_${d}`]=O.get(r.name),t[`pe_${d}`]=M.computed(()=>z.value[`profile.${r.name}`]||""),ce(r.type)==="multi"&&ue(r).forEach((te,ae)=>{t[`po_${d}_${ae}`]=M.computed(()=>(O.get(r.name).value||[]).includes(te.value))})});const o=M.applyBindings(t,e);i(()=>o.dispose());const n=r=>{const d=r.target.closest("[data-multi]");if(!d||!r.target.matches("input[data-opt]"))return;const te=O.get(g[Number(d.dataset.multi)].name),ae=r.target.dataset.opt,Ae=te.peek()||[];te.value=r.target.checked?[...new Set([...Ae,ae])]:Ae.filter(Ye=>Ye!==ae)};e.addEventListener("change",n),i(()=>e.removeEventListener("change",n))}fe.value=ie.length>0,i(Je(u,{position:"bottom"})),k.value=G,Z(G),ye.value++,B.value=!1,ve(m,s?"New user":h.name||h.email||"Edit"),I.scan(u),F.peek()||u.querySelector("#uex-name")?.focus();let P=null;try{P=S.get(Q)}catch{}if(P?.form&&!F.peek()){ee();const e=P.at?` (${at(P.at)})`:"",t=!s&&P.basedOn&&h?.updatedAt&&P.basedOn!==h.updatedAt?" This user has been changed since - restoring puts your version over it.":"",o=s?"the new user you started":`"${p(h.name||h.email)}"`;await E.confirm(`You left unsaved changes to ${o}${e}.${t} Restore them?`)&&N()&&Z({...G,...P.form,profile:{...G.profile,...P.form.profile||{}}})}}};
|
|
1
|
+
import{api as O,getUser as Ge}from"../api.js";import{addBannerAction as Re}from"../lib/plugin-chrome.js";import{resetPassword as Je}from"../lib/password-reset-actions.js";import{helpPopovers as We}from"../lib/help-popover.js";import{sidebarCount as _e}from"../lib/tool-kit.js";import{roleIndex as Xe,initials as ze}from"../lib/users-arrange.js";import{fieldKind as ce,fieldOptions as ue}from"../lib/my-profile-arrange.js";import{buildUserPayload as Qe,checkUserForm as Ne,draftOf as Ze,editRights as et,emailConflict as tt,formFromUser as de,roleChoices as at,snapshot as se}from"../lib/user-editor-arrange.js";import{fmtDate as pe,fmtDateTime as st}from"../lib/dates.js";const ot=/^\/users\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,oe=a=>String(a||"").split("?")[0].replace(/\/$/,""),p=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),nt=`<button type="button" class="uex-pick-chip" data-role="{{name}}" data-bind-class="on && 'is-on'" data-bind-disabled="disabled" title="{{title}}" data-bind-aria-pressed="on ? 'true' : 'false'">{{label}}</button>`,rt=`<button type="button" class="uex-pick-chip" data-project="{{slug}}" data-bind-class="on && 'is-on'" data-bind-aria-pressed="on ? 'true' : 'false'" title="{{title}}">{{name}}</button>`;let y=null,Oe=!1;function lt(){Oe||!window.R?.use||(Oe=!0,R.use((a,r,v)=>{const c=oe(r?.path);return!(ot.test(r?.path||"")&&oe(a?.path)!==c)||!y?.dirty()||y.path!==c?v():E.confirm("You have unsaved changes to this user. Leave anyway? They are kept for your next visit - except a password, which is typed again.").then(A=>{A?v():history.replaceState(null,"",`#${c}`)})}),M.subscribe("router:afterChange",({to:a}={})=>{if(y&&oe(a?.path)!==y.path){const r=y;y=null,r.dispose()}}))}function it(a,r){const v=ce(a.type),c=`uex-pf-${r}`,s=a.required?' <span class="text-danger">*</span>':"",A=a.help||a.description?` <span data-help="${p(a.help||a.description)}"></span>`:"",T=`<label class="form-label" for="${c}">${p(a.label||a.name)}${s}${A}</label>`,i=`<small class="uex-err" data-bind-hidden="!pe_${r}.value" data-bind-text="pe_${r}.value"></small>`,K=a.placeholder?` placeholder="${p(a.placeholder)}"`:"";switch(v){case"textarea":return`<div class="uex-wide">${T}<textarea id="${c}" class="form-input" rows="3"${K} data-model="pf_${r}.value"></textarea>${i}</div>`;case"select":return`<div>${T}<select id="${c}" class="form-input" data-model="pf_${r}.value"><option value="">-</option>${ue(a).map(m=>`<option value="${p(m.value)}">${p(m.label)}</option>`).join("")}</select>${i}</div>`;case"checkbox":return`<div><label class="uex-check"><input id="${c}" type="checkbox" data-model="pf_${r}.value"> ${p(a.label||a.name)}${s}</label>${A}${i}</div>`;case"multi":return`<div class="uex-wide">${T}<div data-multi="${r}">${ue(a).map((m,u)=>`<label class="uex-check"><input type="checkbox" data-opt="${p(m.value)}" data-bind-checked="po_${r}_${u}.value"> ${p(m.label)}</label>`).join("")}</div>${i}</div>`;default:return`<div>${T}<input id="${c}" type="${v}" class="form-input"${K} data-model="pf_${r}.value">${i}</div>`}}function ve(a,r){const v=a.querySelector(".dm-plugin-banner-name");if(!v)return;const c=v.querySelector(".dm-plugin-banner-parent")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-parent",textContent:"Users"}),s=v.querySelector(".dm-plugin-banner-sep")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-sep",textContent:"\u203A"});s.setAttribute("aria-hidden","true"),v.replaceChildren(c,s,document.createTextNode(r))}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(a){if(lt(),y){const e=y;y=null,e.dispose()}const r=oe(location.hash.slice(1)),v=r.match(/^\/users\/edit\/([^/?#]+)/),c=v?decodeURIComponent(v[1]):null,s=!c,A=s&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",T=[],i=e=>(T.push(e),e);let K=!1;const m=a.get(0),u=m.querySelector(".uex")||m,_=()=>!K&&u.isConnected,B=M.observable(!0),me=M.observable(""),Y=M.observable(!1),f=M.observable(null),q=M.observable(""),w=M.observable(""),J=M.observable(""),Be=M.observable(!1),j=M.observable("user"),b=M.observable([]),W=M.observable(""),C=M.observable([]),X=M.observable(!0),U=M.observable(""),V=M.observable(""),fe=M.observable(!1),he=M.observable(!1),be=M.observable([]),ge=M.observable([]),$=M.observable({self:!1,canEdit:!0,reason:""}),D=M.observable([]);let x={};const ke=M.observable([]);let g=[];const N=new Map,ye=M.observable(0),k=M.observable(de(null,[])),qe=()=>(ye.value,Object.fromEntries([...N].map(([e,t])=>[e,t.value]))),Ie=()=>Object.fromEntries([...N].map(([e,t])=>[e,t.peek()])),we=()=>({name:q.value,email:w.value,password:J.value,role:j.value,additionalRoles:b.value,ownedByProject:W.value,projects:C.value,isActive:X.value,profile:qe()}),ne=()=>({name:q.peek(),email:w.peek(),password:J.peek(),role:j.peek(),additionalRoles:b.peek(),ownedByProject:W.peek(),projects:C.peek(),isActive:X.peek(),profile:Ie()}),z=M.computed(()=>Ne(we(),{isNew:s,fields:g,saved:s?void 0:k.value,choices:D.value})),H=M.computed(()=>!B.value&&$.value.canEdit&&se(we())!==se(k.value)),F=M.computed(()=>!$.value.canEdit);i(M.effect(()=>{const e=new Set(b.value);be.value=D.value.filter(t=>t.name!==j.value).map(t=>({name:t.name,label:t.label,on:e.has(t.name),disabled:!t.assignable&&!e.has(t.name),title:t.assignable||e.has(t.name)?`Level ${t.level}`:t.reason}))})),i(M.effect(()=>{const e=new Set(C.value);ge.value=ke.value.map(t=>({slug:t.slug,name:t.name||t.slug,on:e.has(t.slug),title:t.slug}))}));const re={loading:B,loadError:me,saving:Y,name:q,email:w,password:J,showPw:Be,role:j,ownedByProject:W,isActive:X,hasProfile:fe,hasProjects:he,extraChips:be,projectChips:ge,err:z,dirty:H,locked:F,isSelf:M.computed(()=>$.value.self),lockedReason:M.computed(()=>$.value.reason),emailErr:M.computed(()=>z.value.email||(U.value&&w.value.trim().toLowerCase()===V.value?U.value:"")),cannotSave:M.computed(()=>Y.value||F.value||!H.value||Object.keys(z.value).length>0||!!(U.value&&w.value.trim().toLowerCase()===V.value)),saveText:s?"Create user":"Save",dirtyText:s?"This user has not been created yet.":"You have unsaved changes.",pwLabel:M.computed(()=>s?"Password":"New password"),pwPlaceholder:M.computed(()=>s?"At least 8 characters":"Leave empty to keep it"),avatar:M.computed(()=>ze({name:q.value,email:w.value})),shownName:M.computed(()=>q.value.trim()||(s?"New user":f.value?.name||"")),shownEmail:M.computed(()=>w.value.trim()||""),roleText:M.computed(()=>[j.value,...b.value.filter(t=>t!==j.value)].filter(Boolean).map(t=>x[t]?.label||t).join(" + ")||"-"),accessHint:M.computed(()=>C.value.length?`Sees only ${C.value.length===1?"that project":"those projects"}, plus Core.`:"Nothing ticked: no restriction - sees every project."),factsText:M.computed(()=>{const e=f.value;return s||!e?"Not created yet":[e.createdAt&&`Added ${pe(e.createdAt)}`,e.lastLogin?`last signed in ${pe(e.lastLogin)}`:"never signed in",e.updatedAt&&`changed ${pe(e.updatedAt)}`].filter(Boolean).join(" \xB7 ")})};let $e="";try{$e=S.get("auth_user")?.id||""}catch{}const Q=`user-editor.draft.${$e||"anyone"}.for-${c||"new"}`;function Z(e){q.value=e.name,w.value=e.email,J.value="",j.value=e.role,b.value=[...e.additionalRoles||[]],W.value=e.ownedByProject||"",C.value=[...e.projects||[]],X.value=e.isActive!==!1;for(const[t,o]of N)t in(e.profile||{})&&(o.value=e.profile[t])}function Ue(e,t){const o=tt(e);return o?(U.value=o,V.value=String(t||"").trim().toLowerCase(),u.querySelector("#uex-email")?.focus(),!0):!1}async function le(){if(Y.peek()||B.peek()||F.peek())return;const e=ne(),t=Ne(e,{isNew:s,fields:g,saved:s?void 0:k.peek(),choices:D.peek()});if(Object.keys(t).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const o=Qe(k.peek(),e,{isNew:s,fields:g,meta:f.peek()?.meta||{}});if(!o){E.toast("Nothing has changed.",{type:"info"});return}if(!(!s&&$.peek().self&&o.update.role&&(x[o.update.role]?.level??99)>(x[k.peek().role]?.level??99)&&!await E.confirm(`Give yourself the less senior role "${p(x[o.update.role]?.label||o.update.role)}"? You will not be able to raise it again yourself.`))){Y.value=!0;try{if(s){const d=await O.users.create(o.create);if(!_())return;ee(),k.value=ne(),E.toast(`Created ${d?.name||e.name.trim()}.`,{type:"success"}),_e("#/users"),location.hash=d?.id?`#/users/edit/${encodeURIComponent(d.id)}`:"#/users";return}const n=await O.users.update(c,o.update);if(!_())return;f.value=n;const l=de(n,g);if(k.value=l,Z(l),ee(),ve(m,n.name||n.email||"Edit"),$.peek().self){try{S.set("auth_user",{...S.get("auth_user")||{},name:n.name,email:n.email})}catch{}const d=document.getElementById("topbar-user-name");d&&n.name&&(d.textContent=n.name)}_e("#/users"),E.toast("Saved.",{type:"success"})}catch(n){Ue(n,e.email)||E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{Y.value=!1}}}async function De(){H.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!_()||(Z(k.peek()),ee()))}function Me(){if(!H.peek())return;const e=Ze(ne());if(se({...e,password:""})!==se(k.peek()))try{S.set(Q,{form:e,at:new Date().toISOString(),basedOn:f.peek()?.updatedAt||null})}catch{}}function ee(){try{S.remove?S.remove(Q):S.set(Q,null)}catch{}}u.querySelector(".uex-roles").innerHTML=nt,u.querySelector(".uex-projects").innerHTML=rt;const Ee=e=>{if(F.peek())return;const t=e.target.closest(".uex-pick-chip");if(!(!t||t.disabled)){if(t.dataset.role){const o=t.dataset.role,n=b.peek();b.value=n.includes(o)?n.filter(l=>l!==o):[...n,o]}else if(t.dataset.project){const o=t.dataset.project,n=C.peek();C.value=n.includes(o)?n.filter(l=>l!==o):[...n,o]}}};u.addEventListener("click",Ee),i(()=>u.removeEventListener("click",Ee)),i(M.effect(()=>{U.peek()&&w.value.trim().toLowerCase()!==V.peek()&&(U.value="",V.value="")})),i(M.effect(()=>{const e=j.value;b.peek().includes(e)&&(b.value=b.peek().filter(t=>t!==e))})),Re(a,{id:"user-editor-back",icon:"arrow-left",title:"Back to users",onClick:()=>{location.hash="#/users"}}),Re(a,{id:"user-editor-save",icon:"save",title:s?"Create user (Ctrl+S)":"Save (Ctrl+S)",onClick:()=>le()}),ve(m,s?"New user":"Edit");const xe=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!_()||(e.preventDefault(),e.stopImmediatePropagation(),le())};document.addEventListener("keydown",xe,!0),i(()=>document.removeEventListener("keydown",xe,!0));const Se=e=>{H.peek()&&(Me(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Se),i(()=>window.removeEventListener("beforeunload",Se)),y={path:r,dirty:()=>H.peek(),dispose(){K=!0,Me(),T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}};let h=null,je=null,Ce=null,L=[];try{[h,Ce,je,L]=await Promise.all([s?Promise.resolve(null):O.users.get(c),O.collections.listEntries("roles",{limit:1e3}).catch(()=>null),O.collections.get("user-profiles").catch(()=>null),O.projects.list().catch(()=>[])])}catch(e){if(!_())return;me.value=e?.status===404?"There is no such user - it may have been deleted.":`Could not load this user: ${e.message||e}`,B.value=!1,I.scan(u);return}if(!_())return;L=Array.isArray(L)?L:[],ke.value=L,x=Xe(Ce?.entries||[]),Object.keys(x).length||(x={admin:{label:"Admin",level:1},user:{label:"User",level:2}});const Le=Ge()||{};$.value=et(Le,h,x),D.value=at(x,Le,h),f.value=h;const He=u.querySelector(".uex-role-select");He.innerHTML=D.peek().map(e=>`<option value="${p(e.name)}"${e.assignable?"":" disabled"}>${p(e.label)}${e.assignable?"":" - only a more senior role can give it"}</option>`).join("");const Fe=u.querySelector(".uex-owner-select");Fe.innerHTML=`<option value="">(none)</option>${L.map(e=>`<option value="${p(e.slug)}">${p(e.name||e.slug)}</option>`).join("")}`,he.value=L.length>0;const Pe=D.peek().filter(e=>e.assignable).map(e=>e.name),Ke=Pe.includes("user")?"user":Pe.at(-1)||"user";g=(je?.fields||[]).filter(e=>e&&e.name);const G=de(h,g,{project:L.some(e=>e.slug===A)?A:"",defaultRole:Ke});g.forEach(e=>N.set(e.name,M.observable(G.profile[e.name])));const Ae=async e=>{const t=f.peek();if(t&&await Je(t,e))try{f.value={...t,...await O.users.get(t.id)}}catch{}};re.canReset=M.computed(()=>!s&&!B.value&&$.value.canEdit&&!$.value.self&&f.value?.isActive!==!1),re.resetOpen=M.computed(()=>!!f.value?.resetLinkExpires&&Date.parse(f.value.resetLinkExpires)>Date.now());const Ye=M.applyBindings(re,u,{methods:{save:le,discard:De,resetEmail:()=>Ae("email"),resetLink:()=>Ae("link")}});i(()=>Ye.dispose());const ie=g.map((e,t)=>({f:e,i:t})).filter(({f:e})=>ce(e.type)!=="hidden");if(ie.length){const e=u.querySelector(".uex-profile");e.innerHTML=ie.map(({f:l,i:d})=>it(l,d)).join("");const t={};g.forEach((l,d)=>{t[`pf_${d}`]=N.get(l.name),t[`pe_${d}`]=M.computed(()=>z.value[`profile.${l.name}`]||""),ce(l.type)==="multi"&&ue(l).forEach((te,ae)=>{t[`po_${d}_${ae}`]=M.computed(()=>(N.get(l.name).value||[]).includes(te.value))})});const o=M.applyBindings(t,e);i(()=>o.dispose());const n=l=>{const d=l.target.closest("[data-multi]");if(!d||!l.target.matches("input[data-opt]"))return;const te=N.get(g[Number(d.dataset.multi)].name),ae=l.target.dataset.opt,Te=te.peek()||[];te.value=l.target.checked?[...new Set([...Te,ae])]:Te.filter(Ve=>Ve!==ae)};e.addEventListener("change",n),i(()=>e.removeEventListener("change",n))}fe.value=ie.length>0,i(We(u,{position:"bottom"})),k.value=G,Z(G),ye.value++,B.value=!1,ve(m,s?"New user":h.name||h.email||"Edit"),I.scan(u),F.peek()||u.querySelector("#uex-name")?.focus();let P=null;try{P=S.get(Q)}catch{}if(P?.form&&!F.peek()){ee();const e=P.at?` (${st(P.at)})`:"",t=!s&&P.basedOn&&h?.updatedAt&&P.basedOn!==h.updatedAt?" This user has been changed since - restoring puts your version over it.":"",o=s?"the new user you started":`"${p(h.name||h.email)}"`;await E.confirm(`You left unsaved changes to ${o}${e}.${t} Restore them?`)&&_()&&Z({...G,...P.form,profile:{...G.profile,...P.form.profile||{}}})}}};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "domma-cms",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.60",
|
|
4
4
|
"description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/server.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@fastify/rate-limit": "^10.3.0",
|
|
85
85
|
"@fastify/static": "9.1.1",
|
|
86
86
|
"bcryptjs": "^3.0.3",
|
|
87
|
-
"domma-js": "0.52.
|
|
87
|
+
"domma-js": "0.52.3",
|
|
88
88
|
"dotenv": "^17.2.3",
|
|
89
89
|
"fastify": "5.8.5",
|
|
90
90
|
"gray-matter": "^4.0.3",
|
|
@@ -76,6 +76,7 @@ import {applyTemplate, expandTemplate} from './templates.js';
|
|
|
76
76
|
import {makeResizable} from './resizable.js';
|
|
77
77
|
import {whenClosed} from './when-closed.js';
|
|
78
78
|
import {esc} from './reader-kit.js';
|
|
79
|
+
import {installFontControls} from './font-controls.js';
|
|
79
80
|
import {
|
|
80
81
|
collectGroups,
|
|
81
82
|
currentFragment,
|
|
@@ -568,10 +569,12 @@ export function openCompose({
|
|
|
568
569
|
bodyInput.value = draft.body ?? '';
|
|
569
570
|
|
|
570
571
|
let editor = null;
|
|
572
|
+
let fontControls = null;
|
|
571
573
|
if (typeof E?.editor === 'function') {
|
|
572
574
|
try {
|
|
573
575
|
editor = E.editor(editorHost, {mode: 'rich', height: '34vh'});
|
|
574
576
|
editor.setValue(draft.html || textToHtml(draft.body ?? ''));
|
|
577
|
+
try { fontControls = installFontControls(editor); } catch { fontControls = null; }
|
|
575
578
|
} catch {
|
|
576
579
|
editor = null;
|
|
577
580
|
}
|
|
@@ -930,6 +933,9 @@ export function openCompose({
|
|
|
930
933
|
// The editor is a live component with its own listeners and autosave.
|
|
931
934
|
whenClosed(slideover, () => {
|
|
932
935
|
closed = true;
|
|
936
|
+
// Before the editor: it closes an open menu (left on <body> by a
|
|
937
|
+
// Ctrl+Enter send) and drops its dropdowns and window listener.
|
|
938
|
+
fontControls?.destroy();
|
|
933
939
|
editor?.destroy?.();
|
|
934
940
|
stopResizing();
|
|
935
941
|
for (const suggester of suggesters) suggester.destroy?.();
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font and size menus for the mail editor (Domma's `E.editor`, rich mode).
|
|
3
|
+
*
|
|
4
|
+
* Domma's editor has no font controls of its own, so these are added to its
|
|
5
|
+
* toolbar here - the same way Domma's own editor extensions do, through the
|
|
6
|
+
* instance's `_toolbarEl` / `_editorEl`. The lists are `fonts.js`.
|
|
7
|
+
*
|
|
8
|
+
* The style is applied by hand, not with `execCommand('fontName')`: that
|
|
9
|
+
* command writes `<font>` tags, and re-styling part of an already-styled run
|
|
10
|
+
* makes the browser push the parent's style down as more `<font>` tags. The
|
|
11
|
+
* sanitiser drops `<font>` (so signatures and templates lost their faces) and
|
|
12
|
+
* mail carried them out. Instead every text node in the selection gets the
|
|
13
|
+
* property on its own span - an existing span it alone fills, or a new one -
|
|
14
|
+
* written as a plain string so the stack keeps its single quotes. With only a
|
|
15
|
+
* caret, an empty span (holding a zero-width space until something is typed)
|
|
16
|
+
* takes the style for what comes next. Any `<font>` that arrives anyway, by
|
|
17
|
+
* paste, is turned into a span on the next input.
|
|
18
|
+
*/
|
|
19
|
+
import {FONT_SIZES, findFont, fontTagStyle, groupedFonts, setStyleProperty, splitStyle} from './fonts.js';
|
|
20
|
+
import {esc} from './reader-kit.js';
|
|
21
|
+
|
|
22
|
+
const ZWSP = '';
|
|
23
|
+
const CARET_ATTR = 'data-dm-font-caret';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add the Font and Size menus to a rich editor's toolbar.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} editor - an `E.editor(..., {mode: 'rich'})` instance
|
|
29
|
+
* @returns {{destroy: Function}|null} null where the editor or E.dropdown is absent
|
|
30
|
+
*/
|
|
31
|
+
export function installFontControls(editor) {
|
|
32
|
+
const content = editor?._editorEl;
|
|
33
|
+
const toolbar = editor?._toolbarEl;
|
|
34
|
+
if (!content || !toolbar || typeof E?.dropdown !== 'function') return null;
|
|
35
|
+
|
|
36
|
+
// The selection as it was when a menu was opened - taken from the live
|
|
37
|
+
// selection at that moment, so nothing an earlier apply moved is stale.
|
|
38
|
+
let saved = null;
|
|
39
|
+
const capture = () => {
|
|
40
|
+
const selection = document.getSelection();
|
|
41
|
+
if (!selection?.rangeCount) return;
|
|
42
|
+
const range = selection.getRangeAt(0);
|
|
43
|
+
if (content.contains(range.commonAncestorContainer)) saved = range.cloneRange();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const restore = () => {
|
|
47
|
+
content.focus({preventScroll: true});
|
|
48
|
+
const selection = document.getSelection();
|
|
49
|
+
const range = saved && content.contains(saved.startContainer) && content.contains(saved.endContainer)
|
|
50
|
+
? saved
|
|
51
|
+
: endOf(content);
|
|
52
|
+
selection.removeAllRanges();
|
|
53
|
+
selection.addRange(range);
|
|
54
|
+
return range;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const setStyle = (el, prop, value) => {
|
|
58
|
+
const style = setStyleProperty(el.getAttribute('style') ?? '', prop, value);
|
|
59
|
+
if (style) el.setAttribute('style', style); else el.removeAttribute('style');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Style every text node in a (non-collapsed) range. */
|
|
63
|
+
const styleRange = (range, prop, value) => {
|
|
64
|
+
// Split the boundary text nodes; a live range follows the split.
|
|
65
|
+
if (range.endContainer.nodeType === Node.TEXT_NODE && range.endOffset < range.endContainer.length) {
|
|
66
|
+
range.endContainer.splitText(range.endOffset);
|
|
67
|
+
}
|
|
68
|
+
if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
|
|
69
|
+
range.startContainer.splitText(range.startOffset);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const nodes = [];
|
|
73
|
+
const root = range.commonAncestorContainer;
|
|
74
|
+
if (root.nodeType === Node.TEXT_NODE) {
|
|
75
|
+
nodes.push(root);
|
|
76
|
+
} else {
|
|
77
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
78
|
+
let node;
|
|
79
|
+
while ((node = walker.nextNode())) {
|
|
80
|
+
if (!node.length || !range.intersectsNode(node)) continue;
|
|
81
|
+
if (node === range.endContainer && range.endOffset === 0) continue;
|
|
82
|
+
if (node === range.startContainer && range.startOffset >= node.length) continue;
|
|
83
|
+
nodes.push(node);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!nodes.length) return;
|
|
87
|
+
|
|
88
|
+
for (const node of nodes) {
|
|
89
|
+
const parent = node.parentNode;
|
|
90
|
+
if (parent.nodeName === 'SPAN' && parent !== content && parent.childNodes.length === 1) {
|
|
91
|
+
setStyle(parent, prop, value);
|
|
92
|
+
} else {
|
|
93
|
+
const span = document.createElement('span');
|
|
94
|
+
span.setAttribute('style', `${prop}: ${value}`);
|
|
95
|
+
parent.insertBefore(span, node);
|
|
96
|
+
span.appendChild(node);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
range.setStart(nodes[0], 0);
|
|
101
|
+
range.setEnd(nodes.at(-1), nodes.at(-1).length);
|
|
102
|
+
const selection = document.getSelection();
|
|
103
|
+
selection.removeAllRanges();
|
|
104
|
+
selection.addRange(range);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** A caret: the style goes on a placeholder span for what is typed next. */
|
|
108
|
+
const styleCaret = (range, prop, value) => {
|
|
109
|
+
const holder = range.startContainer.parentElement?.closest?.(`[${CARET_ATTR}]`);
|
|
110
|
+
if (holder && content.contains(holder)) {
|
|
111
|
+
setStyle(holder, prop, value);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const span = document.createElement('span');
|
|
115
|
+
span.setAttribute(CARET_ATTR, '');
|
|
116
|
+
span.setAttribute('style', `${prop}: ${value}`);
|
|
117
|
+
const text = document.createTextNode(ZWSP);
|
|
118
|
+
span.appendChild(text);
|
|
119
|
+
range.insertNode(span);
|
|
120
|
+
const selection = document.getSelection();
|
|
121
|
+
selection.removeAllRanges();
|
|
122
|
+
selection.collapse(text, 1);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Keep the editor free of <font> and of placeholder clutter. Runs after
|
|
127
|
+
* every apply and on every input, before Domma's own input listener
|
|
128
|
+
* records the content (capture on the parent).
|
|
129
|
+
*/
|
|
130
|
+
const tidy = () => {
|
|
131
|
+
const selection = document.getSelection();
|
|
132
|
+
const caret = selection?.rangeCount
|
|
133
|
+
? [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset]
|
|
134
|
+
: null;
|
|
135
|
+
const swapped = new Map();
|
|
136
|
+
|
|
137
|
+
for (const el of [...content.querySelectorAll('font')]) {
|
|
138
|
+
const style = splitStyle(el.getAttribute('style') ?? '')
|
|
139
|
+
.reduce((acc, [p, v]) => setStyleProperty(acc, p, v), fontTagStyle({
|
|
140
|
+
face: el.getAttribute('face'),
|
|
141
|
+
size: el.getAttribute('size'),
|
|
142
|
+
color: el.getAttribute('color')
|
|
143
|
+
}));
|
|
144
|
+
const span = document.createElement('span');
|
|
145
|
+
if (style) span.setAttribute('style', style);
|
|
146
|
+
while (el.firstChild) span.appendChild(el.firstChild);
|
|
147
|
+
el.replaceWith(span);
|
|
148
|
+
swapped.set(el, span);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Moving the text out of a <font> collapses a caret inside it; the
|
|
152
|
+
// nodes survive the move, so the caret goes back onto them.
|
|
153
|
+
if (caret && swapped.size) {
|
|
154
|
+
const node = n => swapped.get(n) ?? n;
|
|
155
|
+
const [anchor, anchorOffset, focus, focusOffset] = caret;
|
|
156
|
+
if (content.contains(node(anchor)) && content.contains(node(focus))) {
|
|
157
|
+
selection.setBaseAndExtent(node(anchor), anchorOffset, node(focus), focusOffset);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const live = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;
|
|
162
|
+
for (const holder of [...content.querySelectorAll(`[${CARET_ATTR}]`)]) {
|
|
163
|
+
const typed = holder.textContent.replaceAll(ZWSP, '');
|
|
164
|
+
if (typed) {
|
|
165
|
+
// Something was typed: drop the placeholder character. A live
|
|
166
|
+
// selection follows deleteData, so the caret stays put.
|
|
167
|
+
const walker = document.createTreeWalker(holder, NodeFilter.SHOW_TEXT);
|
|
168
|
+
let text;
|
|
169
|
+
while ((text = walker.nextNode())) {
|
|
170
|
+
let at;
|
|
171
|
+
while ((at = text.data.indexOf(ZWSP)) >= 0) text.deleteData(at, 1);
|
|
172
|
+
}
|
|
173
|
+
holder.removeAttribute(CARET_ATTR);
|
|
174
|
+
} else if (!holder.contains(live)) {
|
|
175
|
+
holder.remove();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const scope = content.parentNode ?? content;
|
|
181
|
+
scope.addEventListener('input', tidy, true);
|
|
182
|
+
|
|
183
|
+
const apply = (prop, value) => {
|
|
184
|
+
const range = restore();
|
|
185
|
+
if (range.collapsed) styleCaret(range, prop, value);
|
|
186
|
+
else styleRange(range, prop, value);
|
|
187
|
+
tidy();
|
|
188
|
+
// Domma's editor keeps its undo history and counters from input.
|
|
189
|
+
content.dispatchEvent(new Event('input', {bubbles: true}));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ---- The menus ----------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
const group = document.createElement('div');
|
|
195
|
+
group.className = 'dm-editor-toolbar-group mail-font-controls';
|
|
196
|
+
|
|
197
|
+
const fontButton = menuButton('Font', 'Font - the reader sees the first face in its list they have');
|
|
198
|
+
const sizeButton = menuButton('Size', 'Font size');
|
|
199
|
+
group.append(fontButton, sizeButton);
|
|
200
|
+
toolbar.appendChild(group);
|
|
201
|
+
|
|
202
|
+
let currentFont = null;
|
|
203
|
+
let currentSize = null;
|
|
204
|
+
|
|
205
|
+
const fontItems = groupedFonts().flatMap(g => [
|
|
206
|
+
{header: g.label},
|
|
207
|
+
...g.fonts.map(font => ({label: font.label, value: font.id, stack: font.stack, webFont: font.webFont}))
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const fontMenu = E.dropdown(fontButton, {
|
|
211
|
+
items: fontItems,
|
|
212
|
+
itemTemplate: item => `<span class="mail-font-item" data-mail-menu="font" style="font-family: ${esc(item.stack)}">${esc(item.label)}</span>`
|
|
213
|
+
+ (item.webFont ? '<span class="mail-font-web" title="Not loaded into the message - readers without it see the fallback">web</span>' : ''),
|
|
214
|
+
onSelect: ({value}) => {
|
|
215
|
+
const font = findFont(value);
|
|
216
|
+
if (!font) return;
|
|
217
|
+
apply('font-family', font.stack);
|
|
218
|
+
currentFont = font.id;
|
|
219
|
+
fontButton.textContent = font.label;
|
|
220
|
+
fontButton.style.fontFamily = font.stack;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const sizeMenu = E.dropdown(sizeButton, {
|
|
225
|
+
items: FONT_SIZES.map(px => ({label: `${px}px`, value: String(px)})),
|
|
226
|
+
itemTemplate: item => `<span class="mail-size-item" data-mail-menu="size">${esc(item.label)}</span>`,
|
|
227
|
+
onSelect: ({value}) => {
|
|
228
|
+
const px = Number(value);
|
|
229
|
+
if (!FONT_SIZES.includes(px)) return;
|
|
230
|
+
apply('font-size', `${px}px`);
|
|
231
|
+
currentSize = String(px);
|
|
232
|
+
sizeButton.textContent = `${px}px`;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const menus = [
|
|
237
|
+
{kind: 'font', dropdown: fontMenu, button: fontButton, current: () => currentFont},
|
|
238
|
+
{kind: 'size', dropdown: sizeMenu, button: sizeButton, current: () => currentSize}
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const menuElement = kind => document
|
|
242
|
+
.querySelector(`.domma-dropdown-menu.show [data-mail-menu="${kind}"]`)
|
|
243
|
+
?.closest('.domma-dropdown-menu') ?? null;
|
|
244
|
+
|
|
245
|
+
const closeMenu = (menu, focusButton) => {
|
|
246
|
+
menu.dropdown.close();
|
|
247
|
+
if (focusButton) menu.button.focus();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Keyboard inside an open menu. Every key handled here stops here, so an
|
|
251
|
+
// Enter that picks a font can never reach the compose window's send keys.
|
|
252
|
+
const onMenuKey = (menu, event) => {
|
|
253
|
+
const items = [...event.currentTarget.querySelectorAll('.domma-dropdown-item')];
|
|
254
|
+
const index = items.indexOf(document.activeElement);
|
|
255
|
+
const move = to => items[(to + items.length) % items.length]?.focus();
|
|
256
|
+
const keys = {
|
|
257
|
+
ArrowDown: () => move(index + 1),
|
|
258
|
+
ArrowUp: () => move(index < 0 ? items.length - 1 : index - 1),
|
|
259
|
+
Home: () => move(0),
|
|
260
|
+
End: () => move(items.length - 1),
|
|
261
|
+
Enter: () => (items[index] ?? items[0])?.click(),
|
|
262
|
+
' ': () => (items[index] ?? items[0])?.click(),
|
|
263
|
+
Tab: () => closeMenu(menu, true),
|
|
264
|
+
Escape: () => closeMenu(menu, true)
|
|
265
|
+
};
|
|
266
|
+
const act = keys[event.key];
|
|
267
|
+
if (!act) return;
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
act();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const bound = new WeakSet();
|
|
274
|
+
const focusMenu = menu => {
|
|
275
|
+
const el = menuElement(menu.kind);
|
|
276
|
+
if (!el) return;
|
|
277
|
+
const items = [...el.querySelectorAll('.domma-dropdown-item')];
|
|
278
|
+
for (const item of items) item.tabIndex = -1;
|
|
279
|
+
if (!bound.has(el)) {
|
|
280
|
+
bound.add(el);
|
|
281
|
+
el.addEventListener('keydown', event => onMenuKey(menu, event));
|
|
282
|
+
}
|
|
283
|
+
const current = items.find(item => item.dataset.value === menu.current()) ?? items[0];
|
|
284
|
+
if (!current) return;
|
|
285
|
+
current.scrollIntoView?.({block: 'nearest'});
|
|
286
|
+
// The menu fades in from `visibility: hidden`, and an invisible item
|
|
287
|
+
// refuses focus - so keep trying for the length of the fade.
|
|
288
|
+
let tries = 0;
|
|
289
|
+
const attempt = () => {
|
|
290
|
+
if (!el.isConnected || !el.classList.contains('show')) return;
|
|
291
|
+
current.focus({preventScroll: true});
|
|
292
|
+
if (document.activeElement !== current && ++tries < 30) requestAnimationFrame(attempt);
|
|
293
|
+
};
|
|
294
|
+
attempt();
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
for (const menu of menus) {
|
|
298
|
+
// The button keeps the editor's selection (mousedown is cancelled), so
|
|
299
|
+
// it is still live here; capture only takes one inside the editor.
|
|
300
|
+
menu.button.addEventListener('click', () => {
|
|
301
|
+
capture();
|
|
302
|
+
queueMicrotask(() => { if (menu.dropdown.isOpen?.()) focusMenu(menu); });
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Escape with a menu open closes the MENU. Domma's dropdown and the
|
|
307
|
+
// compose slideover both listen on document, so without this one Escape
|
|
308
|
+
// shuts the menu and the whole compose window. Window capture runs first.
|
|
309
|
+
const onEscape = event => {
|
|
310
|
+
if (event.key !== 'Escape') return;
|
|
311
|
+
if (!content.isConnected) {
|
|
312
|
+
window.removeEventListener('keydown', onEscape, true);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const open = menus.find(menu => menu.dropdown?.isOpen?.());
|
|
316
|
+
if (!open) return;
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
event.stopPropagation();
|
|
319
|
+
closeMenu(open, true);
|
|
320
|
+
};
|
|
321
|
+
window.addEventListener('keydown', onEscape, true);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
destroy() {
|
|
325
|
+
window.removeEventListener('keydown', onEscape, true);
|
|
326
|
+
scope.removeEventListener('input', tidy, true);
|
|
327
|
+
for (const menu of menus) {
|
|
328
|
+
try { menu.dropdown?.close?.(); } catch { /* gone */ }
|
|
329
|
+
try { menu.dropdown?.destroy?.(); } catch { /* gone */ }
|
|
330
|
+
}
|
|
331
|
+
group.remove();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A collapsed range at the end of an element.
|
|
338
|
+
*
|
|
339
|
+
* @param {HTMLElement} el
|
|
340
|
+
* @returns {Range}
|
|
341
|
+
*/
|
|
342
|
+
function endOf(el) {
|
|
343
|
+
const range = document.createRange();
|
|
344
|
+
range.selectNodeContents(el);
|
|
345
|
+
range.collapse(false);
|
|
346
|
+
return range;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A toolbar button that opens a menu without taking the editor's focus.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} label
|
|
353
|
+
* @param {string} title
|
|
354
|
+
* @returns {HTMLButtonElement}
|
|
355
|
+
*/
|
|
356
|
+
function menuButton(label, title) {
|
|
357
|
+
const button = document.createElement('button');
|
|
358
|
+
button.type = 'button';
|
|
359
|
+
button.className = 'dm-editor-toolbar-btn mail-font-btn';
|
|
360
|
+
button.title = title;
|
|
361
|
+
button.setAttribute('aria-haspopup', 'menu');
|
|
362
|
+
button.textContent = label;
|
|
363
|
+
button.addEventListener('mousedown', event => event.preventDefault());
|
|
364
|
+
// The mail reader's single-key shortcuts listen on document and claim
|
|
365
|
+
// Enter (open message) - which would cancel the click Enter makes here.
|
|
366
|
+
button.addEventListener('keydown', event => {
|
|
367
|
+
if (event.key === 'Enter' || event.key === ' ') event.stopPropagation();
|
|
368
|
+
if (event.key === 'ArrowDown') {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
event.stopPropagation();
|
|
371
|
+
button.click();
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return button;
|
|
375
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fonts and sizes the mail editor offers - pure, so both the compose
|
|
3
|
+
* window and the settings editors (signatures, templates, away message) use
|
|
4
|
+
* one list, and it can be checked without a browser.
|
|
5
|
+
*
|
|
6
|
+
* Mail is not a web page: there is no font loading worth relying on. Outlook
|
|
7
|
+
* on Windows ignores web fonts outright, Gmail only honours its own Google
|
|
8
|
+
* faces, and a font the reader has not got is replaced by whatever the client
|
|
9
|
+
* likes. So every entry is a STACK - the face asked for, then the nearest
|
|
10
|
+
* faces commonly installed, then a generic family - and the stack is what
|
|
11
|
+
* goes into the message as inline `font-family`. A reader without Calibri
|
|
12
|
+
* still gets a clean sans-serif, never Times by accident.
|
|
13
|
+
*
|
|
14
|
+
* Stacks quote multi-word names with SINGLE quotes, and the editor writes
|
|
15
|
+
* them into `style` as plain strings (`setStyleProperty`) rather than through
|
|
16
|
+
* the CSSOM, so they stay that way. Where the browser does rewrite a style it
|
|
17
|
+
* uses `"`, serialised as `"` - legal HTML that the sanitiser and mail
|
|
18
|
+
* clients read the same; the single quotes are only the tidier form.
|
|
19
|
+
*
|
|
20
|
+
* Web fonts (Roboto, Open Sans, ...) are offered because people ask for them
|
|
21
|
+
* by name, but are flagged `webFont`: nothing loads them into the message, so
|
|
22
|
+
* the mail shows the face only where the reader has it installed (or the
|
|
23
|
+
* client supplies it) and the fallback everywhere else.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Group ids in menu order, with their headings. */
|
|
27
|
+
export const FONT_GROUPS = Object.freeze([
|
|
28
|
+
Object.freeze({id: 'sans', label: 'Sans-serif'}),
|
|
29
|
+
Object.freeze({id: 'serif', label: 'Serif'}),
|
|
30
|
+
Object.freeze({id: 'mono', label: 'Monospace'}),
|
|
31
|
+
Object.freeze({id: 'display', label: 'Display & handwriting'}),
|
|
32
|
+
Object.freeze({id: 'web', label: 'Web fonts (need a fallback)'}),
|
|
33
|
+
Object.freeze({id: 'generic', label: 'Generic families'})
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const SANS = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
37
|
+
const SERIF = "Georgia, 'Times New Roman', Times, serif";
|
|
38
|
+
const MONO = "'Courier New', Courier, monospace";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* [id, label, group, stack, webFont?]. The id is stable (it may be stored);
|
|
42
|
+
* the label is what the menu says.
|
|
43
|
+
*/
|
|
44
|
+
const TABLE = [
|
|
45
|
+
// ---- Sans-serif -------------------------------------------------------
|
|
46
|
+
['arial', 'Arial', 'sans', "Arial, 'Helvetica Neue', Helvetica, sans-serif"],
|
|
47
|
+
['arial-black', 'Arial Black', 'sans', "'Arial Black', 'Arial Bold', Gadget, Arial, sans-serif"],
|
|
48
|
+
['helvetica', 'Helvetica', 'sans', "'Helvetica Neue', Helvetica, Arial, sans-serif"],
|
|
49
|
+
['verdana', 'Verdana', 'sans', 'Verdana, Geneva, Tahoma, sans-serif'],
|
|
50
|
+
['tahoma', 'Tahoma', 'sans', 'Tahoma, Verdana, Geneva, Segoe, sans-serif'],
|
|
51
|
+
['trebuchet', 'Trebuchet MS', 'sans', "'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif"],
|
|
52
|
+
['segoe', 'Segoe UI', 'sans', "'Segoe UI', Segoe, Tahoma, Geneva, Verdana, sans-serif"],
|
|
53
|
+
['calibri', 'Calibri', 'sans', "Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif"],
|
|
54
|
+
['candara', 'Candara', 'sans', "Candara, Calibri, Segoe, 'Segoe UI', Optima, Arial, sans-serif"],
|
|
55
|
+
['century-gothic', 'Century Gothic', 'sans', "'Century Gothic', CenturyGothic, AppleGothic, Futura, Arial, sans-serif"],
|
|
56
|
+
['franklin-gothic', 'Franklin Gothic', 'sans', "'Franklin Gothic Medium', 'Franklin Gothic', 'ITC Franklin Gothic', 'Arial Narrow', Arial, sans-serif"],
|
|
57
|
+
['gill-sans', 'Gill Sans', 'sans', "'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif"],
|
|
58
|
+
['lucida-sans', 'Lucida Sans', 'sans', "'Lucida Sans', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif"],
|
|
59
|
+
['optima', 'Optima', 'sans', "Optima, Segoe, 'Segoe UI', Candara, Calibri, Arial, sans-serif"],
|
|
60
|
+
['futura', 'Futura', 'sans', "Futura, 'Trebuchet MS', 'Century Gothic', Arial, sans-serif"],
|
|
61
|
+
|
|
62
|
+
// ---- Serif ------------------------------------------------------------
|
|
63
|
+
['georgia', 'Georgia', 'serif', "Georgia, 'Times New Roman', Times, serif"],
|
|
64
|
+
['times', 'Times New Roman', 'serif', "'Times New Roman', Times, Georgia, serif"],
|
|
65
|
+
['cambria', 'Cambria', 'serif', "Cambria, Georgia, 'Times New Roman', Times, serif"],
|
|
66
|
+
['garamond', 'Garamond', 'serif', "Garamond, 'EB Garamond', Baskerville, 'Baskerville Old Face', 'Hoefler Text', 'Times New Roman', serif"],
|
|
67
|
+
['palatino', 'Palatino', 'serif', "Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua', Georgia, serif"],
|
|
68
|
+
['book-antiqua', 'Book Antiqua', 'serif', "'Book Antiqua', Palatino, 'Palatino Linotype', 'Palatino LT STD', Georgia, serif"],
|
|
69
|
+
['baskerville', 'Baskerville', 'serif', "Baskerville, 'Baskerville Old Face', 'Hoefler Text', Garamond, 'Times New Roman', serif"],
|
|
70
|
+
['didot', 'Didot', 'serif', "Didot, 'Didot LT STD', 'Hoefler Text', Garamond, 'Times New Roman', serif"],
|
|
71
|
+
['rockwell', 'Rockwell', 'serif', "Rockwell, 'Courier Bold', Courier, Georgia, Times, 'Times New Roman', serif"],
|
|
72
|
+
|
|
73
|
+
// ---- Monospace --------------------------------------------------------
|
|
74
|
+
['courier-new', 'Courier New', 'mono', "'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace"],
|
|
75
|
+
['lucida-console', 'Lucida Console', 'mono', "'Lucida Console', 'Lucida Sans Typewriter', Monaco, 'Bitstream Vera Sans Mono', monospace"],
|
|
76
|
+
['consolas', 'Consolas', 'mono', "Consolas, Monaco, 'Lucida Console', 'Liberation Mono', 'Courier New', monospace"],
|
|
77
|
+
['monaco', 'Monaco', 'mono', "Monaco, Consolas, 'Lucida Console', 'Courier New', monospace"],
|
|
78
|
+
|
|
79
|
+
// ---- Display & handwriting -------------------------------------------
|
|
80
|
+
['comic-sans', 'Comic Sans MS', 'display', "'Comic Sans MS', 'Comic Sans', 'Chalkboard SE', 'Comic Neue', cursive"],
|
|
81
|
+
['impact', 'Impact', 'display', "Impact, Haettenschweiler, 'Franklin Gothic Bold', Charcoal, 'Helvetica Inserat', 'Arial Black', sans-serif"],
|
|
82
|
+
['brush-script', 'Brush Script MT', 'display', "'Brush Script MT', 'Brush Script Std', 'Lucida Calligraphy', 'Lucida Handwriting', 'Apple Chancery', cursive"],
|
|
83
|
+
|
|
84
|
+
// ---- Web fonts: offered, never loaded - the fallback is what most see --
|
|
85
|
+
['roboto', 'Roboto', 'web', `Roboto, 'Helvetica Neue', ${SANS}`, true],
|
|
86
|
+
['open-sans', 'Open Sans', 'web', `'Open Sans', 'Segoe UI', ${SANS}`, true],
|
|
87
|
+
['lato', 'Lato', 'web', `Lato, 'Segoe UI', ${SANS}`, true],
|
|
88
|
+
['montserrat', 'Montserrat', 'web', `Montserrat, 'Century Gothic', 'Trebuchet MS', ${SANS}`, true],
|
|
89
|
+
['poppins', 'Poppins', 'web', `Poppins, 'Century Gothic', 'Segoe UI', ${SANS}`, true],
|
|
90
|
+
['source-sans', 'Source Sans 3', 'web', `'Source Sans 3', 'Source Sans Pro', 'Segoe UI', ${SANS}`, true],
|
|
91
|
+
['inter', 'Inter', 'web', `Inter, 'Segoe UI', ${SANS}`, true],
|
|
92
|
+
['nunito', 'Nunito', 'web', `Nunito, 'Segoe UI', ${SANS}`, true],
|
|
93
|
+
['raleway', 'Raleway', 'web', `Raleway, 'Century Gothic', ${SANS}`, true],
|
|
94
|
+
['oswald', 'Oswald', 'web', `Oswald, 'Arial Narrow', Impact, ${SANS}`, true],
|
|
95
|
+
['merriweather', 'Merriweather', 'web', `Merriweather, ${SERIF}`, true],
|
|
96
|
+
['playfair', 'Playfair Display', 'web', `'Playfair Display', Didot, ${SERIF}`, true],
|
|
97
|
+
['lora', 'Lora', 'web', `Lora, ${SERIF}`, true],
|
|
98
|
+
['roboto-mono', 'Roboto Mono', 'web', `'Roboto Mono', Consolas, Monaco, ${MONO}`, true],
|
|
99
|
+
|
|
100
|
+
// ---- Generic families: the reader's own choice -------------------------
|
|
101
|
+
['sans-serif', 'Sans-serif (reader’s default)', 'generic', 'sans-serif'],
|
|
102
|
+
['serif', 'Serif (reader’s default)', 'generic', 'serif'],
|
|
103
|
+
['monospace', 'Monospace (reader’s default)', 'generic', 'monospace'],
|
|
104
|
+
['cursive', 'Cursive (reader’s default)', 'generic', 'cursive']
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** Every font, in menu order. Stacks are de-duplicated (the shared tails overlap). */
|
|
108
|
+
export const FONTS = Object.freeze(TABLE.map(([id, label, group, stack, webFont = false]) =>
|
|
109
|
+
Object.freeze({id, label, group, stack: joinStack(stackFamilies(stack)), webFont})));
|
|
110
|
+
|
|
111
|
+
/** Font sizes in px - the common 8-72 ladder. */
|
|
112
|
+
export const FONT_SIZES = Object.freeze([8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 26, 28, 32, 36, 40, 48, 56, 64, 72]);
|
|
113
|
+
|
|
114
|
+
/** The CSS generic families a stack may end on. */
|
|
115
|
+
export const GENERIC_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui']);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Look a font up by id.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} id
|
|
121
|
+
* @returns {object|null}
|
|
122
|
+
*/
|
|
123
|
+
export function findFont(id) {
|
|
124
|
+
return FONTS.find(font => font.id === id) ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The fonts grouped for a menu: `[{id, label, fonts: [...]}]`, empty groups
|
|
129
|
+
* left out.
|
|
130
|
+
*
|
|
131
|
+
* @returns {{id: string, label: string, fonts: object[]}[]}
|
|
132
|
+
*/
|
|
133
|
+
export function groupedFonts() {
|
|
134
|
+
return FONT_GROUPS
|
|
135
|
+
.map(group => ({...group, fonts: FONTS.filter(font => font.group === group.id)}))
|
|
136
|
+
.filter(group => group.fonts.length);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The families of a stack, unquoted: "Calibri, 'Segoe UI', sans-serif" →
|
|
141
|
+
* ['Calibri', 'Segoe UI', 'sans-serif'].
|
|
142
|
+
*
|
|
143
|
+
* @param {string} stack
|
|
144
|
+
* @returns {string[]}
|
|
145
|
+
*/
|
|
146
|
+
export function stackFamilies(stack) {
|
|
147
|
+
return String(stack ?? '')
|
|
148
|
+
.split(',')
|
|
149
|
+
.map(part => part.trim().replace(/^(['"])(.*)\1$/, '$2').trim())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Join families back into a stack: each once, first mention wins, names
|
|
155
|
+
* that are not a single identifier quoted with single quotes.
|
|
156
|
+
*
|
|
157
|
+
* @param {string[]} families
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
export function joinStack(families) {
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const family of families) {
|
|
164
|
+
const key = family.toLowerCase();
|
|
165
|
+
if (!family || seen.has(key)) continue;
|
|
166
|
+
seen.add(key);
|
|
167
|
+
out.push(/^[A-Za-z][A-Za-z0-9-]*$/.test(family) ? family : `'${family.replace(/'/g, '')}'`);
|
|
168
|
+
}
|
|
169
|
+
return out.join(', ');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether a font size (px) is one the menu offers.
|
|
174
|
+
*
|
|
175
|
+
* @param {number|string} size
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
export function isFontSize(size) {
|
|
179
|
+
return FONT_SIZES.includes(Number(size));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The inline style a chosen face or size becomes. Values outside the lists
|
|
184
|
+
* give '' - nothing free-form reaches the message.
|
|
185
|
+
*
|
|
186
|
+
* @param {{font?: string, size?: number|string}} choice - font id and/or px size
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
export function inlineStyle({font, size} = {}) {
|
|
190
|
+
const parts = [];
|
|
191
|
+
const entry = font ? findFont(font) : null;
|
|
192
|
+
if (entry) parts.push(`font-family: ${entry.stack}`);
|
|
193
|
+
if (size !== undefined && size !== null && isFontSize(size)) parts.push(`font-size: ${Number(size)}px`);
|
|
194
|
+
return parts.join('; ');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** What the legacy `<font size="1..7">` values mean, in px. */
|
|
198
|
+
export const LEGACY_FONT_SIZES = Object.freeze({1: 10, 2: 13, 3: 16, 4: 18, 5: 24, 6: 32, 7: 48});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Split a `style` attribute into [property, value] pairs. Semicolons inside
|
|
202
|
+
* quotes or parentheses (a data: URI, a quoted family) do not split.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} style
|
|
205
|
+
* @returns {[string, string][]}
|
|
206
|
+
*/
|
|
207
|
+
export function splitStyle(style) {
|
|
208
|
+
const out = [];
|
|
209
|
+
let depth = 0;
|
|
210
|
+
let quote = '';
|
|
211
|
+
let start = 0;
|
|
212
|
+
const text = String(style ?? '');
|
|
213
|
+
const push = end => {
|
|
214
|
+
const chunk = text.slice(start, end);
|
|
215
|
+
const colon = chunk.indexOf(':');
|
|
216
|
+
if (colon > 0) {
|
|
217
|
+
const prop = chunk.slice(0, colon).trim().toLowerCase();
|
|
218
|
+
const value = chunk.slice(colon + 1).trim();
|
|
219
|
+
if (prop && value) out.push([prop, value]);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
for (let i = 0; i < text.length; i++) {
|
|
223
|
+
const ch = text[i];
|
|
224
|
+
if (quote) {
|
|
225
|
+
if (ch === quote) quote = '';
|
|
226
|
+
} else if (ch === '"' || ch === "'") {
|
|
227
|
+
quote = ch;
|
|
228
|
+
} else if (ch === '(') {
|
|
229
|
+
depth++;
|
|
230
|
+
} else if (ch === ')') {
|
|
231
|
+
depth = Math.max(0, depth - 1);
|
|
232
|
+
} else if (ch === ';' && !depth) {
|
|
233
|
+
push(i);
|
|
234
|
+
start = i + 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
push(text.length);
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Set (or with a null value, remove) one property in a `style` string,
|
|
243
|
+
* leaving the rest as written - so a stack keeps its single quotes rather
|
|
244
|
+
* than going through the browser's CSSOM, which re-quotes with `"`.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} style
|
|
247
|
+
* @param {string} prop
|
|
248
|
+
* @param {string|null} value
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
export function setStyleProperty(style, prop, value) {
|
|
252
|
+
const name = String(prop).toLowerCase();
|
|
253
|
+
const pairs = splitStyle(style).filter(([p]) => p !== name);
|
|
254
|
+
if (value !== null && value !== undefined && value !== '') pairs.push([name, String(value)]);
|
|
255
|
+
return pairs.map(([p, v]) => `${p}: ${v}`).join('; ');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The inline style that replaces a `<font>` tag - one the browser wrote, or
|
|
260
|
+
* one pasted in. `<font>` is not kept by the sanitiser (signatures,
|
|
261
|
+
* templates) and is unreliable in mail, so none is left in the editor.
|
|
262
|
+
*
|
|
263
|
+
* @param {{face?: string|null, size?: string|null, color?: string|null}} attrs
|
|
264
|
+
* @returns {string}
|
|
265
|
+
*/
|
|
266
|
+
export function fontTagStyle({face, size, color} = {}) {
|
|
267
|
+
let style = '';
|
|
268
|
+
const families = stackFamilies(face);
|
|
269
|
+
if (families.length) style = setStyleProperty(style, 'font-family', joinStack(families));
|
|
270
|
+
const raw = String(size ?? '').trim();
|
|
271
|
+
if (/^[+-]?[1-7]$/.test(raw)) {
|
|
272
|
+
const level = /^[+-]/.test(raw) ? 3 + Number(raw) : Number(raw);
|
|
273
|
+
const px = LEGACY_FONT_SIZES[Math.min(7, Math.max(1, level))];
|
|
274
|
+
style = setStyleProperty(style, 'font-size', `${px}px`);
|
|
275
|
+
}
|
|
276
|
+
const tint = String(color ?? '').trim();
|
|
277
|
+
if (/^(#[0-9a-f]{3,8}|[a-z]+|rgba?\([0-9.,%\s]+\))$/i.test(tint)) style = setStyleProperty(style, 'color', tint);
|
|
278
|
+
return style;
|
|
279
|
+
}
|
|
@@ -33,6 +33,55 @@ export function isTyping(target) {
|
|
|
33
33
|
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Anything with an activation key of its own: Enter (and Space) belong to it. */
|
|
37
|
+
const INTERACTIVE = 'button, a[href], summary, select, [role="button"], [role="link"], [role="menuitem"], '
|
|
38
|
+
+ '[role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="checkbox"], [role="radio"], '
|
|
39
|
+
+ '[role="switch"], [role="tab"]';
|
|
40
|
+
|
|
41
|
+
/** A panel over the reader - a slideover, modal, menu or compose window - keeps its own keys. */
|
|
42
|
+
const OVERLAY = '[role="dialog"], [aria-modal="true"], [role="menu"], [role="listbox"], .dm-slideover, .dm-modal, .modal, '
|
|
43
|
+
+ '.dm-dialog, .dm-context-menu, .mail-compose-form';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The folder rows: Enter on one is "into this folder" (the shortcut), not the
|
|
47
|
+
* click that selects it. A message row is left to its own click, which opens
|
|
48
|
+
* the row that has focus - the shortcut opens the cursor's, the same row once
|
|
49
|
+
* j/k have moved there, and nothing at all when focus came by Tab or a click.
|
|
50
|
+
*/
|
|
51
|
+
export const READER_ROWS = '.mail-folder';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Should the reader's single-key shortcuts stay out of this keypress?
|
|
55
|
+
*
|
|
56
|
+
* The handler listens on the document, so it hears keys pressed anywhere -
|
|
57
|
+
* in the compose window, a settings slideover, a menu, the admin's own
|
|
58
|
+
* sidebar. Claiming Enter there cancelled the click Enter makes on the
|
|
59
|
+
* focused button (Send, Attach, ...), and a letter typed on one would reply
|
|
60
|
+
* or delete behind the panel. So the reader only speaks for keys pressed at
|
|
61
|
+
* rest (on the page body) or inside its own container, and even there a
|
|
62
|
+
* button or link keeps Enter and Space - except a folder row, whose Enter
|
|
63
|
+
* is "into this folder".
|
|
64
|
+
*
|
|
65
|
+
* @param {{key?: string, target: any}} event
|
|
66
|
+
* @param {{contains?: Function}|null} root the reader's container
|
|
67
|
+
* @returns {boolean} true when the shortcuts must not act
|
|
68
|
+
*/
|
|
69
|
+
export function keyBelongsElsewhere(event, root) {
|
|
70
|
+
const target = event?.target;
|
|
71
|
+
if (!target) return false;
|
|
72
|
+
if (isTyping(target)) return true;
|
|
73
|
+
// At rest the keyboard lands on the body (or the document itself).
|
|
74
|
+
if (target.tagName === 'BODY' || target.tagName === 'HTML' || target.nodeType === 9) return false;
|
|
75
|
+
if (root && typeof root.contains === 'function' && !root.contains(target)) return true;
|
|
76
|
+
if (typeof target.closest !== 'function') return false;
|
|
77
|
+
if (target.closest(OVERLAY)) return true;
|
|
78
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
79
|
+
const control = target.closest(INTERACTIVE);
|
|
80
|
+
if (control && !control.matches?.(READER_ROWS)) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
36
85
|
/**
|
|
37
86
|
* Should this event be considered at all?
|
|
38
87
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}
|
|
1
|
+
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}.mail-font-controls .mail-font-btn{width:auto;min-width:3.5rem;max-width:10rem;padding:0 .5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.8125rem}.domma-dropdown-menu:has(.mail-font-item),.domma-dropdown-menu:has(.mail-size-item){max-height:min(60vh,26rem);overflow-y:auto;z-index:10050}.domma-dropdown-item:has(.mail-font-item){display:flex;align-items:center;justify-content:space-between;gap:.75rem}.mail-font-item{font-size:.9375rem}.mail-font-web{font-size:.6875rem;color:var(--dm-text-muted);text-transform:uppercase;letter-spacing:.04em}
|
|
@@ -47,7 +47,7 @@ const {makeResizable} = await import(versioned('resizable.js'));
|
|
|
47
47
|
const {whenClosed} = await import(versioned('when-closed.js'));
|
|
48
48
|
const {addContact, isKnownAddress, loadContacts} = await import(versioned('contacts.js'));
|
|
49
49
|
const {fillMessageAction, loadMessageActions, messageFacts} = await import(versioned('message-actions.js'));
|
|
50
|
-
const {isPlainKey,
|
|
50
|
+
const {isPlainKey, keyBelongsElsewhere, resolveKey, SHORTCUTS} = await import(versioned('keyboard.js'));
|
|
51
51
|
const {menuPoint, quoteSelection, readBridgeMessage, selectionItems} = await import(versioned('body-menu.js'));
|
|
52
52
|
const {describeWhen, quickTimes} = await import(versioned('scheduling.js'));
|
|
53
53
|
// Sign in with Microsoft / Google (Email Pro). Optional: an older copy of this
|
|
@@ -3323,7 +3323,9 @@ return {
|
|
|
3323
3323
|
// unmount, so it never speaks for another admin screen.
|
|
3324
3324
|
keyboardHandler = event => {
|
|
3325
3325
|
if (!root.isConnected) return;
|
|
3326
|
-
|
|
3326
|
+
// Keys pressed in a panel over the reader (compose, a slideover,
|
|
3327
|
+
// a menu) or on a button are theirs, not shortcuts (keyboard.js).
|
|
3328
|
+
if (keyBelongsElsewhere(event, root) || !isPlainKey(event)) return;
|
|
3327
3329
|
|
|
3328
3330
|
const intent = resolveKey(event, {pane});
|
|
3329
3331
|
if (!intent) return;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* @module _lib/admin/mail/section-kit
|
|
24
24
|
*/
|
|
25
|
+
import {installFontControls} from './font-controls.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* A `<div>` with Domma classes on it, appended to a parent.
|
|
@@ -215,12 +216,14 @@ export function richEditor(parent, html, {height = '10rem', text = ''} = {}) {
|
|
|
215
216
|
try {
|
|
216
217
|
const editor = E.editor(host, {mode: 'rich', height});
|
|
217
218
|
editor.setValue(html ?? '');
|
|
219
|
+
let fonts = null;
|
|
220
|
+
try { fonts = installFontControls(editor); } catch { /* no menus, still an editor */ }
|
|
218
221
|
return {
|
|
219
222
|
rich: true,
|
|
220
223
|
get: () => editor.getValue(),
|
|
221
224
|
getText: () => editor.getText(),
|
|
222
225
|
set: value => editor.setValue(value ?? ''),
|
|
223
|
-
destroy: () => editor.destroy?.()
|
|
226
|
+
destroy: () => { fonts?.destroy(); editor.destroy?.(); }
|
|
224
227
|
};
|
|
225
228
|
} catch {
|
|
226
229
|
host.textContent = '';
|