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.
@@ -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 &gt; Blocks</strong>. Each one is a file in <code>content/blocks/</code>:
16
+ <code>&lt;name&gt;.html</code> holds the template, <code>&lt;name&gt;.css</code> its styles (optional) and
17
+ <code>&lt;name&gt;.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="&lt;name&gt;"</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_&lt;field&gt;</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>&amp;#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>&lt;span data-icon="mail"&gt;&lt;/span&gt;</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>&#123;&#123;field&#125;&#125;</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>&#123;&#123;field:DD MMM YYYY&#125;&#125;</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>&#123;&#123;&#123;field&#125;&#125;&#125;</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>&#123;&#123;_id&#125;&#125;</code>, <code>&#123;&#123;_createdAt&#125;&#125;</code>,
122
+ <code>&#123;&#123;_updatedAt&#125;&#125;</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>&#123;&#123;_createdAt:D MMMM YYYY&#125;&#125;</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>&lt;script&gt;</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>&lt;!--
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
+ --&gt;
148
+ &lt;div class="card team-card"&gt;
149
+ &lt;img class="team-card-photo" src="&#123;&#123;photo&#125;&#125;" alt="&#123;&#123;name&#125;&#125;"&gt;
150
+ &lt;div class="card-body"&gt;
151
+ &lt;h3&gt;&#123;&#123;name&#125;&#125;&lt;/h3&gt;
152
+ &lt;span class="badge badge-secondary"&gt;&#123;&#123;role&#125;&#125;&lt;/span&gt;
153
+ &lt;p&gt;&#123;&#123;bio&#125;&#125;&lt;/p&gt;
154
+ &lt;/div&gt;
155
+ &lt;/div&gt;</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 &gt; 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 &gt; 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>&lt;name&gt;.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 &gt; 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 &gt; 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 &amp; 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 &amp; 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 Plugin</a> - build your own.</li>
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 &amp; 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 Plugin</h3>
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 sidebar</div><div class="stx-grid">
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 Plugin</h3>
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 ue}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 ft,snapshot as de}from"../lib/block-editor-arrange.js";import{fmtDateTime as He}from"../lib/dates.js";const ht=/^\/blocks\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,J=u=>String(u||"").split("?")[0],pe="block-editor.mode",bt=["write","split","preview"],Be="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=`
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 f=null,Ue=!1;function St(){Ue||!window.R?.use||(Ue=!0,R.use((u,p,s)=>{if(!(ht.test(p?.path||"")&&J(u?.path)!==J(p?.path))||!f?.dirty())return s();const c=f;return E.confirm(`You have unsaved changes to ${Ne(c.label())}. Leave anyway? They are kept as a draft for your next visit.`).then(B=>{B?s():history.replaceState(null,"",c.hash)})}),M.subscribe("router:afterChange",({to:u}={})=>{if(f&&J(u?.path)!==f.path){const p=f;f=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 G(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(),f){const e=f;f=null,e.dispose()}const p=u.get(0),s=p.querySelector(".bex")||p,g=[],c=e=>(g.push(e),e);let B=!1;const C=()=>!B&&s.isConnected,ve=location.hash.match(/^#\/blocks\/edit\/([^/?#]+)/),l=ve?decodeURIComponent(ve[1]):null,d=!l,Ye=location.hash,ze=J(location.hash.slice(1)),Je=d&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",fe=E.loader(p,{type:"dots"});let y=null,Q=[],X=[];try{[y,Q,X]=await Promise.all([l?$.blocks.get(l):Promise.resolve(null),$.blocks.list().catch(()=>[]),$.projects.list().catch(()=>[])])}catch(e){fe.destroy(),E.toast(e.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}if(fe.destroy(),!s.isConnected)return;const he=(Array.isArray(Q)?Q:[]).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 Z=()=>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:he}));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(Be)||{}}catch{}const ee=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:ee.value,repeat:U.value,theme:m.value};try{S.set(Be,e)}catch{}}));const h={},P={},Y=M.observable(0),te=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 h)||o in P&&P[o]!==w){const H=o in h;if(h[o]=_e(o,new Date,{raw:w}),P[o]=w,H){n=!0;const i=s.querySelector(`.bex-samples [data-sample="${CSS.escape(o)}"]`);i&&(i.value=h[o])}}}te.value=e.map(o=>({key:o,label:t.has(o)?`{{{${o}}}}`:`{{${o}}}`,value:h[o]})),n&&queueMicrotask(()=>Y.value++)}));const Ge={...a,dirty:D,saving:q,mode:k,cursor:we,sampleRows:te,isNew:d,notNew:!d,pvWidth:ee,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 ${He(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(()=>te.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)},oe=s.querySelector("#block-project");for(const e of Array.isArray(X)?X:[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,oe.appendChild(t)}if(v.project&&![...oe.options].some(e=>e.value===v.project)){const e=document.createElement("option");e.value=v.project,e.textContent=`${v.project} (no longer a project)`,oe.appendChild(e)}const F=s.querySelector("#block-quick-switch");for(const e of he){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 ae=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,h,{repeat:Number(U.peek()),sanitize:t});try{ae.contentWindow?.postMessage({source:"dm-block-editor",type:"paint",tick:W,html:n},"*")}catch{}},Me=e=>{const t=dt(e,ae.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||(ae.srcdoc=pt(t,e,me))}let ne=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(ne),ne=requestAnimationFrame(Ee)}}));let xe=m.peek();c(M.effect(()=>{const e=m.value;e!==xe&&(xe=e,T!=="none"&&Ce())})),c(()=>{cancelAnimationFrame(ne),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,le=>le.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)}))],H=w.map(i=>i.id);for(const{id:i,label:le}of w){const ie=document.createElement("option");ie.value=i,ie.textContent=le,n.appendChild(ie)}m.peek()&&!H.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&&(h[t]=e.target.value,delete P[t],Y.value++)};b.addEventListener("input",Ae),c(()=>b.removeEventListener("input",Ae));function se(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 G(r.value.slice(r.selectionStart,r.selectionEnd)),se(""));break;case"copy":r.selectionStart!==r.selectionEnd&&await G(r.value.slice(r.selectionStart,r.selectionEnd));break;case"paste":try{se(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(Z(),be);if(await $.blocks.put(n,o),!C())return;be=o.meta,O.value=de(Z());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,H]=JSON.parse(e);return{name:t,project:n,bundled:o,content:w,css:H}}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))&&(ee.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">
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 re=e=>ge().has(e)?`{{{${e}}}}`:`{{${e}}}`,tt=e=>[{header:re(e)},{label:"Insert into the template",icon:"corner-down-right",action:()=>se(re(e))},{label:"Copy placeholder",icon:"copy",action:async()=>{const t=re(e);E.toast(await G(t)?"Copied.":t,{type:"info"})}},{label:"Reset sample value",icon:"rotate-ccw",action:()=>{const t=ge().has(e);h[e]=_e(e,new Date,{raw:t}),P[e]=t;const n=b.querySelector(`[data-sample="${CSS.escape(e)}"]`);n&&(n.value=h[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),ue(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():ue(u,{id:"block-editor-shortcode",icon:"copy",title:"Copy shortcode",onClick:async()=>{const e=ft(l);E.toast(await G(e)?"Shortcode copied - set the collection slug.":e,{type:"info",duration:5e3})}}),ue(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:Z(),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)),f={hash:Ye,path:ze,dirty:()=>D.peek(),label:()=>d?"the new block":`"${l}"`,dispose(){B=!0,qe(),g.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(s);let ce=null;try{ce=S.get(j(l))}catch{}const _=rt(ce,O.peek());if(ce)try{S.remove?S.remove(j(l)):S.set(j(l),null)}catch{}if(_){const e=_.at?` (${He(_.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)}}}};
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)}}}};
@@ -1,4 +1,4 @@
1
- import{api as v,apiRequest as Ce}from"../api.js";import{addBannerAction as ee}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 De,copyName as se,shortcodeFor as oe,usageText as le}from"../lib/blocks-arrange.js";import{fmtDate as Be}from"../lib/dates.js";const ce=/^#\/(blocks|projects\/[^/?#]+\/blocks)\/?(\?.*)?$/,Fe="#/blocks",N="blocks.draft",A="blocks.filter",Re=`
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=c=>String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ie(c){const a=document.createElement("template");return a.innerHTML=c.trim(),a.content.firstElementChild}const i=Me(ce);async function qe(c){try{return await navigator.clipboard.writeText(c),!0}catch{const a=document.createElement("textarea");a.value=c,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(c){i.reset();const a=c.get(0).querySelector(".blx")||c.get(0),h=()=>ce.test(location.hash)&&a.isConnected,w=Ie(),p=M.observable([]),U=M.observable(!0),D=M.observable("");let H="all";try{H=je.includes(S.get(A))?S.get(A):"all"}catch{}const y=M.observable(H),O=M.observable("");i.add(M.effect(()=>{const e=y.value;try{S.set(A,e)}catch{}}));const re=Be,B=M.observable([]);i.add(M.effect(()=>{B.value=$e(p.value,{filter:y.value,query:O.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",P=e=>`#/blocks/edit/${encodeURIComponent(e)}`;async function K(){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,D.value=""}catch(e){D.value=`Could not load the blocks: ${e.message||e}`}finally{U.value=!1}}const T=async()=>{Se(Fe),h()&&await K()};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=oe(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 W(){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",W),i.add(()=>k.removeEventListener("change",W));const x=new Set;let f=null,z=!1;function fe(e){const t=E.slideover({title:`Where ${g(e.name)} is used`,size:"sm",position:"right"}),n=ie(`
13
+ </div>`,g=s=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");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=oe(e);const r=n.querySelector(".blx-where"),b=(o,j,$)=>{const L=document.createElement("li"),s=document.createElement("a");s.href=o,s.textContent=j,s.addEventListener("click",()=>t.close());const d=document.createElement("small");d.textContent=$,L.append(s,d),r.appendChild(L)};for(const o of e.usage?.pages||[])b(`#/pages/edit${o}`,o,"page");for(const o of e.usage?.views||[])b(`#/views/edit/${encodeURIComponent(o)}`,o,"view");t.element.appendChild(n),x.add(t),ne(t,()=>x.delete(t)),t.open()}function _(e,t=null){const n=()=>p.peek().map(l=>l.name),r=t?.name??se(e.name,n()),b=M.observable(r),o=M.observable(!1),j=M.computed(()=>De(b.value,p.value.map(l=>l.name))),$={name:b,saving:o,err:j,cannot:M.computed(()=>o.value||!!j.value)},L=()=>b.peek()!==se(e.name,n())||!!t,s=E.slideover({title:`Duplicate ${g(e.name)}`,size:"sm",position:"right"}),d=ie(`
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 Q(){if($.cannot.peek())return;o.value=!0;const l=b.peek().trim();try{const u=await v.blocks.get(e.name);await v.blocks.put(l,{content:u.content,css:u.css||"",...u.meta?.project&&{meta:{project:u.meta.project}}}),E.toast(`Made "${l}".`,{type:"success"}),R=!0,s.close(),await T()}catch(u){E.toast(`Not duplicated: ${u.message||u}`,{type:"error"})}finally{o.value=!1}}s.element.appendChild(d);const we=te(d),ke=M.applyBindings($,d,{methods:{save:Q,cancel:()=>s.close()}}),xe=l=>{l.key==="Enter"&&l.target.matches("input")&&(l.preventDefault(),Q())};d.addEventListener("keydown",xe);const X={dirty:L,values:()=>({name:b.peek()}),source:e.name};f=X,x.add(s),ne(s,()=>{ke.dispose(),we(),x.delete(s),f===X&&(f=null)});const Ee=s.close.bind(s);let q=!1;s.close=async l=>{if(!R&&!z&&l!==!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),s.open();const Z=d.querySelector("#blx-dup-name");Z?.focus(),Z?.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=P(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:()=>_(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:U,loadError:D,filter:y,query:O,rows:B,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(()=>{B.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 Y=e=>{F(e)&&(location.hash=P(e))},J=e=>{const t=e.target.closest(".blx-row");if(!t)return;const n=e.target.closest(".blx-kebab");if(n){ae(n);return}Y(t.dataset.name)},V=e=>{const t=e.target.closest(".blx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ae(t)):e.key==="Enter"&&e.target===t&&Y(t.dataset.name))};m.addEventListener("click",J),m.addEventListener("keydown",V),i.add(()=>{m.removeEventListener("click",J),m.removeEventListener("keydown",V)}),ee(c,{id:"blocks-import",icon:"upload",title:"Import a .dmblock.json bundle",onClick:()=>k.click()}),ee(c,{id:"blocks-new",icon:"plus",title:"New block",onClick:()=>{location.hash=pe}});const G=e=>{f?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",G),i.add(()=>window.removeEventListener("beforeunload",G)),i.add(()=>{if(f?.dirty())try{S.set(N,{source:f.source,values:f.values(),at:new Date().toISOString()})}catch{}z=!0,[...x].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(a),await K(),!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()&&_(e,C.values)}}};
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,n){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof n=="function"&&n(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const n=o.find("#tutorial-scaffolder-mount").get(0);n&&i(n,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","tools","dashboard"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],s=o=>o.split("-").map(n=>n.charAt(0).toUpperCase()+n.slice(1)).join("");for(const o of a)docViews["usage"+s(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+s(o)]=t(`api-${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}`);