domma-cms 1.8.23 → 1.8.24

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.
@@ -1,251 +0,0 @@
1
- <!-- Block editor. Bound with M.applyBindings - see admin/js/views/block-editor.js, which also inserts the
2
- sample-data row template (the router renders a templateUrl through _.render, eating any {{…}} - so the
3
- placeholder text below is written as &#123;&#123;…&#125;&#125;). No page heading: the Tool banner above
4
- is the heading ("Blocks › <name>"), and All blocks, Copy shortcode and Save are banner actions.
5
- #block-editor-body stays: admin.css lifts the view-width cap on it. -->
6
- <div class="bex-page">
7
- <div class="bex">
8
- <!-- lib/editor-hop.js: find any page or block, recent edits; going there keeps this block's changes. -->
9
- <div id="block-hop" class="dm-hop-slot"></div>
10
- <div class="bex-bar">
11
- <div class="bex-ident">
12
- <strong data-bind-text="titleText"></strong>
13
- <small data-bind-text="metaText.value"></small>
14
- <span class="bex-chip is-warn" data-bind-hidden="!dirty.value" title="Changes not saved yet">Unsaved</span>
15
- </div>
16
- </div>
17
-
18
- <div class="bex-unsaved" data-bind-hidden="!dirty.value">
19
- <span data-icon="alert-circle" data-icon-size="15"></span>
20
- <span>You have unsaved changes.</span>
21
- <span class="bex-spacer"></span>
22
- <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
23
- <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
24
- <span data-icon="save" data-icon-size="13"></span> Save</button>
25
- </div>
26
-
27
- <!-- Details -->
28
- <div class="bex-card">
29
- <div class="bex-card-title">Details</div>
30
- <div class="bex-grid">
31
- <div>
32
- <label class="form-label" for="block-name">Name <span class="text-danger">*</span>
33
- <span data-help-title="Name" data-help="The block's id - what a shortcode names (block=&quot;…&quot;) and what its CSS is scoped to. Lowercase letters, digits and hyphens; it cannot be changed once the block exists, and a new block cannot take a name already in use."></span></label>
34
- <input id="block-name" type="text" class="form-input font-mono" maxlength="80" placeholder="e.g. feedback-card"
35
- data-model="name.value" data-bind-disabled="notNew" autocomplete="off">
36
- <small class="bex-err" data-bind-hidden="!nameErr.value" data-bind-text="nameErr.value"></small>
37
- </div>
38
- <div>
39
- <label class="form-label" for="block-project">Project
40
- <span data-help="Files the block under a project - for the project's sidebar section and access scope. Optional."></span></label>
41
- <select id="block-project" class="form-input" data-model="project.value">
42
- <option value="">- none -</option>
43
- </select>
44
- </div>
45
- <div class="bex-setting">
46
- <label class="form-label" for="block-bundled">Bundled
47
- <span data-help="Shipped with fresh installs by the seed script. Leave it off for a block that belongs to this site only."></span></label>
48
- <input id="block-bundled" type="checkbox" class="form-switch" data-model="bundled.value">
49
- </div>
50
- </div>
51
- </div>
52
-
53
- <!-- Template HTML + preview -->
54
- <div class="bex-card bex-editor-card">
55
- <div class="bex-card-title">Template HTML
56
- <span data-help-title="Template" data-help="The block's HTML. &#123;&#123;field&#125;&#125; inserts an entry's value (HTML-escaped); &#123;&#123;field:DD MMM YYYY&#125;&#125; formats it as a date. The preview fills each placeholder from the sample values beside it. HTML comments are stripped when the block is rendered."></span>
57
- </div>
58
- <div class="bex-toolbar" role="toolbar" aria-label="Template tools">
59
- <button type="button" class="btn btn-ghost btn-sm" data-action="undo" title="Undo (Ctrl+Z)" aria-label="Undo"><span data-icon="corner-up-left" data-icon-size="14"></span></button>
60
- <button type="button" class="btn btn-ghost btn-sm" data-action="redo" title="Redo (Ctrl+Y)" aria-label="Redo"><span data-icon="corner-up-right" data-icon-size="14"></span></button>
61
- <span class="bex-sep"></span>
62
- <button type="button" class="btn btn-ghost btn-sm" data-action="cut" title="Cut (Ctrl+X)" aria-label="Cut"><span data-icon="scissors" data-icon-size="14"></span></button>
63
- <button type="button" class="btn btn-ghost btn-sm" data-action="copy" title="Copy (Ctrl+C)" aria-label="Copy"><span data-icon="copy" data-icon-size="14"></span></button>
64
- <button type="button" class="btn btn-ghost btn-sm" data-action="paste" title="Paste (Ctrl+V)" aria-label="Paste"><span data-icon="clipboard" data-icon-size="14"></span></button>
65
- <span class="bex-sep"></span>
66
- <button type="button" class="btn btn-ghost btn-sm" data-action="select-all" title="Select all (Ctrl+A)" aria-label="Select all"><span data-icon="maximize-2" data-icon-size="14"></span></button>
67
- <button type="button" class="btn btn-ghost btn-sm" data-action="format" title="Indent the HTML by nesting"><span data-icon="code" data-icon-size="14"></span> Format</button>
68
- <span class="bex-spacer"></span>
69
- <small class="bex-cursor" data-bind-text="cursor.value"></small>
70
- <div class="bex-seg" role="group" aria-label="Show">
71
- <button type="button" data-on-click="setWrite" data-bind-class="mode.value === 'write' && 'is-on'" title="Template only">Edit</button>
72
- <button type="button" data-on-click="setSplit" data-bind-class="mode.value === 'split' && 'is-on'" title="Template and preview">Split</button>
73
- <button type="button" data-on-click="setPreview" data-bind-class="mode.value === 'preview' && 'is-on'" title="Preview only">Preview</button>
74
- </div>
75
- </div>
76
- <div id="block-editor-body" data-bind-class="modeCls.value">
77
- <div class="bex-pane bex-write"></div>
78
- <div class="bex-pane bex-preview">
79
- <div class="bex-samples-wrap">
80
- <div class="bex-samples-title">Sample values
81
- <span data-help-title="Sample values" data-help="One per placeholder the template uses, so the preview shows something real. They are only for the preview - nothing here is saved. Right-click one (or ⋮, or Shift+F10) to insert its placeholder, copy it or reset the sample."></span></div>
82
- <p class="bex-note" data-bind-hidden="!noSamples.value" data-bind-text="noSamplesText"></p>
83
- <div class="bex-samples" data-each="sampleRows.value key=key"></div>
84
- </div>
85
- <div class="bex-output">
86
- <div class="bex-output-bar">
87
- <div class="live-preview-widths" role="group" aria-label="Preview width">
88
- <button type="button" class="live-preview-width" title="Full width" aria-label="Full width" data-on-click="setWidth('full')"
89
- data-bind-class="pvWidth.value === 'full' && 'active'"><span data-icon="monitor" data-icon-size="14"></span></button>
90
- <button type="button" class="live-preview-width" title="Tablet (1024px)" aria-label="Tablet width" data-on-click="setWidth('1024')"
91
- data-bind-class="pvWidth.value === '1024' && 'active'"><span data-icon="tablet" data-icon-size="14"></span></button>
92
- <button type="button" class="live-preview-width" title="Mobile (390px)" aria-label="Mobile width" data-on-click="setWidth('390')"
93
- data-bind-class="pvWidth.value === '390' && 'active'"><span data-icon="smartphone" data-icon-size="14"></span></button>
94
- </div>
95
- <select id="block-preview-on" class="form-select form-select-sm bex-pv-select bex-pv-on" aria-label="Show the block on"
96
- title="Show the block on a page that uses it" data-model="pvOn.value" data-bind-hidden="noPages">
97
- <option value="">A blank page</option>
98
- </select>
99
- <select class="form-select form-select-sm bex-pv-select" aria-label="How many copies" data-model="pvRepeat.value" data-bind-hidden="pvOn.value">
100
- <option value="1">One</option>
101
- <option value="3">Three in a grid</option>
102
- </select>
103
- <select id="block-preview-theme" class="form-select form-select-sm bex-pv-select" aria-label="Site theme for the preview" data-model="pvTheme.value">
104
- <option value="">Site theme</option>
105
- </select>
106
- <small class="bex-pv-status" data-bind-text="pvStatus.value"></small>
107
- <span data-help-title="Preview" data-help="The block drawn inside your site's own page - its theme, fonts, site styles and Custom CSS - exactly as a page would show it, from what is in the editor now. Try it at tablet or mobile width, as three entries in a collection grid, or in another theme. Show on puts it on a page that uses it - the page you came from first - drawn whole, with your changes to the block (and any unsaved text on that page) and the block outlined. Links, buttons and forms in the preview do nothing."></span>
108
- </div>
109
- <div class="bex-stage">
110
- <iframe id="block-preview-frame" class="page-live-preview-frame" src="about:blank" title="Block preview"
111
- data-bind-data-width="pvWidth.value === 'full' ? null : pvWidth.value"
112
- sandbox="allow-same-origin allow-scripts"></iframe>
113
- </div>
114
- </div>
115
- </div>
116
- </div>
117
- </div>
118
-
119
- <!-- Custom CSS -->
120
- <div class="bex-card">
121
- <div class="bex-card-title">Custom CSS
122
- <span data-help-title="Custom CSS" data-help="Scoped to this block automatically - the rules below are wrapped in [data-block=&quot;name&quot;] { … }, so use nesting for child rules. @keyframes, @font-face and @import cannot be scoped: put those in Site Settings › Custom CSS. Up to 50 KB."></span>
123
- </div>
124
- <p class="bex-note">Scoped to <code class="dm-code-inline" id="block-css-scope-hint" data-bind-text="scopeHint.value"></code>.</p>
125
- <div class="bex-css"></div>
126
- <small class="bex-err" data-bind-hidden="!cssErr.value" data-bind-text="cssErr.value"></small>
127
- </div>
128
-
129
- </div>
130
-
131
- <!-- Outside the bound root (.bex): the reference is full of literal placeholders, which the binder would flag as un-interpolated. -->
132
- <!-- Placeholder reference -->
133
- <details class="bex-card bex-ref">
134
- <summary class="bex-card-title">Placeholder reference</summary>
135
- <div class="bex-ref-body">
136
- <p>Use <code class="dm-code-inline">&#123;&#123;fieldName&#125;&#125;</code> to insert entry field values. Values are HTML-escaped
137
- automatically to prevent XSS.</p>
138
- <table class="table bex-ref-table">
139
- <thead><tr><th>Placeholder</th><th>Value</th></tr></thead>
140
- <tbody>
141
- <tr><td><code class="dm-code-inline">&#123;&#123;fieldName&#125;&#125;</code></td>
142
- <td>Any entry field - e.g. <code class="dm-code-inline">&#123;&#123;title&#125;&#125;</code>, <code class="dm-code-inline">&#123;&#123;rating&#125;&#125;</code>,
143
- <code class="dm-code-inline">&#123;&#123;message&#125;&#125;</code></td></tr>
144
- <tr><td><code class="dm-code-inline">&#123;&#123;_id&#125;&#125;</code></td><td>Entry UUID</td></tr>
145
- <tr><td><code class="dm-code-inline">&#123;&#123;_createdAt&#125;&#125;</code></td><td>Creation timestamp (ISO 8601 by default)</td></tr>
146
- <tr><td><code class="dm-code-inline">&#123;&#123;_updatedAt&#125;&#125;</code></td><td>Last-updated timestamp (ISO 8601 by default)</td></tr>
147
- <tr><td><code class="dm-code-inline">&#123;&#123;field:FORMAT&#125;&#125;</code></td>
148
- <td>Any placeholder with a <code class="dm-code-inline">:FORMAT</code> suffix applies moment-style date formatting when
149
- the value parses as a date. E.g. <code class="dm-code-inline">&#123;&#123;_createdAt:DD MMM YYYY&#125;&#125;</code> →
150
- <em>05 Jan 2026</em>, or <code class="dm-code-inline">&#123;&#123;_createdAt:HH:mm&#125;&#125;</code> → <em>14:30</em>.</td></tr>
151
- </tbody>
152
- </table>
153
- <p><strong>Format tokens:</strong>
154
- <code class="dm-code-inline">YYYY</code> <code class="dm-code-inline">YY</code>
155
- <code class="dm-code-inline">MMMM</code> <code class="dm-code-inline">MMM</code>
156
- <code class="dm-code-inline">MM</code> <code class="dm-code-inline">M</code>
157
- <code class="dm-code-inline">DD</code> <code class="dm-code-inline">D</code>
158
- <code class="dm-code-inline">dddd</code> <code class="dm-code-inline">ddd</code>
159
- <code class="dm-code-inline">HH</code> <code class="dm-code-inline">H</code>
160
- <code class="dm-code-inline">hh</code> <code class="dm-code-inline">h</code>
161
- <code class="dm-code-inline">mm</code> <code class="dm-code-inline">m</code>
162
- <code class="dm-code-inline">ss</code> <code class="dm-code-inline">s</code>
163
- <code class="dm-code-inline">A</code> <code class="dm-code-inline">a</code>
164
- - any other character passes through unchanged (so <code class="dm-code-inline">DD/MM/YYYY</code>,
165
- <code class="dm-code-inline">HH:mm</code> and <code class="dm-code-inline">dddd D MMMM YYYY</code> all work).</p>
166
- <p><strong>Use in a shortcode:</strong></p>
167
- <pre class="dm-code-block">[collection slug="feedback" display="block" block="feedback-card" /]
168
- [view slug="my-view" display="block" block="feedback-card" /]</pre>
169
- <p><strong>Example block template:</strong></p>
170
- <pre class="dm-code-block">&lt;div class="card mb-3"&gt;
171
- &lt;div class="card-body"&gt;
172
- &lt;h3&gt;&#123;&#123;name&#125;&#125;&lt;/h3&gt;
173
- &lt;p&gt;&#123;&#123;message&#125;&#125;&lt;/p&gt;
174
- &lt;small&gt;Rating: &#123;&#123;rating&#125;&#125; &amp;mdash; &#123;&#123;_createdAt&#125;&#125;&lt;/small&gt;
175
- &lt;/div&gt;
176
- &lt;/div&gt;</pre>
177
- </div>
178
- </details>
179
- </div>
180
- <style>
181
- .bex-page { display: flex; flex-direction: column; gap: .75rem; }
182
- .bex { display: flex; flex-direction: column; gap: .75rem; }
183
- .bex [hidden] { display: none !important; }
184
- .bex-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
185
- .bex-ident { display: flex; flex-wrap: wrap; align-items: baseline; gap: .5rem; min-width: 0; }
186
- .bex-ident strong { font-family: var(--dm-font-mono, monospace); }
187
- .bex-ident small, .bex-note, .bex-cursor { color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
188
- .bex-note { margin: 0; }
189
- .bex-cursor { font-size: var(--dm-text-xs); font-variant-numeric: tabular-nums; }
190
- .bex-spacer { flex: 1; }
191
- .bex-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
192
- background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
193
- .bex-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
194
- .bex-unsaved { display: flex; align-items: center; gap: .5rem; padding: .5rem .75rem; border-radius: var(--dm-radius-md);
195
- background: color-mix(in srgb, var(--dm-warning) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dm-warning) 35%, transparent);
196
- position: sticky; top: 0; z-index: 5; }
197
- .bex-unsaved .btn { display: inline-flex; align-items: center; gap: .3rem; }
198
- .bex-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem;
199
- background: var(--dm-surface); }
200
- .bex-card-title { display: flex; align-items: center; gap: .3rem; font-weight: 600; }
201
- .bex-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 2fr) minmax(0, 1fr); gap: .85rem; align-items: start; }
202
- .bex-grid .form-label { display: flex; align-items: center; gap: .3rem; }
203
- .bex-setting { display: flex; flex-direction: column; gap: .35rem; }
204
- .bex-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
205
- .bex-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: .15rem; padding-bottom: .5rem; border-bottom: 1px solid var(--dm-border); }
206
- .bex-toolbar .btn { display: inline-flex; align-items: center; gap: .3rem; }
207
- .bex-sep { width: 1px; align-self: stretch; margin: .2rem .3rem; background: var(--dm-border); }
208
- .bex-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; margin-left: .5rem; }
209
- .bex-seg button { border: 0; background: none; color: var(--dm-text); padding: .25rem .65rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
210
- .bex-seg button + button { border-left: 1px solid var(--dm-border); }
211
- .bex-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
212
- .bex-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
213
- .bex-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0; height: min(82vh, 980px); min-height: 520px;
214
- border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
215
- .bex-body.is-write { grid-template-columns: minmax(0, 1fr); }
216
- .bex-body.is-write .bex-preview { display: none; }
217
- .bex-body.is-preview { grid-template-columns: minmax(0, 1fr); }
218
- .bex-body.is-preview .bex-write { display: none; }
219
- .bex-pane { min-width: 0; min-height: 0; overflow: hidden; }
220
- .bex-write { border-right: 1px solid var(--dm-border); }
221
- .bex-body.is-write .bex-write { border-right: 0; }
222
- .bex-preview { display: flex; flex-direction: column; }
223
- .bex-samples-wrap { flex-shrink: 0; max-height: 30%; overflow-y: auto; padding: .6rem .75rem; border-bottom: 1px solid var(--dm-border);
224
- background: var(--dm-surface-raised, color-mix(in srgb, var(--dm-text) 4%, transparent)); display: flex; flex-direction: column; gap: .4rem; }
225
- .bex-samples-title { display: flex; align-items: center; gap: .3rem; font-size: var(--dm-text-sm); font-weight: 600; }
226
- .bex-samples { display: flex; flex-direction: column; gap: .3rem; }
227
- .bex-sample { display: flex; align-items: center; gap: .5rem; border-radius: var(--dm-radius-sm); }
228
- .bex-sample:focus-visible { outline: 2px solid var(--dm-primary); outline-offset: 1px; }
229
- .bex-sample-label { font-family: var(--dm-font-mono, monospace); font-size: var(--dm-text-xs); color: var(--dm-text-muted); flex: 0 0 9.5rem; text-align: right;
230
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
231
- .bex-sample input { flex: 1; min-width: 0; }
232
- .bex-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
233
- .bex-kebab:hover { color: var(--dm-primary); }
234
- .bex-output { flex: 1; min-height: 0; display: flex; flex-direction: column; }
235
- .bex-output-bar { flex: none; display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; padding: .4rem .75rem; border-bottom: 1px solid var(--dm-border); }
236
- .bex-pv-select { width: auto; }
237
- .bex-pv-on { max-width: 15rem; }
238
- .bex-pv-status { flex: 1 1 auto; color: var(--dm-text-muted); font-size: var(--dm-text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
239
- .bex-stage { flex: 1; min-height: 0; display: flex; justify-content: center; overflow: auto; background: var(--dm-background-alt, var(--dm-surface)); }
240
- .bex-stage .page-live-preview-frame { border-radius: 0; }
241
- .bex-css { height: 260px; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
242
- .bex-ref summary { cursor: pointer; list-style: revert; }
243
- .bex-ref-body { font-size: var(--dm-text-sm); line-height: 1.7; }
244
- .bex-ref-table { max-width: 40rem; margin: .5rem 0; }
245
- @media (max-width: 900px) {
246
- .bex-grid { grid-template-columns: 1fr; }
247
- .bex-body, .bex-body.is-split { grid-template-columns: minmax(0, 1fr); height: auto; }
248
- .bex-body .bex-write { height: 55vh; border-right: 0; border-bottom: 1px solid var(--dm-border); }
249
- .bex-body .bex-preview { min-height: 60vh; }
250
- }
251
- </style>
@@ -1,150 +0,0 @@
1
- <!-- Component editor. Bound with M.applyBindings - see admin/js/views/component-editor.js. No page
2
- heading or Back button: the Tool banner ("Components › <name>") carries the title, Save (Ctrl+S),
3
- Export and All components. Keep double-brace placeholders out of this file: the router renders it through _.render. -->
4
- <div class="cex">
5
- <div class="cex-bar">
6
- <span class="cex-status" data-bind-class="statusCls.value">
7
- <span class="cex-dot" aria-hidden="true"></span><span data-bind-text="statusText.value">Compiling…</span>
8
- </span>
9
- <code class="cex-tag" data-bind-text="tagText.value"></code>
10
- <span class="cex-chip is-warn" data-bind-hidden="!dirty.value">Unsaved</span>
11
- <span class="cex-chip" data-bind-hidden="!readOnly.value">Read-only - from a plugin</span>
12
- <span class="cex-spacer"></span>
13
- <select id="component-quick-switch" class="form-input form-input--sm cex-switch" hidden
14
- aria-label="Open another component" title="Open another component">
15
- <option value="">Open another…</option>
16
- </select>
17
- </div>
18
-
19
- <p class="cex-alert" data-bind-hidden="!readOnly.value">
20
- <span data-icon="lock" data-icon-size="14"></span>
21
- This component comes from a plugin, so it cannot be changed here. <button type="button" class="btn btn-sm btn-ghost"
22
- data-on-click="copyToNew">Copy to a new component</button> to make your own version.
23
- </p>
24
-
25
- <div class="cex-unsaved" data-bind-hidden="!dirty.value">
26
- <span>You have unsaved changes.</span>
27
- <span class="cex-spacer"></span>
28
- <button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
29
- <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saveBlocked.value">
30
- <span data-icon="save" data-icon-size="13"></span> Save</button>
31
- </div>
32
-
33
- <div class="cex-card">
34
- <div class="cex-card-title">Details</div>
35
- <div class="cex-grid">
36
- <div>
37
- <label class="form-label" for="component-name">Name <span class="cex-req">*</span>
38
- <span data-help-title="Name" data-help="The component is used on a page as a tag made from its name - pricing-table becomes &lt;dm-pricing-table&gt;. Start with a letter; then lowercase letters, digits and hyphens. It cannot be changed once the component exists (every page that uses it names it)."></span></label>
39
- <input id="component-name" type="text" class="form-input" placeholder="e.g. pricing-table" maxlength="64"
40
- data-model="name.value" data-bind-disabled="nameLocked.value">
41
- <small class="cex-err" data-bind-hidden="!nameErr.value" data-bind-text="nameErr.value"></small>
42
- </div>
43
- <div>
44
- <label class="form-label" for="component-project">Project
45
- <span data-help="File the component under a project - it then shows in that project's lists. Leave it on none for a site-wide one."></span></label>
46
- <select id="component-project" class="form-input" data-model="project.value" data-bind-disabled="readOnly.value">
47
- <option value="">- none -</option>
48
- </select>
49
- </div>
50
- <div class="cex-setting">
51
- <label class="form-label" for="component-bundled">Bundled
52
- <span data-help="A bundled component is included in fresh installs by the seed script. Leave it off for one that belongs to this site only."></span></label>
53
- <input id="component-bundled" type="checkbox" class="form-switch" data-model="bundled.value" data-bind-disabled="readOnly.value">
54
- </div>
55
- </div>
56
- </div>
57
-
58
- <div class="cex-card cex-source">
59
- <div class="cex-card-title">Source
60
- <span data-help-title="The four blocks" data-help="template: the markup, with a prop's name in double curly braces where its value goes. props: a JSON object - one entry per prop, each with a type (string, number, boolean, array, object), an optional default and a label. script: an optional export default with data() and methods. style: CSS scoped to the component. It recompiles as you type; a component that does not compile cannot be saved."></span>
61
- </div>
62
- <pre class="cex-errors" data-bind-hidden="!hasErrors.value" data-bind-text="compileErrors.value"></pre>
63
- <div class="cex-split">
64
- <div class="cex-editors">
65
- <div class="cex-tabs" role="tablist" aria-label="Source blocks">
66
- <button type="button" class="cex-tab" role="tab" data-on-click="showTab('template')" data-bind-class="activeTab.value === 'template' && 'is-on'">&lt;template&gt;</button>
67
- <button type="button" class="cex-tab" role="tab" data-on-click="showTab('props')" data-bind-class="activeTab.value === 'props' && 'is-on'">&lt;props&gt;</button>
68
- <button type="button" class="cex-tab" role="tab" data-on-click="showTab('script')" data-bind-class="activeTab.value === 'script' && 'is-on'">&lt;script&gt;</button>
69
- <button type="button" class="cex-tab" role="tab" data-on-click="showTab('style')" data-bind-class="activeTab.value === 'style' && 'is-on'">&lt;style&gt;</button>
70
- </div>
71
- <!-- Four simple-editors stacked in one grid cell; switching tabs flips `hidden`. -->
72
- <div class="cex-surface">
73
- <div id="component-src-template" class="cex-pane" data-bind-hidden="activeTab.value !== 'template'"></div>
74
- <div id="component-src-props" class="cex-pane" data-bind-hidden="activeTab.value !== 'props'"></div>
75
- <div id="component-src-script" class="cex-pane" data-bind-hidden="activeTab.value !== 'script'"></div>
76
- <div id="component-src-style" class="cex-pane" data-bind-hidden="activeTab.value !== 'style'"></div>
77
- </div>
78
- </div>
79
- <div class="cex-preview">
80
- <div class="cex-head">Live preview
81
- <span data-help="The component as a page would show it, rebuilt after every change. Try props in the panel below without touching the source."></span></div>
82
- <iframe id="component-preview-iframe" src="/admin/preview/component-preview.html" title="Live preview"></iframe>
83
- <div class="cex-head">Preview props
84
- <span data-help="One row per prop from the props block. Change a value to see it in the preview - it is not saved. Right-click a row (or ⋮, or Shift+F10) to reset it, copy it as an attribute or as a whole tag, or jump to it in the props block."></span></div>
85
- <div id="component-preview-props-panel" class="cex-props">
86
- <p class="cex-note">Declare props in the &lt;props&gt; tab to see them here.</p>
87
- </div>
88
- </div>
89
- </div>
90
- </div>
91
- </div>
92
-
93
- <style>
94
- .cex { display: flex; flex-direction: column; gap: .75rem; }
95
- .cex [hidden] { display: none !important; }
96
- .cex-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
97
- .cex-spacer { flex: 1; }
98
- .cex-status { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--dm-text-sm); color: var(--dm-text-muted); }
99
- .cex-dot { width: .6rem; height: .6rem; border-radius: 50%; background: var(--dm-text-muted); }
100
- .cex-status.is-ok .cex-dot { background: var(--dm-success); }
101
- .cex-status.is-error { color: var(--dm-danger); }
102
- .cex-status.is-error .cex-dot { background: var(--dm-danger); }
103
- .cex-tag { font-size: var(--dm-text-sm); }
104
- .cex-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px;
105
- background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); white-space: nowrap; }
106
- .cex-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
107
- .cex-switch { width: auto; max-width: 16rem; }
108
- .cex-alert { margin: 0; display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; padding: .55rem .8rem; border-radius: var(--dm-radius-md);
109
- background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 12%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
110
- .cex-unsaved { display: flex; align-items: center; gap: .5rem; padding: .5rem .8rem; border-radius: var(--dm-radius-md);
111
- background: color-mix(in srgb, var(--dm-warning) 14%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
112
- .cex-unsaved .btn { display: inline-flex; align-items: center; gap: .3rem; }
113
- .cex-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
114
- .cex-card-title { display: flex; align-items: center; gap: .3rem; font-weight: 600; }
115
- .cex-grid { display: grid; grid-template-columns: minmax(12rem, 1fr) minmax(10rem, 1fr) auto; gap: .85rem; align-items: start; }
116
- .cex-grid .form-label { display: flex; align-items: center; gap: .3rem; }
117
- .cex-req { color: var(--dm-danger); }
118
- .cex-err { display: block; margin-top: .25rem; color: var(--dm-danger); font-size: var(--dm-text-xs); }
119
- .cex-setting { display: flex; flex-direction: column; gap: .35rem; }
120
- .cex-errors { margin: 0; padding: .6rem .75rem; border-radius: var(--dm-radius-sm); white-space: pre-wrap; font-size: var(--dm-text-xs);
121
- font-family: var(--dm-font-mono, monospace); background: color-mix(in srgb, var(--dm-danger) 10%, transparent); color: var(--dm-danger); }
122
- .cex-split { display: flex; min-height: 460px; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-sm); overflow: hidden; }
123
- .cex-editors { flex: 1.4; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--dm-border); }
124
- .cex-tabs { display: flex; gap: .15rem; padding: .25rem .5rem 0; border-bottom: 1px solid var(--dm-border); }
125
- .cex-tab { background: transparent; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; padding: .4rem .75rem; cursor: pointer;
126
- font: inherit; font-size: var(--dm-text-sm); font-family: var(--dm-font-mono, monospace); color: var(--dm-text-muted); }
127
- .cex-tab:hover { color: var(--dm-text); }
128
- .cex-tab.is-on { color: var(--dm-text); border-bottom-color: var(--dm-primary); font-weight: 600; }
129
- .cex-surface { height: min(72vh, 860px); min-height: 360px; resize: vertical; overflow: hidden; display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr; }
130
- .cex-pane { grid-column: 1 / 2; grid-row: 1 / 2; min-height: 0; overflow: hidden; }
131
- .cex-preview { flex: 1; display: flex; flex-direction: column; min-width: 260px; }
132
- .cex-head { display: flex; align-items: center; gap: .3rem; padding: .4rem .75rem; font-size: var(--dm-text-xs); letter-spacing: .05em; text-transform: uppercase;
133
- color: var(--dm-text-muted); border-bottom: 1px solid var(--dm-border); }
134
- .cex-head + iframe + .cex-head { border-top: 1px solid var(--dm-border); }
135
- .cex-preview iframe { flex: 1; min-height: 200px; border: 0; background: var(--dm-surface); }
136
- .cex-props { padding: .5rem .5rem .75rem; overflow-y: auto; max-height: 260px; }
137
- .cex-note { margin: 0; padding: .25rem; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
138
- .cex-prop { display: flex; align-items: center; gap: .5rem; padding: .3rem .25rem; border-radius: var(--dm-radius-sm); }
139
- .cex-prop:hover, .cex-prop:focus-within, .cex-prop:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
140
- .cex-prop-label { min-width: 7.5rem; font-size: var(--dm-text-xs); color: var(--dm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
141
- .cex-prop-input { flex: 1; font-family: inherit; }
142
- textarea.cex-prop-input { font-family: var(--dm-font-mono, monospace); }
143
- .cex-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
144
- .cex-kebab:hover { color: var(--dm-primary); }
145
- @media (max-width: 900px) {
146
- .cex-split { flex-direction: column; }
147
- .cex-editors { border-right: 0; border-bottom: 1px solid var(--dm-border); }
148
- .cex-grid { grid-template-columns: 1fr; }
149
- }
150
- </style>
@@ -1,14 +0,0 @@
1
- import{api as g}from"../api.js";import{addBannerAction as ee}from"../lib/plugin-chrome.js";import{helpPopovers as kt}from"../lib/help-popover.js";import{openMenuAt as ze,sidebarCount as gt}from"../lib/tool-kit.js";import{createSimpleEditor as Je}from"../lib/simple-editor.js";import{checkForm as wt,draftKey as B,draftOffer as St,escapeHtml as Ge,extractPlaceholders as Et,formFromBlock as Mt,formatHtml as Ct,draftOlderThan as xt,pageFrontmatter as At,pageInFrame as Tt,payloadFromForm as Lt,previewMessage as $t,previewPageOptions as Ot,previewShell as Rt,rawPlaceholders as Qe,renderPreview as Dt,sampleValue as Xe,scopeName as jt,shortcodeFor as Ft,snapshot as Ee}from"../lib/block-editor-arrange.js";import{draftKey as Pt,readDraft as qt}from"../lib/page-editor-arrange.js";import{fmtDateTime as Ze}from"../lib/dates.js";import{hold as It,lastPage as Bt,mountEditorHop as Ht,takeHeld as Nt}from"../lib/editor-hop.js";import{isHop as Wt}from"../lib/editor-hop-arrange.js";const _t=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,te=u=>String(u||"").split("?")[0],Me="block-editor.mode",Kt=["write","split","preview"],et="block-editor.preview",tt=["full","1024","390"],Vt="/__block-preview",Ce="dm-block-preview-root",ot={title:"Block preview",layout:"blank",breadcrumbs:!1},Yt={ADD_ATTR:["target","loading"],ALLOW_DATA_ATTR:!0,FORCE_BODY:!0},Ut=()=>window.DOMPurify?.sanitize?u=>window.DOMPurify.sanitize(u,Yt):null,zt=`
2
- <div class="bex-sample" data-key="{{key}}" tabindex="0">
3
- <label class="bex-sample-label" for="bex-sample-{{key}}">{{label}}</label>
4
- <input id="bex-sample-{{key}}" type="text" class="form-input form-input--sm" data-bind-value="value" data-sample="{{key}}" placeholder="Sample {{key}}">
5
- <button type="button" class="bex-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
6
- </div>`;let w=null,at=!1;function Jt(){at||!window.R?.use||(at=!0,R.use((u,m,l)=>{if(!(_t.test(m?.path||"")&&te(u?.path)!==te(m?.path))||!w?.dirty())return l();const s=w;return Wt(m?.path,u?.path)?(It(s.item),l()):E.confirm(`You have unsaved changes to ${Ge(s.label())}. Leave anyway? They are kept as a draft for your next visit.`).then(Y=>{Y?l():history.replaceState(null,"",s.hash)})}),M.subscribe("router:afterChange",({to:u}={})=>{if(w&&te(u?.path)!==w.path){const m=w;w=null,m.dispose()}}))}function Gt(u,m){const l=u.querySelector(".dm-plugin-banner-name"),O=l?.lastChild;O&&O.nodeType===3&&l.querySelector(".dm-plugin-banner-sep")&&(O.textContent=m)}function oe(u){return navigator.clipboard?.writeText(u).then(()=>!0,()=>!1)??Promise.resolve(!1)}export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(u){if(Jt(),w){const e=w;w=null,e.dispose()}const m=u.get(0),l=m.querySelector(".bex")||m,O=[],s=e=>(O.push(e),e);let Y=!1;const f=()=>!Y&&l.isConnected,xe=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),r=xe?decodeURIComponent(xe[1]):null,d=!r,nt=location.hash,st=te(location.hash.slice(1)),rt=d&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",Ae=E.loader(m,{type:"dots"});let T=null,P=[],ae=[],ne=[],Te=null;const U=Bt();try{[T,P,ae,ne]=await Promise.all([r?g.blocks.get(r):Promise.resolve(null),g.get("/blocks?usage=1").catch(()=>g.blocks.list()).catch(()=>[]),g.projects.list().catch(()=>[]),g.pages.list().catch(()=>[])]),Te=(Array.isArray(P)?P:[]).find(e=>e.name===r)?.usage||null}catch(e){Ae.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(Ae.destroy(),!l.isConnected)return;const lt=(Array.isArray(P)?P:[]).map(e=>e.name).sort((e,t)=>e.localeCompare(t));let Le=T?.meta||{};const v=Mt(T?{...T,name:r}:null,{project:rt}),n={};for(const[e,t]of Object.entries(v))n[e]=M.observable(t);const se=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),$e=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),H=M.observable(Ee(v)),N=M.computed(()=>Ee($e())!==H.value),W=M.observable(!1),z=M.observable(!1),J=M.computed(()=>wt($e(),{isNew:d,taken:lt}));let Oe="split";try{Oe=Kt.includes(S.get(Me))?S.get(Me):"split"}catch{}const h=M.observable(Oe);s(M.effect(()=>{const e=h.value;try{S.set(Me,e)}catch{}}));const Re=M.observable("Ln 1, Col 1");let q={};try{q=S.get(et)||{}}catch{}const re=M.observable(tt.includes(q.width)?q.width:"full"),G=M.observable(String(q.repeat)==="3"?"3":"1"),p=M.observable(typeof q.theme=="string"?q.theme:""),b=M.observable(""),le=Ot(Te,ne,U),y=M.observable(le[0]?.value===U&&U?U:"");s(M.effect(()=>{const e={width:re.value,repeat:G.value,theme:p.value};try{S.set(et,e)}catch{}}));const C={},_={},Q=M.observable(0),ce=M.observable([]),De=()=>Qe(n.content.peek());s(M.effect(()=>{const e=Et(n.content.value),t=Qe(n.content.value);let a=!1;for(const o of e){const k=t.has(o);if(!(o in C)||o in _&&_[o]!==k){const F=o in C;if(C[o]=Xe(o,new Date,{raw:k}),_[o]=k,F){a=!0;const c=l.querySelector(`.bex-samples [data-sample="${CSS.escape(o)}"]`);c&&(c.value=C[o])}}}ce.value=e.map(o=>({key:o,label:t.has(o)?`{{{${o}}}}`:`{{${o}}}`,value:C[o]})),a&&queueMicrotask(()=>Q.value++)}));const ct={...n,dirty:N,saving:W,mode:h,cursor:Re,sampleRows:ce,isNew:d,notNew:!d,pvWidth:re,pvRepeat:G,pvTheme:p,pvStatus:b,pvOn:y,noPages:le.length===0,titleText:d?"New block":r,metaText:M.computed(()=>{if(d)return"Not saved yet";const e=[];return(T?.updatedAt||T?.meta?.updatedAt)&&e.push(`changed ${Ze(T.updatedAt||T.meta.updatedAt)}`),e.push(`${n.content.value.split(`
7
- `).length} lines`),n.css.value.trim()&&e.push(`${Math.ceil(n.css.value.length/1024)} KB CSS`),e.join(" \xB7 ")}),nameErr:M.computed(()=>(z.value||n.name.value)&&J.value.errors.name||""),cssErr:M.computed(()=>J.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${jt(n.name.value)}"]`),noSamples:M.computed(()=>ce.value.length===0),noSamplesText:"No {{placeholders}} in the template yet.",modeCls:M.computed(()=>`bex-body is-${h.value}`),cannotSave:M.computed(()=>W.value||z.value&&Object.keys(J.value.errors).length>0)},ie=l.querySelector("#block-project");for(const e of Array.isArray(ae)?ae:[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,ie.appendChild(t)}if(v.project&&![...ie.options].some(e=>e.value===v.project)){const e=document.createElement("option");e.value=v.project,e.textContent=`${v.project} (no longer a project)`,ie.appendChild(e)}const ue={kind:"block",id:r||"",label:r||""},it=l.querySelector("#block-preview-on");for(const e of le)it.appendChild(new Option(e.label,e.value));const x=l.querySelector(".bex-samples");x.innerHTML=zt;let D=null,K=null,i=null;function de(e){for(const[t,a]of Object.entries(e))n[t]&&(n[t].value=a);D.setValue(String(e.content??"")),K.setValue(String(e.css??""))}const A=l.querySelector("#block-preview-frame");let j="none",L=0;const ut="As your site shows it \xB7 not saved",je=()=>{if(!f()||h.peek()==="write"||j!=="ready"||y.peek())return;const e={content:n.content.peek(),css:n.css.peek(),name:n.name.peek()},t=Ut();if(!t){b.value="The preview needs the HTML cleaner, which did not load - reload the admin.";return}const a=Dt(e,C,{repeat:Number(G.peek()),sanitize:t});try{A.contentWindow?.postMessage({source:"dm-block-editor",type:"paint",tick:L,html:a},"*")}catch{}},Fe=e=>{const t=$t(e,A.contentWindow,L);if(!(!t||!f())){if(t.type==="save"){Z();return}if(!y.peek()){if(j=t.host?"ready":"failed",j==="failed"){b.value="Your site's page came back without a place for the block - the preview cannot show it.";return}b.peek()==="Loading your site\u2026"&&(b.value=ut),je()}}};window.addEventListener("message",Fe),s(()=>window.removeEventListener("message",Fe));async function pe(){const e=++L;j="loading",b.value="Loading your site\u2026";let t;try{const a=p.peek()?{...ot,theme:p.peek()}:ot;({html:t}=await g.pages.previewFull(Vt,a,`<div id="${Ce}"></div>`))}catch(a){t=`<!doctype html><html><head><meta charset="utf-8"></head><body style="font:14px system-ui;margin:0"><div id="${Ce}" style="padding:1rem"></div></body></html>`,f()&&e===L&&(b.value=`Without your site's styles - ${a?.message||"the page did not render"}`)}!f()||e!==L||(A.srcdoc=Rt(t,e,Ce))}let V=null,me=null,fe=0,ve=!0,X=null;const Pe=()=>{const e=X;if(X=null,!(!e||!y.peek()))try{const t=e.toBlock?A.contentDocument?.querySelector(`[data-block="${CSS.escape(r)}"]`):null;t?t.scrollIntoView({block:"center"}):A.contentWindow.scrollTo(0,e.y)}catch{}};A.addEventListener("load",Pe),s(()=>A.removeEventListener("load",Pe));async function dt(e){if(V?.path===e)return V;const t=await g.pages.get(e);let a=null;try{a=qt(S.get(Pt(e)))}catch{}a&&xt(a,t?.updatedAt)&&(a=null);const o=typeof a?.state?.body=="string"?a.state.body:String(t?.content??"");return V={path:e,frontmatter:At(t),body:o,withDraft:typeof a?.state?.body=="string"},V}const he=(e=!1)=>{clearTimeout(me),me=setTimeout(async()=>{const t=y.peek();if(!t||!f()||h.peek()==="write")return;const a=++fe;try{const o=await dt(t),k=p.peek()?{...o.frontmatter,theme:p.peek()}:o.frontmatter,{html:F}=await g.pages.previewFull(t,k,o.body,{[r]:{content:n.content.peek(),css:n.css.peek()}});if(!f()||a!==fe||y.peek()!==t)return;let c=0;try{c=A.contentWindow.scrollY||0}catch{}X={toBlock:ve||!!X?.toBlock,y:c},ve=!1,A.srcdoc=Tt(F,r,L),b.value=`On ${t} \xB7 not saved${o.withDraft?" \xB7 with the page's unsaved text":""}`}catch(o){f()&&a===fe&&(b.value=`Could not show ${t} - ${o?.message||"the page did not render"}`)}},e?0:450)};s(()=>clearTimeout(me));let qe=y.peek();s(M.effect(()=>{const e=y.value;e!==qe&&(qe=e,ve=!0,V=null,L++,j="none",e?(b.value=`Loading ${e}\u2026`,he(!0)):h.peek()!=="write"&&pe())}));let be=0;s(M.effect(()=>{if(n.content.value,n.css.value,n.name.value,Q.value,G.value,h.value!=="write"){if(y.peek()){he();return}if(j==="none"){pe();return}cancelAnimationFrame(be),be=requestAnimationFrame(je)}}));let Ie=p.peek();s(M.effect(()=>{const e=p.value;e!==Ie&&(Ie=e,y.peek()?he(!0):j!=="none"&&pe())})),s(()=>{cancelAnimationFrame(be),L++}),g.theme.catalog().then(({themes:e=[],custom:t=[]}={})=>{if(!f())return;const a=l.querySelector("#block-preview-theme"),o=c=>String(c).replace(/-/g," ").replace(/\b\w/g,we=>we.toUpperCase()),k=[...e.filter(c=>!String(c).startsWith("admin-")).map(c=>({id:c,label:o(c)})),...(Array.isArray(t)?t:[]).filter(c=>c?.id).map(c=>({id:c.id,label:c.label||o(c.id)}))],F=k.map(c=>c.id);for(const{id:c,label:we}of k){const Se=document.createElement("option");Se.value=c,Se.textContent=we,a.appendChild(Se)}p.peek()&&!F.includes(p.peek())?p.value="":a.value=p.peek()}).catch(()=>{l.querySelector("#block-preview-theme")?.remove(),f()&&p.peek()&&(p.value="")});const Be=e=>{const t=e.target?.dataset?.sample;t&&(C[t]=e.target.value,delete _[t],Q.value++)};x.addEventListener("input",Be),s(()=>x.removeEventListener("input",Be));function ye(e){i.focus(),i.setRangeText(e,i.selectionStart,i.selectionEnd,"end"),i.dispatchEvent(new Event("input",{bubbles:!0}))}async function He(e){switch(e){case"undo":i.focus(),document.execCommand("undo");break;case"redo":i.focus(),document.execCommand("redo");break;case"cut":i.selectionStart!==i.selectionEnd&&(await oe(i.value.slice(i.selectionStart,i.selectionEnd)),ye(""));break;case"copy":i.selectionStart!==i.selectionEnd&&await oe(i.value.slice(i.selectionStart,i.selectionEnd));break;case"paste":try{ye(await navigator.clipboard.readText())}catch{E.toast("Use Ctrl+V to paste.",{type:"info"})}break;case"select-all":i.focus(),i.select();break;case"format":{const t=Ct(i.value);D.setValue(t),n.content.value=t;break}default:break}}async function Z(){if(W.peek())return;z.value=!0;const{errors:e,name:t}=J.peek();if(Object.keys(e).length){E.toast(e.name||e.css,{type:"warning"});return}const a=d?t:r;W.value=!0;try{const o=Lt(se(),Le);if(await g.blocks.put(a,o),!f())return;Le=o.meta,H.value=Ee(se());try{S.remove?S.remove(B(r)):S.set(B(r),null)}catch{}gt("#/blocks"),E.toast(d?`Created "${a}".`:"Block saved.",{type:"success"}),d&&(location.hash=`#/blocks/edit/${encodeURIComponent(a)}`)}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{W.value=!1}}async function pt(){N.peek()&&await E.confirm("Discard the changes you have not saved?")&&(z.value=!1,de(mt(H.peek())))}function mt(e){try{const[t,a,o,k,F]=JSON.parse(e);return{name:t,project:a,bundled:o,content:k,css:F}}catch{return v}}const ft=M.applyBindings(ct,l,{methods:{save:()=>Z(),discard:()=>pt(),setWrite:()=>{h.value="write"},setSplit:()=>{h.value="split"},setPreview:()=>{h.value="preview"},setWidth:e=>{tt.includes(String(e))&&(re.value=String(e))},act:e=>He(e)}});s(()=>ft.dispose());const vt=Ht(l.querySelector("#block-hop"),{current:ue,pages:ne,blocks:P});s(()=>vt.dispose()),s(kt(l,{position:"bottom"})),D=Je(l.querySelector(".bex-write"),{initialValue:v.content,language:"html",autoIndent:!0,onChange:e=>{n.content.value=e},onCursor:({line:e,col:t})=>{Re.value=`Ln ${e}, Col ${t}`}}),K=Je(l.querySelector(".bex-css"),{initialValue:v.css,language:"css",autoIndent:!0,onChange:e=>{n.css.value=e}}),D.textarea.setAttribute("aria-label","Template HTML"),D.textarea.placeholder=`<div class="card mb-3">
8
- <div class="card-body">
9
- <h3>{{title}}</h3>
10
- <p>{{message}}</p>
11
- </div>
12
- </div>`,K.textarea.setAttribute("aria-label","Custom CSS"),K.textarea.placeholder=`/* Scoped to this block - e.g.
13
- color: var(--dm-primary);
14
- h3 { font-size: 1.25rem; } */`,s(()=>{D.destroy(),K.destroy()}),i=D.textarea;const Ne=l.querySelector(".bex-toolbar"),We=e=>{const t=e.target.closest("[data-action]");t&&He(t.dataset.action)};Ne.addEventListener("click",We),s(()=>Ne.removeEventListener("click",We));const ke=e=>De().has(e)?`{{{${e}}}}`:`{{${e}}}`,ht=e=>[{header:ke(e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>ye(ke(e))},{label:"Copy placeholder",icon:"copy",action:async()=>{const t=ke(e);E.toast(await oe(t)?"Copied.":t,{type:"info"})}},{label:"Reset sample value",icon:"rotate-ccw",action:()=>{const t=De().has(e);C[e]=Xe(e,new Date,{raw:t}),_[e]=t;const a=x.querySelector(`[data-sample="${CSS.escape(e)}"]`);a&&(a.value=C[e]),Q.value++}}],bt=E.contextMenu(x,{cursor:"glyph",match:".bex-sample",inherit:!1,density:"compact",items:e=>ht(e.dataset.key)});s(()=>bt?.destroy?.());const _e=e=>{const t=e.target.closest(".bex-kebab");t&&ze(t)},Ke=e=>{const t=e.target.closest(".bex-sample");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ze(t))};x.addEventListener("click",_e),x.addEventListener("keydown",Ke),s(()=>{x.removeEventListener("click",_e),x.removeEventListener("keydown",Ke)}),Gt(m,d?"New block":r),ee(u,{id:"block-editor-list",icon:"arrow-left",title:"All blocks",onClick:()=>{location.hash="#/blocks"}}),d?m.querySelector('[data-banner-action="block-editor-shortcode"]')?.remove():ee(u,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=Ft(r);E.toast(await oe(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),ee(u,{id:"block-editor-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),ee(u,{id:"block-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Z()});const Ve=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),Z())};document.addEventListener("keydown",Ve,!0),s(()=>document.removeEventListener("keydown",Ve,!0));const Ye=()=>{if(N.peek())try{S.set(B(r),{form:se(),base:H.peek(),at:new Date().toISOString()})}catch{}},Ue=e=>{N.peek()&&(Ye(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ue),s(()=>window.removeEventListener("beforeunload",Ue)),w={hash:nt,path:st,item:ue,dirty:()=>N.peek(),label:()=>d?"the new block":`"${r}"`,dispose(){Y=!0,Ye(),O.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(l);let ge=null;try{ge=S.get(B(r))}catch{}const $=St(ge,H.peek()),yt=Nt(ue);if(ge)try{S.remove?S.remove(B(r)):S.set(B(r),null)}catch{}if($&&yt&&!$.stale){const e={...v,...$.form};d||(e.name=r),de(e),E.toast("Your unsaved changes to this block are back.",{type:"info"})}else if($){const e=$.at?` (${Ze($.at)})`:"",t=d?"a new block":`"${Ge(r)}"`,a=$.stale?`You left unsaved changes to ${t}${e}, but the block has been saved since. Restore your version anyway?`:`You left unsaved changes to ${t}${e}. Restore them?`;if(await E.confirm(a)&&f()){const o={...v,...$.form};d||(o.name=r),de(o)}}}};
@@ -1,2 +0,0 @@
1
- import{api as P}from"../api.js";import{createSimpleEditor as De}from"../lib/simple-editor.js";import{addBannerAction as H}from"../lib/plugin-chrome.js";import{helpPopovers as Ie}from"../lib/help-popover.js";import{openMenuAt as Ce,sidebarCount as Ue}from"../lib/tool-kit.js";import{PANES as F,STARTER_SOURCE as Ke,assembleSource as Be,attributeFor as Fe,draftKey as J,draftStatus as Je,nameProblem as Ye,previewRows as xe,readValue as We,snapshot as se,splitSource as _e,tagFor as ze,writeValue as Me}from"../lib/component-editor-arrange.js";import{fmtDateTime as Ge}from"../lib/dates.js";const He=/^\/components\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,Q=n=>String(n||"").split("?")[0],$e=n=>String(n??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),Y={get(n){try{return S.get(n)}catch{return null}},set(n,i){try{S.set(n,i)}catch{}},drop(n){try{S.remove?S.remove(n):S.set(n,null)}catch{}}};let h=null,Ne=!1;function Qe(){Ne||!window.R?.use||(Ne=!0,R.use((n,i,c)=>{const p=i?.path||"";return!(He.test(p)&&Q(p)!==Q(n?.path))||!h?.dirty()?c():E.confirm(h.question()).then(W=>{W?c():history.replaceState(null,"",`#${p}`)})}),M.subscribe("router:afterChange",({to:n}={})=>{if(h&&Q(n?.path)!==h.path){const i=h;h=null,i.dispose()}}))}function Xe(n,i){const c=n.querySelector(".dm-plugin-banner-name");if(!c)return;const p=c.querySelector(".dm-plugin-banner-sep");if(p){for(;p.nextSibling;)p.nextSibling.remove();p.after(document.createTextNode(i))}else c.textContent=`Components \u203A ${i}`}export const componentEditorView={templateUrl:"/admin/js/templates/component-editor.html",async onMount(n){if(Qe(),h){const e=h;h=null,e.dispose()}const i=n.get(0),c=i.querySelector(".cex")||i,p=[],v=e=>(p.push(e),e);let W=!1;const x=()=>!W&&c.isConnected,ce=location.hash.match(/^#\/components\/edit\/([^/?#]+)/);let a=ce?decodeURIComponent(ce[1]):null;const je=Q(location.hash.slice(1)),C=M.observable(!a),u=M.observable(!1),m=M.observable(a||""),d=M.observable(""),$=M.observable(!1),O=M.observable(0),j=M.observable(""),A=M.observable(!1),b=M.observable("compiling"),V=M.observable(""),X=M.observable("template"),le=M.observable([]),Z={value:{}},g={},D=()=>Be(Object.fromEntries(F.map(e=>[e,g[e]?.getValue()??""]))),ee=()=>({name:m.peek(),project:d.peek(),bundled:$.peek(),source:D()}),I=M.computed(()=>(O.value,u.value?!1:se({name:m.value,project:d.value,bundled:$.value,source:D()})!==j.value)),te=M.computed(()=>u.value?"":Ye(m.value,{isNew:C.value,taken:le.value}));let U=null;v(()=>clearTimeout(U));const Te=()=>{O.value=O.peek()+1,clearTimeout(U),U=setTimeout(B,300)};for(const e of F){const t=c.querySelector(`#component-src-${e}`);t&&(g[e]=De(t,{initialValue:"",onChange:Te}))}v(()=>{for(const e of F)try{g[e]?.destroy?.()}catch{}});const _=e=>{const t=_e(e);for(const o of F)g[o]?.setValue(t[o]);O.value=O.peek()+1},K=c.querySelector("#component-project"),ie=E.loader(i,{type:"dots"});let T;try{const[e,t,o]=await Promise.all([a?P.components.get(a):Promise.resolve(null),P.projects.list().catch(()=>[]),P.components.list().catch(()=>[])]);T={comp:e,projects:t,list:o}}catch(e){ie.destroy(),E.toast(e.message||"Component not found.",{type:"error"}),x()&&(location.hash="#/components");return}if(ie.destroy(),!c.isConnected)return;for(const e of T.projects||[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,K?.appendChild(t)}le.value=(T.list||[]).map(e=>e.name);const N=T.comp;if(N)u.value=N.readOnly===!0||N.origin==="plugin",Z.value=N.meta||{},d.value=N.meta?.project||"",$.value=!!N.bundled,_(N.source);else{const e=new URLSearchParams(location.hash.split("?")[1]||"").get("project");e&&(T.projects||[]).some(t=>t.slug===e)&&(d.value=e),_(Ke)}if(j.value=se(ee()),u.peek())for(const e of F)g[e]?.textarea&&(g[e].textarea.readOnly=!0);Xe(i,u.peek()?`View source: ${a}`:a||"New component");const f=c.querySelector("#component-preview-iframe");let oe=null,y={},z=null,ne=0;const G=e=>{try{f?.contentWindow?.postMessage(e,"*")}catch{}};function Le(e){if(!f)return;if(!f.dataset.dmMounted){f.dataset.dmMounted="true",G({type:"mount",payload:e});return}z=e;const t=()=>{f.removeEventListener("load",t),z&&x()&&G({type:"mount",payload:z}),z=null};f.addEventListener("load",t),f.src=f.src}async function B(){const e=++ne,t=m.peek().trim()||"preview";b.value="compiling";let o;try{o=await P.components.compile(t,D())}catch(r){if(e!==ne||!x())return;b.value="error",V.value=r.message||"Compile request failed.";return}if(!(e!==ne||!x())){if(o.errors?.length){b.value="error",V.value=o.errors.map(r=>`[${r.type}] ${r.message}`).join(`
2
- `),oe=null;return}oe=o.js,b.value="ok",V.value="",me(),Le({compiledJs:oe,tagName:`dm-${t}`,props:y})}}const w=c.querySelector("#component-preview-props-panel");let pe=[];function ue(e){w.replaceChildren();const t=document.createElement("p");t.className="cex-note",t.textContent=e,w.appendChild(t)}function me(){const{rows:e,error:t}=xe(g.props?.getValue(),y);if(pe=e,t){ue("The <props> block is not valid JSON - fix it to see the props here.");return}if(!e.length){y={},ue("No props declared. Add them in the <props> tab.");return}y=Object.fromEntries(e.map(o=>[o.name,o.value])),w.replaceChildren();for(const o of e){const r=document.createElement("div");r.className="cex-prop",r.tabIndex=0,r.dataset.prop=o.name;const l=document.createElement("label");l.className="cex-prop-label",l.textContent=o.label,l.title=`${o.name} (${o.type})`;const s=o.type==="array"||o.type==="object"?document.createElement("textarea"):document.createElement("input");o.type==="boolean"?(s.type="checkbox",s.className="form-check",s.checked=Me(o.type,o.value)):(s.tagName==="INPUT"?s.type=o.type==="number"?"number":"text":s.rows=2,s.className="form-input form-input--sm cex-prop-input",s.value=Me(o.type,o.value)),s.setAttribute("aria-label",o.label);const Se=()=>{y[o.name]=We(o.type,o.type==="boolean"?s.checked:s.value),G({type:"update",payload:{props:y}})};s.addEventListener("input",Se),s.addEventListener("change",Se);const q=document.createElement("button");q.type="button",q.className="cex-kebab",q.setAttribute("aria-label",`Actions for ${o.label}`),q.title="Actions",q.textContent="\u22EE",r.append(l,s,q),w.appendChild(r)}}const ae=async(e,t)=>{try{await navigator.clipboard.writeText(e),E.toast(`${t} copied.`,{type:"success"})}catch{E.toast(e,{type:"info",duration:8e3})}};function Re(e){const t=pe.find(l=>l.name===e);if(!t)return[];const o=y[t.name],r=Fe(t.name,t.type,o);return[{header:t.label},{label:"Reset to its default",icon:"rotate-ccw",action:()=>{delete y[t.name];const l=xe(g.props?.getValue(),{}).rows.find(s=>s.name===t.name);y[t.name]=l?l.value:null,me(),G({type:"update",payload:{props:y}})}},{label:"Copy as an attribute",icon:"copy",disabled:!r,action:()=>ae(r,"Attribute")},{label:"Copy the tag with this value",icon:"code",action:()=>ae(`<dm-${m.peek().trim()||"component"}${r?` ${r}`:""}></dm-${m.peek().trim()||"component"}>`,"Tag")},{label:"Copy the prop name",icon:"copy",action:()=>ae(t.name,"Name")},{type:"divider"},{label:"Edit in <props>",icon:"edit",action:()=>{X.value="props";const l=g.props?.textarea;if(!l)return;const s=l.value.indexOf(`"${t.name}"`);l.focus(),s>=0&&l.setSelectionRange(s,s+t.name.length+2)}}]}const qe=E.contextMenu(w,{cursor:"glyph",match:".cex-prop",inherit:!1,density:"compact",items:e=>Re(e.dataset.prop)});v(()=>qe?.destroy?.());const de=e=>{const t=e.target.closest(".cex-kebab");t&&Ce(t)},ve=e=>{const t=e.target.closest(".cex-prop");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),Ce(t))};w.addEventListener("click",de),w.addEventListener("keydown",ve),v(()=>{w.removeEventListener("click",de),w.removeEventListener("keydown",ve)});async function re(){if(u.peek()||A.peek())return;const e=te.peek();if(e){E.toast(e,{type:"warning"});return}if(b.peek()==="error"){E.toast("Fix the compile errors first - a component that does not compile is not saved.",{type:"warning"});return}const t=m.peek().trim(),o={...Z.value||{}};delete o.bundled,d.peek()?o.project=d.peek():delete o.project,A.value=!0;try{if(await P.components.put(t,{source:D(),bundled:$.peek(),meta:o}),!x())return;Z.value=o,j.value=se(ee()),Y.drop(J(a)),E.toast(C.peek()?`Created the component "${t}".`:"Saved.",{type:"success"}),C.peek()&&(Ue("#/components"),a=t,location.hash=`#/components/edit/${encodeURIComponent(t)}`)}catch(r){E.toast(`Not saved: ${r.message||r}`,{type:"error"})}finally{A.value=!1}}async function Pe(){if(!I.peek()||!await E.confirm("Throw away the changes you have not saved?"))return;const e=JSON.parse(j.peek());C.peek()&&(m.value=e[0]),d.value=e[1],$.value=e[2],_(e[3]),clearTimeout(U),B()}function fe(){Y.set(J(null),{name:`${a}-copy`,project:"",bundled:!1,source:D(),base:null,seed:!0,at:new Date().toISOString()}),location.hash="#/components/new"}const Oe={name:m,project:d,bundled:$,dirty:I,saving:A,readOnly:u,isNew:C,nameErr:te,compileErrors:V,activeTab:X,notNew:M.computed(()=>!C.value),canEdit:M.computed(()=>!u.value),tagText:M.computed(()=>ze(m.value)),statusText:M.computed(()=>({compiling:"Compiling\u2026",ok:"Compiles",error:"Does not compile - see below"})[b.value]),statusCls:M.computed(()=>`is-${b.value}`),hasErrors:M.computed(()=>b.value==="error"&&!!V.value),saveBlocked:M.computed(()=>A.value||!!te.value||b.value==="error"),nameLocked:M.computed(()=>!C.value||u.value)},Ae=M.applyBindings(Oe,c,{methods:{save:()=>re(),discard:()=>Pe(),copyToNew:()=>fe(),showTab:e=>{X.value=e}}});v(()=>Ae.dispose()),v(Ie(c,{position:"bottom"})),K&&(K.value=d.peek());const L=c.querySelector("#component-quick-switch");if(L&&a){for(const t of[...T.list||[]].sort((o,r)=>o.name.localeCompare(r.name))){const o=document.createElement("option");o.value=t.name,o.textContent=t.origin==="plugin"?`${t.name} (plugin)`:t.name,t.name===a&&(o.selected=!0),L.appendChild(o)}L.hidden=!1;const e=()=>{const t=L.value;t&&t!==a&&(location.hash=`#/components/edit/${encodeURIComponent(t)}`)};L.addEventListener("change",e),v(()=>L.removeEventListener("change",e))}H(n,{id:"component-editor-back",icon:"arrow-left",title:"All components",onClick:()=>{location.hash="#/components"}}),a&&H(n,{id:"component-editor-export",icon:"download",title:"Export .dmcomponent.json",onClick:async()=>{try{await P.components.exportBundle(a)}catch(e){E.toast(e.message||"Export failed.",{type:"error"})}}}),u.peek()?H(n,{id:"component-editor-copy",icon:"copy",title:"Copy to a new component",onClick:fe}):H(n,{id:"component-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>re()});const ye=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),re())};document.addEventListener("keydown",ye,!0),v(()=>document.removeEventListener("keydown",ye,!0));const he=()=>{I.peek()&&Y.set(J(a),{...ee(),base:j.peek(),at:new Date().toISOString()})},be=e=>{I.peek()&&(he(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",be),v(()=>window.removeEventListener("beforeunload",be)),h={path:je,dirty:()=>I.peek(),question:()=>`You have unsaved changes to ${a?`<dm-${$e(a)}>`:"this new component"}. Leave anyway? They are kept as a draft for your next visit.`,dispose(){W=!0,he(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},Domma.icons.scan(c),f?.contentDocument?.readyState==="complete"?B():f?.addEventListener("load",()=>{x()&&B()},{once:!0}),u.peek())return;const k=Y.get(J(a)),ge=Je(k,j.peek());if(!ge.ok)return;Y.drop(J(a));const we=()=>{C.peek()&&typeof k.name=="string"&&(m.value=k.name),d.value=String(k.project??""),K&&(K.value=d.peek()),$.value=k.bundled===!0,_(k.source),clearTimeout(U),B()};if(k.seed){we();return}const ke=k.at?` (${Ge(k.at)})`:"",Ee=a?`<dm-${$e(a)}>`:"a new component",Ve=ge.stale?`You left unsaved changes to ${Ee}${ke}, but the saved version has changed since. Restore your version anyway?`:`You left unsaved changes to ${Ee}${ke}. Restore them?`;await E.confirm(Ve)&&x()&&we()}};