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
|
@@ -14,12 +14,169 @@ function slugify(s) {
|
|
|
14
14
|
|
|
15
15
|
const BASE = '/api/plugins/blog';
|
|
16
16
|
|
|
17
|
+
// The loader's cache-busting token, passed on so helpers are refetched with this view.
|
|
18
|
+
const ASSET_VERSION = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
19
|
+
|
|
20
|
+
/** One layout choice - the body of the picker's data-each (see mountLayoutCard). */
|
|
21
|
+
const LAYOUT_CHOICE = `
|
|
22
|
+
<button type="button" class="blog-layout {{cls}}" role="radio" aria-checked="{{checked}}" data-layout="{{id}}" title="{{description}}">
|
|
23
|
+
<span class="blog-thumb" data-thumb="{{thumb}}" aria-hidden="true"></span>
|
|
24
|
+
<span class="blog-layout-name">{{label}}</span>
|
|
25
|
+
</button>`;
|
|
26
|
+
|
|
27
|
+
const PREVIEW_PANEL = `
|
|
28
|
+
<div class="blog-preview">
|
|
29
|
+
<div class="blog-preview-bar">
|
|
30
|
+
<select class="form-select form-select-sm" aria-label="Layout" data-model="layout.value"
|
|
31
|
+
data-options="options.value" data-options-text="label" data-options-value="id"></select>
|
|
32
|
+
<div class="blog-seg" role="group" aria-label="Width">
|
|
33
|
+
<button type="button" data-on-click="desktop" data-bind-class="width.value === 'desktop' && 'is-on'" title="Desktop">
|
|
34
|
+
<span data-icon="monitor" data-icon-size="14"></span></button>
|
|
35
|
+
<button type="button" data-on-click="tablet" data-bind-class="width.value === 'tablet' && 'is-on'" title="Tablet">
|
|
36
|
+
<span data-icon="tablet" data-icon-size="14"></span></button>
|
|
37
|
+
<button type="button" data-on-click="phone" data-bind-class="width.value === 'phone' && 'is-on'" title="Phone">
|
|
38
|
+
<span data-icon="smartphone" data-icon-size="14"></span></button>
|
|
39
|
+
</div>
|
|
40
|
+
<span data-help-title="Preview"
|
|
41
|
+
data-help="The post as it will look on the site, from what is in the editor now - nothing is saved. Try another layout here; Use this layout keeps it for the post. Links are switched off inside the preview."></span>
|
|
42
|
+
<span class="blog-spacer"></span>
|
|
43
|
+
<span class="blog-preview-state" data-bind-text="state.value"></span>
|
|
44
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="use" data-bind-disabled="!changed.value">Use this layout</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="blog-preview-stage" data-bind-class="'is-' + width.value">
|
|
47
|
+
<iframe title="Post preview"></iframe>
|
|
48
|
+
</div>
|
|
49
|
+
</div>`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The Layout card: a picker over every post layout on offer (the shipped four
|
|
53
|
+
* and any blog-post-* block the site added), and a Preview panel that renders
|
|
54
|
+
* the UNSAVED post in any of them.
|
|
55
|
+
*
|
|
56
|
+
* domma-reactive, like the rest of the Tool upgrade path; the item template is
|
|
57
|
+
* inserted here because the router renders the page template through _.render,
|
|
58
|
+
* which eats {{…}}.
|
|
59
|
+
*
|
|
60
|
+
* @param {HTMLElement} card
|
|
61
|
+
* @param {{current: string, collect: () => object}} opts
|
|
62
|
+
* @returns {Promise<{value: () => string}>}
|
|
63
|
+
*/
|
|
64
|
+
async function mountLayoutCard(card, {current, collect}) {
|
|
65
|
+
const BASE_URL = '/api/plugins/blog';
|
|
66
|
+
let catalogue = {postLayouts: [], postLayout: 'blog-post-classic'};
|
|
67
|
+
try { catalogue = await H.get(`${BASE_URL}/layouts`); } catch { /* the card still offers Site default */ }
|
|
68
|
+
const known = catalogue.postLayouts ?? [];
|
|
69
|
+
const defaultLabel = known.find(l => l.id === catalogue.postLayout)?.label ?? 'Classic';
|
|
70
|
+
const thumbOf = (id) => (['blog-post-classic', 'blog-post-feature', 'blog-post-minimal', 'blog-post-sidebar'].includes(id) ? id.slice(10) : 'custom');
|
|
71
|
+
|
|
72
|
+
const choices = [
|
|
73
|
+
{id: '', label: 'Site default', description: `Follows Blog settings - currently ${defaultLabel}.`, thumb: thumbOf(catalogue.postLayout)},
|
|
74
|
+
...known.map(l => ({id: l.id, label: l.label, description: l.description, thumb: thumbOf(l.id)})),
|
|
75
|
+
...(current && !known.some(l => l.id === current) ? [{id: current, label: `${current} (missing)`, description: 'This block no longer exists; the site default is used.', thumb: 'custom'}] : [])
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const layout = M.observable(current || '');
|
|
79
|
+
const layoutChoices = M.observable([]);
|
|
80
|
+
M.effect(() => {
|
|
81
|
+
layoutChoices.value = choices.map(c => ({...c, id: c.id || 'default', checked: String(c.id === layout.value), cls: c.id === layout.value ? 'is-on' : ''}));
|
|
82
|
+
});
|
|
83
|
+
const data = {
|
|
84
|
+
layoutChoices,
|
|
85
|
+
layoutDesc: M.computed(() => choices.find(c => c.id === layout.value)?.description ?? '')
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
card.querySelector('.blog-layouts').innerHTML = LAYOUT_CHOICE;
|
|
89
|
+
M.applyBindings(data, card.querySelector('.card-body'));
|
|
90
|
+
card.querySelector('.blog-layouts').addEventListener('click', (e) => {
|
|
91
|
+
const b = e.target.closest('[data-layout]');
|
|
92
|
+
if (b) layout.value = b.dataset.layout === 'default' ? '' : b.dataset.layout;
|
|
93
|
+
});
|
|
94
|
+
card.querySelector('#btn-preview').addEventListener('click', () => openPreview());
|
|
95
|
+
|
|
96
|
+
async function openPreview() {
|
|
97
|
+
const kit = await import('./kit.js?v=' + (new URL(import.meta.url).searchParams.get('v') ?? '1'));
|
|
98
|
+
const trial = M.observable(layout.peek() || catalogue.postLayout || 'blog-post-classic');
|
|
99
|
+
const width = M.observable('desktop');
|
|
100
|
+
const state = M.observable('');
|
|
101
|
+
const pdata = {
|
|
102
|
+
layout: trial, width, state,
|
|
103
|
+
options: M.observable(known.map(l => ({id: l.id, label: l.label}))),
|
|
104
|
+
changed: M.computed(() => trial.value !== (layout.value || catalogue.postLayout))
|
|
105
|
+
};
|
|
106
|
+
const so = E.slideover({title: 'Preview', size: 'xl', position: 'right'});
|
|
107
|
+
const el = kit.fragment(PREVIEW_PANEL);
|
|
108
|
+
so.element.appendChild(el);
|
|
109
|
+
const disposeHelp = await kit.scanHelp(el);
|
|
110
|
+
const frame = el.querySelector('iframe');
|
|
111
|
+
const handle = M.applyBindings(pdata, el, {
|
|
112
|
+
methods: {
|
|
113
|
+
desktop: () => { width.value = 'desktop'; },
|
|
114
|
+
tablet: () => { width.value = 'tablet'; },
|
|
115
|
+
phone: () => { width.value = 'phone'; },
|
|
116
|
+
use() {
|
|
117
|
+
layout.value = trial.peek() === catalogue.postLayout && !layout.peek() ? '' : trial.peek();
|
|
118
|
+
E.toast('Layout chosen - Save to keep it.', {type: 'success'});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (typeof I !== 'undefined' && I.scan) I.scan(el);
|
|
123
|
+
|
|
124
|
+
let seq = 0;
|
|
125
|
+
const stop = M.effect(async () => {
|
|
126
|
+
const chosen = trial.value;
|
|
127
|
+
const mine = ++seq;
|
|
128
|
+
state.value = 'Rendering…';
|
|
129
|
+
try {
|
|
130
|
+
const res = await H.post(`${BASE_URL}/preview`, {...collect(), layout: chosen});
|
|
131
|
+
if (mine !== seq) return;
|
|
132
|
+
if (res?.error) throw new Error(res.error);
|
|
133
|
+
// The page's own addresses (/dist/…, /plugins/…) resolve against the site.
|
|
134
|
+
frame.srcdoc = String(res.html).replace(/<head>/i, `<head><base href="${location.origin}/">`);
|
|
135
|
+
state.value = '';
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (mine === seq) state.value = err?.message ?? 'Preview failed.';
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Links inside the preview would navigate the frame away from the post.
|
|
141
|
+
frame.addEventListener('load', () => {
|
|
142
|
+
try {
|
|
143
|
+
frame.contentDocument.addEventListener('click', (e) => {
|
|
144
|
+
if (e.target.closest('a[href]') && !e.target.closest('a[href^="#"]')) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
E.toast('Links are switched off in the preview.', {type: 'info'});
|
|
147
|
+
}
|
|
148
|
+
}, true);
|
|
149
|
+
} catch { /* not ours to reach */ }
|
|
150
|
+
});
|
|
151
|
+
kit.onceClosed(so, () => { stop?.(); handle.dispose(); disposeHelp(); });
|
|
152
|
+
so.open();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {value: () => layout.peek()};
|
|
156
|
+
}
|
|
157
|
+
|
|
17
158
|
export const postEditorView = {
|
|
18
159
|
templateUrl: '/plugins/blog/admin/templates/post-editor.html',
|
|
19
160
|
|
|
20
161
|
async onMount($container) {
|
|
21
|
-
|
|
22
|
-
|
|
162
|
+
// Help (?) popovers, loaded at run time - a plugin must not import admin/.
|
|
163
|
+
// Scanned now, before anything below reshapes the form.
|
|
164
|
+
import('/admin/js/lib/help-popover.js')
|
|
165
|
+
.then(m => m.helpPopovers?.($container, {position: 'bottom'}))
|
|
166
|
+
.catch(() => null);
|
|
167
|
+
import(`./settings-cog.js?v=${ASSET_VERSION}`)
|
|
168
|
+
.then(m => m.addSettingsCog($container, '.view-header-actions'))
|
|
169
|
+
.catch(() => { /* no cog is better than no screen */ });
|
|
170
|
+
// Domma's router hands onMount ONE argument and never the route params,
|
|
171
|
+
// and `R.current` is a METHOD - `R.current?.params` is undefined on the
|
|
172
|
+
// function object, so this view opened blank on every edit and Save
|
|
173
|
+
// created a duplicate. Read the slug off the hash, as every core editor
|
|
174
|
+
// view does.
|
|
175
|
+
const routeMatch = location.hash.match(/^#\/plugins\/blog\/posts\/([^?#]+)/);
|
|
176
|
+
const routeId = routeMatch ? decodeURIComponent(routeMatch[1]) : null;
|
|
177
|
+
// `let`: a new post becomes an existing one on its first Save.
|
|
178
|
+
let id = routeId && routeId !== 'new' ? routeId : null;
|
|
179
|
+
let isEdit = Boolean(id);
|
|
23
180
|
|
|
24
181
|
let post = null;
|
|
25
182
|
let categories = [];
|
|
@@ -40,7 +197,7 @@ export const postEditorView = {
|
|
|
40
197
|
post = res.data ?? res;
|
|
41
198
|
} catch {
|
|
42
199
|
E.toast('Failed to load post.', { type: 'error' });
|
|
43
|
-
R.navigate('#/plugins/blog');
|
|
200
|
+
R.navigate('#/plugins/blog/posts');
|
|
44
201
|
return;
|
|
45
202
|
}
|
|
46
203
|
}
|
|
@@ -62,7 +219,7 @@ export const postEditorView = {
|
|
|
62
219
|
const imagePreviewEl = $container.find('#featured-image-preview').get(0);
|
|
63
220
|
const clearImageBtn = $container.find('#btn-clear-image').get(0);
|
|
64
221
|
|
|
65
|
-
if (titleEl) titleEl.textContent = isEdit ? '
|
|
222
|
+
if (titleEl) titleEl.textContent = isEdit ? 'Editing post' : 'New post';
|
|
66
223
|
|
|
67
224
|
if (isEdit && post) {
|
|
68
225
|
if (postTitleEl) postTitleEl.value = post.title ?? '';
|
|
@@ -87,11 +244,11 @@ export const postEditorView = {
|
|
|
87
244
|
const img = document.createElement('img');
|
|
88
245
|
img.src = featuredImageUrl;
|
|
89
246
|
img.alt = 'Featured image';
|
|
90
|
-
img.
|
|
247
|
+
img.className = 'blog-image-preview';
|
|
91
248
|
imagePreviewEl.appendChild(img);
|
|
92
|
-
if (clearImageBtn) clearImageBtn.
|
|
249
|
+
if (clearImageBtn) clearImageBtn.classList.remove('blog-hidden');
|
|
93
250
|
} else {
|
|
94
|
-
if (clearImageBtn) clearImageBtn.
|
|
251
|
+
if (clearImageBtn) clearImageBtn.classList.add('blog-hidden');
|
|
95
252
|
}
|
|
96
253
|
}
|
|
97
254
|
|
|
@@ -99,14 +256,13 @@ export const postEditorView = {
|
|
|
99
256
|
while (catsListEl.firstChild) catsListEl.removeChild(catsListEl.firstChild);
|
|
100
257
|
if (categories.length === 0) {
|
|
101
258
|
const p = document.createElement('p');
|
|
102
|
-
p.className = 'text-muted
|
|
259
|
+
p.className = 'text-muted text-sm';
|
|
103
260
|
p.textContent = 'No categories yet.';
|
|
104
261
|
catsListEl.appendChild(p);
|
|
105
262
|
} else {
|
|
106
263
|
categories.forEach((cat) => {
|
|
107
264
|
const label = document.createElement('label');
|
|
108
|
-
label.className = '
|
|
109
|
-
label.style.cursor = 'pointer';
|
|
265
|
+
label.className = 'flex items-center gap-2 mb-2 blog-clickable';
|
|
110
266
|
|
|
111
267
|
const cb = document.createElement('input');
|
|
112
268
|
cb.type = 'checkbox';
|
|
@@ -130,8 +286,14 @@ export const postEditorView = {
|
|
|
130
286
|
}
|
|
131
287
|
}
|
|
132
288
|
|
|
289
|
+
const layoutCard = await mountLayoutCard($container.find('#layout-card').get(0), {
|
|
290
|
+
current: post?.template ?? '',
|
|
291
|
+
collect: () => ({...collectValues(), id})
|
|
292
|
+
});
|
|
293
|
+
|
|
133
294
|
function collectValues() {
|
|
134
295
|
return {
|
|
296
|
+
template: layoutCard.value(),
|
|
135
297
|
title: postTitleEl?.value?.trim() ?? '',
|
|
136
298
|
slug: postSlugEl?.value?.trim() ?? '',
|
|
137
299
|
excerpt: excerptEl?.value?.trim() ?? '',
|
|
@@ -150,6 +312,7 @@ export const postEditorView = {
|
|
|
150
312
|
const body = { ...collectValues(), ...extra };
|
|
151
313
|
if (!body.title) {
|
|
152
314
|
E.toast('Title is required.', { type: 'error' });
|
|
315
|
+
postTitleEl?.focus();
|
|
153
316
|
return null;
|
|
154
317
|
}
|
|
155
318
|
try {
|
|
@@ -157,13 +320,107 @@ export const postEditorView = {
|
|
|
157
320
|
? await H.put(`${BASE}/posts/${id}`, body)
|
|
158
321
|
: await H.post(`${BASE}/posts`, body);
|
|
159
322
|
if (res.error) throw new Error(res.error);
|
|
160
|
-
|
|
323
|
+
const saved = res.data ?? res;
|
|
324
|
+
// A new post is an existing one from here on: later saves
|
|
325
|
+
// update it, and the address says which post this is (no
|
|
326
|
+
// hashchange, so the view is not re-mounted).
|
|
327
|
+
if (!isEdit && saved?.id) {
|
|
328
|
+
id = saved.id;
|
|
329
|
+
isEdit = true;
|
|
330
|
+
history.replaceState(history.state, '', `#/plugins/blog/posts/${encodeURIComponent(id)}`);
|
|
331
|
+
if (titleEl) titleEl.textContent = 'Editing post';
|
|
332
|
+
}
|
|
333
|
+
post = saved;
|
|
334
|
+
baseline = JSON.stringify(collectValues());
|
|
335
|
+
setStatus(saved.status ?? status);
|
|
336
|
+
showSaved();
|
|
337
|
+
return saved;
|
|
161
338
|
} catch (err) {
|
|
162
339
|
E.toast(err?.message ?? 'Failed to save post.', { type: 'error' });
|
|
163
340
|
return null;
|
|
164
341
|
}
|
|
165
342
|
}
|
|
166
343
|
|
|
344
|
+
// ---- Status, and whether anything is unsaved ----
|
|
345
|
+
let status = post?.status ?? 'draft';
|
|
346
|
+
let baseline = JSON.stringify(collectValues());
|
|
347
|
+
let savedAt = null;
|
|
348
|
+
const saveStateEl = $container.find('#save-state').get(0);
|
|
349
|
+
const draftsBtn = $container.find('#btn-drafts').get(0);
|
|
350
|
+
const isDirty = () => JSON.stringify(collectValues()) !== baseline;
|
|
351
|
+
|
|
352
|
+
function showSaved() {
|
|
353
|
+
savedAt = new Date();
|
|
354
|
+
refreshState();
|
|
355
|
+
}
|
|
356
|
+
function refreshState() {
|
|
357
|
+
if (!saveStateEl) return;
|
|
358
|
+
const dirty = isDirty();
|
|
359
|
+
saveStateEl.classList.toggle('is-dirty', dirty);
|
|
360
|
+
saveStateEl.textContent = dirty
|
|
361
|
+
? 'Unsaved changes'
|
|
362
|
+
: savedAt ? `Saved ${savedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` : '';
|
|
363
|
+
}
|
|
364
|
+
function setStatus(next) {
|
|
365
|
+
status = next || 'draft';
|
|
366
|
+
if (statusBadge) {
|
|
367
|
+
statusBadge.textContent = status;
|
|
368
|
+
statusBadge.className = `badge ${({ published: 'badge-success', scheduled: 'badge-warning' })[status] ?? 'badge-secondary'}`;
|
|
369
|
+
}
|
|
370
|
+
const live = status === 'published';
|
|
371
|
+
$container.find('#btn-publish').get(0)?.classList.toggle('blog-hidden', live);
|
|
372
|
+
$container.find('#btn-schedule').get(0)?.classList.toggle('blog-hidden', live);
|
|
373
|
+
draftsBtn?.classList.toggle('blog-hidden', status === 'draft');
|
|
374
|
+
}
|
|
375
|
+
setStatus(status);
|
|
376
|
+
|
|
377
|
+
// Anything typed, ticked or picked may change what would be saved.
|
|
378
|
+
let queued = false;
|
|
379
|
+
const recheck = () => {
|
|
380
|
+
if (queued) return;
|
|
381
|
+
queued = true;
|
|
382
|
+
queueMicrotask(() => { queued = false; refreshState(); });
|
|
383
|
+
};
|
|
384
|
+
['input', 'change', 'click'].forEach(ev => $container.get(0).addEventListener(ev, recheck));
|
|
385
|
+
|
|
386
|
+
async function save() {
|
|
387
|
+
if (await doSave()) E.toast(status === 'published' ? 'Saved - the live post is updated.' : 'Saved.', { type: 'success' });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function goBack() {
|
|
391
|
+
if (isDirty() && !(await E.confirm('Leave without saving? Your changes to this post will be lost.'))) return;
|
|
392
|
+
baseline = JSON.stringify(collectValues()); // do not ask twice (beforeunload)
|
|
393
|
+
R.navigate('#/plugins/blog/posts');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
$container.find('#btn-save').get(0)?.addEventListener('click', save);
|
|
397
|
+
$container.find('#btn-back').get(0)?.addEventListener('click', goBack);
|
|
398
|
+
draftsBtn?.addEventListener('click', async () => {
|
|
399
|
+
const saved = await doSave({ status: 'draft' });
|
|
400
|
+
if (saved) E.toast('Moved to drafts - it is no longer on the site.', { type: 'success' });
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Ctrl/Cmd+S saves; a closing tab with unsaved work asks first. Both
|
|
404
|
+
// come off when the hash leaves this editor (the router has no unmount).
|
|
405
|
+
const onKey = (e) => {
|
|
406
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
save();
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const onUnload = (e) => {
|
|
412
|
+
if (isDirty()) { e.preventDefault(); e.returnValue = ''; }
|
|
413
|
+
};
|
|
414
|
+
document.addEventListener('keydown', onKey);
|
|
415
|
+
window.addEventListener('beforeunload', onUnload);
|
|
416
|
+
const onHash = () => {
|
|
417
|
+
if (/^#\/plugins\/blog\/posts\/[^/]+$/.test(location.hash)) return;
|
|
418
|
+
document.removeEventListener('keydown', onKey);
|
|
419
|
+
window.removeEventListener('beforeunload', onUnload);
|
|
420
|
+
window.removeEventListener('hashchange', onHash);
|
|
421
|
+
};
|
|
422
|
+
window.addEventListener('hashchange', onHash);
|
|
423
|
+
|
|
167
424
|
if (postTitleEl) {
|
|
168
425
|
postTitleEl.addEventListener('input', () => {
|
|
169
426
|
if (slugAutoEl?.checked && postSlugEl) {
|
|
@@ -175,9 +432,9 @@ export const postEditorView = {
|
|
|
175
432
|
if (slugAutoEl) {
|
|
176
433
|
slugAutoEl.addEventListener('change', () => {
|
|
177
434
|
if (!slugAutoEl.checked && isEdit && post?.status === 'published') {
|
|
178
|
-
if (slugWarnEl) slugWarnEl.
|
|
435
|
+
if (slugWarnEl) slugWarnEl.classList.remove('blog-hidden');
|
|
179
436
|
} else {
|
|
180
|
-
if (slugWarnEl) slugWarnEl.
|
|
437
|
+
if (slugWarnEl) slugWarnEl.classList.add('blog-hidden');
|
|
181
438
|
}
|
|
182
439
|
if (slugAutoEl.checked && postTitleEl && postSlugEl) {
|
|
183
440
|
postSlugEl.value = slugify(postTitleEl.value);
|
|
@@ -189,18 +446,7 @@ export const postEditorView = {
|
|
|
189
446
|
const seoPanel = $container.find('#seo-panel').get(0);
|
|
190
447
|
if (seoToggle && seoPanel) {
|
|
191
448
|
seoToggle.addEventListener('click', () => {
|
|
192
|
-
seoPanel.
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const saveDraftBtn = $container.find('#btn-save-draft').get(0);
|
|
197
|
-
if (saveDraftBtn) {
|
|
198
|
-
saveDraftBtn.addEventListener('click', async () => {
|
|
199
|
-
const saved = await doSave({ status: 'draft' });
|
|
200
|
-
if (saved) {
|
|
201
|
-
E.toast('Draft saved.', { type: 'success' });
|
|
202
|
-
R.navigate('#/plugins/blog/posts');
|
|
203
|
-
}
|
|
449
|
+
seoPanel.classList.toggle('blog-hidden');
|
|
204
450
|
});
|
|
205
451
|
}
|
|
206
452
|
|
|
@@ -211,10 +457,11 @@ export const postEditorView = {
|
|
|
211
457
|
if (!saved) return;
|
|
212
458
|
const savedId = saved.id ?? id;
|
|
213
459
|
try {
|
|
214
|
-
const res = await H.post(`${BASE}/posts/${savedId}/publish
|
|
460
|
+
const res = await H.post(`${BASE}/posts/${savedId}/publish`, {});
|
|
215
461
|
if (res.error) throw new Error(res.error);
|
|
216
|
-
|
|
217
|
-
|
|
462
|
+
post = res.data ?? res;
|
|
463
|
+
setStatus('published');
|
|
464
|
+
E.toast('Published - the post is live.', { type: 'success' });
|
|
218
465
|
} catch (err) {
|
|
219
466
|
E.toast(err?.message ?? 'Failed to publish post.', { type: 'error' });
|
|
220
467
|
}
|
|
@@ -223,15 +470,15 @@ export const postEditorView = {
|
|
|
223
470
|
|
|
224
471
|
const scheduleBtn = $container.find('#btn-schedule').get(0);
|
|
225
472
|
if (scheduleBtn) {
|
|
226
|
-
scheduleBtn.style.display = '';
|
|
227
473
|
scheduleBtn.addEventListener('click', async () => {
|
|
228
|
-
|
|
229
|
-
if (scheduleEl
|
|
474
|
+
// First click reveals the picker; the second one schedules.
|
|
475
|
+
if (scheduleEl) scheduleEl.classList.toggle('blog-hidden');
|
|
476
|
+
if (scheduleEl && !scheduleEl.classList.contains('blog-hidden')) return;
|
|
230
477
|
|
|
231
478
|
const publishedAt = schedAtEl?.value;
|
|
232
479
|
if (!publishedAt) {
|
|
233
480
|
E.toast('Please select a scheduled date/time.', { type: 'error' });
|
|
234
|
-
if (scheduleEl) scheduleEl.
|
|
481
|
+
if (scheduleEl) scheduleEl.classList.remove('blog-hidden');
|
|
235
482
|
return;
|
|
236
483
|
}
|
|
237
484
|
|
|
@@ -241,8 +488,10 @@ export const postEditorView = {
|
|
|
241
488
|
try {
|
|
242
489
|
const res = await H.post(`${BASE}/posts/${savedId}/schedule`, { publishedAt });
|
|
243
490
|
if (res.error) throw new Error(res.error);
|
|
244
|
-
|
|
245
|
-
|
|
491
|
+
post = res.data ?? res;
|
|
492
|
+
setStatus('scheduled');
|
|
493
|
+
if (scheduleEl) scheduleEl.classList.add('blog-hidden');
|
|
494
|
+
E.toast(`Scheduled for ${new Date(publishedAt).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' })}.`, { type: 'success' });
|
|
246
495
|
} catch (err) {
|
|
247
496
|
E.toast(err?.message ?? 'Failed to schedule post.', { type: 'error' });
|
|
248
497
|
}
|
|
@@ -271,7 +520,7 @@ export const postEditorView = {
|
|
|
271
520
|
}
|
|
272
521
|
|
|
273
522
|
const grid = document.createElement('div');
|
|
274
|
-
grid.
|
|
523
|
+
grid.className = 'blog-media-grid';
|
|
275
524
|
|
|
276
525
|
if (!media.length) {
|
|
277
526
|
const p = document.createElement('p');
|
|
@@ -282,14 +531,14 @@ export const postEditorView = {
|
|
|
282
531
|
media.forEach((m) => {
|
|
283
532
|
const tile = document.createElement('button');
|
|
284
533
|
tile.type = 'button';
|
|
285
|
-
tile.
|
|
534
|
+
tile.className = 'blog-media-tile';
|
|
286
535
|
const img = document.createElement('img');
|
|
287
536
|
img.src = m.url;
|
|
288
537
|
img.alt = m.name;
|
|
289
|
-
|
|
538
|
+
|
|
290
539
|
const cap = document.createElement('span');
|
|
291
540
|
cap.textContent = m.name;
|
|
292
|
-
|
|
541
|
+
|
|
293
542
|
tile.appendChild(img);
|
|
294
543
|
tile.appendChild(cap);
|
|
295
544
|
tile.addEventListener('click', () => {
|
|
@@ -301,10 +550,28 @@ export const postEditorView = {
|
|
|
301
550
|
});
|
|
302
551
|
}
|
|
303
552
|
|
|
304
|
-
|
|
553
|
+
// close() only hides the panel and destroy() tears the component down
|
|
554
|
+
// without detaching its element, so every trip to the media library
|
|
555
|
+
// used to leave another slideover in the document.
|
|
556
|
+
const slideover = E.slideover({
|
|
557
|
+
title: 'Pick Featured Image',
|
|
558
|
+
size: 'lg',
|
|
559
|
+
position: 'right',
|
|
560
|
+
content: grid,
|
|
561
|
+
onClose: () => setTimeout(() => {
|
|
562
|
+
slideover.destroy();
|
|
563
|
+
slideover.element?.remove();
|
|
564
|
+
}, 350)
|
|
565
|
+
});
|
|
305
566
|
slideover.open();
|
|
306
567
|
}
|
|
307
568
|
|
|
569
|
+
// Again at the end: the plugin chrome rebuilds the header after the
|
|
570
|
+
// scan at the top, so its (?) would otherwise never appear. Markers
|
|
571
|
+
// already converted are skipped.
|
|
572
|
+
import('/admin/js/lib/help-popover.js')
|
|
573
|
+
.then(m => m.helpPopovers?.($container, {position: 'bottom'}))
|
|
574
|
+
.catch(() => null);
|
|
308
575
|
Domma.icons.scan();
|
|
309
576
|
}
|
|
310
577
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings cog in the admin's plugin banner, to the left of the licence.
|
|
3
|
+
* It opens the settings in a (resizable) slideover rather than leaving the
|
|
4
|
+
* screen; #/plugins/blog/settings still works for anything that links there.
|
|
5
|
+
*
|
|
6
|
+
* Every plugin puts its settings in the same place, so it is not a sidebar item
|
|
7
|
+
* here any more. The banner belongs to the admin, and addBannerAction() is its
|
|
8
|
+
* door - imported at RUN time, because a static import would take the view
|
|
9
|
+
* down on a CMS whose admin predates it. There, the cog goes at the end of the
|
|
10
|
+
* view's own header row instead.
|
|
11
|
+
*
|
|
12
|
+
* @module blog/admin/views/settings-cog
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// The loader's cache-busting token, passed on so the kit is refetched with this file.
|
|
16
|
+
const ASSET_VERSION = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
17
|
+
|
|
18
|
+
/** Settings open in a slideover over whichever Blog screen you are on. */
|
|
19
|
+
async function openSettings() {
|
|
20
|
+
try {
|
|
21
|
+
const kit = await import(`./kit.js?v=${ASSET_VERSION}`);
|
|
22
|
+
await kit.openSettings();
|
|
23
|
+
} catch {
|
|
24
|
+
// The slideover is the better place, not the only one.
|
|
25
|
+
R.navigate('#/plugins/blog/settings');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} $container - the view container onMount was given
|
|
31
|
+
* @param {string} fallback - selector for the header row to use without a banner
|
|
32
|
+
*/
|
|
33
|
+
export async function addSettingsCog($container, fallback) {
|
|
34
|
+
const onClick = () => openSettings();
|
|
35
|
+
try {
|
|
36
|
+
const chrome = await import('/admin/js/lib/plugin-chrome.js');
|
|
37
|
+
const placed = chrome.addBannerAction?.($container, {
|
|
38
|
+
id: 'blog-settings', icon: 'settings', title: 'Blog settings', onClick
|
|
39
|
+
});
|
|
40
|
+
if (placed) return;
|
|
41
|
+
} catch { /* older admin: fall through */ }
|
|
42
|
+
|
|
43
|
+
const row = $container.find(fallback).get(0);
|
|
44
|
+
if (!row) return;
|
|
45
|
+
const cog = document.createElement('button');
|
|
46
|
+
cog.type = 'button';
|
|
47
|
+
cog.className = 'btn btn-sm btn-ghost';
|
|
48
|
+
cog.title = 'Settings';
|
|
49
|
+
const glyph = document.createElement('span');
|
|
50
|
+
glyph.setAttribute('data-icon', 'settings');
|
|
51
|
+
glyph.setAttribute('data-icon-size', '16');
|
|
52
|
+
cog.appendChild(glyph);
|
|
53
|
+
cog.addEventListener('click', onClick);
|
|
54
|
+
row.appendChild(cog);
|
|
55
|
+
if (typeof I !== 'undefined' && I.scan) I.scan(cog);
|
|
56
|
+
}
|
|
@@ -1,100 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Blog settings
|
|
2
|
+
* Blog settings as a page - the old #/plugins/blog/settings route.
|
|
3
|
+
*
|
|
4
|
+
* Settings live behind the banner cog now, in a slideover over whichever Blog
|
|
5
|
+
* screen you are on. This route stays so a bookmark or a link from elsewhere
|
|
6
|
+
* still lands somewhere useful: it renders the SAME panel (kit.settingsPanel),
|
|
7
|
+
* so the page and the slideover cannot drift apart.
|
|
3
8
|
*
|
|
4
9
|
* @module blog/admin/views/settings
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
// The loader's cache-busting token, passed on so the kit is refetched with this view.
|
|
13
|
+
const ASSET_VERSION = new URL(import.meta.url).searchParams.get('v') ?? '1';
|
|
8
14
|
|
|
9
15
|
export const settingsView = {
|
|
10
16
|
templateUrl: '/plugins/blog/admin/templates/settings.html',
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
|
-
* Mount the blog settings view.
|
|
14
|
-
*
|
|
15
19
|
* @param {object} $container - Domma-wrapped container element
|
|
16
20
|
* @returns {Promise<void>}
|
|
17
21
|
*/
|
|
18
22
|
async onMount($container) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (res.error) throw new Error(res.error);
|
|
28
|
-
settings = res.data ?? res ?? {};
|
|
29
|
-
} catch {
|
|
30
|
-
E.toast('Failed to load settings.', { type: 'error' });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------
|
|
34
|
-
// Render form using F.create
|
|
35
|
-
// ---------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
const formEl = $container.find('#settings-form').get(0);
|
|
38
|
-
if (!formEl) return;
|
|
39
|
-
|
|
40
|
-
F.create(formEl, {
|
|
41
|
-
blueprint: [
|
|
42
|
-
{
|
|
43
|
-
key: 'basePath',
|
|
44
|
-
label: 'Base path',
|
|
45
|
-
type: 'text',
|
|
46
|
-
placeholder: '/blog',
|
|
47
|
-
value: settings.basePath ?? '/blog',
|
|
48
|
-
hint: 'URL prefix for all blog pages (e.g. /blog).'
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
key: 'postsPerPage',
|
|
52
|
-
label: 'Posts per page',
|
|
53
|
-
type: 'number',
|
|
54
|
-
placeholder: '10',
|
|
55
|
-
value: settings.postsPerPage ?? 10,
|
|
56
|
-
hint: 'Number of posts shown per listing page.'
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
key: 'commentsEnabled',
|
|
60
|
-
label: 'Enable comments',
|
|
61
|
-
type: 'toggle',
|
|
62
|
-
value: settings.commentsEnabled ?? true,
|
|
63
|
-
hint: 'Allow readers to submit comments on posts.'
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
key: 'commentModeration',
|
|
67
|
-
label: 'Moderate comments',
|
|
68
|
-
type: 'toggle',
|
|
69
|
-
value: settings.commentModeration ?? true,
|
|
70
|
-
hint: 'Hold new comments for approval before they appear.'
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
key: 'feedLength',
|
|
74
|
-
label: 'RSS feed length',
|
|
75
|
-
type: 'number',
|
|
76
|
-
placeholder: '20',
|
|
77
|
-
value: settings.feedLength ?? 20,
|
|
78
|
-
hint: 'Maximum number of items in the RSS/Atom feed.'
|
|
79
|
-
}
|
|
80
|
-
],
|
|
81
|
-
onSubmit: async (values) => {
|
|
82
|
-
try {
|
|
83
|
-
const res = await H.put(`${BASE}/settings`, {
|
|
84
|
-
basePath: values.basePath,
|
|
85
|
-
postsPerPage: Number(values.postsPerPage) || 10,
|
|
86
|
-
commentsEnabled: Boolean(values.commentsEnabled),
|
|
87
|
-
commentModeration: Boolean(values.commentModeration),
|
|
88
|
-
feedLength: Number(values.feedLength) || 20
|
|
89
|
-
});
|
|
90
|
-
if (res.error) throw new Error(res.error);
|
|
91
|
-
E.toast('Settings saved.', { type: 'success' });
|
|
92
|
-
} catch (err) {
|
|
93
|
-
E.toast(err?.message ?? 'Failed to save settings.', { type: 'error' });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
Domma.icons.scan();
|
|
23
|
+
const kit = await import(`./kit.js?v=${ASSET_VERSION}`);
|
|
24
|
+
const add = kit.viewScope('settings', /^#\/plugins\/blog\/settings\b/);
|
|
25
|
+
|
|
26
|
+
const host = $container.find('#settings-form').get(0);
|
|
27
|
+
if (!host) return;
|
|
28
|
+
const {el, dispose} = await kit.settingsPanel();
|
|
29
|
+
host.appendChild(el);
|
|
30
|
+
add(dispose);
|
|
99
31
|
}
|
|
100
32
|
};
|