domma-cms 0.55.1 → 0.69.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Blog screens share: the settings panel, and the small pieces every
|
|
3
|
+
* view on the Tool upgrade path needs (help popovers, teardown, slideover
|
|
4
|
+
* cleanup, a ⋮ that opens the row's context menu).
|
|
5
|
+
*
|
|
6
|
+
* Loaded by the views at run time with the loader's cache-busting token, the
|
|
7
|
+
* way settings-cog.js is, so an update is never served a stale copy.
|
|
8
|
+
*
|
|
9
|
+
* @module blog/admin/views/kit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const BASE = '/api/plugins/blog';
|
|
13
|
+
|
|
14
|
+
export function escapeHtml(s) {
|
|
15
|
+
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The blog's public base path (`/blog` unless configured), for links to the
|
|
20
|
+
* live site. Read once per page load; a change needs a restart anyway.
|
|
21
|
+
*
|
|
22
|
+
* @returns {Promise<string>}
|
|
23
|
+
*/
|
|
24
|
+
let basePathCache = null;
|
|
25
|
+
export async function sitePath() {
|
|
26
|
+
if (basePathCache) return basePathCache;
|
|
27
|
+
try {
|
|
28
|
+
const s = await H.get(`${BASE}/settings`);
|
|
29
|
+
basePathCache = String((s?.data ?? s)?.basePath || '/blog').replace(/\/+$/, '') || '/blog';
|
|
30
|
+
} catch {
|
|
31
|
+
return '/blog';
|
|
32
|
+
}
|
|
33
|
+
return basePathCache;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A shared admin helper, loaded at run time - a plugin must not import admin/
|
|
38
|
+
* statically, because an admin that predates the helper would take the whole
|
|
39
|
+
* view down with it. Null when it is not there.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} file - e.g. 'help-popover.js'
|
|
42
|
+
* @returns {Promise<object|null>}
|
|
43
|
+
*/
|
|
44
|
+
export async function adminLib(file) {
|
|
45
|
+
try { return await import(`/admin/js/lib/${file}`); } catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Give every `[data-help]` under `el` its (?) popover. Run it BEFORE
|
|
50
|
+
* M.applyBindings: a `data-if` lifts its element out of the document, and
|
|
51
|
+
* a marker scanned after that is never found.
|
|
52
|
+
*
|
|
53
|
+
* @param {HTMLElement} el
|
|
54
|
+
* @returns {Promise<() => void>} dispose
|
|
55
|
+
*/
|
|
56
|
+
export async function scanHelp(el) {
|
|
57
|
+
const lib = await adminLib('help-popover.js');
|
|
58
|
+
return lib?.helpPopovers ? lib.helpPopovers(el, {position: 'bottom'}) : () => {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The admin's shared Tool kit (admin/js/lib/tool-kit.js), loaded once at run
|
|
63
|
+
* time. Everything below prefers it, so the Blog behaves exactly like Notes,
|
|
64
|
+
* Todo and Analytics; the local copies are only for an admin that predates it.
|
|
65
|
+
*/
|
|
66
|
+
const core = await adminLib('tool-kit.js');
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Per-view teardown. Domma's router has no unmount hook, so a view's effects,
|
|
70
|
+
* bindings and listeners are dropped when the hash leaves its route - and on
|
|
71
|
+
* a remount, in case it never did.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} name - a key per view
|
|
74
|
+
* @param {RegExp} route - the hashes this view owns
|
|
75
|
+
* @returns {(fn: Function) => void} add a disposer
|
|
76
|
+
*/
|
|
77
|
+
const lifetimes = new Map();
|
|
78
|
+
export function viewScope(name, route) {
|
|
79
|
+
let life = lifetimes.get(name);
|
|
80
|
+
if (!life) {
|
|
81
|
+
life = core?.viewLifetime ? core.viewLifetime(route) : localLifetime(route);
|
|
82
|
+
lifetimes.set(name, life);
|
|
83
|
+
}
|
|
84
|
+
life.reset();
|
|
85
|
+
return (fn) => { if (typeof fn === 'function') life.add(fn); };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function localLifetime(pattern) {
|
|
89
|
+
let fns = [];
|
|
90
|
+
const run = () => { const list = fns; fns = []; list.forEach(fn => { try { fn(); } catch { /* gone */ } }); };
|
|
91
|
+
const onHash = () => { if (!pattern.test(location.hash)) run(); };
|
|
92
|
+
return {
|
|
93
|
+
add(fn) { fns.push(fn); },
|
|
94
|
+
reset() {
|
|
95
|
+
run();
|
|
96
|
+
window.addEventListener('hashchange', onHash);
|
|
97
|
+
fns.push(() => window.removeEventListener('hashchange', onHash));
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run `cleanup` once when the slideover closes, then take it out of the page -
|
|
104
|
+
* Domma hides a closed slideover rather than removing it.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} so
|
|
107
|
+
* @param {Function} [cleanup]
|
|
108
|
+
*/
|
|
109
|
+
export function onceClosed(so, cleanup = () => {}) {
|
|
110
|
+
if (core?.onceClosed) return core.onceClosed(so, cleanup);
|
|
111
|
+
const close = so.close.bind(so);
|
|
112
|
+
let done = false;
|
|
113
|
+
so.close = (...args) => {
|
|
114
|
+
const result = close(...args);
|
|
115
|
+
if (!done) {
|
|
116
|
+
done = true;
|
|
117
|
+
cleanup();
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
try { so.destroy?.(); } catch { /* already gone */ }
|
|
120
|
+
so.element?.remove();
|
|
121
|
+
}, 400);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Open the row's context menu from its ⋮, anchored under the button - the same
|
|
129
|
+
* menu a right-click opens, so there is one list of actions, not two.
|
|
130
|
+
*
|
|
131
|
+
* @param {HTMLElement} button
|
|
132
|
+
*/
|
|
133
|
+
export function openMenuFrom(button) {
|
|
134
|
+
if (core?.openMenuAt) return core.openMenuAt(button);
|
|
135
|
+
const r = button.getBoundingClientRect();
|
|
136
|
+
button.dispatchEvent(new MouseEvent('contextmenu', {
|
|
137
|
+
bubbles: true, cancelable: true, clientX: r.left, clientY: r.bottom, view: window
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Drop hidden items, and the dividers that hiding leaves doubled or dangling.
|
|
143
|
+
*
|
|
144
|
+
* @param {object[]} items
|
|
145
|
+
* @returns {object[]}
|
|
146
|
+
*/
|
|
147
|
+
export function tidy(items) {
|
|
148
|
+
const shown = items.filter(i => i.visible !== false);
|
|
149
|
+
return shown.filter((i, n) => i.type !== 'divider'
|
|
150
|
+
|| (n > 0 && n < shown.length - 1 && shown[n - 1].type !== 'divider'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Build a small DOM tree from an HTML string that holds NO user data. */
|
|
154
|
+
export function fragment(html) {
|
|
155
|
+
const t = document.createElement('template');
|
|
156
|
+
t.innerHTML = html.trim();
|
|
157
|
+
return t.content.firstElementChild;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A filter handed from one Blog screen to the next - "Show its posts" on a
|
|
162
|
+
* category lands on Posts already filtered. sessionStorage, because it is a
|
|
163
|
+
* one-shot hand-off rather than a preference.
|
|
164
|
+
*/
|
|
165
|
+
export function handOff(key, value) {
|
|
166
|
+
try { sessionStorage.setItem(`blog.handoff.${key}`, value); } catch { /* storage blocked */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function takeHandOff(key) {
|
|
170
|
+
try {
|
|
171
|
+
const v = sessionStorage.getItem(`blog.handoff.${key}`);
|
|
172
|
+
sessionStorage.removeItem(`blog.handoff.${key}`);
|
|
173
|
+
return v;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Settings
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
const SETTINGS_PANEL = `
|
|
184
|
+
<div class="blog-panel">
|
|
185
|
+
<div class="blog-field">
|
|
186
|
+
<label class="form-label" for="blog-set-base">Base path
|
|
187
|
+
<span data-help="The URL every blog page lives under - /blog gives /blog/my-post. Takes effect after a server restart."></span>
|
|
188
|
+
</label>
|
|
189
|
+
<input id="blog-set-base" type="text" class="form-input font-mono" placeholder="/blog" data-model="basePath.value">
|
|
190
|
+
</div>
|
|
191
|
+
<div class="blog-field">
|
|
192
|
+
<label class="form-label" for="blog-set-ppp">Posts per page
|
|
193
|
+
<span data-help="How many posts a listing page shows before paging. Takes effect after a server restart."></span>
|
|
194
|
+
</label>
|
|
195
|
+
<input id="blog-set-ppp" type="number" min="1" max="100" class="form-input" data-model="postsPerPage.value">
|
|
196
|
+
</div>
|
|
197
|
+
<div class="blog-field">
|
|
198
|
+
<label class="form-label" for="blog-set-feed">RSS feed length
|
|
199
|
+
<span data-help="The newest posts included in the RSS/Atom feed."></span>
|
|
200
|
+
</label>
|
|
201
|
+
<input id="blog-set-feed" type="number" min="1" max="200" class="form-input" data-model="feedLength.value">
|
|
202
|
+
</div>
|
|
203
|
+
<div class="blog-field blog-field-row">
|
|
204
|
+
<label class="form-label" for="blog-set-comments">Comments
|
|
205
|
+
<span data-help="Readers can leave a comment under each post. Off hides the form; existing comments stay in Comments."></span>
|
|
206
|
+
</label>
|
|
207
|
+
<input id="blog-set-comments" type="checkbox" class="form-switch" data-model="commentsEnabled.value">
|
|
208
|
+
</div>
|
|
209
|
+
<div class="blog-field blog-field-row">
|
|
210
|
+
<label class="form-label" for="blog-set-moderate">Hold comments for approval
|
|
211
|
+
<span data-help="New comments wait in Comments > Pending until approved. Off publishes them straight away."></span>
|
|
212
|
+
</label>
|
|
213
|
+
<input id="blog-set-moderate" type="checkbox" class="form-switch" data-model="commentModeration.value"
|
|
214
|
+
data-bind-disabled="!commentsEnabled.value">
|
|
215
|
+
</div>
|
|
216
|
+
<h4 class="blog-panel-heading">On the site
|
|
217
|
+
<span data-help-title="Layouts are blocks"
|
|
218
|
+
data-help="Every post layout and listing display is a block - blog-post-… for posts, blog-card-… for listings. Edit one under Blocks and every post using it changes. Copy one under a new name (say blog-post-long-read) and it appears here, and in the post editor, as a new choice."></span>
|
|
219
|
+
</h4>
|
|
220
|
+
<div class="blog-field">
|
|
221
|
+
<label class="form-label" for="blog-set-layout">Post layout
|
|
222
|
+
<span data-help="How a post looks unless it picks its own in the editor. Classic: title, image, story. Feature: a full-width hero. Minimal: just the words. Sidebar: contents list and details beside the story."></span>
|
|
223
|
+
</label>
|
|
224
|
+
<select id="blog-set-layout" class="form-select" data-model="postLayout.value"
|
|
225
|
+
data-options="layoutOptions.value" data-options-text="label" data-options-value="id"></select>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="blog-field">
|
|
228
|
+
<label class="form-label" for="blog-set-display">Listing display
|
|
229
|
+
<span data-help="How the blog index and the category, tag and author pages show posts. Cards: a picture grid. List: one per row. Magazine: the newest large, the rest as cards. Compact: titles and dates."></span>
|
|
230
|
+
</label>
|
|
231
|
+
<select id="blog-set-display" class="form-select" data-model="listDisplay.value"
|
|
232
|
+
data-options="displayOptions.value" data-options-text="label" data-options-value="id"></select>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="blog-field blog-field-row">
|
|
235
|
+
<label class="form-label" for="blog-set-switch">Readers can switch display
|
|
236
|
+
<span data-help="Shows the Cards / List / Magazine / Compact switch above listings. A reader's choice is remembered in their browser."></span>
|
|
237
|
+
</label>
|
|
238
|
+
<input id="blog-set-switch" type="checkbox" class="form-switch" data-model="displaySwitcher.value">
|
|
239
|
+
</div>
|
|
240
|
+
<div class="blog-field blog-field-row">
|
|
241
|
+
<label class="form-label" for="blog-set-live">Search and filter in the page
|
|
242
|
+
<span data-help="Listings get a search box, category pills, sorting and a right-click menu on each post (More in this category, More tagged…, More by this author), all without reloading. Off: plain pages of posts. Blogs with more than 300 posts always page on the server."></span>
|
|
243
|
+
</label>
|
|
244
|
+
<input id="blog-set-live" type="checkbox" class="form-switch" data-model="interactive.value">
|
|
245
|
+
</div>
|
|
246
|
+
<div class="blog-field">
|
|
247
|
+
<label class="form-label" for="blog-set-related">Related posts
|
|
248
|
+
<span data-help="How many posts sharing a category or tag are suggested under each post, in layouts that show them. 0 turns them off."></span>
|
|
249
|
+
</label>
|
|
250
|
+
<input id="blog-set-related" type="number" min="0" max="6" class="form-input" data-model="relatedCount.value">
|
|
251
|
+
</div>
|
|
252
|
+
<button type="button" class="btn btn-sm btn-ghost blog-panel-link" data-on-click="openBlocks">
|
|
253
|
+
<span data-icon="layout" data-icon-size="13"></span> Edit the layouts in Blocks
|
|
254
|
+
</button>
|
|
255
|
+
<p class="blog-panel-note" data-if="needsRestart.value">Base path and posts per page change after the server restarts.</p>
|
|
256
|
+
<div class="blog-panel-actions">
|
|
257
|
+
<span class="blog-panel-status" data-bind-text="status.value"></span>
|
|
258
|
+
<span class="blog-spacer"></span>
|
|
259
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel" data-if="closable">Cancel</button>
|
|
260
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="!dirty.value || saving.value">
|
|
261
|
+
<span data-icon="save" data-icon-size="13"></span> Save
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
</div>`;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The settings form as an element, bound to its own observables. Used by the
|
|
268
|
+
* banner cog's slideover and by the old #/plugins/blog/settings page, so the
|
|
269
|
+
* two can never drift apart.
|
|
270
|
+
*
|
|
271
|
+
* @param {{onClose?: Function}} [opts] - with onClose, a Cancel button closes the host
|
|
272
|
+
* @returns {Promise<{el: HTMLElement, dispose: Function}>}
|
|
273
|
+
*/
|
|
274
|
+
export async function settingsPanel({onClose = null} = {}) {
|
|
275
|
+
let stored = {};
|
|
276
|
+
try {
|
|
277
|
+
const res = await H.get(`${BASE}/settings`);
|
|
278
|
+
stored = res?.data ?? res ?? {};
|
|
279
|
+
} catch {
|
|
280
|
+
E.toast('Failed to load blog settings.', {type: 'error'});
|
|
281
|
+
}
|
|
282
|
+
let layouts = {postLayouts: [], listDisplays: []};
|
|
283
|
+
try {
|
|
284
|
+
layouts = await H.get(`${BASE}/layouts`);
|
|
285
|
+
} catch { /* the selects fall back to the stored value alone */ }
|
|
286
|
+
const initial = {
|
|
287
|
+
basePath: stored.basePath ?? '/blog',
|
|
288
|
+
postsPerPage: Number(stored.postsPerPage ?? 10),
|
|
289
|
+
feedLength: Number(stored.feedLength ?? 20),
|
|
290
|
+
commentsEnabled: stored.commentsEnabled ?? false,
|
|
291
|
+
commentModeration: stored.commentModeration ?? true,
|
|
292
|
+
postLayout: stored.postLayout ?? 'blog-post-classic',
|
|
293
|
+
listDisplay: stored.listDisplay ?? 'cards',
|
|
294
|
+
displaySwitcher: stored.displaySwitcher ?? true,
|
|
295
|
+
interactive: stored.interactive ?? true,
|
|
296
|
+
relatedCount: Number(stored.relatedCount ?? 3)
|
|
297
|
+
};
|
|
298
|
+
// A stored choice whose block has since gone still shows, rather than a blank select.
|
|
299
|
+
const withCurrent = (list, id) => (list.some(o => o.id === id) ? list : [...list, {id, label: `${id} (missing)`}]);
|
|
300
|
+
const baseline = M.observable({...initial});
|
|
301
|
+
|
|
302
|
+
const f = Object.fromEntries(Object.entries(initial).map(([k, v]) => [k, M.observable(v)]));
|
|
303
|
+
const saving = M.observable(false);
|
|
304
|
+
const status = M.observable('');
|
|
305
|
+
const current = () => ({
|
|
306
|
+
basePath: String(f.basePath.value).trim() || '/blog',
|
|
307
|
+
postsPerPage: Number(f.postsPerPage.value) || 10,
|
|
308
|
+
feedLength: Number(f.feedLength.value) || 20,
|
|
309
|
+
commentsEnabled: Boolean(f.commentsEnabled.value),
|
|
310
|
+
commentModeration: Boolean(f.commentModeration.value),
|
|
311
|
+
postLayout: String(f.postLayout.value),
|
|
312
|
+
listDisplay: String(f.listDisplay.value),
|
|
313
|
+
displaySwitcher: Boolean(f.displaySwitcher.value),
|
|
314
|
+
interactive: Boolean(f.interactive.value),
|
|
315
|
+
relatedCount: Math.max(0, Math.min(6, Number(f.relatedCount.value) || 0))
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const data = {
|
|
319
|
+
...f, saving, status,
|
|
320
|
+
closable: Boolean(onClose),
|
|
321
|
+
layoutOptions: M.observable(withCurrent(layouts.postLayouts ?? [], initial.postLayout)),
|
|
322
|
+
displayOptions: M.observable(withCurrent(layouts.listDisplays ?? [], initial.listDisplay)),
|
|
323
|
+
dirty: M.computed(() => JSON.stringify(current()) !== JSON.stringify(baseline.value)),
|
|
324
|
+
needsRestart: M.computed(() => {
|
|
325
|
+
const b = baseline.value;
|
|
326
|
+
return String(f.basePath.value).trim() !== b.basePath || Number(f.postsPerPage.value) !== b.postsPerPage;
|
|
327
|
+
})
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const el = fragment(SETTINGS_PANEL);
|
|
331
|
+
const disposeHelp = await scanHelp(el);
|
|
332
|
+
const handle = M.applyBindings(data, el, {
|
|
333
|
+
methods: {
|
|
334
|
+
cancel: () => onClose?.(),
|
|
335
|
+
openBlocks() {
|
|
336
|
+
onClose?.();
|
|
337
|
+
R.navigate('#/blocks');
|
|
338
|
+
},
|
|
339
|
+
async save() {
|
|
340
|
+
const body = current();
|
|
341
|
+
if (!body.basePath.startsWith('/')) {
|
|
342
|
+
E.toast('Base path must start with /.', {type: 'error'});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
saving.value = true;
|
|
346
|
+
try {
|
|
347
|
+
const res = await H.put(`${BASE}/settings`, body);
|
|
348
|
+
if (res?.error) throw new Error(res.error);
|
|
349
|
+
baseline.value = {...body};
|
|
350
|
+
basePathCache = null;
|
|
351
|
+
status.value = 'Saved.';
|
|
352
|
+
E.toast('Blog settings saved. Display changes are live now.', {type: 'success'});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
E.toast(err?.message ?? 'Failed to save settings.', {type: 'error'});
|
|
355
|
+
} finally {
|
|
356
|
+
saving.value = false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
if (typeof I !== 'undefined' && I.scan) I.scan(el);
|
|
362
|
+
return {el, dispose: () => { handle.dispose(); disposeHelp(); }};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The banner cog's slideover. Resizable, like every admin slideover. */
|
|
366
|
+
export async function openSettings() {
|
|
367
|
+
const so = E.slideover({title: 'Blog settings', size: 'sm', position: 'right'});
|
|
368
|
+
const {el, dispose} = await settingsPanel({onClose: () => so.close()});
|
|
369
|
+
so.element.appendChild(el);
|
|
370
|
+
onceClosed(so, dispose);
|
|
371
|
+
so.open();
|
|
372
|
+
}
|