domma-cms 1.6.47 → 1.6.49
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/admin/index.html +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/admin-brand.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +6 -6
- package/admin/js/templates/blocks.html +1 -1
- package/admin/js/templates/docs/tutorial-crud.html +1 -1
- package/admin/js/templates/docs/tutorial-plugin.html +114 -11
- package/admin/js/templates/docs/usage-blocks.html +243 -0
- package/admin/js/templates/docs/usage-plugins.html +1 -1
- package/admin/js/templates/docs/usage-site-settings.html +7 -0
- package/admin/js/templates/documentation.html +5 -1
- package/admin/js/templates/settings.html +2 -1
- package/admin/js/templates/tutorials.html +1 -1
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/plugins.js +6 -6
- package/config/menus/admin-sidebar.json +1 -1
- package/package.json +1 -1
- package/server/routes/api/auth.js +3 -1
- package/server/server.js +1 -0
- package/server/services/adminBrand.js +32 -0
- package/server/services/docs.js +2 -1
- package/server/services/markdown.js +87 -72
- package/server/services/sidebar-migration.js +13 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="box"></span> Blocks</h1>
|
|
3
|
+
<a href="#/documentation" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All usage topics</a>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="row">
|
|
7
|
+
<div class="col-12">
|
|
8
|
+
<div class="docs-body">
|
|
9
|
+
|
|
10
|
+
<p>A <strong>block</strong> is a small piece of your own HTML, with an optional stylesheet, that you write once
|
|
11
|
+
and place anywhere. Its <strong>placeholders</strong> are filled in when the page is drawn: from a collection's
|
|
12
|
+
entries, from a view, or from values you type into the shortcode. Use a block when the built-in displays are
|
|
13
|
+
not quite the design you want. Team cards, product tiles, testimonials, a price table row and a contact panel
|
|
14
|
+
are all one block each, and the data behind them stays in a collection where anyone can edit it.</p>
|
|
15
|
+
<p>Blocks are kept in <strong>Data > Blocks</strong>. Each one is a file in <code>content/blocks/</code>:
|
|
16
|
+
<code><name>.html</code> holds the template, <code><name>.css</code> its styles (optional) and
|
|
17
|
+
<code><name>.meta.json</code> its project and bundled flag (optional).</p>
|
|
18
|
+
|
|
19
|
+
<h3>Three ways to place a block</h3>
|
|
20
|
+
<table class="table table-sm">
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>You want</th>
|
|
24
|
+
<th>Write</th>
|
|
25
|
+
<th>Placeholders come from</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<tr>
|
|
30
|
+
<td>One copy per collection entry</td>
|
|
31
|
+
<td><code>[collection slug="team" display="block" block="team-card" cols="3" /]</code></td>
|
|
32
|
+
<td>Each entry's fields</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr>
|
|
35
|
+
<td>One copy per row of a view (filtered, joined, sorted)</td>
|
|
36
|
+
<td><code>[view slug="open-jobs" display="block" block="job-card" /]</code></td>
|
|
37
|
+
<td>Each row of the view</td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr>
|
|
40
|
+
<td>One copy by itself, with values you type</td>
|
|
41
|
+
<td><code>[block template="cta-banner" heading="Book a call" /]</code> or the short tag
|
|
42
|
+
<code>[cta-banner heading="Book a call" /]</code></td>
|
|
43
|
+
<td>The shortcode's own attributes</td>
|
|
44
|
+
</tr>
|
|
45
|
+
</tbody>
|
|
46
|
+
</table>
|
|
47
|
+
<p>A block also draws each slide of a carousel or each row of a list group:
|
|
48
|
+
<code>[collection slug="team" display="carousel" block="team-card" /]</code> and
|
|
49
|
+
<code>display="listgroup" block="…"</code> work the same way.</p>
|
|
50
|
+
|
|
51
|
+
<hr>
|
|
52
|
+
|
|
53
|
+
<h3>Blocks with collections and views</h3>
|
|
54
|
+
<p>This is where blocks are most useful. The collection holds the data and the block holds the design, so a new
|
|
55
|
+
entry shows up on the page, styled, as soon as it is saved. There is no page to edit.</p>
|
|
56
|
+
<ul>
|
|
57
|
+
<li><code>display="block"</code> plus <code>block="<name>"</code> draws every entry through the block.</li>
|
|
58
|
+
<li>On a <code>[collection]</code>, <code>cols="2"</code> to <code>cols="6"</code> lays the copies out in a
|
|
59
|
+
responsive grid. Leave it out to stack them. A <code>[view]</code> always stacks them.</li>
|
|
60
|
+
<li>Everything else on <code>[collection]</code> still works: <code>limit</code>, <code>sort</code>,
|
|
61
|
+
<code>order</code>, the <code>where_<field></code> filters, <code>scope="mine"</code>,
|
|
62
|
+
<code>empty</code>, <code>item-class</code> and the per-entry <code>cta</code> buttons (drawn under each
|
|
63
|
+
copy).</li>
|
|
64
|
+
<li>On a <code>[view]</code>, <code>block="…"</code> may be left out when the view's Display tab already names
|
|
65
|
+
a block.</li>
|
|
66
|
+
<li>Visitors can still right-click any copy to filter, sort, group, copy or export, as they can on the other
|
|
67
|
+
displays. Only the fields the block actually uses are offered.</li>
|
|
68
|
+
</ul>
|
|
69
|
+
<p>If the named block does not exist, the page shows <em>Block template "name" not found</em> where the entries
|
|
70
|
+
would be, so a typo is easy to spot.</p>
|
|
71
|
+
|
|
72
|
+
<h3>Blocks on their own</h3>
|
|
73
|
+
<p><code>[block template="name" … /]</code> draws one copy, with each other attribute filling the placeholder
|
|
74
|
+
of the same name. Every block can also be written with its name as the tag, so
|
|
75
|
+
<code>[contact-card full_name="Jane Doe" /]</code> means the same as
|
|
76
|
+
<code>[block template="contact-card" full_name="Jane Doe" /]</code>.</p>
|
|
77
|
+
<ul>
|
|
78
|
+
<li>Short tags are self-closing (end with <code>/]</code>). They work for any block whose name does not clash
|
|
79
|
+
with a built-in shortcode. A block called <code>card</code> or <code>menu</code> can only be placed with
|
|
80
|
+
<code>[block template="…"]</code>.</li>
|
|
81
|
+
<li><code>class</code> and <code>id</code> go on the wrapper around the copy.</li>
|
|
82
|
+
<li>A placeholder with no value comes out empty, and a block that does not exist is removed without a
|
|
83
|
+
message.</li>
|
|
84
|
+
<li>Values are written in double quotes. For a value that itself contains a double quote, use single quotes
|
|
85
|
+
around it instead.</li>
|
|
86
|
+
<li>Blocks are expanded even inside inline code and code fences on a page. To mention a block's shortcode in
|
|
87
|
+
ordinary text, write its opening bracket as <code>&#91;</code>. Inside code that does not work (it shows
|
|
88
|
+
as written), so keep block shortcodes out of code samples on your pages.</li>
|
|
89
|
+
</ul>
|
|
90
|
+
|
|
91
|
+
<hr>
|
|
92
|
+
|
|
93
|
+
<h3>Writing the template</h3>
|
|
94
|
+
<p>A template is ordinary HTML. Domma classes (<code>card</code>, <code>badge</code>, <code>grid</code>,
|
|
95
|
+
<code>btn</code>) and icons (<code><span data-icon="mail"></span></code>) work as they do anywhere
|
|
96
|
+
else on the site. Placeholders are field names in double curly braces:</p>
|
|
97
|
+
<table class="table table-sm">
|
|
98
|
+
<thead>
|
|
99
|
+
<tr>
|
|
100
|
+
<th>Placeholder</th>
|
|
101
|
+
<th>What goes in</th>
|
|
102
|
+
</tr>
|
|
103
|
+
</thead>
|
|
104
|
+
<tbody>
|
|
105
|
+
<tr>
|
|
106
|
+
<td><code>{{field}}</code></td>
|
|
107
|
+
<td>The value, made safe as text (HTML is escaped). Use this almost everywhere.</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<tr>
|
|
110
|
+
<td><code>{{field:DD MMM YYYY}}</code></td>
|
|
111
|
+
<td>The value formatted as a date. Tokens: <code>YYYY YY MMMM MMM MM M DD D dddd ddd HH H hh h mm m ss s A
|
|
112
|
+
a</code>; anything else passes through, so <code>DD/MM/YYYY</code> and <code>HH:mm</code> work. A value
|
|
113
|
+
that is not a date is shown as it is.</td>
|
|
114
|
+
</tr>
|
|
115
|
+
<tr>
|
|
116
|
+
<td><code>{{{field}}}</code></td>
|
|
117
|
+
<td>The value inserted <strong>as HTML</strong>, unescaped. Only for fields you trust, such as rich text
|
|
118
|
+
written by your own editors.</td>
|
|
119
|
+
</tr>
|
|
120
|
+
<tr>
|
|
121
|
+
<td><code>{{_id}}</code>, <code>{{_createdAt}}</code>,
|
|
122
|
+
<code>{{_updatedAt}}</code></td>
|
|
123
|
+
<td>The entry's id and its created and updated times (collections and views only). Combine them with a
|
|
124
|
+
format: <code>{{_createdAt:D MMMM YYYY}}</code>.</td>
|
|
125
|
+
</tr>
|
|
126
|
+
</tbody>
|
|
127
|
+
</table>
|
|
128
|
+
<p>The page is cleaned before it is sent, so anything in a block that could run code (a
|
|
129
|
+
<code><script></code>, an <code>onclick</code> or <code>onerror</code> handler, a
|
|
130
|
+
<code>javascript:</code> link) is removed, exactly as it would be in page content.</p>
|
|
131
|
+
|
|
132
|
+
<h4>Describe the block in a comment</h4>
|
|
133
|
+
<p>Start the template with an HTML comment saying what it is, what it expects and how to use it. The Blocks list
|
|
134
|
+
shows the first line as the block's description, and <strong>Copy shortcode</strong> copies the example.
|
|
135
|
+
Comments are stripped when the block is drawn, so they never reach the page.</p>
|
|
136
|
+
<pre class="code-block"><code><!--
|
|
137
|
+
team-card - One team member: photo, name, role and a short bio.
|
|
138
|
+
|
|
139
|
+
Parameters:
|
|
140
|
+
name - Full name
|
|
141
|
+
role - Job title
|
|
142
|
+
photo - Image URL
|
|
143
|
+
bio - One or two sentences
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
[collection slug="team" display="block" block="team-card" cols="3" /]
|
|
147
|
+
-->
|
|
148
|
+
<div class="card team-card">
|
|
149
|
+
<img class="team-card-photo" src="{{photo}}" alt="{{name}}">
|
|
150
|
+
<div class="card-body">
|
|
151
|
+
<h3>{{name}}</h3>
|
|
152
|
+
<span class="badge badge-secondary">{{role}}</span>
|
|
153
|
+
<p>{{bio}}</p>
|
|
154
|
+
</div>
|
|
155
|
+
</div></code></pre>
|
|
156
|
+
|
|
157
|
+
<h4>Styles</h4>
|
|
158
|
+
<p>A block's <strong>Custom CSS</strong> applies to that block only: the CMS wraps it in
|
|
159
|
+
<code>[data-block="name"] { … }</code>, so plain rules like <code>.team-card-photo { … }</code> cannot leak
|
|
160
|
+
onto the rest of the page. Use CSS nesting for child rules, and the theme's <code>--dm-*</code> variables
|
|
161
|
+
(<code>var(--dm-primary)</code>, <code>var(--dm-border)</code>) so the block follows the site theme and dark
|
|
162
|
+
mode.</p>
|
|
163
|
+
<pre class="code-block"><code>.team-card-photo {
|
|
164
|
+
width: 100%;
|
|
165
|
+
aspect-ratio: 1;
|
|
166
|
+
object-fit: cover;
|
|
167
|
+
border-bottom: 3px solid var(--dm-primary);
|
|
168
|
+
}</code></pre>
|
|
169
|
+
<p><code>@keyframes</code>, <code>@font-face</code> and <code>@import</code> cannot be scoped. Put those in
|
|
170
|
+
<strong>Site Settings > Custom CSS</strong>. A block's stylesheet may be up to 50 KB. It is sent once for
|
|
171
|
+
each shortcode that uses the block, however many entries that shortcode shows.</p>
|
|
172
|
+
|
|
173
|
+
<hr>
|
|
174
|
+
|
|
175
|
+
<h3>The Blocks screen</h3>
|
|
176
|
+
<p><strong>Data > Blocks</strong> lists every block with its description, whether it has CSS, and how many
|
|
177
|
+
pages and views use it.</p>
|
|
178
|
+
<ul>
|
|
179
|
+
<li><strong>All</strong>, <strong>In use</strong> and <strong>Unused</strong> filter the list, and the search
|
|
180
|
+
box finds a block by name or description.</li>
|
|
181
|
+
<li><strong>Click</strong> a block to edit it. <strong>Right-click</strong> it (or use ⋮, or
|
|
182
|
+
<kbd>Shift</kbd>+<kbd>F10</kbd>) to copy its shortcode, see where it is used, duplicate, export or
|
|
183
|
+
delete it.</li>
|
|
184
|
+
<li><strong>New block</strong> and <strong>Import</strong> are in the banner at the top.</li>
|
|
185
|
+
<li>The sidebar badge counts your blocks. Hover over it for the unused ones and the latest changes.</li>
|
|
186
|
+
</ul>
|
|
187
|
+
<p><em>In use</em> is worked out from page sources and view settings. A Tool that draws a block from its own code
|
|
188
|
+
is not counted, so check before deleting a block marked unused.</p>
|
|
189
|
+
|
|
190
|
+
<h3>The block editor</h3>
|
|
191
|
+
<ul>
|
|
192
|
+
<li><strong>Details</strong>: the name (lowercase letters, digits and hyphens, fixed once the block is saved),
|
|
193
|
+
an optional project, and <strong>Bundled</strong>, which ships the block with fresh installs.</li>
|
|
194
|
+
<li><strong>Template HTML</strong>: the editor, with undo, cut, copy, paste and <strong>Format</strong> (tidies
|
|
195
|
+
the indentation). <strong>Edit</strong>, <strong>Split</strong> and <strong>Preview</strong> choose what you
|
|
196
|
+
see.</li>
|
|
197
|
+
<li><strong>Sample values</strong>: one box per placeholder the template uses, filled in for the preview only.
|
|
198
|
+
Nothing in them is saved. Right-click one to insert its placeholder into the template.</li>
|
|
199
|
+
<li><strong>Preview</strong>: the block drawn inside your own site page, with its theme, fonts and Custom CSS.
|
|
200
|
+
Try it at full, tablet or mobile width, as <strong>Three in a grid</strong> (as a collection with
|
|
201
|
+
<code>cols="3"</code> shows it), or in another theme. The site itself is not changed.</li>
|
|
202
|
+
<li><strong>Custom CSS</strong>: the block's scoped stylesheet (see Styles above).</li>
|
|
203
|
+
<li><strong>Placeholder reference</strong>: the table above, at the foot of the screen.</li>
|
|
204
|
+
</ul>
|
|
205
|
+
<p><kbd>Ctrl</kbd>+<kbd>S</kbd> saves from anywhere on the screen. If you leave with unsaved changes you are
|
|
206
|
+
asked first, and your work is kept as a draft for that block until you come back to it.</p>
|
|
207
|
+
|
|
208
|
+
<h3>Copying blocks between sites</h3>
|
|
209
|
+
<p><strong>Export</strong> on a block's right-click menu downloads it as a <code><name>.dmblock.json</code>
|
|
210
|
+
file, with its HTML and CSS. On the other site, use <strong>Import</strong> in the Blocks banner. If a block
|
|
211
|
+
with that name already exists, you are asked before it is replaced.</p>
|
|
212
|
+
|
|
213
|
+
<hr>
|
|
214
|
+
|
|
215
|
+
<h3>Worked example: a team page</h3>
|
|
216
|
+
<ol>
|
|
217
|
+
<li>In <strong>Data > Collections</strong>, create a <code>team</code> collection with the fields
|
|
218
|
+
<code>name</code>, <code>role</code>, <code>photo</code> and <code>bio</code>, and add a few people.</li>
|
|
219
|
+
<li>In <strong>Data > Blocks</strong>, choose <strong>New block</strong>, call it <code>team-card</code> and
|
|
220
|
+
paste the template and CSS from above. Fill in the sample values and check the preview at mobile width and as
|
|
221
|
+
three in a grid. Save.</li>
|
|
222
|
+
<li>On your Team page, write
|
|
223
|
+
<code>[collection slug="team" display="block" block="team-card" cols="3" sort="name" order="asc" /]</code>.</li>
|
|
224
|
+
<li>From now on, adding a person to the collection adds a card to the page. Changing the block restyles every
|
|
225
|
+
card at once.</li>
|
|
226
|
+
</ol>
|
|
227
|
+
|
|
228
|
+
<h3>Who can use Blocks</h3>
|
|
229
|
+
<p>Blocks share the <strong>pages</strong> permission: <em>read</em> to see them, <em>update</em> to create, edit
|
|
230
|
+
and import, and <em>delete</em> to remove them. A block filed under a project is only shown to users who can see
|
|
231
|
+
that project.</p>
|
|
232
|
+
|
|
233
|
+
<h3>Blocks in your own Tools</h3>
|
|
234
|
+
<p>A Tool (plugin) can rely on blocks too. It can render one of its pages with a block shortcode, or
|
|
235
|
+
create the block it needs the first time it is switched on, so the site owner can restyle it here. See <a href="#/tutorials/plugin">Writing a Tool</a>.</p>
|
|
236
|
+
|
|
237
|
+
<p class="text-muted" style="font-size:.9rem;">Full shortcode syntax:
|
|
238
|
+
<a href="#/docs/usage/shortcodes">Shortcodes</a>. Collection displays:
|
|
239
|
+
<a href="#/docs/usage/collections">Collections & Forms</a> and <a href="#/docs/usage/views">Views</a>.</p>
|
|
240
|
+
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
what it does, its screens, settings and permissions.</li>
|
|
121
121
|
<li><a href="#/docs/usage/editions">Editions & Licences</a> - free and Pro, rental and
|
|
122
122
|
perpetual licences, and what happens when one ends.</li>
|
|
123
|
-
<li><a href="#/tutorials/plugin">Writing a
|
|
123
|
+
<li><a href="#/tutorials/plugin">Writing a Tool</a> - build your own.</li>
|
|
124
124
|
</ul>
|
|
125
125
|
|
|
126
126
|
</div>
|
|
@@ -28,6 +28,13 @@
|
|
|
28
28
|
is none (a <code>favicon</code> in <code>config/site.json</code> takes first
|
|
29
29
|
place).</td>
|
|
30
30
|
</tr>
|
|
31
|
+
<tr>
|
|
32
|
+
<td>Branding</td>
|
|
33
|
+
<td>The public site's brand (name, logo or icon, link) and the admin's own. The
|
|
34
|
+
<strong>Admin name</strong> is shown in the admin's top bar, its sidebar and the
|
|
35
|
+
browser tab, for everyone who signs in; empty, it uses the site title. The admin logo
|
|
36
|
+
takes priority over the admin icon.</td>
|
|
37
|
+
</tr>
|
|
31
38
|
<tr>
|
|
32
39
|
<td>SEO</td>
|
|
33
40
|
<td>Default page title, title separator and default description. The full SEO Tool is
|
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="users"></span> Users & Roles</h3>
|
|
59
59
|
<p class="text-muted" style="margin-bottom:0;">Accounts, roles, the role ladder, confined roles and resets.</p></div>
|
|
60
60
|
</a>
|
|
61
|
+
<a href="#/docs/usage/blocks" class="card" style="text-decoration:none;display:block;">
|
|
62
|
+
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="box"></span> Blocks</h3>
|
|
63
|
+
<p class="text-muted" style="margin-bottom:0;">Your own HTML templates for collection entries, views and one-off pieces.</p></div>
|
|
64
|
+
</a>
|
|
61
65
|
<a href="#/docs/usage/tools" class="card" style="text-decoration:none;display:block;">
|
|
62
66
|
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="list"></span> Built-in Tools</h3>
|
|
63
67
|
<p class="text-muted" style="margin-bottom:0;">Contacts, Notes, Todo, Analytics, SEO, notifications and search.</p></div>
|
|
@@ -101,7 +105,7 @@
|
|
|
101
105
|
<p class="text-muted" style="margin-bottom:0;">Emails, webhooks, Actions and what happens after a submission.</p></div>
|
|
102
106
|
</a>
|
|
103
107
|
<a href="#/tutorials/plugin" class="card" style="text-decoration:none;display:block;">
|
|
104
|
-
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="package"></span> Writing a
|
|
108
|
+
<div class="card-body"><h3 style="margin-top:0;"><span data-icon="package"></span> Writing a Tool</h3>
|
|
105
109
|
<p class="text-muted" style="margin-bottom:0;">Server routes, admin screens and page snippets of your own.</p></div>
|
|
106
110
|
</a>
|
|
107
111
|
</div>
|
|
@@ -105,7 +105,8 @@
|
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
107
|
</div><p class="text-muted">The logo takes priority over the icon. Set the browser icon in the Favicon tab.</p></div>
|
|
108
|
-
<div class="stx-card"><div class="stx-card-title">Admin
|
|
108
|
+
<div class="stx-card"><div class="stx-card-title">Admin
|
|
109
|
+
<span data-help-title="Admin name and mark" data-help="The name and logo or icon at the top of the admin - in the top bar, the sidebar and the browser tab - for everyone who signs in. Leave the name empty to use the site title."></span></div><div class="stx-grid">
|
|
109
110
|
<div><label class="form-label" for="stx-adminBrandTitle">Admin name</label>
|
|
110
111
|
<input id="stx-adminBrandTitle" type="text" class="form-input" placeholder="Uses the site title when empty" data-model="adminBrandTitle.value">
|
|
111
112
|
</div>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
<a href="#/tutorials/plugin" class="card" style="text-decoration:none;display:block;">
|
|
28
28
|
<div class="card-body">
|
|
29
|
-
<h3 style="margin-top:0;"><span data-icon="package"></span> Writing a
|
|
29
|
+
<h3 style="margin-top:0;"><span data-icon="package"></span> Writing a Tool</h3>
|
|
30
30
|
<p class="text-muted" style="margin-bottom:0;">
|
|
31
31
|
Extend the CMS with your own server routes, admin pages and page-injection snippets:
|
|
32
32
|
the manifest, the Fastify entry point, switching it on and testing it.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import{api as $}from"../api.js";import{addBannerAction as
|
|
1
|
+
import{api as $}from"../api.js";import{addBannerAction as J}from"../lib/plugin-chrome.js";import{helpPopovers as at}from"../lib/help-popover.js";import{openMenuAt as Fe,sidebarCount as nt}from"../lib/tool-kit.js";import{createSimpleEditor as Ie}from"../lib/simple-editor.js";import{checkForm as st,draftKey as j,draftOffer as rt,escapeHtml as Ne,extractPlaceholders as ct,formFromBlock as lt,formatHtml as it,payloadFromForm as ut,previewMessage as dt,previewShell as pt,rawPlaceholders as We,renderPreview as mt,sampleValue as _e,scopeName as vt,shortcodeFor as ht,snapshot as de}from"../lib/block-editor-arrange.js";import{fmtDateTime as Be}from"../lib/dates.js";const ft=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,G=u=>String(u||"").split("?")[0],pe="block-editor.mode",bt=["write","split","preview"],He="block-editor.preview",Ve=["full","1024","390"],yt="/__block-preview",me="dm-block-preview-root",Ke={title:"Block preview",layout:"blank",breadcrumbs:!1},kt={ADD_ATTR:["target","loading"],ALLOW_DATA_ATTR:!0,FORCE_BODY:!0},wt=()=>window.DOMPurify?.sanitize?u=>window.DOMPurify.sanitize(u,kt):null,gt=`
|
|
2
2
|
<div class="bex-sample" data-key="{{key}}" tabindex="0">
|
|
3
3
|
<label class="bex-sample-label" for="bex-sample-{{key}}">{{label}}</label>
|
|
4
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
5
|
<button type="button" class="bex-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
|
|
6
|
-
</div>`;let
|
|
7
|
-
`).length} lines`),a.css.value.trim()&&e.push(`${Math.ceil(a.css.value.length/1024)} KB CSS`),e.join(" \xB7 ")}),nameErr:M.computed(()=>(V.value||a.name.value)&&K.value.errors.name||""),cssErr:M.computed(()=>K.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${vt(a.name.value)}"]`),noSamples:M.computed(()=>
|
|
6
|
+
</div>`;let h=null,Ue=!1;function St(){Ue||!window.R?.use||(Ue=!0,R.use((u,p,s)=>{if(!(ft.test(p?.path||"")&&G(u?.path)!==G(p?.path))||!h?.dirty())return s();const c=h;return E.confirm(`You have unsaved changes to ${Ne(c.label())}. Leave anyway? They are kept as a draft for your next visit.`).then(H=>{H?s():history.replaceState(null,"",c.hash)})}),M.subscribe("router:afterChange",({to:u}={})=>{if(h&&G(u?.path)!==h.path){const p=h;h=null,p.dispose()}}))}function Et(u,p){const s=u.querySelector(".dm-plugin-banner-name"),g=s?.lastChild;g&&g.nodeType===3&&s.querySelector(".dm-plugin-banner-sep")&&(g.textContent=p)}function Q(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(St(),h){const e=h;h=null,e.dispose()}const p=u.get(0),s=p.querySelector(".bex")||p,g=[],c=e=>(g.push(e),e);let H=!1;const C=()=>!H&&s.isConnected,ve=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),l=ve?decodeURIComponent(ve[1]):null,d=!l,Ye=location.hash,ze=G(location.hash.slice(1)),Je=d&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",he=E.loader(p,{type:"dots"});let y=null,X=[],Z=[];try{[y,X,Z]=await Promise.all([l?$.blocks.get(l):Promise.resolve(null),$.blocks.list().catch(()=>[]),$.projects.list().catch(()=>[])])}catch(e){he.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(he.destroy(),!s.isConnected)return;const fe=(Array.isArray(X)?X:[]).map(e=>e.name).sort((e,t)=>e.localeCompare(t));let be=y?.meta||{};const v=lt(y?{...y,name:l}:null,{project:Je}),a={};for(const[e,t]of Object.entries(v))a[e]=M.observable(t);const ee=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].peek()])),ye=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].value])),O=M.observable(de(v)),D=M.computed(()=>de(ye())!==O.value),q=M.observable(!1),V=M.observable(!1),K=M.computed(()=>st(ye(),{isNew:d,taken:fe}));let ke="split";try{ke=bt.includes(S.get(pe))?S.get(pe):"split"}catch{}const k=M.observable(ke);c(M.effect(()=>{const e=k.value;try{S.set(pe,e)}catch{}}));const we=M.observable("Ln 1, Col 1");let L={};try{L=S.get(He)||{}}catch{}const te=M.observable(Ve.includes(L.width)?L.width:"full"),U=M.observable(String(L.repeat)==="3"?"3":"1"),m=M.observable(typeof L.theme=="string"?L.theme:""),x=M.observable("");c(M.effect(()=>{const e={width:te.value,repeat:U.value,theme:m.value};try{S.set(He,e)}catch{}}));const f={},P={},Y=M.observable(0),oe=M.observable([]),ge=()=>We(a.content.peek());c(M.effect(()=>{const e=ct(a.content.value),t=We(a.content.value);let n=!1;for(const o of e){const w=t.has(o);if(!(o in f)||o in P&&P[o]!==w){const B=o in f;if(f[o]=_e(o,new Date,{raw:w}),P[o]=w,B){n=!0;const i=s.querySelector(`.bex-samples [data-sample="${CSS.escape(o)}"]`);i&&(i.value=f[o])}}}oe.value=e.map(o=>({key:o,label:t.has(o)?`{{{${o}}}}`:`{{${o}}}`,value:f[o]})),n&&queueMicrotask(()=>Y.value++)}));const Ge={...a,dirty:D,saving:q,mode:k,cursor:we,sampleRows:oe,isNew:d,notNew:!d,pvWidth:te,pvRepeat:U,pvTheme:m,pvStatus:x,titleText:d?"New block":l,metaText:M.computed(()=>{if(d)return"Not saved yet";const e=[];return(y?.updatedAt||y?.meta?.updatedAt)&&e.push(`changed ${Be(y.updatedAt||y.meta.updatedAt)}`),e.push(`${a.content.value.split(`
|
|
7
|
+
`).length} lines`),a.css.value.trim()&&e.push(`${Math.ceil(a.css.value.length/1024)} KB CSS`),e.join(" \xB7 ")}),nameErr:M.computed(()=>(V.value||a.name.value)&&K.value.errors.name||""),cssErr:M.computed(()=>K.value.errors.css||""),scopeHint:M.computed(()=>`[data-block="${vt(a.name.value)}"]`),noSamples:M.computed(()=>oe.value.length===0),noSamplesText:"No {{placeholders}} in the template yet.",modeCls:M.computed(()=>`bex-body is-${k.value}`),cannotSave:M.computed(()=>q.value||V.value&&Object.keys(K.value.errors).length>0)},ae=s.querySelector("#block-project");for(const e of Array.isArray(Z)?Z:[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,ae.appendChild(t)}if(v.project&&![...ae.options].some(e=>e.value===v.project)){const e=document.createElement("option");e.value=v.project,e.textContent=`${v.project} (no longer a project)`,ae.appendChild(e)}const F=s.querySelector("#block-quick-switch");for(const e of fe){const t=document.createElement("option");t.value=e,t.textContent=e,e===l&&(t.selected=!0),F.appendChild(t)}const b=s.querySelector(".bex-samples");b.innerHTML=gt;let A=null,N=null,r=null;function Se(e){for(const[t,n]of Object.entries(e))a[t]&&(a[t].value=n);A.setValue(String(e.content??"")),N.setValue(String(e.css??""))}const ne=s.querySelector("#block-preview-frame");let T="none",W=0;const Qe="As your site shows it \xB7 not saved",Ee=()=>{if(!C()||k.peek()==="write"||T!=="ready")return;const e={content:a.content.peek(),css:a.css.peek(),name:a.name.peek()},t=wt();if(!t){x.value="The preview needs the HTML cleaner, which did not load - reload the admin.";return}const n=mt(e,f,{repeat:Number(U.peek()),sanitize:t});try{ne.contentWindow?.postMessage({source:"dm-block-editor",type:"paint",tick:W,html:n},"*")}catch{}},Me=e=>{const t=dt(e,ne.contentWindow,W);if(!(!t||!C())){if(t.type==="save"){z();return}if(T=t.host?"ready":"failed",T==="failed"){x.value="Your site's page came back without a place for the block - the preview cannot show it.";return}x.peek()==="Loading your site\u2026"&&(x.value=Qe),Ee()}};window.addEventListener("message",Me),c(()=>window.removeEventListener("message",Me));async function Ce(){const e=++W;T="loading",x.value="Loading your site\u2026";let t;try{const n=m.peek()?{...Ke,theme:m.peek()}:Ke;({html:t}=await $.pages.previewFull(yt,n,`<div id="${me}"></div>`))}catch(n){t=`<!doctype html><html><head><meta charset="utf-8"></head><body style="font:14px system-ui;margin:0"><div id="${me}" style="padding:1rem"></div></body></html>`,x.value=`Without your site's styles - ${n?.message||"the page did not render"}`}!C()||e!==W||(ne.srcdoc=pt(t,e,me))}let se=0;c(M.effect(()=>{if(a.content.value,a.css.value,a.name.value,Y.value,U.value,k.value!=="write"){if(T==="none"){Ce();return}cancelAnimationFrame(se),se=requestAnimationFrame(Ee)}}));let xe=m.peek();c(M.effect(()=>{const e=m.value;e!==xe&&(xe=e,T!=="none"&&Ce())})),c(()=>{cancelAnimationFrame(se),W++}),$.theme.catalog().then(({themes:e=[],custom:t=[]}={})=>{if(!C())return;const n=s.querySelector("#block-preview-theme"),o=i=>String(i).replace(/-/g," ").replace(/\b\w/g,ie=>ie.toUpperCase()),w=[...e.filter(i=>!String(i).startsWith("admin-")).map(i=>({id:i,label:o(i)})),...(Array.isArray(t)?t:[]).filter(i=>i?.id).map(i=>({id:i.id,label:i.label||o(i.id)}))],B=w.map(i=>i.id);for(const{id:i,label:ie}of w){const ue=document.createElement("option");ue.value=i,ue.textContent=ie,n.appendChild(ue)}m.peek()&&!B.includes(m.peek())?m.value="":n.value=m.peek()}).catch(()=>{s.querySelector("#block-preview-theme")?.remove(),C()&&m.peek()&&(m.value="")});const Ae=e=>{const t=e.target?.dataset?.sample;t&&(f[t]=e.target.value,delete P[t],Y.value++)};b.addEventListener("input",Ae),c(()=>b.removeEventListener("input",Ae));function re(e){r.focus(),r.setRangeText(e,r.selectionStart,r.selectionEnd,"end"),r.dispatchEvent(new Event("input",{bubbles:!0}))}async function Le(e){switch(e){case"undo":r.focus(),document.execCommand("undo");break;case"redo":r.focus(),document.execCommand("redo");break;case"cut":r.selectionStart!==r.selectionEnd&&(await Q(r.value.slice(r.selectionStart,r.selectionEnd)),re(""));break;case"copy":r.selectionStart!==r.selectionEnd&&await Q(r.value.slice(r.selectionStart,r.selectionEnd));break;case"paste":try{re(await navigator.clipboard.readText())}catch{E.toast("Use Ctrl+V to paste.",{type:"info"})}break;case"select-all":r.focus(),r.select();break;case"format":{const t=it(r.value);A.setValue(t),a.content.value=t;break}default:break}}async function z(){if(q.peek())return;V.value=!0;const{errors:e,name:t}=K.peek();if(Object.keys(e).length){E.toast(e.name||e.css,{type:"warning"});return}const n=d?t:l;q.value=!0;try{const o=ut(ee(),be);if(await $.blocks.put(n,o),!C())return;be=o.meta,O.value=de(ee());try{S.remove?S.remove(j(l)):S.set(j(l),null)}catch{}nt("#/blocks"),E.toast(d?`Created "${n}".`:"Block saved.",{type:"success"}),d&&(location.hash=`#/blocks/edit/${encodeURIComponent(n)}`)}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{q.value=!1}}async function Xe(){D.peek()&&await E.confirm("Discard the changes you have not saved?")&&(V.value=!1,Se(Ze(O.peek())))}function Ze(e){try{const[t,n,o,w,B]=JSON.parse(e);return{name:t,project:n,bundled:o,content:w,css:B}}catch{return v}}const et=M.applyBindings(Ge,s,{methods:{save:()=>z(),discard:()=>Xe(),setWrite:()=>{k.value="write"},setSplit:()=>{k.value="split"},setPreview:()=>{k.value="preview"},setWidth:e=>{Ve.includes(String(e))&&(te.value=String(e))},act:e=>Le(e)}});c(()=>et.dispose()),c(at(s,{position:"bottom"})),A=Ie(s.querySelector(".bex-write"),{initialValue:v.content,language:"html",autoIndent:!0,onChange:e=>{a.content.value=e},onCursor:({line:e,col:t})=>{we.value=`Ln ${e}, Col ${t}`}}),N=Ie(s.querySelector(".bex-css"),{initialValue:v.css,language:"css",autoIndent:!0,onChange:e=>{a.css.value=e}}),A.textarea.setAttribute("aria-label","Template HTML"),A.textarea.placeholder=`<div class="card mb-3">
|
|
8
8
|
<div class="card-body">
|
|
9
9
|
<h3>{{title}}</h3>
|
|
10
10
|
<p>{{message}}</p>
|
|
11
11
|
</div>
|
|
12
12
|
</div>`,N.textarea.setAttribute("aria-label","Custom CSS"),N.textarea.placeholder=`/* Scoped to this block - e.g.
|
|
13
13
|
color: var(--dm-primary);
|
|
14
|
-
h3 { font-size: 1.25rem; } */`,c(()=>{A.destroy(),N.destroy()}),r=A.textarea;const Te=s.querySelector(".bex-toolbar"),Re=e=>{const t=e.target.closest("[data-action]");t&&Le(t.dataset.action)};Te.addEventListener("click",Re),c(()=>Te.removeEventListener("click",Re));const $e=()=>{const e=F.value;e&&e!==l?location.hash=`#/blocks/edit/${encodeURIComponent(e)}`:F.value=l||""};F.addEventListener("change",$e),c(()=>F.removeEventListener("change",$e));const
|
|
14
|
+
h3 { font-size: 1.25rem; } */`,c(()=>{A.destroy(),N.destroy()}),r=A.textarea;const Te=s.querySelector(".bex-toolbar"),Re=e=>{const t=e.target.closest("[data-action]");t&&Le(t.dataset.action)};Te.addEventListener("click",Re),c(()=>Te.removeEventListener("click",Re));const $e=()=>{const e=F.value;e&&e!==l?location.hash=`#/blocks/edit/${encodeURIComponent(e)}`:F.value=l||""};F.addEventListener("change",$e),c(()=>F.removeEventListener("change",$e));const ce=e=>ge().has(e)?`{{{${e}}}}`:`{{${e}}}`,tt=e=>[{header:ce(e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>re(ce(e))},{label:"Copy placeholder",icon:"copy",action:async()=>{const t=ce(e);E.toast(await Q(t)?"Copied.":t,{type:"info"})}},{label:"Reset sample value",icon:"rotate-ccw",action:()=>{const t=ge().has(e);f[e]=_e(e,new Date,{raw:t}),P[e]=t;const n=b.querySelector(`[data-sample="${CSS.escape(e)}"]`);n&&(n.value=f[e]),Y.value++}}],ot=E.contextMenu(b,{cursor:"glyph",match:".bex-sample",inherit:!1,density:"compact",items:e=>tt(e.dataset.key)});c(()=>ot?.destroy?.());const je=e=>{const t=e.target.closest(".bex-kebab");t&&Fe(t)},Oe=e=>{const t=e.target.closest(".bex-sample");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),Fe(t))};b.addEventListener("click",je),b.addEventListener("keydown",Oe),c(()=>{b.removeEventListener("click",je),b.removeEventListener("keydown",Oe)}),Et(p,d?"New block":l),J(u,{id:"block-editor-list",icon:"arrow-left",title:"All blocks",onClick:()=>{location.hash="#/blocks"}}),d?p.querySelector('[data-banner-action="block-editor-shortcode"]')?.remove():J(u,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=ht(l);E.toast(await Q(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),J(u,{id:"block-editor-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),J(u,{id:"block-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>z()});const De=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),z())};document.addEventListener("keydown",De,!0),c(()=>document.removeEventListener("keydown",De,!0));const qe=()=>{if(D.peek())try{S.set(j(l),{form:ee(),base:O.peek(),at:new Date().toISOString()})}catch{}},Pe=e=>{D.peek()&&(qe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Pe),c(()=>window.removeEventListener("beforeunload",Pe)),h={hash:Ye,path:ze,dirty:()=>D.peek(),label:()=>d?"the new block":`"${l}"`,dispose(){H=!0,qe(),g.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let le=null;try{le=S.get(j(l))}catch{}const _=rt(le,O.peek());if(le)try{S.remove?S.remove(j(l)):S.set(j(l),null)}catch{}if(_){const e=_.at?` (${Be(_.at)})`:"",t=d?"a new block":`"${Ne(l)}"`,n=_.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(n)&&C()){const o={...v,..._.form};d||(o.name=l),Se(o)}}}};
|
package/admin/js/views/blocks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as
|
|
1
|
+
import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as A}from"../lib/plugin-chrome.js";import{helpPopovers as te}from"../lib/help-popover.js";import{onceClosed as ne,openMenuAt as ae,sidebarCount as Se,viewLifetime as Me}from"../lib/tool-kit.js";import{filterByProject as Le,getProjectFromHash as Ie,renderProjectFilterNotice as Te}from"../lib/project-context.js";import{FILTERS as je,arrangeBlocks as $e,blockRow as Ne,checkNewName as Be,copyName as oe,shortcodeFor as se,usageText as le}from"../lib/blocks-arrange.js";import{fmtDate as De}from"../lib/dates.js";const ce=/^#\/(blocks|projects\/[^/?#]+\/blocks)\/?(\?.*)?$/,Fe="#/blocks",N="blocks.draft",U="blocks.filter",Re=`
|
|
2
2
|
<div class="blx-row" data-name="{{name}}" tabindex="0" data-bind-class="unused && 'is-unused'">
|
|
3
3
|
<span class="blx-icon" aria-hidden="true"><span data-icon="box" data-icon-size="16"></span></span>
|
|
4
4
|
<span class="blx-main"><strong>{{name}}</strong><span data-bind-hidden="!description">{{description}}</span><small>{{meta}}</small></span>
|
|
@@ -10,13 +10,13 @@ import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as ee}fr
|
|
|
10
10
|
</span>
|
|
11
11
|
<span class="blx-uses" title="Pages and views that use it">{{uses}}</span>
|
|
12
12
|
<button type="button" class="blx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
13
|
-
</div>`,g=
|
|
13
|
+
</div>`,g=s=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function ie(s){const a=document.createElement("template");return a.innerHTML=s.trim(),a.content.firstElementChild}const i=Me(ce);async function qe(s){try{return await navigator.clipboard.writeText(s),!0}catch{const a=document.createElement("textarea");a.value=s,a.style.cssText="position:fixed;opacity:0;",document.body.appendChild(a),a.select();let h=!1;try{h=document.execCommand("copy")}catch{}return a.remove(),h}}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(s){i.reset();const a=s.get(0).querySelector(".blx")||s.get(0),h=()=>ce.test(location.hash)&&a.isConnected,w=Ie(),p=M.observable([]),H=M.observable(!0),B=M.observable("");let O="all";try{O=je.includes(S.get(U))?S.get(U):"all"}catch{}const y=M.observable(O),P=M.observable("");i.add(M.effect(()=>{const e=y.value;try{S.set(U,e)}catch{}}));const re=De,D=M.observable([]);i.add(M.effect(()=>{D.value=$e(p.value,{filter:y.value,query:P.value}).map(e=>Ne(e,re))}));const de=M.computed(()=>{const e=p.value,t=e.filter(n=>(n.usage?.pages?.length||0)+(n.usage?.views?.length||0)>0).length;return{all:e.length,used:t,unused:e.length-t}}),ue=M.computed(()=>p.value.length?"No blocks match.":"No blocks yet - New block in the banner makes one, or Import brings one in."),F=e=>p.peek().find(t=>t.name===e),pe=w?`#/blocks/new?project=${encodeURIComponent(w)}`:"#/blocks/new",K=e=>`#/blocks/edit/${encodeURIComponent(e)}`;async function W(){try{let e=await Ce("/blocks?usage=1",{method:"GET"});if(!h())return;if(e=Array.isArray(e)?e:[],w){const t=e.length;e=Le(e,w),Te(a,{total:t,shown:e.length,slug:w,allHref:"#/blocks",noun:"blocks"})}p.value=e,B.value=""}catch(e){B.value=`Could not load the blocks: ${e.message||e}`}finally{H.value=!1}}const T=async()=>{Se(Fe),h()&&await W()};async function me(e){const n=(e.usage?.pages?.length||0)+(e.usage?.views?.length||0)?` It is ${g(le(e).toLowerCase())} - they will show nothing where it was.`:"";if(await E.confirm(`Delete the block "${g(e.name)}"?${n} This cannot be undone.`))try{await v.blocks.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),await T()}catch(r){E.toast(r.message||"Failed to delete block.",{type:"error"})}}async function be(e){try{await v.blocks.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}async function he(e){const t=se(e),n=await qe(t);E.toast(n?`Copied ${t}`:`Could not copy - your browser refused. The shortcode is ${t}`,{type:n?"success":"error"})}const k=a.querySelector(".blx-import-file");async function z(){const e=k.files?.[0];if(!e)return;k.value="";let t;try{t=JSON.parse(await e.text())}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!t||typeof t!="object"||!t.name||typeof t.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const n=await v.blocks.importBundle(t);E.toast(`Imported "${n.name}".`,{type:"success"}),await T()}catch(n){if(n.code==="CONFLICT"){if(!await E.confirm(`A block named "${g(n.name)}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const r=await v.blocks.importBundle(t,{overwrite:!0});E.toast(`Overwrote "${r.name}".`,{type:"success"}),await T()}catch(r){E.toast(r.message||"Import failed.",{type:"error"})}return}E.toast(n.message||"Import failed.",{type:"error"})}}k.addEventListener("change",z),i.add(()=>k.removeEventListener("change",z));const x=new Set;let f=null,_=!1;function fe(e){const t=E.slideover({title:`Where ${g(e.name)} is used`,size:"sm",position:"right"}),n=ie(`
|
|
14
14
|
<div class="blx-form">
|
|
15
15
|
<p class="blx-note blx-where-sum"></p>
|
|
16
16
|
<ul class="blx-where"></ul>
|
|
17
17
|
<div><div class="form-label">Shortcode</div><div class="blx-code"></div>
|
|
18
18
|
<small class="blx-hint">Right-click the block in the list and Copy shortcode to paste it into a page.</small></div>
|
|
19
|
-
</div>`);n.querySelector(".blx-where-sum").textContent=le(e)+".",n.querySelector(".blx-code").textContent=
|
|
19
|
+
</div>`);n.querySelector(".blx-where-sum").textContent=le(e)+".",n.querySelector(".blx-code").textContent=se(e);const r=n.querySelector(".blx-where"),b=(l,j,$)=>{const L=document.createElement("li"),o=document.createElement("a");o.href=l,o.textContent=j,o.addEventListener("click",()=>t.close());const d=document.createElement("small");d.textContent=$,L.append(o,d),r.appendChild(L)};for(const l of e.usage?.pages||[])b(`#/pages/edit${l}`,l,"page");for(const l of e.usage?.views||[])b(`#/views/edit/${encodeURIComponent(l)}`,l,"view");t.element.appendChild(n),x.add(t),ne(t,()=>x.delete(t)),t.open()}function Y(e,t=null){const n=()=>p.peek().map(c=>c.name),r=t?.name??oe(e.name,n()),b=M.observable(r),l=M.observable(!1),j=M.computed(()=>Be(b.value,p.value.map(c=>c.name))),$={name:b,saving:l,err:j,cannot:M.computed(()=>l.value||!!j.value)},L=()=>b.peek()!==oe(e.name,n())||!!t,o=E.slideover({title:`Duplicate ${g(e.name)}`,size:"sm",position:"right"}),d=ie(`
|
|
20
20
|
<div class="blx-form">
|
|
21
21
|
<div>
|
|
22
22
|
<label class="form-label" for="blx-dup-name">Name of the copy
|
|
@@ -31,4 +31,4 @@ import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as ee}fr
|
|
|
31
31
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
|
|
32
32
|
<span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
|
|
33
33
|
</div>
|
|
34
|
-
</div>`);let R=!1;async function
|
|
34
|
+
</div>`);let R=!1;async function X(){if($.cannot.peek())return;l.value=!0;const c=b.peek().trim();try{const u=await v.blocks.get(e.name);await v.blocks.put(c,{content:u.content,css:u.css||"",...u.meta?.project&&{meta:{project:u.meta.project}}}),E.toast(`Made "${c}".`,{type:"success"}),R=!0,o.close(),await T()}catch(u){E.toast(`Not duplicated: ${u.message||u}`,{type:"error"})}finally{l.value=!1}}o.element.appendChild(d);const we=te(d),ke=M.applyBindings($,d,{methods:{save:X,cancel:()=>o.close()}}),xe=c=>{c.key==="Enter"&&c.target.matches("input")&&(c.preventDefault(),X())};d.addEventListener("keydown",xe);const Z={dirty:L,values:()=>({name:b.peek()}),source:e.name};f=Z,x.add(o),ne(o,()=>{ke.dispose(),we(),x.delete(o),f===Z&&(f=null)});const Ee=o.close.bind(o);let q=!1;o.close=async c=>{if(!R&&!_&&c!==!0&&L()){if(q)return;q=!0;const u=await E.confirm("Discard the name you typed?");if(q=!1,!u)return}return R=!0,Ee()},I.scan(d),o.open();const ee=d.querySelector("#blx-dup-name");ee?.focus(),ee?.select()}function ye(e){const t=F(e);if(!t)return[];const n=(t.usage?.pages?.length||0)+(t.usage?.views?.length||0);return[{header:t.name},{label:"Edit",icon:"edit",action:()=>{location.hash=K(t.name)}},{label:"Copy shortcode",icon:"copy",action:()=>he(t)},{label:n?`Where it is used (${n})\u2026`:"Where it is used\u2026",icon:"link",action:()=>fe(t)},{type:"divider"},{label:"Duplicate\u2026",icon:"layers",action:()=>Y(t)},{label:"Export (.dmblock.json)",icon:"download",action:()=>be(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>me(t)}]}const m=a.querySelector(".blx-list");m.innerHTML=Re;const ve=M.applyBindings({blocks:p,loading:H,loadError:B,filter:y,query:P,rows:D,counts:de,emptyText:ue},a,{methods:{showAll:()=>{y.value="all"},showUsed:()=>{y.value="used"},showUnused:()=>{y.value="unused"}}});i.add(()=>ve.dispose()),i.add(te(a,{position:"bottom"})),i.add(M.effect(()=>{D.value,requestAnimationFrame(()=>{m.isConnected&&I.scan(m)})}));const ge=E.contextMenu(m,{cursor:"glyph",match:".blx-row",inherit:!1,density:"compact",items:e=>ye(e.dataset.name)});i.add(()=>ge?.destroy?.());const J=e=>{F(e)&&(location.hash=K(e))},V=e=>{const t=e.target.closest(".blx-row");if(!t)return;const n=e.target.closest(".blx-kebab");if(n){ae(n);return}J(t.dataset.name)},G=e=>{const t=e.target.closest(".blx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ae(t)):e.key==="Enter"&&e.target===t&&J(t.dataset.name))};m.addEventListener("click",V),m.addEventListener("keydown",G),i.add(()=>{m.removeEventListener("click",V),m.removeEventListener("keydown",G)}),A(s,{id:"blocks-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),A(s,{id:"blocks-import",icon:"upload",title:"Import a .dmblock.json bundle",onClick:()=>k.click()}),A(s,{id:"blocks-new",icon:"plus",title:"New block",onClick:()=>{location.hash=pe}});const Q=e=>{f?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Q),i.add(()=>window.removeEventListener("beforeunload",Q)),i.add(()=>{if(f?.dirty())try{S.set(N,{source:f.source,values:f.values(),at:new Date().toISOString()})}catch{}_=!0,[...x].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(a),await W(),!h())return;let C=null;try{C=S.get(N)}catch{}if(C?.values&&C.source){try{S.remove?S.remove(N):S.set(N,null)}catch{}const e=F(C.source);e&&await E.confirm(`You were duplicating "${g(e.name)}" and left it unfinished. Pick up where you left off?`)&&h()&&Y(e,C.values)}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,
|
|
1
|
+
import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,s){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof s=="function"&&s(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const s=o.find("#tutorial-scaffolder-mount").get(0);s&&i(s,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","blocks","tools","dashboard"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],n=o=>o.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");for(const o of a)docViews["usage"+n(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+n(o)]=t(`api-${o}`);
|