domma-cms 0.1.0 → 0.2.1
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/README.md +2 -3
- package/admin/css/admin.css +78 -1
- package/admin/js/api.js +32 -0
- package/admin/js/app.js +24 -7
- package/admin/js/config/sidebar-config.js +8 -0
- package/admin/js/templates/collection-editor.html +80 -0
- package/admin/js/templates/collection-entries.html +36 -0
- package/admin/js/templates/collections.html +12 -0
- package/admin/js/templates/documentation.html +136 -0
- package/admin/js/templates/navigation.html +26 -4
- package/admin/js/templates/page-editor.html +91 -85
- package/admin/js/templates/settings.html +433 -172
- package/admin/js/views/collection-editor.js +487 -0
- package/admin/js/views/collection-entries.js +484 -0
- package/admin/js/views/collections.js +153 -0
- package/admin/js/views/dashboard.js +14 -6
- package/admin/js/views/index.js +9 -3
- package/admin/js/views/login.js +3 -2
- package/admin/js/views/navigation.js +77 -11
- package/admin/js/views/page-editor.js +207 -25
- package/admin/js/views/pages.js +14 -6
- package/admin/js/views/settings.js +137 -2
- package/admin/js/views/users.js +10 -7
- package/bin/cli.js +53 -17
- package/config/auth.json +2 -1
- package/config/content.json +1 -0
- package/config/navigation.json +14 -4
- package/config/plugins.json +0 -18
- package/config/presets.json +4 -8
- package/config/site.json +44 -3
- package/package.json +6 -2
- package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
- package/plugins/domma-effects/plugin.js +125 -0
- package/plugins/domma-effects/public/inject-body.html +19 -0
- package/plugins/example-analytics/admin/views/analytics.js +2 -2
- package/plugins/example-analytics/plugin.json +8 -0
- package/plugins/example-analytics/stats.json +15 -1
- package/plugins/form-builder/admin/templates/form-editor.html +19 -6
- package/plugins/form-builder/admin/views/form-editor.js +634 -9
- package/plugins/form-builder/admin/views/form-submissions.js +4 -4
- package/plugins/form-builder/admin/views/forms-list.js +5 -5
- package/plugins/form-builder/data/forms/consent.json +104 -0
- package/plugins/form-builder/data/forms/contacts.json +66 -0
- package/plugins/form-builder/data/submissions/consent.json +13 -0
- package/plugins/form-builder/data/submissions/contacts.json +26 -0
- package/plugins/form-builder/plugin.js +62 -11
- package/plugins/form-builder/plugin.json +12 -16
- package/plugins/form-builder/public/form-logic-engine.js +568 -0
- package/plugins/form-builder/public/inject-body.html +88 -6
- package/plugins/form-builder/public/inject-head.html +16 -0
- package/plugins/form-builder/public/package.json +1 -0
- package/public/css/site.css +113 -0
- package/public/js/btt.js +90 -0
- package/public/js/cookie-consent.js +61 -0
- package/public/js/site.js +129 -34
- package/scripts/build.js +129 -0
- package/scripts/seed.js +517 -7
- package/scripts/setup.js +12 -9
- package/server/routes/api/collections.js +301 -0
- package/server/routes/api/settings.js +66 -2
- package/server/server.js +19 -15
- package/server/services/collections.js +430 -0
- package/server/services/content.js +11 -2
- package/server/services/hooks.js +109 -0
- package/server/services/markdown.js +500 -149
- package/server/services/plugins.js +6 -1
- package/server/services/renderer.js +73 -7
- package/server/templates/page.html +38 -3
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
- package/plugins/back-to-top/config.js +0 -10
- package/plugins/back-to-top/plugin.js +0 -24
- package/plugins/back-to-top/plugin.json +0 -36
- package/plugins/back-to-top/public/inject-body.html +0 -105
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
- package/plugins/cookie-consent/config.js +0 -30
- package/plugins/cookie-consent/plugin.js +0 -24
- package/plugins/cookie-consent/plugin.json +0 -36
- package/plugins/cookie-consent/public/inject-body.html +0 -69
- package/plugins/custom-css/admin/templates/custom-css.html +0 -17
- package/plugins/custom-css/admin/views/custom-css.js +0 -35
- package/plugins/custom-css/config.js +0 -1
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +0 -63
- package/plugins/custom-css/plugin.json +0 -32
- package/plugins/custom-css/public/inject-head.html +0 -1
- package/plugins/form-builder/data/forms/contact.json +0 -52
- package/plugins/form-builder/data/submissions/contact.json +0 -14
|
@@ -14,6 +14,7 @@ import fs from 'fs/promises';
|
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import {getConfig, saveConfig} from '../config.js';
|
|
16
16
|
import {authenticate, requireAdmin, requireRole} from '../middleware/auth.js';
|
|
17
|
+
import {hooks, registerSanitizeRules, registerShortcode, registerTransform} from './hooks.js';
|
|
17
18
|
|
|
18
19
|
const PLUGINS_DIR = path.resolve('plugins');
|
|
19
20
|
|
|
@@ -153,7 +154,11 @@ export async function registerPlugins(fastify) {
|
|
|
153
154
|
const { default: plugin } = await import(entryPath);
|
|
154
155
|
await loadConfigDefaults(manifest.name);
|
|
155
156
|
const prefix = `/api/plugins/${manifest.name}`;
|
|
156
|
-
await fastify.register(plugin, {
|
|
157
|
+
await fastify.register(plugin, {
|
|
158
|
+
prefix,
|
|
159
|
+
auth: {authenticate, requireRole, requireAdmin},
|
|
160
|
+
hooks: {registerShortcode, registerSanitizeRules, registerTransform, on: hooks.on.bind(hooks)}
|
|
161
|
+
});
|
|
157
162
|
loaded.push(manifest.name);
|
|
158
163
|
} catch (err) {
|
|
159
164
|
fastify.log.error(`Plugin "${manifest.name}" server failed to load: ${err.message}`);
|
|
@@ -7,6 +7,17 @@ import path from 'path';
|
|
|
7
7
|
import {fileURLToPath} from 'url';
|
|
8
8
|
import {getConfig} from '../config.js';
|
|
9
9
|
import {getInjectionSnippets} from './plugins.js';
|
|
10
|
+
import {applyTransforms} from './hooks.js';
|
|
11
|
+
|
|
12
|
+
const CUSTOM_CSS_PATH = new URL('../../content/custom.css', import.meta.url).pathname;
|
|
13
|
+
|
|
14
|
+
async function getCustomCss() {
|
|
15
|
+
try {
|
|
16
|
+
return await fs.readFile(CUSTOM_CSS_PATH, 'utf8');
|
|
17
|
+
} catch {
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
10
21
|
|
|
11
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
23
|
const TEMPLATE_PATH = path.join(__dirname, '../templates/page.html');
|
|
@@ -26,12 +37,13 @@ let _templateCache = null;
|
|
|
26
37
|
* @returns {Promise<string>}
|
|
27
38
|
*/
|
|
28
39
|
export async function renderPage(page) {
|
|
29
|
-
const [template, site, navigation, presets, injection] = await Promise.all([
|
|
40
|
+
const [template, site, navigation, presets, injection, customCss] = await Promise.all([
|
|
30
41
|
getTemplate(),
|
|
31
42
|
Promise.resolve(getConfig('site')),
|
|
32
43
|
Promise.resolve(getConfig('navigation')),
|
|
33
44
|
Promise.resolve(getConfig('presets')),
|
|
34
|
-
getInjectionSnippets()
|
|
45
|
+
getInjectionSnippets(),
|
|
46
|
+
getCustomCss()
|
|
35
47
|
]);
|
|
36
48
|
|
|
37
49
|
const preset = presets[page.layout] || presets['default'] || {};
|
|
@@ -40,6 +52,24 @@ export async function renderPage(page) {
|
|
|
40
52
|
const seoDescription = page.seo?.description || site.seo?.defaultDescription || '';
|
|
41
53
|
const ogImage = page.seo?.image || site.seo?.defaultImage || '';
|
|
42
54
|
|
|
55
|
+
const dconfig = page.dconfig || null;
|
|
56
|
+
// Escape </script> to prevent injection via dconfig values in the inline script block
|
|
57
|
+
const dconfigJson = dconfig
|
|
58
|
+
? JSON.stringify(dconfig).replace(/<\/script>/gi, '<\\/script>')
|
|
59
|
+
: null;
|
|
60
|
+
const dconfigScript = dconfigJson
|
|
61
|
+
? `window.__CMS_DCONFIG__ = ${dconfigJson};`
|
|
62
|
+
: '';
|
|
63
|
+
|
|
64
|
+
const {fontLink, fontOverride} = buildFontVars(site.fontFamily, site.fontSize);
|
|
65
|
+
const fontStyleTag = fontOverride
|
|
66
|
+
? `<style>${fontOverride}</style>`
|
|
67
|
+
: '';
|
|
68
|
+
|
|
69
|
+
const customCssTag = customCss.trim()
|
|
70
|
+
? `<style>${customCss.replace(/<\/style>/gi, '<\\/style>')}</style>`
|
|
71
|
+
: '';
|
|
72
|
+
|
|
43
73
|
const vars = {
|
|
44
74
|
seoTitle,
|
|
45
75
|
seoDescription,
|
|
@@ -47,18 +77,54 @@ export async function renderPage(page) {
|
|
|
47
77
|
title: page.title,
|
|
48
78
|
html: page.html,
|
|
49
79
|
theme: site.theme || 'charcoal-dark',
|
|
80
|
+
fontLink,
|
|
81
|
+
fontStyleTag,
|
|
50
82
|
layout: page.layout || 'default',
|
|
51
83
|
showNavbar: preset.navbar !== false,
|
|
52
84
|
showFooter: preset.footer !== false,
|
|
53
85
|
showSidebar: preset.sidebar === true || page.sidebar === true,
|
|
54
|
-
navJson: JSON.stringify(navigation),
|
|
55
|
-
siteJson: JSON.stringify(
|
|
86
|
+
navJson: JSON.stringify(navigation).replace(/<\/script>/gi, '<\\/script>'),
|
|
87
|
+
siteJson: JSON.stringify(Object.assign(
|
|
88
|
+
{ title: site.title, footer: site.footer, social: site.social || null },
|
|
89
|
+
site.backToTop?.enabled ? { backToTop: site.backToTop } : {},
|
|
90
|
+
site.cookieConsent?.enabled ? { cookieConsent: site.cookieConsent } : {}
|
|
91
|
+
)).replace(/<\/script>/gi, '<\\/script>'),
|
|
56
92
|
headInject: injection.head,
|
|
57
|
-
|
|
58
|
-
bodyEndInject:
|
|
93
|
+
headInjectLate: [injection.headLate, customCssTag].filter(Boolean).join('\n'),
|
|
94
|
+
bodyEndInject: [
|
|
95
|
+
injection.bodyEnd,
|
|
96
|
+
site.backToTop?.enabled ? '<script src="/public/js/btt.js"></script>' : '',
|
|
97
|
+
site.cookieConsent?.enabled ? '<script src="/public/js/cookie-consent.js"></script>' : ''
|
|
98
|
+
].filter(Boolean).join('\n'),
|
|
99
|
+
dconfigScript,
|
|
59
100
|
};
|
|
60
101
|
|
|
61
|
-
|
|
102
|
+
const finalVars = applyTransforms('render:beforeRender', vars, {page});
|
|
103
|
+
const html = interpolate(template, finalVars);
|
|
104
|
+
return applyTransforms('render:afterRender', html, {page});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildFontVars(fontFamily, fontSize) {
|
|
108
|
+
const PRECONNECT = '<link rel="preconnect" href="https://fonts.googleapis.com">\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>';
|
|
109
|
+
const family = fontFamily || 'Roboto';
|
|
110
|
+
const rules = [];
|
|
111
|
+
|
|
112
|
+
if (fontSize && fontSize !== 16) rules.push(`html { font-size: ${fontSize}px; }`);
|
|
113
|
+
|
|
114
|
+
let fontLink;
|
|
115
|
+
if (family === 'system-ui') {
|
|
116
|
+
fontLink = null;
|
|
117
|
+
rules.push('body, button, input, select, textarea { font-family: system-ui, -apple-system, sans-serif; }');
|
|
118
|
+
} else {
|
|
119
|
+
const encoded = family.replace(/ /g, '+');
|
|
120
|
+
const href = `https://fonts.googleapis.com/css2?family=${encoded}:ital,wght@0,300;0,400;0,500;0,700;1,400&display=swap`;
|
|
121
|
+
fontLink = `${PRECONNECT}\n <link rel="stylesheet" href="${href}">`;
|
|
122
|
+
if (family !== 'Roboto') {
|
|
123
|
+
rules.push(`body, button, input, select, textarea { font-family: '${family}', sans-serif; }`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {fontLink, fontOverride: rules.length ? rules.join(' ') : null};
|
|
62
128
|
}
|
|
63
129
|
|
|
64
130
|
/**
|
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
{{#if ogImage}}<meta property="og:image" content="{{ogImage}}">{{/if}}
|
|
9
9
|
|
|
10
10
|
<!-- Fonts -->
|
|
11
|
-
|
|
12
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,400&display=swap">
|
|
11
|
+
{{#if fontLink}}{{fontLink}}{{/if}}
|
|
14
12
|
|
|
15
13
|
<!-- DommaJS CSS -->
|
|
16
14
|
<link rel="stylesheet" href="/dist/domma/domma.css">
|
|
@@ -21,6 +19,9 @@
|
|
|
21
19
|
<!-- Site CSS -->
|
|
22
20
|
<link rel="stylesheet" href="/public/css/site.css">
|
|
23
21
|
|
|
22
|
+
<!-- Font overrides -->
|
|
23
|
+
{{fontStyleTag}}
|
|
24
|
+
|
|
24
25
|
<!-- Plugin head injection -->
|
|
25
26
|
{{headInject}}
|
|
26
27
|
|
|
@@ -59,6 +60,39 @@
|
|
|
59
60
|
|
|
60
61
|
<!-- Initialise DommaJS before module loads -->
|
|
61
62
|
<script>
|
|
63
|
+
(function () {
|
|
64
|
+
var _stored;
|
|
65
|
+
try {
|
|
66
|
+
_stored = JSON.parse(localStorage.getItem('domma:reduced_motion'));
|
|
67
|
+
} catch (e) {
|
|
68
|
+
}
|
|
69
|
+
if (_stored === true) {
|
|
70
|
+
document.documentElement.classList.add('dm-reduced-motion');
|
|
71
|
+
}
|
|
72
|
+
// Override window.matchMedia so Domma JS effects (scribe, breathe, etc.)
|
|
73
|
+
// respect the stored preference. When explicitly set to false the user is
|
|
74
|
+
// overriding the OS "reduce" preference to allow motion on this site.
|
|
75
|
+
if (_stored !== null && _stored !== undefined && window.matchMedia) {
|
|
76
|
+
var _orig = window.matchMedia.bind(window);
|
|
77
|
+
window.matchMedia = function (q) {
|
|
78
|
+
if (q === '(prefers-reduced-motion: reduce)') {
|
|
79
|
+
return {
|
|
80
|
+
matches: !!_stored, media: q, onchange: null,
|
|
81
|
+
addListener: function () {
|
|
82
|
+
}, removeListener: function () {
|
|
83
|
+
},
|
|
84
|
+
addEventListener: function () {
|
|
85
|
+
}, removeEventListener: function () {
|
|
86
|
+
},
|
|
87
|
+
dispatchEvent: function () {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return _orig(q);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}());
|
|
62
96
|
if (window.Domma && typeof window.Domma.init === 'function') {
|
|
63
97
|
window.Domma.init();
|
|
64
98
|
}
|
|
@@ -67,6 +101,7 @@
|
|
|
67
101
|
}
|
|
68
102
|
window.__CMS_NAV__ = {{navJson}};
|
|
69
103
|
window.__CMS_SITE__ = {{siteJson}};
|
|
104
|
+
{{dconfigScript}}
|
|
70
105
|
</script>
|
|
71
106
|
|
|
72
107
|
<!-- Site initialisation -->
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="arrow-up"></span> Back to Top</h1>
|
|
3
|
-
<div>
|
|
4
|
-
<button id="save-settings-btn" class="btn btn-primary">
|
|
5
|
-
<span data-icon="save"></span> Save
|
|
6
|
-
</button>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
<div class="card mb-4">
|
|
11
|
-
<div class="card-header"><h2>Appearance</h2></div>
|
|
12
|
-
<div class="card-body">
|
|
13
|
-
<div class="row mb-3">
|
|
14
|
-
<div class="col-6">
|
|
15
|
-
<label class="form-label">Position</label>
|
|
16
|
-
<select id="field-position" class="form-select">
|
|
17
|
-
<option value="bottom-right">Bottom right</option>
|
|
18
|
-
<option value="bottom-left">Bottom left</option>
|
|
19
|
-
</select>
|
|
20
|
-
</div>
|
|
21
|
-
<div class="col-6">
|
|
22
|
-
<label class="form-label">Edge offset (px)</label>
|
|
23
|
-
<input id="field-offset" type="number" class="form-input" min="0" max="200" placeholder="32">
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
<div class="row">
|
|
27
|
-
<div class="col">
|
|
28
|
-
<label class="form-label">Button label <span class="text-muted">(optional text beside icon)</span></label>
|
|
29
|
-
<input id="field-label" type="text" class="form-input" placeholder="Leave empty for icon only">
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div class="card mb-4">
|
|
36
|
-
<div class="card-header"><h2>Behaviour</h2></div>
|
|
37
|
-
<div class="card-body">
|
|
38
|
-
<div class="row mb-3">
|
|
39
|
-
<div class="col-6">
|
|
40
|
-
<label class="form-label">Scroll threshold (px)</label>
|
|
41
|
-
<input id="field-threshold" type="number" class="form-input" min="0" max="9999" placeholder="300">
|
|
42
|
-
<span class="form-hint">Button appears after scrolling this far down.</span>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
<div class="row">
|
|
46
|
-
<div class="col">
|
|
47
|
-
<label class="form-check-label">
|
|
48
|
-
<input id="field-smooth" type="checkbox">
|
|
49
|
-
Smooth scroll
|
|
50
|
-
</label>
|
|
51
|
-
<span class="form-hint">Animates scroll to top instead of jumping instantly.</span>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Back to Top Plugin — Admin Settings View
|
|
3
|
-
*/
|
|
4
|
-
import {apiRequest} from '/admin/js/api.js';
|
|
5
|
-
|
|
6
|
-
export const backToTopSettingsView = {
|
|
7
|
-
templateUrl: '/plugins/back-to-top/admin/templates/back-to-top-settings.html',
|
|
8
|
-
|
|
9
|
-
async onMount($container) {
|
|
10
|
-
let settings = {};
|
|
11
|
-
try {
|
|
12
|
-
settings = await apiRequest('/plugins/back-to-top/settings');
|
|
13
|
-
} catch {
|
|
14
|
-
E.toast('Could not load settings.', {type: 'error'});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
$container.find('#field-threshold').val(settings.scrollThreshold ?? 300);
|
|
18
|
-
$container.find('#field-position').val(settings.position || 'bottom-right');
|
|
19
|
-
$container.find('#field-label').val(settings.label || '');
|
|
20
|
-
$container.find('#field-smooth').prop('checked', settings.smooth !== false);
|
|
21
|
-
$container.find('#field-offset').val(settings.offset ?? 32);
|
|
22
|
-
|
|
23
|
-
$container.find('#save-settings-btn').off('click').on('click', async () => {
|
|
24
|
-
const data = {
|
|
25
|
-
scrollThreshold: parseInt($container.find('#field-threshold').val(), 10) || 300,
|
|
26
|
-
position: $container.find('#field-position').val(),
|
|
27
|
-
label: $container.find('#field-label').val().trim(),
|
|
28
|
-
smooth: $container.find('#field-smooth').prop('checked'),
|
|
29
|
-
offset: parseInt($container.find('#field-offset').val(), 10) || 32
|
|
30
|
-
};
|
|
31
|
-
try {
|
|
32
|
-
await apiRequest('/plugins/back-to-top/settings', {
|
|
33
|
-
method: 'PUT',
|
|
34
|
-
body: JSON.stringify(data)
|
|
35
|
-
});
|
|
36
|
-
E.toast('Settings saved.', {type: 'success'});
|
|
37
|
-
} catch {
|
|
38
|
-
E.toast('Failed to save settings.', {type: 'error'});
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
Domma.icons.scan();
|
|
43
|
-
}
|
|
44
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Back to Top Plugin — Server
|
|
3
|
-
*
|
|
4
|
-
* Endpoints (prefix: /api/plugins/back-to-top):
|
|
5
|
-
* GET /settings — public (called from public site IIFE)
|
|
6
|
-
* PUT /settings — admin-auth — saves user overrides
|
|
7
|
-
*/
|
|
8
|
-
import {getPluginSettings, savePluginState} from '../../server/services/plugins.js';
|
|
9
|
-
|
|
10
|
-
export default async function backToTopPlugin(fastify, options) {
|
|
11
|
-
const {authenticate, requireAdmin} = options.auth;
|
|
12
|
-
|
|
13
|
-
// GET /settings — no auth, public read (returns non-sensitive display config)
|
|
14
|
-
fastify.get('/settings', async () => {
|
|
15
|
-
return getPluginSettings('back-to-top');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// PUT /settings — admin only
|
|
19
|
-
fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request) => {
|
|
20
|
-
const body = request.body || {};
|
|
21
|
-
savePluginState('back-to-top', {settings: body});
|
|
22
|
-
return {ok: true};
|
|
23
|
-
});
|
|
24
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "back-to-top",
|
|
3
|
-
"displayName": "Back to Top",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "Injects a configurable scroll-to-top button into every public page.",
|
|
6
|
-
"author": "Darryl Waterhouse",
|
|
7
|
-
"date": "2026-03-02",
|
|
8
|
-
"icon": "arrow-up",
|
|
9
|
-
"admin": {
|
|
10
|
-
"sidebar": [
|
|
11
|
-
{
|
|
12
|
-
"id": "back-to-top",
|
|
13
|
-
"text": "Back to Top",
|
|
14
|
-
"icon": "arrow-up",
|
|
15
|
-
"url": "#/plugins/back-to-top",
|
|
16
|
-
"section": "#/plugins/back-to-top"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
|
-
"routes": [
|
|
20
|
-
{
|
|
21
|
-
"path": "/plugins/back-to-top",
|
|
22
|
-
"view": "plugin-back-to-top-settings",
|
|
23
|
-
"title": "Back to Top - Domma CMS"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"views": {
|
|
27
|
-
"plugin-back-to-top-settings": {
|
|
28
|
-
"entry": "back-to-top/admin/views/back-to-top-settings.js",
|
|
29
|
-
"exportName": "backToTopSettingsView"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"inject": {
|
|
34
|
-
"bodyEnd": "public/inject-body.html"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
(function () {
|
|
3
|
-
'use strict';
|
|
4
|
-
|
|
5
|
-
var BTN_ID = 'back-to-top-btn';
|
|
6
|
-
|
|
7
|
-
fetch('/api/plugins/back-to-top/settings')
|
|
8
|
-
.then(function (r) {
|
|
9
|
-
return r.json();
|
|
10
|
-
})
|
|
11
|
-
.then(function (cfg) {
|
|
12
|
-
var threshold = cfg.scrollThreshold || 300;
|
|
13
|
-
var position = cfg.position || 'bottom-right';
|
|
14
|
-
var label = cfg.label || '';
|
|
15
|
-
var smooth = cfg.smooth !== false;
|
|
16
|
-
var offset = cfg.offset || 32;
|
|
17
|
-
|
|
18
|
-
var btn = document.createElement('button');
|
|
19
|
-
btn.id = BTN_ID;
|
|
20
|
-
btn.type = 'button';
|
|
21
|
-
btn.setAttribute('aria-label', 'Back to top');
|
|
22
|
-
btn.setAttribute('title', 'Back to top');
|
|
23
|
-
|
|
24
|
-
// Inline SVG — arrow-up (Domma icon set path)
|
|
25
|
-
var svgNS = 'http://www.w3.org/2000/svg';
|
|
26
|
-
var svg = document.createElementNS(svgNS, 'svg');
|
|
27
|
-
svg.setAttribute('viewBox', '0 0 24 24');
|
|
28
|
-
svg.setAttribute('width', '20');
|
|
29
|
-
svg.setAttribute('height', '20');
|
|
30
|
-
svg.setAttribute('fill', 'none');
|
|
31
|
-
svg.setAttribute('stroke', 'currentColor');
|
|
32
|
-
svg.setAttribute('stroke-width', '2');
|
|
33
|
-
svg.setAttribute('stroke-linecap', 'round');
|
|
34
|
-
svg.setAttribute('stroke-linejoin', 'round');
|
|
35
|
-
var path = document.createElementNS(svgNS, 'path');
|
|
36
|
-
path.setAttribute('d', 'M12 19V5M5 12l7-7 7 7');
|
|
37
|
-
svg.appendChild(path);
|
|
38
|
-
btn.appendChild(svg);
|
|
39
|
-
|
|
40
|
-
if (label) {
|
|
41
|
-
var span = document.createElement('span');
|
|
42
|
-
span.textContent = label;
|
|
43
|
-
span.style.marginLeft = '6px';
|
|
44
|
-
btn.appendChild(span);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Inline styles — self-contained, no site.css dependency
|
|
48
|
-
var isRight = position !== 'bottom-left';
|
|
49
|
-
Object.assign(btn.style, {
|
|
50
|
-
position: 'fixed',
|
|
51
|
-
bottom: offset + 'px',
|
|
52
|
-
right: isRight ? offset + 'px' : 'auto',
|
|
53
|
-
left: isRight ? 'auto' : offset + 'px',
|
|
54
|
-
zIndex: '9999',
|
|
55
|
-
display: 'flex',
|
|
56
|
-
alignItems: 'center',
|
|
57
|
-
padding: '10px',
|
|
58
|
-
borderRadius: '50%',
|
|
59
|
-
border: 'none',
|
|
60
|
-
cursor: 'pointer',
|
|
61
|
-
background: 'rgba(0,0,0,0.5)',
|
|
62
|
-
color: '#fff',
|
|
63
|
-
backdropFilter: 'blur(4px)',
|
|
64
|
-
opacity: '0',
|
|
65
|
-
transform: 'translateY(12px)',
|
|
66
|
-
transition: 'opacity .25s, transform .25s',
|
|
67
|
-
pointerEvents: 'none'
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
if (label) {
|
|
71
|
-
btn.style.borderRadius = '999px';
|
|
72
|
-
btn.style.padding = '10px 16px';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
document.body.appendChild(btn);
|
|
76
|
-
|
|
77
|
-
function show() {
|
|
78
|
-
btn.style.opacity = '1';
|
|
79
|
-
btn.style.transform = 'translateY(0)';
|
|
80
|
-
btn.style.pointerEvents = 'auto';
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function hide() {
|
|
84
|
-
btn.style.opacity = '0';
|
|
85
|
-
btn.style.transform = 'translateY(12px)';
|
|
86
|
-
btn.style.pointerEvents = 'none';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
window.addEventListener('scroll', function () {
|
|
90
|
-
if (window.scrollY > threshold) {
|
|
91
|
-
show();
|
|
92
|
-
} else {
|
|
93
|
-
hide();
|
|
94
|
-
}
|
|
95
|
-
}, {passive: true});
|
|
96
|
-
|
|
97
|
-
btn.addEventListener('click', function () {
|
|
98
|
-
window.scrollTo({top: 0, behavior: smooth ? 'smooth' : 'auto'});
|
|
99
|
-
});
|
|
100
|
-
})
|
|
101
|
-
.catch(function () {
|
|
102
|
-
// Settings fetch failed — don't inject button
|
|
103
|
-
});
|
|
104
|
-
})();
|
|
105
|
-
</script>
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
<div class="view-header">
|
|
2
|
-
<h1><span data-icon="shield"></span> Cookie Consent</h1>
|
|
3
|
-
<div>
|
|
4
|
-
<button id="save-settings-btn" class="btn btn-primary">
|
|
5
|
-
<span data-icon="save"></span> Save
|
|
6
|
-
</button>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
<div class="card mb-4">
|
|
11
|
-
<div class="card-header"><h2>Banner Wording</h2></div>
|
|
12
|
-
<div class="card-body">
|
|
13
|
-
<div class="row mb-3">
|
|
14
|
-
<div class="col">
|
|
15
|
-
<label class="form-label">Message</label>
|
|
16
|
-
<textarea id="field-message" class="form-input" rows="3"></textarea>
|
|
17
|
-
</div>
|
|
18
|
-
</div>
|
|
19
|
-
<div class="row mb-3">
|
|
20
|
-
<div class="col-6">
|
|
21
|
-
<label class="form-label">Accept All button</label>
|
|
22
|
-
<input id="field-accept-all-text" type="text" class="form-input">
|
|
23
|
-
</div>
|
|
24
|
-
<div class="col-6">
|
|
25
|
-
<label class="form-label">Reject All button</label>
|
|
26
|
-
<input id="field-reject-all-text" type="text" class="form-input">
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="row mb-3">
|
|
30
|
-
<div class="col-6">
|
|
31
|
-
<label class="form-label">Customize button</label>
|
|
32
|
-
<input id="field-customize-text" type="text" class="form-input">
|
|
33
|
-
</div>
|
|
34
|
-
<div class="col-6">
|
|
35
|
-
<label class="form-label">Save Preferences button</label>
|
|
36
|
-
<input id="field-save-preferences-text" type="text" class="form-input">
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
<div class="row mb-3">
|
|
40
|
-
<div class="col-6">
|
|
41
|
-
<label class="form-label">Privacy Policy link text</label>
|
|
42
|
-
<input id="field-privacy-policy-text" type="text" class="form-input">
|
|
43
|
-
</div>
|
|
44
|
-
<div class="col-6">
|
|
45
|
-
<label class="form-label">Privacy Policy URL</label>
|
|
46
|
-
<input id="field-privacy-policy-url" type="text" class="form-input" placeholder="/privacy-policy">
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
<div class="row">
|
|
50
|
-
<div class="col-6">
|
|
51
|
-
<label class="form-label">Cookie Policy link text</label>
|
|
52
|
-
<input id="field-cookie-policy-text" type="text" class="form-input">
|
|
53
|
-
</div>
|
|
54
|
-
<div class="col-6">
|
|
55
|
-
<label class="form-label">Cookie Policy URL <span class="text-muted">(leave empty to hide)</span></label>
|
|
56
|
-
<input id="field-cookie-policy-url" type="text" class="form-input" placeholder="/cookie-policy">
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<div class="card mb-4">
|
|
63
|
-
<div class="card-header"><h2>Appearance & Categories</h2></div>
|
|
64
|
-
<div class="card-body">
|
|
65
|
-
<div class="row mb-3">
|
|
66
|
-
<div class="col-4">
|
|
67
|
-
<label class="form-label">Position</label>
|
|
68
|
-
<select id="field-position" class="form-select">
|
|
69
|
-
<option value="bottom">Bottom</option>
|
|
70
|
-
<option value="top">Top</option>
|
|
71
|
-
</select>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="col-4">
|
|
74
|
-
<label class="form-label">Layout</label>
|
|
75
|
-
<select id="field-layout" class="form-select">
|
|
76
|
-
<option value="bar">Bar</option>
|
|
77
|
-
<option value="modal">Modal</option>
|
|
78
|
-
</select>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="col-4">
|
|
81
|
-
<label class="form-label">Theme</label>
|
|
82
|
-
<select id="field-theme" class="form-select">
|
|
83
|
-
<option value="dark">Dark</option>
|
|
84
|
-
<option value="light">Light</option>
|
|
85
|
-
</select>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
<div class="row mb-3">
|
|
89
|
-
<div class="col">
|
|
90
|
-
<label class="form-label">Cookie categories shown in Customize panel</label>
|
|
91
|
-
<span class="form-hint">Necessary cookies are always shown and always required.</span>
|
|
92
|
-
<div style="margin-top:.75rem;display:flex;flex-direction:column;gap:.5rem;">
|
|
93
|
-
<label class="form-check-label">
|
|
94
|
-
<input id="field-show-functional" type="checkbox"> Functional Cookies
|
|
95
|
-
</label>
|
|
96
|
-
<label class="form-check-label">
|
|
97
|
-
<input id="field-show-analytics" type="checkbox"> Analytics Cookies
|
|
98
|
-
</label>
|
|
99
|
-
<label class="form-check-label">
|
|
100
|
-
<input id="field-show-marketing" type="checkbox"> Marketing Cookies
|
|
101
|
-
</label>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
<div class="row">
|
|
106
|
-
<div class="col-4">
|
|
107
|
-
<label class="form-label">Consent version</label>
|
|
108
|
-
<input id="field-consent-version" type="text" class="form-input" placeholder="1.0">
|
|
109
|
-
<span class="form-hint">Increment to re-prompt users who already accepted.</span>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|