domma-cms 1.8.37 → 1.8.39
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/CHANGELOG.md +12 -0
- package/admin/css/admin.css +1 -1
- package/admin/index.html +2 -2
- package/admin/js/app.js +35 -9
- package/admin/js/lib/avatar.js +1 -1
- package/admin/js/lib/sidebar-count-cache.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/topbar-account.js +1 -0
- package/admin/js/lib/topbar-navigation.js +1 -0
- package/admin/js/lib/topbar-project.js +1 -0
- package/admin/js/lib/update-notice.js +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -0
- package/admin/js/templates/docs/usage-site-settings.html +31 -0
- package/admin/js/templates/settings.html +83 -19
- package/admin/js/views/settings.js +3 -5
- package/docs/api-reference.md +1 -1
- package/docs/configuration.md +18 -0
- package/docs/screenshots/admin-navigation.png +0 -0
- package/docs/screenshots/admin-versions.png +0 -0
- package/docs/screenshots/site-settings-navigation.png +0 -0
- package/package.json +1 -1
- package/public/js/btt.js +1 -1
- package/public/js/cookie-consent.js +1 -1
- package/public/js/google-analytics.js +1 -0
- package/public/js/site.js +1 -1
- package/server/routes/api/auth.js +2 -1
- package/server/routes/api/settings.js +19 -3
- package/server/server.js +6 -3
- package/server/services/frameworkVersions.js +11 -0
- package/server/services/googleAnalytics.js +82 -0
- package/server/services/renderer.js +37 -4
- package/server/templates/page.html +1 -1
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
<button type="button" class="tab-item">SEO <i class="stx-dot" data-bind-hidden="!tabs.value.seo" title="Changed"></i></button>
|
|
22
22
|
<button type="button" class="tab-item">Footer <i class="stx-dot" data-bind-hidden="!tabs.value.footer" title="Changed"></i></button>
|
|
23
23
|
<button type="button" class="tab-item">Email <i class="stx-dot" data-bind-hidden="!tabs.value.email" title="Changed"></i><i class="stx-dot is-bad" data-bind-hidden="!noSmtp.value || tabs.value.email" title="No SMTP host"></i></button>
|
|
24
|
-
<button type="button" class="tab-item">
|
|
24
|
+
<button type="button" class="tab-item">Page navigation <i class="stx-dot" data-bind-hidden="!tabs.value.pageNavigation" title="Changed"></i></button>
|
|
25
25
|
<button type="button" class="tab-item">Cookie consent <i class="stx-dot" data-bind-hidden="!tabs.value.cookies" title="Changed"></i></button>
|
|
26
|
+
<button type="button" class="tab-item">Analytics <i class="stx-dot" data-bind-hidden="!tabs.value.analytics" title="Changed"></i></button>
|
|
26
27
|
<button type="button" class="tab-item">Breadcrumbs <i class="stx-dot" data-bind-hidden="!tabs.value.breadcrumbs" title="Changed"></i></button>
|
|
27
|
-
<button type="button" class="tab-item">Custom CSS <i class="stx-dot" data-bind-hidden="!tabs.value.css" title="Changed"></i></button>
|
|
28
28
|
<button type="button" class="tab-item">Cache</button>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
<span data-help-title="Needs attention" data-help="Settings that are saved but quietly wrong - the same list the badge on Site Settings in the sidebar counts. They go away once fixed and saved."></span></div>
|
|
36
36
|
<div class="stx-problems" data-each="problems.value key=text"></div>
|
|
37
37
|
</div>
|
|
38
|
+
<div class="stx-card-columns">
|
|
38
39
|
<div class="stx-card">
|
|
39
40
|
<div class="stx-card-title">Site</div>
|
|
40
41
|
<div class="stx-grid">
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
</div>
|
|
76
77
|
</div>
|
|
77
78
|
</div>
|
|
79
|
+
</div>
|
|
78
80
|
<div class="stx-card stx-moved">
|
|
79
81
|
<span data-icon="droplet"></span>
|
|
80
82
|
<div class="stx-moved-text">
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
|
|
88
90
|
<!-- Branding -->
|
|
89
91
|
<div class="tab-panel">
|
|
92
|
+
<div class="stx-card-columns">
|
|
90
93
|
<div class="stx-card"><div class="stx-card-title">Public site</div><div class="stx-grid">
|
|
91
94
|
<div><label class="form-label" for="stx-brandText">Brand name</label>
|
|
92
95
|
<input id="stx-brandText" type="text" class="form-input" placeholder="My Site" data-model="brandText.value">
|
|
@@ -120,10 +123,12 @@
|
|
|
120
123
|
<div data-brand-icon="adminBrandIcon"></div>
|
|
121
124
|
</div>
|
|
122
125
|
</div><p class="text-muted">The logo takes priority over the icon. Leave both empty to use the default icon.</p></div>
|
|
126
|
+
</div>
|
|
123
127
|
</div>
|
|
124
128
|
|
|
125
129
|
<!-- Favicon -->
|
|
126
130
|
<div class="tab-panel">
|
|
131
|
+
<div class="stx-card-columns">
|
|
127
132
|
<div class="stx-card"><div class="stx-card-title">Generate favicon.ico</div>
|
|
128
133
|
<p>Choose or upload an image. Generate a single ICO file with 16, 32, 48 and 256 pixel icons. Images keep their proportions with transparent padding.</p>
|
|
129
134
|
<label class="form-label" for="stx-favicon-source">Source image</label>
|
|
@@ -142,6 +147,7 @@
|
|
|
142
147
|
</div>
|
|
143
148
|
<p class="text-muted">Save to apply the browser icon. Leave it empty to use the public logo.</p>
|
|
144
149
|
</div>
|
|
150
|
+
</div>
|
|
145
151
|
</div>
|
|
146
152
|
|
|
147
153
|
<!-- SEO -->
|
|
@@ -170,6 +176,7 @@
|
|
|
170
176
|
|
|
171
177
|
<!-- Footer -->
|
|
172
178
|
<div class="tab-panel">
|
|
179
|
+
<div class="stx-card-columns">
|
|
173
180
|
<div class="stx-card">
|
|
174
181
|
<div class="stx-card-title">Footer</div>
|
|
175
182
|
<div class="stx-grid">
|
|
@@ -248,6 +255,7 @@
|
|
|
248
255
|
<input id="stx-s-yt" type="url" class="form-input" placeholder="https://youtube.com/@yourchannel" data-model="socialYoutube.value"></div>
|
|
249
256
|
</div>
|
|
250
257
|
</div>
|
|
258
|
+
</div>
|
|
251
259
|
</div>
|
|
252
260
|
|
|
253
261
|
<!-- Email -->
|
|
@@ -318,10 +326,11 @@
|
|
|
318
326
|
</div>
|
|
319
327
|
</div>
|
|
320
328
|
|
|
321
|
-
<!--
|
|
329
|
+
<!-- Page navigation -->
|
|
322
330
|
<div class="tab-panel">
|
|
331
|
+
<div class="stx-card-columns">
|
|
323
332
|
<div class="stx-card">
|
|
324
|
-
<div class="stx-card-title">Back to top
|
|
333
|
+
<div class="stx-card-title"><span class="stx-scroll-icon" data-icon="arrow-up"></span>Back to top
|
|
325
334
|
<span data-help="A button that appears once a visitor has scrolled down, and takes them back to the top of the page."></span></div>
|
|
326
335
|
<div class="stx-grid">
|
|
327
336
|
<div class="stx-setting"><label class="form-label" for="stx-btt-on">Show the button</label>
|
|
@@ -354,6 +363,41 @@
|
|
|
354
363
|
</div>
|
|
355
364
|
</div>
|
|
356
365
|
</div>
|
|
366
|
+
<div class="stx-card">
|
|
367
|
+
<div class="stx-card-title"><span class="stx-scroll-icon" data-icon="arrow-down"></span>Go to bottom
|
|
368
|
+
<span data-help="A matching down-arrow button that takes visitors to the bottom of the page. It hides when they are already near the bottom."></span></div>
|
|
369
|
+
<div class="stx-grid">
|
|
370
|
+
<div class="stx-setting"><label class="form-label" for="stx-gtb-on">Show the button</label>
|
|
371
|
+
<input id="stx-gtb-on" type="checkbox" class="form-switch" data-model="gtbEnabled.value"></div>
|
|
372
|
+
<div class="stx-setting"><label class="form-label" for="stx-gtb-smooth">Smooth scroll</label>
|
|
373
|
+
<input id="stx-gtb-smooth" type="checkbox" class="form-switch" data-model="gtbSmooth.value"></div>
|
|
374
|
+
<div class="stx-wide">
|
|
375
|
+
<label class="form-label" for="stx-gtb-label">Label <small class="stx-muted">(optional - empty is icon-only)</small></label>
|
|
376
|
+
<input id="stx-gtb-label" type="text" class="form-input" placeholder="Go to bottom" data-model="gtbLabel.value">
|
|
377
|
+
</div>
|
|
378
|
+
<div>
|
|
379
|
+
<label class="form-label" for="stx-gtb-thr">Hide near bottom (px)
|
|
380
|
+
<span data-help="How close to the bottom the visitor must be before this button hides."></span></label>
|
|
381
|
+
<input id="stx-gtb-thr" type="number" class="form-input" placeholder="300" data-model="gtbThreshold.value">
|
|
382
|
+
</div>
|
|
383
|
+
<div>
|
|
384
|
+
<label class="form-label" for="stx-gtb-pos">Position</label>
|
|
385
|
+
<select id="stx-gtb-pos" class="form-select" data-model="gtbPosition.value">
|
|
386
|
+
<option value="bottom-right">Bottom right</option>
|
|
387
|
+
<option value="bottom-left">Bottom left</option>
|
|
388
|
+
</select>
|
|
389
|
+
</div>
|
|
390
|
+
<div>
|
|
391
|
+
<label class="form-label" for="stx-gtb-off">Side offset (px)</label>
|
|
392
|
+
<input id="stx-gtb-off" type="number" class="form-input" placeholder="32" data-model="gtbOffset.value">
|
|
393
|
+
</div>
|
|
394
|
+
<div>
|
|
395
|
+
<label class="form-label" for="stx-gtb-boff">Bottom offset (px)</label>
|
|
396
|
+
<input id="stx-gtb-boff" type="number" class="form-input" placeholder="32" data-model="gtbBottomOffset.value">
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
357
401
|
</div>
|
|
358
402
|
|
|
359
403
|
<!-- Cookie consent -->
|
|
@@ -364,6 +408,11 @@
|
|
|
364
408
|
<div class="stx-grid">
|
|
365
409
|
<div class="stx-wide stx-setting"><label class="form-label" for="stx-cc-on">Show the banner</label>
|
|
366
410
|
<input id="stx-cc-on" type="checkbox" class="form-switch" data-model="ccEnabled.value"></div>
|
|
411
|
+
<div class="stx-setting"><label class="form-label" for="stx-cc-link">"Cookie settings" link in the footer
|
|
412
|
+
<span data-help="Lets visitors reopen the banner and change their answer. You can also add a link to #cookie-settings to any menu instead - the footer link is then left out."></span></label>
|
|
413
|
+
<input id="stx-cc-link" type="checkbox" class="form-switch" data-model="ccSettingsLink.value"></div>
|
|
414
|
+
<div><label class="form-label" for="stx-cc-lt">Link text</label>
|
|
415
|
+
<input id="stx-cc-lt" type="text" class="form-input" placeholder="Cookie settings" data-model="ccSettingsLinkText.value"></div>
|
|
367
416
|
<div class="stx-wide">
|
|
368
417
|
<label class="form-label" for="stx-cc-msg">Message</label>
|
|
369
418
|
<textarea id="stx-cc-msg" class="form-input" rows="3" placeholder="We use cookies…" data-model="ccMessage.value"></textarea>
|
|
@@ -416,18 +465,39 @@
|
|
|
416
465
|
</div>
|
|
417
466
|
</div>
|
|
418
467
|
|
|
468
|
+
<!-- Analytics -->
|
|
469
|
+
<div class="tab-panel">
|
|
470
|
+
<div class="stx-card">
|
|
471
|
+
<div class="stx-card-title">Google Analytics
|
|
472
|
+
<span data-help-title="Google Analytics" data-help="Paste the measurement ID of your Google Analytics 4 web stream (Admin > Data streams). Google Analytics sets cookies, so it loads only for visitors who accept Analytics cookies in the cookie banner - the banner is shown whenever an ID is set. Leave it empty to switch Google Analytics off. The built-in Analytics Tool is separate and needs no cookies."></span></div>
|
|
473
|
+
<div class="stx-grid">
|
|
474
|
+
<div class="stx-wide"><label class="form-label" for="stx-ga-id">Measurement ID</label>
|
|
475
|
+
<input id="stx-ga-id" type="text" class="form-input" placeholder="G-XXXXXXXXXX" autocomplete="off" spellcheck="false" data-model="gaId.value">
|
|
476
|
+
<small class="text-danger" data-bind-hidden="!gaIdError.value" data-bind-text="gaIdError.value"></small></div>
|
|
477
|
+
</div>
|
|
478
|
+
<p class="stx-note">Loads only after a visitor accepts Analytics cookies. Visitors who reject them, or have not answered yet, are not sent to Google.</p>
|
|
479
|
+
<p class="stx-note" data-bind-hidden="!gaForcesBanner.value">The cookie banner is switched off under Cookie consent, but it will still be shown while Google Analytics is set - it is how visitors give consent.</p>
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
419
483
|
<!-- Breadcrumbs -->
|
|
420
484
|
<div class="tab-panel">
|
|
485
|
+
<div class="stx-card-columns">
|
|
421
486
|
<div class="stx-card">
|
|
422
487
|
<div class="stx-card-title">Breadcrumb trail
|
|
423
488
|
<span data-help-title="Breadcrumbs" data-help="A small frosted pill fixed to a corner of every public page, showing where the page sits (Home › Services › Web). A page can switch it off with “Show Breadcrumbs” in the page editor."></span></div>
|
|
424
489
|
<div class="stx-grid">
|
|
425
|
-
<div class="stx-wide stx-setting"><label class="form-label" for="stx-bc-on">Show breadcrumbs
|
|
490
|
+
<div class="stx-wide stx-setting"><label class="form-label" for="stx-bc-on">Show breadcrumbs</label>
|
|
426
491
|
<input id="stx-bc-on" type="checkbox" class="form-switch" data-model="bcEnabled.value"></div>
|
|
427
|
-
<div><label class="form-label" for="stx-bc-home">Home label</label>
|
|
492
|
+
<div class="stx-wide"><label class="form-label" for="stx-bc-home">Home label</label>
|
|
428
493
|
<input id="stx-bc-home" type="text" class="form-input" placeholder="Home" data-model="bcHomeLabel.value"></div>
|
|
429
|
-
|
|
430
|
-
|
|
494
|
+
</div>
|
|
495
|
+
<p class="stx-hint">Shows the path from Home to the current page. Individual pages can hide their trail in the page editor.</p>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="stx-card">
|
|
498
|
+
<div class="stx-card-title">Placement</div>
|
|
499
|
+
<div class="stx-grid">
|
|
500
|
+
<div class="stx-wide">
|
|
431
501
|
<span class="form-label">Corner</span>
|
|
432
502
|
<div class="stx-corners" role="group" aria-label="Corner">
|
|
433
503
|
<button type="button" data-on-click="corner('TL')" data-bind-class="bcPosition.value === 'TL' && 'is-on'">↖ Top left</button>
|
|
@@ -436,7 +506,7 @@
|
|
|
436
506
|
<button type="button" data-on-click="corner('BR')" data-bind-class="bcPosition.value === 'BR' && 'is-on'">Bottom right ↘</button>
|
|
437
507
|
</div>
|
|
438
508
|
</div>
|
|
439
|
-
<div class="stx-pair">
|
|
509
|
+
<div class="stx-pair stx-wide">
|
|
440
510
|
<div><label class="form-label" for="stx-bc-x">From the side (px)</label>
|
|
441
511
|
<input id="stx-bc-x" type="number" class="form-input" min="0" max="500" placeholder="8" data-model="bcOffsetX.value"></div>
|
|
442
512
|
<div><label class="form-label" for="stx-bc-y">From the top/bottom (px)</label>
|
|
@@ -444,15 +514,6 @@
|
|
|
444
514
|
</div>
|
|
445
515
|
</div>
|
|
446
516
|
</div>
|
|
447
|
-
</div>
|
|
448
|
-
|
|
449
|
-
<!-- Custom CSS -->
|
|
450
|
-
<div class="tab-panel">
|
|
451
|
-
<div class="stx-card">
|
|
452
|
-
<div class="stx-card-title">Custom CSS
|
|
453
|
-
<span data-help="Added to the head of every public page, after the theme - so it has the last word. Saved with everything else (Save, or Ctrl+S). Up to 100 KB."></span></div>
|
|
454
|
-
<textarea class="form-input stx-css" spellcheck="false" placeholder="/* Add your custom CSS here */"></textarea>
|
|
455
|
-
<small class="stx-hint" data-bind-text="cssSize.value"></small>
|
|
456
517
|
</div>
|
|
457
518
|
</div>
|
|
458
519
|
|
|
@@ -502,6 +563,10 @@
|
|
|
502
563
|
.stx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .65rem; margin-top: .75rem; }
|
|
503
564
|
.stx-card-title { display: flex; align-items: center; gap: .4rem; font-weight: 600; flex-wrap: wrap; }
|
|
504
565
|
.stx-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; font-weight: 500; }
|
|
566
|
+
.stx-card-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1rem; align-items: start; }
|
|
567
|
+
.stx-card-columns > .stx-card { min-width: 0; }
|
|
568
|
+
.stx-scroll-icon { display: inline-flex; padding: .35rem; border-radius: 50%; background: var(--dm-surface-raised); }
|
|
569
|
+
@media (max-width: 900px) { .stx-card-columns { grid-template-columns: minmax(0, 1fr); } }
|
|
505
570
|
.stx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem 1rem; }
|
|
506
571
|
.stx-wide { grid-column: 1 / -1; }
|
|
507
572
|
.stx-pair { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
|
|
@@ -530,7 +595,6 @@
|
|
|
530
595
|
.stx-corners button:nth-child(-n+2) { border-bottom: 1px solid var(--dm-border); }
|
|
531
596
|
.stx-corners button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
|
|
532
597
|
.stx-corners button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
533
|
-
.stx-css { width: 100%; min-height: 420px; font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-sm); }
|
|
534
598
|
.stx-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
|
|
535
599
|
.stx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
|
|
536
600
|
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as B,apiRequest as x}from"../api.js";import{placeTools as Re}from"../lib/sidebar-grouping.js";import{addBannerAction as pe}from"../lib/plugin-chrome.js";import{helpPopovers as Ie}from"../lib/help-popover.js";import{openMenuAt as W,sidebarCount as me}from"../lib/tool-kit.js";import{changedTabs as Ae,describeCacheKey as Y,draftOf as $e,expiresIn as Pe,formFromSettings as je,gaIdError as fe,homeValue as Be,payloadFromForm as De,siteUrlError as he,settingsProblems as He,snapshot as ve,TAB_FIELDS as qe}from"../lib/site-settings-arrange.js";import{pickMedia as Fe}from"../lib/media-picker.js";import{faviconGenerator as Ue}from"../lib/favicon-generator.js";import{makeIconInput as _e}from"../lib/shortcode-modal.js";import{fmtDateTime as Ke}from"../lib/dates.js";const J=/^\/settings(\?.*)?$/,be="#/settings",w="settings.draft",Ne=`
|
|
2
2
|
<div class="stx-problem">
|
|
3
3
|
<span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
|
|
4
4
|
<span>{{text}}</span>
|
|
5
5
|
<button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
|
|
6
|
-
</div>`,
|
|
6
|
+
</div>`,ze=`
|
|
7
7
|
<div class="stx-row" data-id="{{id}}" tabindex="0">
|
|
8
8
|
<span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
|
|
9
9
|
<span class="stx-chip">{{expires}}</span>
|
|
10
10
|
<button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
|
|
11
|
-
</div>`,Ge=["general","branding","favicon","seo","footer","email","backToTop","cookies","breadcrumbs","css","cache"];async function Je(l,p){const[r,L]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),a=new Map([["/","Dashboard (built in)"]]),c=(C,m)=>{for(const i of C||[]){if(!i||i.hidden||i.restricted)continue;const f=typeof i.url=="string"?i.url:"";f.startsWith("#/")&&f.length>2&&!a.has(f.slice(1))&&a.set(f.slice(1),`${m?`${m} \u203A `:""}${i.text||f}`),c(i.items,i.url?m:i.text||m)}};c($e(r?.items||[],L||[]),""),a.has(p)||a.set(p,`${p} (not in the sidebar)`),l.replaceChildren();for(const[C,m]of a){const i=document.createElement("option");i.value=C,i.textContent=m,l.appendChild(i)}}let g=null,Ee=!1;function Qe(){Ee||!window.R?.use||(Ee=!0,R.use((l,p,r)=>!(Z.test(p?.path||"")&&!Z.test(l?.path||""))||!g?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(a=>{a?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(g&&!Z.test(l?.path||"")){const p=g;g=null,p.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Qe(),g){const e=g;g=null,e.dispose()}const p=[],r=e=>(p.push(e),e);let L=!1;const a=l.get(0).querySelector(".stx")||l.get(0),c=()=>!L&&a.isConnected,C=E.loader(l.get(0),{type:"dots"});let m,i;try{[m,i]=await Promise.all([K.settings.get(),w("/settings/custom-css").then(e=>e?.css||"").catch(()=>null)])}catch(e){C.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(C.destroy(),!a.isConnected)return;const f=i!==null,ee=M.observable(m||{}),te=je(m||{},i||""),n={};for(const[e,t]of Object.entries(te))n[e]=M.observable(t);const _=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),se=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),y=M.observable(te),q=M.computed(()=>ke(y.value)),O=M.computed(()=>ke(_())!==q.value),A=M.observable(!1),$=M.observable(""),ne=M.observable(!1);K.plugins.policy().then(e=>{c()&&(ne.value=e?.signedOnly===!1)}).catch(()=>{});const U=M.observable(!1),ae=M.computed(()=>{const e=new Set(Be(_(),y.value));return Object.fromEntries(Object.keys(Ue).map(t=>[t,e.has(t)]))}),F=M.observable([]);r(M.effect(()=>{F.value=qe(_()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const oe=M.observable(""),P=M.observable(!1),B=M.observable(""),D=M.observable(""),v=M.observable(null),z=M.observable([]),N=M.observable(""),V=M.observable(!0),W=M.observable(""),re=M.observable([]);r(M.effect(()=>{const e=N.value.trim().toLowerCase(),t=Date.now();re.value=[...z.value].reverse().filter(s=>!e||String(s.key).toLowerCase().includes(e)||(s.tags||[]).some(o=>String(o).toLowerCase().includes(e))).map(s=>{const o=X(s.key);return{id:s.key,path:o.path,meta:[o.page?`as ${o.role}`:"not a page",(s.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:He(s.expiresAt,t)}})}));const ie=ze(n.favicon,n.brandLogo,c),Se={...ie.data,...n,saving:A,dirty:O,tabs:ae,problems:F,testTo:oe,testing:P,testResult:B,testTone:D,cacheQuery:N,cacheLoading:V,cacheError:W,cacheRows:re,errorText:$,problemsText:M.computed(()=>F.value.length?"yes":""),siteUrlError:M.computed(()=>ye(n.siteUrl.value)),noSmtp:M.computed(()=>!String(n.smtpHost.value||"").trim()),mayAllowInsecureTLS:ne,dirtyEmail:M.computed(()=>ae.value.email),seoLength:M.computed(()=>{const e=String(n.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cssSize:M.computed(()=>{if(!f)return"Could not load content/custom.css - reload to try again. It will not be saved until it loads.";const e=new Blob([String(n.css.value||"")]).size/1024;return`${e.toFixed(1)} KB of 100 KB${e>100?" - too big to save":""}`}),cacheOn:M.computed(()=>v.value?.enabled===!0),cacheState:M.computed(()=>{const e=v.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=v.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=v.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>v.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>N.value.trim()?"Nothing matches.":v.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function Y(){if(A.peek())return;const e=se(),t=y.peek(),s=String(e.css)!==String(t.css);if(s&&!f){E.toast("The custom CSS did not load, so it cannot be saved - reload the screen first.",{type:"error"});return}const o=ye(e.siteUrl);if(o){E.toast(`Site URL: ${o}`,{type:"error"});return}A.value=!0,$.value="";const h=String(e.title).trim()!==String(t.title).trim();try{await K.settings.save(_e(e,ee.peek(),{adminHomeReady:U.peek()})),s&&await w("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:String(e.css)})});const d=await K.settings.get().catch(()=>null);if(!c())return;d&&(ee.value=d);const J={...e,smtpPass:""};n.smtpPass.value="",y.value=J;try{S.remove?S.remove(T):S.set(T,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(h||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(ve=>e[ve]!==t[ve]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),ge(we),E.toast("Settings saved.",{type:"success"})}catch(d){$.value=d.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{A.value=!1}}async function Ce(){await E.confirm("Throw away your unsaved settings?")&&(ce(y.peek()),$.value="")}function ce(e){for(const[t,s]of Object.entries(e))n[t]&&(n[t].value=s);u&&u.getValue()!==String(n.css.peek())?u.setValue(String(n.css.peek())):!u&&b&&(b.value=String(n.css.peek()))}async function Me(){if(!P.peek()){P.value=!0,D.value="",B.value="Sending\u2026";try{const e=await w("/settings/test-email",{method:"POST",body:JSON.stringify({to:oe.peek().trim()||void 0})});if(!c())return;B.value=e?.message||"Test email sent.",D.value="is-ok"}catch(e){if(!c())return;B.value=e.message||"Could not send the test email.",D.value="is-bad"}finally{P.value=!1}}}async function j(){V.value=!0;try{const[e,t]=await Promise.all([w("/cache/status").catch(()=>null),w("/cache/keys")]);if(!c())return;v.value=e,z.value=Array.isArray(t?.entries)?t.entries:[],W.value=""}catch(e){if(!c())return;W.value=`Could not read the cache: ${e.message||e}`}finally{V.value=!1}}async function xe(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await w("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}c()&&await j()}}async function Te(){const e=v.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{v.value=await w("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),ge(we)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}c()&&await j()}}const le=e=>z.peek().find(t=>t.key===e);function Le(e){const t=le(e);if(!t)return[];const s=X(t.key);return[{header:s.path},{label:"Open the page",icon:"external-link",disabled:!s.page,action:()=>window.open(s.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...s.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(s.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}a.querySelector(".stx-problems").innerHTML=We;const x=a.querySelector(".stx-list");x.innerHTML=Ye;const Oe=E.tabs(a.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Re=M.applyBindings(Se,a,{methods:{...ie.methods,save:Y,discard:Ce,sendTest:Me,refreshCache:j,clearCache:xe,toggleCache:Te,pickBrandImage:async e=>{const t=await Fe({title:"Choose a branding image",current:n[e].peek()});t&&c()&&(n[e].value=t)},clearBrandImage:e=>{n[e].value=""},corner:e=>{n.bcPosition.value=e}}});r(()=>Re.dispose());for(const e of a.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,s=Ne("Choose an icon",n[t].peek());s.input.id=`stx-${t}`,s.input.classList.add("form-input");const o=()=>{n[t].value=s.input.value};s.input.addEventListener("input",o),r(()=>s.input.removeEventListener("input",o)),r(M.effect(()=>{s.input.value=n[t].value})),e.appendChild(s.el)}r(Pe(a,{position:"bottom"}));const Ie=e=>{const t=e.target.closest("[data-go]");if(!t)return;const s=Ge.indexOf(t.dataset.go);s>=0&&Oe.activate(s)};a.querySelector(".stx-problems").addEventListener("click",Ie);const G=a.querySelector("#stx-admin-home");Je(G,Ke(n.adminHome.peek())).then(()=>{c()&&(G.value=n.adminHome.peek(),U.value=!0)}).catch(()=>{});const b=a.querySelector(".stx-css");b.value=String(n.css.peek());let u=null;if(!f)b.disabled=!0;else if(E.editor)try{u=E.editor(b,{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0,onChange:e=>{n.css.value=e}})}catch{u=null}if(u)u._editorEl&&u._editorEl.addEventListener("keydown",e=>{if(!(e.ctrlKey||e.metaKey)||e.key!=="c"&&e.key!=="x")return;const t=u._editorEl;if(t.selectionStart!==t.selectionEnd||typeof t.value!="string")return;e.preventDefault();const s=t.value,o=t.selectionStart,h=s.lastIndexOf(`
|
|
12
|
-
`,o-1)+1,d=s.indexOf(`
|
|
13
|
-
`,o),J=d===-1?s.slice(h):s.slice(h,d+1);navigator.clipboard?.writeText(J).catch(()=>{}),e.key==="x"?(t.value=s.slice(0,h)+(d===-1?"":s.slice(d+1)),t.selectionStart=t.selectionEnd=h,t.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})):E.toast("Line copied",{type:"info",duration:1500})});else{const e=()=>{n.css.value=b.value};b.addEventListener("input",e),r(()=>b.removeEventListener("input",e))}r(()=>{try{u?.destroy?.()}catch{}});const Ae=E.contextMenu(x,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Le(e.dataset.id)});r(()=>Ae?.destroy?.());const ue=e=>{const t=le(e.dataset.id),s=t&&X(t.key);s?.page?window.open(s.path,"_blank","noopener"):Q(e)},de=e=>{const t=e.target.closest(".stx-row");if(!t)return;const s=e.target.closest(".stx-kebab");if(s){Q(s);return}ue(t)},pe=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Q(t)):e.key==="Enter"&&e.target===t&&ue(t))};x.addEventListener("click",de),x.addEventListener("keydown",pe),r(()=>{x.removeEventListener("click",de),x.removeEventListener("keydown",pe)}),be(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),be(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Y()});const me=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),Y())};document.addEventListener("keydown",me,!0),r(()=>document.removeEventListener("keydown",me,!0));const fe=()=>{if(O.peek())try{S.set(T,{form:De(se()),base:q.peek(),at:new Date().toISOString()})}catch{}},he=e=>{O.peek()&&(fe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",he),r(()=>window.removeEventListener("beforeunload",he)),g={dirty:()=>O.peek(),dispose(){L=!0,fe(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(a),j();let k=null;try{k=S.get(T)}catch{}if(k?.form){try{S.remove?S.remove(T):S.set(T,null)}catch{}const e=k.base&&k.base!==q.peek(),t=k.at?` (${Ve(k.at)})`:"",s=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(s)&&c()){const o={...y.peek()};for(const[h,d]of Object.entries(k.form))h in o&&h!=="smtpPass"&&(o[h]=d);f||(o.css=y.peek().css),ce(o),U.peek()&&(G.value=n.adminHome.peek())}}}};
|
|
11
|
+
</div>`,Ge=["general","branding","favicon","seo","footer","email","pageNavigation","cookies","analytics","breadcrumbs","cache"];async function Ve(l,u){const[r,k]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),n=new Map([["/","Dashboard (built in)"]]),i=(v,d)=>{for(const o of v||[]){if(!o||o.hidden||o.restricted)continue;const m=typeof o.url=="string"?o.url:"";m.startsWith("#/")&&m.length>2&&!n.has(m.slice(1))&&n.set(m.slice(1),`${d?`${d} \u203A `:""}${o.text||m}`),i(o.items,o.url?d:o.text||d)}};i(Re(r?.items||[],k||[]),""),n.has(u)||n.set(u,`${u} (not in the sidebar)`),l.replaceChildren();for(const[v,d]of n){const o=document.createElement("option");o.value=v,o.textContent=d,l.appendChild(o)}}let f=null,ge=!1;function We(){ge||!window.R?.use||(ge=!0,R.use((l,u,r)=>!(J.test(u?.path||"")&&!J.test(l?.path||""))||!f?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(n=>{n?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(f&&!J.test(l?.path||"")){const u=f;f=null,u.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(We(),f){const e=f;f=null,e.dispose()}const u=[],r=e=>(u.push(e),e);let k=!1;const n=l.get(0).querySelector(".stx")||l.get(0),i=()=>!k&&n.isConnected,v=E.loader(l.get(0),{type:"dots"});let d;try{d=await B.settings.get()}catch(e){v.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(v.destroy(),!n.isConnected)return;const o=M.observable(d||{}),m=je(d||{}),s={};for(const[e,t]of Object.entries(m))s[e]=M.observable(t);const D=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),Q=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),b=M.observable(m),q=M.computed(()=>ve(b.value)),T=M.computed(()=>ve(D())!==q.value),L=M.observable(!1),O=M.observable(""),X=M.observable(!1);B.plugins.policy().then(e=>{i()&&(X.value=e?.signedOnly===!1)}).catch(()=>{});const F=M.observable(!1),Z=M.computed(()=>{const e=new Set(Ae(D(),b.value));return Object.fromEntries(Object.keys(qe).map(t=>[t,e.has(t)]))}),U=M.observable([]);r(M.effect(()=>{U.value=He(D()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const ee=M.observable(""),A=M.observable(!1),$=M.observable(""),P=M.observable(""),p=M.observable(null),_=M.observable([]),K=M.observable(""),N=M.observable(!0),z=M.observable(""),te=M.observable([]);r(M.effect(()=>{const e=K.value.trim().toLowerCase(),t=Date.now();te.value=[..._.value].reverse().filter(a=>!e||String(a.key).toLowerCase().includes(e)||(a.tags||[]).some(c=>String(c).toLowerCase().includes(e))).map(a=>{const c=Y(a.key);return{id:a.key,path:c.path,meta:[c.page?`as ${c.role}`:"not a page",(a.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:Pe(a.expiresAt,t)}})}));const ae=Ue(s.favicon,s.brandLogo,i),ye={...ae.data,...s,saving:L,dirty:T,tabs:Z,problems:U,testTo:ee,testing:A,testResult:$,testTone:P,cacheQuery:K,cacheLoading:N,cacheError:z,cacheRows:te,errorText:O,problemsText:M.computed(()=>U.value.length?"yes":""),siteUrlError:M.computed(()=>he(s.siteUrl.value)),gaIdError:M.computed(()=>fe(s.gaId.value)),gaForcesBanner:M.computed(()=>!!String(s.gaId.value||"").trim()&&s.ccEnabled.value!==!0),noSmtp:M.computed(()=>!String(s.smtpHost.value||"").trim()),mayAllowInsecureTLS:X,dirtyEmail:M.computed(()=>Z.value.email),seoLength:M.computed(()=>{const e=String(s.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cacheOn:M.computed(()=>p.value?.enabled===!0),cacheState:M.computed(()=>{const e=p.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=p.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=p.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>p.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>K.value.trim()?"Nothing matches.":p.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function G(){if(L.peek())return;const e=Q(),t=b.peek(),a=he(e.siteUrl);if(a){E.toast(`Site URL: ${a}`,{type:"error"});return}const c=fe(e.gaId);if(c){E.toast(`Google Analytics: ${c}`,{type:"error"});return}L.value=!0,O.value="";const C=String(e.title).trim()!==String(t.title).trim();try{await B.settings.save(De(e,o.peek(),{adminHomeReady:F.peek()}));const y=await B.settings.get().catch(()=>null);if(!i())return;y&&(o.value=y);const Oe={...e,smtpPass:""};s.smtpPass.value="",b.value=Oe;try{S.remove?S.remove(w):S.set(w,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),(C||["adminBrandTitle","adminBrandLogo","adminBrandIcon"].some(de=>e[de]!==t[de]))&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),me(be),E.toast("Settings saved.",{type:"success"})}catch(y){O.value=y.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{L.value=!1}}async function we(){await E.confirm("Throw away your unsaved settings?")&&(se(b.peek()),O.value="")}function se(e){for(const[t,a]of Object.entries(e))s[t]&&(s[t].value=a)}async function ke(){if(!A.peek()){A.value=!0,P.value="",$.value="Sending\u2026";try{const e=await x("/settings/test-email",{method:"POST",body:JSON.stringify({to:ee.peek().trim()||void 0})});if(!i())return;$.value=e?.message||"Test email sent.",P.value="is-ok"}catch(e){if(!i())return;$.value=e.message||"Could not send the test email.",P.value="is-bad"}finally{A.value=!1}}}async function j(){N.value=!0;try{const[e,t]=await Promise.all([x("/cache/status").catch(()=>null),x("/cache/keys")]);if(!i())return;p.value=e,_.value=Array.isArray(t?.entries)?t.entries:[],z.value=""}catch(e){if(!i())return;z.value=`Could not read the cache: ${e.message||e}`}finally{N.value=!1}}async function Ee(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await x("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}i()&&await j()}}async function Se(){const e=p.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{p.value=await x("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),me(be)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}i()&&await j()}}const ne=e=>_.peek().find(t=>t.key===e);function Me(e){const t=ne(e);if(!t)return[];const a=Y(t.key);return[{header:a.path},{label:"Open the page",icon:"external-link",disabled:!a.page,action:()=>window.open(a.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...a.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(a.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}n.querySelector(".stx-problems").innerHTML=Ne;const g=n.querySelector(".stx-list");g.innerHTML=ze;const Ce=E.tabs(n.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),xe=M.applyBindings(ye,n,{methods:{...ae.methods,save:G,discard:we,sendTest:ke,refreshCache:j,clearCache:Ee,toggleCache:Se,pickBrandImage:async e=>{const t=await Fe({title:"Choose a branding image",current:s[e].peek()});t&&i()&&(s[e].value=t)},clearBrandImage:e=>{s[e].value=""},corner:e=>{s.bcPosition.value=e}}});r(()=>xe.dispose());for(const e of n.querySelectorAll("[data-brand-icon]")){const t=e.dataset.brandIcon,a=_e("Choose an icon",s[t].peek());a.input.id=`stx-${t}`,a.input.classList.add("form-input");const c=()=>{s[t].value=a.input.value};a.input.addEventListener("input",c),r(()=>a.input.removeEventListener("input",c)),r(M.effect(()=>{a.input.value=s[t].value})),e.appendChild(a.el)}r(Ie(n,{position:"bottom"}));const Te=e=>{const t=e.target.closest("[data-go]");if(!t)return;const a=Ge.indexOf(t.dataset.go);a>=0&&Ce.activate(a)};n.querySelector(".stx-problems").addEventListener("click",Te);const V=n.querySelector("#stx-admin-home");Ve(V,Be(s.adminHome.peek())).then(()=>{i()&&(V.value=s.adminHome.peek(),F.value=!0)}).catch(()=>{});const Le=E.contextMenu(g,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Me(e.dataset.id)});r(()=>Le?.destroy?.());const oe=e=>{const t=ne(e.dataset.id),a=t&&Y(t.key);a?.page?window.open(a.path,"_blank","noopener"):W(e)},re=e=>{const t=e.target.closest(".stx-row");if(!t)return;const a=e.target.closest(".stx-kebab");if(a){W(a);return}oe(t)},ie=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),W(t)):e.key==="Enter"&&e.target===t&&oe(t))};g.addEventListener("click",re),g.addEventListener("keydown",ie),r(()=>{g.removeEventListener("click",re),g.removeEventListener("keydown",ie)}),pe(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),pe(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>G()});const ce=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),G())};document.addEventListener("keydown",ce,!0),r(()=>document.removeEventListener("keydown",ce,!0));const le=()=>{if(T.peek())try{S.set(w,{form:$e(Q()),base:q.peek(),at:new Date().toISOString()})}catch{}},ue=e=>{T.peek()&&(le(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ue),r(()=>window.removeEventListener("beforeunload",ue)),f={dirty:()=>T.peek(),dispose(){k=!0,le(),u.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(n),j();let h=null;try{h=S.get(w)}catch{}if(h?.form){try{S.remove?S.remove(w):S.set(w,null)}catch{}const e=h.base&&h.base!==q.peek(),t=h.at?` (${Ke(h.at)})`:"",a=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(a)&&i()){const c={...b.peek()};for(const[C,y]of Object.entries(h.form))C in c&&C!=="smtpPass"&&(c[C]=y);se(c),F.peek()&&(V.value=s.adminHome.peek())}}}};
|
package/docs/api-reference.md
CHANGED
|
@@ -615,7 +615,7 @@ Return the site config. The SMTP password is stripped from the response.
|
|
|
615
615
|
|
|
616
616
|
### PUT `/api/settings`
|
|
617
617
|
|
|
618
|
-
Update site settings. Only the
|
|
618
|
+
Update site settings. Only the keys Site Settings uses are accepted (an unknown key is a 400), among them `title`, `tagline`, `description`, `logo`, `favicon`, `theme`, `smtp`, `footer`, `analytics`, `social`, `locale`, `layoutOptions`, `seo`, `backToTop`, `cookieConsent`, `breadcrumbs`, `baseUrl`, `adminHome`, `bodyClass` and `googleAnalytics` (`{"measurementId": "G-XXXXXXXXXX"}`; an empty ID switches Google Analytics off, anything that is not a GA4 ID is a 400).
|
|
619
619
|
|
|
620
620
|
**Auth required:** Yes (`settings:update`)
|
|
621
621
|
|
package/docs/configuration.md
CHANGED
|
@@ -196,11 +196,29 @@ The main site-wide configuration. This file is editable from the **Settings** ad
|
|
|
196
196
|
| `footer` | Footer copyright, legacy link list, and the "Reduce motion" switch - see below |
|
|
197
197
|
| `backToTop` | Scroll-to-top button configuration |
|
|
198
198
|
| `cookieConsent` | Cookie consent banner configuration |
|
|
199
|
+
| `googleAnalytics` | `{measurementId: "G-…"}` - Google Analytics 4, set at Site Settings > Analytics. See **Google Analytics** below |
|
|
199
200
|
| `breadcrumbs` | Breadcrumb navigation configuration |
|
|
200
201
|
| `bodyClass` | Your own CSS classes on the `<body>` of every public page (Site Settings > General > Body CSS classes) |
|
|
201
202
|
| `baseUrl` | **Optional override.** Absolute origin (e.g. `https://example.com`). By default the CMS derives the origin from the incoming request (`request.protocol` + `request.host`), so sitemap URLs and canonical tags automatically match whatever domain you serve from - set this only if you sit behind infrastructure that doesn't forward `Host` correctly. Password reset links always use it when it is set; without it they go only to an address someone has signed in at, never to whatever host a reset request claims. Editable as **Site URL** in Settings › General. |
|
|
202
203
|
| `seo` | SEO defaults applied when a page has no overrides - see below |
|
|
203
204
|
|
|
205
|
+
### Google Analytics
|
|
206
|
+
|
|
207
|
+
Paste a GA4 measurement ID (`G-XXXXXXXXXX`, from Google Analytics > Admin > Data streams) at
|
|
208
|
+
**Site Settings > Analytics**; empty switches it off. Only a real ID is saved.
|
|
209
|
+
|
|
210
|
+
- GA sets cookies, so it waits for consent: Google's tag is fetched only after a visitor accepts the cookie
|
|
211
|
+
banner's **Analytics** category. No answer, or a rejection, means nothing is sent to Google. Withdrawing
|
|
212
|
+
consent later switches GA off for the page and deletes its `_ga` cookies.
|
|
213
|
+
- Visitors change their answer with the footer's **Cookie settings** link, added to the legal links while the
|
|
214
|
+
banner is on (`cookieConsent.showSettingsLink: false` hides it, `settingsLinkText` words it). Any link to
|
|
215
|
+
`#cookie-settings` - a menu item, say - opens the banner too, and the footer then leaves its own out.
|
|
216
|
+
- While an ID is set the cookie banner is shown, with its Analytics category, even if its own switch is off -
|
|
217
|
+
it is the only way GA ever loads. Give it a privacy policy address (Site Settings flags one that is missing).
|
|
218
|
+
- The site's Content-Security-Policy gains Google's hosts (`*.googletagmanager.com`, `*.google-analytics.com`,
|
|
219
|
+
`*.analytics.google.com`) only while an ID is set, and loses them when it is cleared - no restart.
|
|
220
|
+
- Editor previews are never sent. The built-in Analytics Tool is separate and cookieless; both can run.
|
|
221
|
+
|
|
204
222
|
### Outgoing email
|
|
205
223
|
|
|
206
224
|
**Certificates are checked.** The mail server's TLS certificate must be valid for the name in
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/public/js/btt.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";var
|
|
1
|
+
(function(){"use strict";var c=window.__CMS_SITE__||{},a=[],h="http://www.w3.org/2000/svg";function f(t,e){var n=Number(t);return t!=null&&t!==""&&Number.isFinite(n)?Math.max(0,n):e}function p(){return Math.max(0,document.documentElement.scrollHeight-window.innerHeight)}function v(t,e){if(!(!t||t.enabled===!1)){var n=e?"Go to bottom":"Back to top",s=e?"go-to-bottom-btn":"back-to-top-btn";if(!document.getElementById(s)){var o=document.createElement("button");o.id=s,o.type="button",o.setAttribute("aria-label",n),o.title=n;var r=document.createElementNS(h,"svg");Object.entries({viewBox:"0 0 24 24",width:"20",height:"20",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"}).forEach(function(l){r.setAttribute(l[0],l[1])});var i=document.createElementNS(h,"path");if(i.setAttribute("d",e?"M12 5v14m-7-7 7 7 7-7":"M12 19V5M5 12l7-7 7 7"),r.appendChild(i),o.appendChild(r),t.label){var m=document.createElement("span");m.textContent=t.label,m.style.marginLeft="6px",o.appendChild(m)}var b=f(t.offset,32),x=f(t.bottomOffset,b),u=t.position!=="bottom-left";Object.assign(o.style,{position:"fixed",bottom:x+"px",right:u?b+"px":"auto",left:u?"auto":b+"px",zIndex:"9999",display:"flex",alignItems:"center",padding:t.label?"10px 16px":"10px",borderRadius:t.label?"999px":"50%",border:"none",cursor:"pointer",background:"rgba(0,0,0,0.5)",color:"#fff",backdropFilter:"blur(4px)",opacity:"0",transform:"translateY(12px)",transition:"opacity .25s, transform .25s",pointerEvents:"none"}),o.tabIndex=-1,document.body.appendChild(o),o.addEventListener("click",function(){var l=document.documentElement.classList.contains("dm-reduced-motion")||document.body.classList.contains("dm-reduced-motion")||window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;window.scrollTo({top:e?p():0,behavior:t.smooth!==!1&&!l?"smooth":"auto"})}),a.push({btn:o,down:e,right:u,offset:b,bottomOffset:x,threshold:f(t.scrollThreshold,300)})}}}if(v(c.backToTop,!1),v(c.goToBottom,!0),!a.length)return;function d(){var t=p();if(a.forEach(function(r){var i=t>0&&(r.down?t-window.scrollY>r.threshold:window.scrollY>r.threshold);r.btn.style.opacity=i?"1":"0",r.btn.style.transform=i?"translateY(0)":"translateY(12px)",r.btn.style.pointerEvents=i?"auto":"none",r.btn.tabIndex=i?0:-1,r.btn.setAttribute("aria-hidden",String(!i))}),a.length===2){var e=a[0],n=a[1];if(e.right===n.right&&Math.abs(e.offset-n.offset)<Math.max(e.btn.offsetWidth,n.btn.offsetWidth)){var s=n.btn.offsetHeight||40,o=Math.abs(e.bottomOffset-n.bottomOffset)<s+12;e.btn.style.bottom=(o?n.bottomOffset+s+12:e.bottomOffset)+"px"}}}window.addEventListener("scroll",d,{passive:!0}),window.addEventListener("resize",d),typeof ResizeObserver=="function"&&new ResizeObserver(d).observe(document.body),d()})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";
|
|
1
|
+
(function(){"use strict";var o=window.Domma,s=o&&(o.elements&&o.elements.cookieConsent||o.components&&o.components.cookieConsent);if(!s)return;var e=window.__CMS_SITE__&&window.__CMS_SITE__.cookieConsent;if(!e)return;var i={necessary:{label:"Necessary Cookies",description:"These cookies are essential for the website to function properly.",required:!0}};e.showFunctional!==!1&&(i.functional={label:"Functional Cookies",description:"These cookies enable personalised features and functionality.",required:!1}),e.showAnalytics!==!1&&(i.analytics={label:"Analytics Cookies",description:"These cookies help us understand how visitors interact with our website.",required:!1}),e.showMarketing!==!1&&(i.marketing={label:"Marketing Cookies",description:"These cookies are used to deliver relevant ads and marketing campaigns.",required:!1});var n={message:e.message,acceptAllText:e.acceptAllText,rejectAllText:e.rejectAllText,customizeText:e.customizeText,savePreferencesText:e.savePreferencesText,privacyPolicyText:e.privacyPolicyText,privacyPolicyUrl:e.privacyPolicyUrl||null,cookiePolicyText:e.cookiePolicyText,cookiePolicyUrl:e.cookiePolicyUrl||null,position:e.position||"bottom",layout:e.layout||"bar",theme:e.theme||"dark",categories:i,consentVersion:e.consentVersion||"1.0",autoShow:!0,animation:!0,reopenSelector:'[data-cookie-consent-open], a[href="#cookie-settings"]',onChange:function(t,c){a(t,c)}};Object.keys(n).forEach(function(t){(n[t]===void 0||n[t]==="")&&delete n[t]});function a(t,c){document.dispatchEvent(new CustomEvent("dm-cookie-consent",{detail:{state:t,preferences:c||{}}}))}window.__CMS_CONSENT__=s.call(o.elements&&o.elements.cookieConsent===s?o.elements:o.components,n)})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";var i=window.__CMS_SITE__||{},n=i.googleAnalytics&&i.googleAnalytics.measurementId;if(!n||!/^G-[A-Z0-9]{4,20}$/.test(n)||window.__CMS_PREVIEW__)return;var t=!1;function d(){if(window["ga-disable-"+n]=!1,!t){t=!0,window.dataLayer=window.dataLayer||[],window.gtag=window.gtag||function(){window.dataLayer.push(arguments)},window.gtag("js",new Date),window.gtag("config",n,{cookie_domain:"none"});var e=document.createElement("script");e.async=!0,e.src="https://www.googletagmanager.com/gtag/js?id="+encodeURIComponent(n),document.head.appendChild(e)}}function c(e){return e==="_ga"||e.indexOf("_ga_")===0||e==="_gid"||e==="_gat"}function a(){return document.cookie.split(";").map(function(e){return e.split("=")[0].trim()}).filter(c)}function s(){a().forEach(function(e){document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+location.hostname})}function u(){window["ga-disable-"+n]=!0,s()}function r(e){e&&e.analytics===!0?d():(t||a().length)&&u()}var o=window.__CMS_CONSENT__;o&&typeof o.getPreferences=="function"&&r(o.getPreferences()),document.addEventListener("dm-cookie-consent",function(e){r(e.detail&&e.detail.preferences)})})();
|