spina-blocks 0.3.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/README.md +18 -0
- data/app/assets/javascripts/spina/controllers/block_collection_controller.js +179 -42
- data/app/controllers/spina/blocks/admin/blocks_controller.rb +39 -5
- data/app/models/concerns/spina/parts/block_filterable.rb +18 -0
- data/app/models/spina/parts/block_collection.rb +2 -0
- data/app/models/spina/parts/block_reference.rb +2 -0
- data/app/views/spina/admin/parts/block_collections/_form.html.erb +33 -21
- data/app/views/spina/admin/parts/block_references/_form.html.erb +1 -1
- data/app/views/spina/blocks/admin/blocks/_new_block_modal_form.html.erb +42 -0
- data/app/views/spina/blocks/admin/blocks/new.html.erb +5 -1
- data/config/routes.rb +1 -0
- data/lib/spina/blocks/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bb4bd87c20dc0578090ce0f7e9b26a0ff511f79c52c1e11319527fb9a132362
|
|
4
|
+
data.tar.gz: 768fbfeb07527abc7d752e7f8af4d51ebd0e86f0e7fc643f313e11d11874de3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6887a086f16feb25de70d36706ef1f5492c8b3f2831a3e1e7d583b1e62dcf79d547fa9e46edeca27805260a3e68835d9b1234a7144df0a1affd7ef5acf548fbc
|
|
7
|
+
data.tar.gz: 3741e38abf77005b798a73bdfbfc335f10c7a6109c88551ad6c649f287761714f15aa2e4467e5d65feed3e957c233210da0411e3691dc5204b3b479311dc52a2
|
data/README.md
CHANGED
|
@@ -119,6 +119,24 @@ theme.parts = [
|
|
|
119
119
|
]
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
#### Filtering blocks by template
|
|
123
|
+
|
|
124
|
+
By default, `BlockReference` and `BlockCollection` show all active blocks in their selects. To limit the list to blocks of a specific template, pass `block_template` in the part's `options`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
theme.parts = [
|
|
128
|
+
{ name: "hero_block", title: "Hero Block",
|
|
129
|
+
part_type: "Spina::Parts::BlockReference",
|
|
130
|
+
options: { block_template: "hero" } },
|
|
131
|
+
|
|
132
|
+
{ name: "sidebar_blocks", title: "Sidebar Blocks",
|
|
133
|
+
part_type: "Spina::Parts::BlockCollection",
|
|
134
|
+
options: { block_template: "sidebar_widget" } }
|
|
135
|
+
]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With this configuration, the "Hero Block" select will only show blocks created with the `hero` template, and "Sidebar Blocks" will only show `sidebar_widget` blocks. If `options` is omitted or does not contain `block_template`, all active blocks are shown (the default behavior).
|
|
139
|
+
|
|
122
140
|
Then in your template:
|
|
123
141
|
|
|
124
142
|
```erb
|
|
@@ -2,17 +2,26 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
2
2
|
import Sortable from "libraries/sortablejs";
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
|
+
#abortController = null;
|
|
6
|
+
#modalWasOpen = false;
|
|
7
|
+
#modalObserver = null;
|
|
8
|
+
|
|
9
|
+
get #searchQuery() {
|
|
10
|
+
return this.hasSearchInputTarget ? this.searchInputTarget.value : "";
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
static get targets() {
|
|
6
14
|
return [
|
|
7
15
|
"list",
|
|
8
16
|
"hiddenFields",
|
|
9
17
|
"dropdown",
|
|
10
|
-
"
|
|
18
|
+
"searchInput",
|
|
11
19
|
"emptyMessage",
|
|
12
20
|
"listItemTemplate",
|
|
13
21
|
"groupHeaderTemplate",
|
|
14
22
|
"dropdownOptionTemplate",
|
|
15
23
|
"dropdownEmptyTemplate",
|
|
24
|
+
"newBlockTemplate",
|
|
16
25
|
];
|
|
17
26
|
}
|
|
18
27
|
|
|
@@ -21,6 +30,8 @@ export default class extends Controller {
|
|
|
21
30
|
blocks: Array, // [{id, name, templateName, templateTitle}]
|
|
22
31
|
selectedIds: Array, // [id, id, ...]
|
|
23
32
|
editUrl: String, // base edit_modal URL with __ID__ placeholder
|
|
33
|
+
newUrl: String, // URL for new block modal
|
|
34
|
+
blocksDataUrl: String, // JSON endpoint to refresh blocks list
|
|
24
35
|
};
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -36,10 +47,17 @@ export default class extends Controller {
|
|
|
36
47
|
onEnd: this.reorderHiddenFields.bind(this),
|
|
37
48
|
});
|
|
38
49
|
this.render();
|
|
50
|
+
this.#observeModal();
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
disconnect() {
|
|
42
54
|
if (this.sortable) this.sortable.destroy();
|
|
55
|
+
if (this.#modalObserver) {
|
|
56
|
+
this.#modalObserver.disconnect();
|
|
57
|
+
this.#modalObserver = null;
|
|
58
|
+
}
|
|
59
|
+
this.#abortController?.abort();
|
|
60
|
+
this.#abortController = null;
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
// --- Actions ---
|
|
@@ -50,6 +68,9 @@ export default class extends Controller {
|
|
|
50
68
|
if (this.selectedIdsValue.indexOf(id) !== -1) return;
|
|
51
69
|
|
|
52
70
|
this.selectedIdsValue = this.selectedIdsValue.concat([id]);
|
|
71
|
+
if (this.hasSearchInputTarget) {
|
|
72
|
+
this.searchInputTarget.value = "";
|
|
73
|
+
}
|
|
53
74
|
this.render();
|
|
54
75
|
this.closeDropdown();
|
|
55
76
|
}
|
|
@@ -61,19 +82,23 @@ export default class extends Controller {
|
|
|
61
82
|
this.render();
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
85
|
+
onSearchInput() {
|
|
86
|
+
this.renderDropdown();
|
|
87
|
+
this.openDropdown();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onSearchFocus() {
|
|
91
|
+
this.renderDropdown();
|
|
92
|
+
this.openDropdown();
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
closeOnOutsideClick(event) {
|
|
76
|
-
|
|
96
|
+
const clickedInsideSearch =
|
|
97
|
+
this.hasSearchInputTarget &&
|
|
98
|
+
this.searchInputTarget.contains(event.target);
|
|
99
|
+
const clickedInsideDropdown = this.dropdownTarget.contains(event.target);
|
|
100
|
+
|
|
101
|
+
if (!clickedInsideSearch && !clickedInsideDropdown) {
|
|
77
102
|
this.closeDropdown();
|
|
78
103
|
}
|
|
79
104
|
}
|
|
@@ -118,43 +143,74 @@ export default class extends Controller {
|
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
renderDropdown() {
|
|
121
|
-
|
|
146
|
+
let availableBlocks = this.blocksValue.filter((b) => {
|
|
122
147
|
return this.selectedIdsValue.indexOf(b.id) === -1;
|
|
123
148
|
});
|
|
124
149
|
|
|
150
|
+
// Filter by search query
|
|
151
|
+
const query = this.#searchQuery.toLowerCase().trim();
|
|
152
|
+
if (query) {
|
|
153
|
+
availableBlocks = availableBlocks.filter((b) => {
|
|
154
|
+
return (
|
|
155
|
+
b.name.toLowerCase().includes(query) ||
|
|
156
|
+
(b.templateTitle && b.templateTitle.toLowerCase().includes(query)) ||
|
|
157
|
+
(b.templateName && b.templateName.toLowerCase().includes(query))
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
this.dropdownTarget.innerHTML = "";
|
|
126
163
|
|
|
127
164
|
if (availableBlocks.length === 0) {
|
|
128
165
|
const empty = this.dropdownEmptyTemplateTarget.content.cloneNode(true);
|
|
129
166
|
this.dropdownTarget.appendChild(empty);
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
} else {
|
|
168
|
+
// Group by templateTitle
|
|
169
|
+
const groups = {};
|
|
170
|
+
availableBlocks.forEach((b) => {
|
|
171
|
+
const key = b.templateTitle || b.templateName || "Other";
|
|
172
|
+
if (!groups[key]) groups[key] = [];
|
|
173
|
+
groups[key].push(b);
|
|
174
|
+
});
|
|
132
175
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
176
|
+
Object.keys(groups)
|
|
177
|
+
.sort()
|
|
178
|
+
.forEach((groupName) => {
|
|
179
|
+
const header = this.groupHeaderTemplateTarget.content.cloneNode(true);
|
|
180
|
+
header.querySelector("[data-role='group-name']").textContent =
|
|
181
|
+
groupName;
|
|
182
|
+
this.dropdownTarget.appendChild(header);
|
|
140
183
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const option =
|
|
151
|
-
this.dropdownOptionTemplateTarget.content.cloneNode(true);
|
|
152
|
-
const button = option.querySelector("button");
|
|
153
|
-
button.dataset.blockId = block.id;
|
|
154
|
-
option.querySelector("[data-role='title']").textContent = block.name;
|
|
155
|
-
this.dropdownTarget.appendChild(option);
|
|
184
|
+
groups[groupName].forEach((block) => {
|
|
185
|
+
const option =
|
|
186
|
+
this.dropdownOptionTemplateTarget.content.cloneNode(true);
|
|
187
|
+
const button = option.querySelector("button");
|
|
188
|
+
button.dataset.blockId = block.id;
|
|
189
|
+
option.querySelector("[data-role='title']").textContent =
|
|
190
|
+
block.name;
|
|
191
|
+
this.dropdownTarget.appendChild(option);
|
|
192
|
+
});
|
|
156
193
|
});
|
|
157
|
-
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// "New block" link at the bottom of the dropdown
|
|
197
|
+
if (
|
|
198
|
+
this.hasNewBlockTemplateTarget &&
|
|
199
|
+
this.hasNewUrlValue &&
|
|
200
|
+
this.newUrlValue
|
|
201
|
+
) {
|
|
202
|
+
const newBlock = this.newBlockTemplateTarget.content.cloneNode(true);
|
|
203
|
+
const link = newBlock.querySelector("[data-role='new-block-link']");
|
|
204
|
+
if (link) {
|
|
205
|
+
link.href = this.newUrlValue;
|
|
206
|
+
}
|
|
207
|
+
this.dropdownTarget.appendChild(newBlock);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reposition if dropdown is currently visible
|
|
211
|
+
if (this.dropdownTarget.style.display !== "none") {
|
|
212
|
+
this.#positionDropdown();
|
|
213
|
+
}
|
|
158
214
|
}
|
|
159
215
|
|
|
160
216
|
renderEmptyMessage() {
|
|
@@ -197,20 +253,101 @@ export default class extends Controller {
|
|
|
197
253
|
}
|
|
198
254
|
|
|
199
255
|
openDropdown() {
|
|
256
|
+
if (this.dropdownTarget.style.display === "block") {
|
|
257
|
+
// Already open, just reposition
|
|
258
|
+
this.#positionDropdown();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
200
262
|
this.dropdownTarget.style.display = "block";
|
|
201
|
-
this
|
|
202
|
-
|
|
263
|
+
this.#positionDropdown();
|
|
264
|
+
|
|
265
|
+
this.#abortController = new AbortController();
|
|
266
|
+
const { signal } = this.#abortController;
|
|
267
|
+
|
|
268
|
+
document.addEventListener("click", (e) => this.closeOnOutsideClick(e), {
|
|
269
|
+
capture: true,
|
|
270
|
+
signal,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let scrollTimeout;
|
|
274
|
+
const debouncedPosition = () => {
|
|
275
|
+
clearTimeout(scrollTimeout);
|
|
276
|
+
scrollTimeout = setTimeout(() => this.#positionDropdown(), 100);
|
|
277
|
+
};
|
|
278
|
+
window.addEventListener("scroll", debouncedPosition, {
|
|
279
|
+
capture: true,
|
|
280
|
+
signal,
|
|
281
|
+
});
|
|
282
|
+
window.addEventListener("resize", debouncedPosition, { signal });
|
|
203
283
|
}
|
|
204
284
|
|
|
205
285
|
closeDropdown() {
|
|
206
286
|
this.dropdownTarget.style.display = "none";
|
|
207
|
-
if (this.
|
|
208
|
-
|
|
209
|
-
this._outsideClickHandler = null;
|
|
287
|
+
if (this.hasSearchInputTarget) {
|
|
288
|
+
this.searchInputTarget.value = "";
|
|
210
289
|
}
|
|
290
|
+
this.#abortController?.abort();
|
|
291
|
+
this.#abortController = null;
|
|
211
292
|
}
|
|
212
293
|
|
|
213
294
|
findBlock(id) {
|
|
214
295
|
return this.blocksValue.find((b) => b.id === id);
|
|
215
296
|
}
|
|
297
|
+
|
|
298
|
+
// --- Private ---
|
|
299
|
+
|
|
300
|
+
#positionDropdown() {
|
|
301
|
+
const dropdown = this.dropdownTarget;
|
|
302
|
+
const container = dropdown.parentElement;
|
|
303
|
+
const containerRect = container.getBoundingClientRect();
|
|
304
|
+
const dropdownHeight = dropdown.offsetHeight;
|
|
305
|
+
const viewportHeight = window.innerHeight;
|
|
306
|
+
|
|
307
|
+
const spaceBelow = viewportHeight - containerRect.bottom;
|
|
308
|
+
const spaceAbove = containerRect.top;
|
|
309
|
+
|
|
310
|
+
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
|
311
|
+
// Not enough space below and more space above, show above
|
|
312
|
+
dropdown.style.bottom = "100%";
|
|
313
|
+
dropdown.style.top = "auto";
|
|
314
|
+
dropdown.style.marginBottom = "4px";
|
|
315
|
+
dropdown.style.marginTop = "0";
|
|
316
|
+
} else {
|
|
317
|
+
// Default: show below
|
|
318
|
+
dropdown.style.bottom = "auto";
|
|
319
|
+
dropdown.style.top = "100%";
|
|
320
|
+
dropdown.style.marginTop = "4px";
|
|
321
|
+
dropdown.style.marginBottom = "0";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#observeModal() {
|
|
326
|
+
const modalFrame = document.querySelector("turbo-frame[id='modal']");
|
|
327
|
+
if (!modalFrame || !this.hasBlocksDataUrlValue || !this.blocksDataUrlValue)
|
|
328
|
+
return;
|
|
329
|
+
|
|
330
|
+
this.#modalWasOpen = false;
|
|
331
|
+
this.#modalObserver = new MutationObserver(() => {
|
|
332
|
+
const hasModal = modalFrame.querySelector(".modal");
|
|
333
|
+
if (hasModal) {
|
|
334
|
+
this.#modalWasOpen = true;
|
|
335
|
+
} else if (this.#modalWasOpen) {
|
|
336
|
+
this.#modalWasOpen = false;
|
|
337
|
+
this.refreshBlocks();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
this.#modalObserver.observe(modalFrame, { childList: true });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
refreshBlocks() {
|
|
344
|
+
fetch(this.blocksDataUrlValue, {
|
|
345
|
+
headers: { Accept: "application/json" },
|
|
346
|
+
})
|
|
347
|
+
.then((response) => response.json())
|
|
348
|
+
.then((blocks) => {
|
|
349
|
+
this.blocksValue = blocks;
|
|
350
|
+
this.render();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
216
353
|
}
|
|
@@ -29,18 +29,31 @@ module Spina
|
|
|
29
29
|
def new
|
|
30
30
|
@block_templates = current_theme.try(:block_templates) || []
|
|
31
31
|
@block = Spina::Blocks::Block.new(block_template: params[:block_template])
|
|
32
|
+
@modal = params[:modal]
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def create
|
|
35
36
|
@block = Spina::Blocks::Block.new(block_params)
|
|
36
37
|
if @block.save
|
|
37
|
-
|
|
38
|
+
if params[:modal]
|
|
39
|
+
redirect_to(spina.edit_modal_blocks_admin_block_url(@block))
|
|
40
|
+
else
|
|
41
|
+
redirect_to(spina.edit_blocks_admin_block_url(@block))
|
|
42
|
+
end
|
|
38
43
|
else
|
|
39
44
|
@block_templates = current_theme.try(:block_templates) || []
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
if params[:modal]
|
|
46
|
+
@modal = true
|
|
47
|
+
render(turbo_stream: turbo_stream.update(
|
|
48
|
+
helpers.dom_id(@block, :new_block_modal_form),
|
|
49
|
+
partial: "new_block_modal_form",
|
|
50
|
+
))
|
|
51
|
+
else
|
|
52
|
+
render(turbo_stream: turbo_stream.update(
|
|
53
|
+
helpers.dom_id(@block, :new_block_form),
|
|
54
|
+
partial: "new_block_form",
|
|
55
|
+
))
|
|
56
|
+
end
|
|
44
57
|
end
|
|
45
58
|
end
|
|
46
59
|
|
|
@@ -79,6 +92,19 @@ module Spina
|
|
|
79
92
|
end
|
|
80
93
|
end
|
|
81
94
|
|
|
95
|
+
def blocks_data
|
|
96
|
+
blocks = Spina::Blocks::Block.active.sorted
|
|
97
|
+
template_titles = build_template_titles
|
|
98
|
+
render(json: blocks.map do |b|
|
|
99
|
+
{
|
|
100
|
+
id: b.id,
|
|
101
|
+
name: b.name,
|
|
102
|
+
templateName: b.block_template.to_s,
|
|
103
|
+
templateTitle: template_titles[b.block_template.to_s] || b.block_template.to_s.titleize,
|
|
104
|
+
}
|
|
105
|
+
end)
|
|
106
|
+
end
|
|
107
|
+
|
|
82
108
|
def sort
|
|
83
109
|
params[:ids].each.with_index do |id, index|
|
|
84
110
|
Spina::Blocks::Block.where(id: id).update_all(position: index + 1)
|
|
@@ -108,6 +134,14 @@ module Spina
|
|
|
108
134
|
@tabs = ["block_content", "block_settings"]
|
|
109
135
|
end
|
|
110
136
|
|
|
137
|
+
def build_template_titles
|
|
138
|
+
titles = {}
|
|
139
|
+
(current_theme.try(:block_templates) || []).each do |bt|
|
|
140
|
+
titles[bt[:name].to_s] = bt[:title].to_s
|
|
141
|
+
end
|
|
142
|
+
titles
|
|
143
|
+
end
|
|
144
|
+
|
|
111
145
|
def block_params
|
|
112
146
|
params.require(:block).permit!
|
|
113
147
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spina
|
|
4
|
+
module Parts
|
|
5
|
+
module BlockFilterable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
def available_blocks
|
|
9
|
+
scope = ::Spina::Blocks::Block.active.sorted
|
|
10
|
+
opts = options.is_a?(Hash) ? options.with_indifferent_access : nil
|
|
11
|
+
if opts&.dig(:block_template).present?
|
|
12
|
+
scope = scope.where(block_template: opts[:block_template])
|
|
13
|
+
end
|
|
14
|
+
scope
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%
|
|
2
|
-
blocks =
|
|
2
|
+
blocks = f.object.available_blocks
|
|
3
3
|
selected_ids = f.object.block_ids || []
|
|
4
4
|
field_name = "#{f.object_name}[block_ids][]"
|
|
5
5
|
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
data-block-collection-blocks-value="<%= blocks_json.to_json %>"
|
|
28
28
|
data-block-collection-selected-ids-value="<%= selected_ids.to_json %>"
|
|
29
29
|
data-block-collection-edit-url-value="<%= spina.edit_modal_blocks_admin_block_path("__ID__") %>"
|
|
30
|
+
data-block-collection-new-url-value="<%= spina.new_blocks_admin_block_path(modal: 1) %>"
|
|
31
|
+
data-block-collection-blocks-data-url-value="<%= spina.blocks_data_blocks_admin_blocks_path(format: :json) %>"
|
|
30
32
|
data-field-name="<%= field_name %>">
|
|
31
33
|
|
|
32
34
|
<label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
|
|
@@ -34,11 +36,26 @@
|
|
|
34
36
|
<div class="text-gray-400 text-sm mb-2"><%= f.object.hint %></div>
|
|
35
37
|
<% end %>
|
|
36
38
|
|
|
39
|
+
<%# Search input + dropdown %>
|
|
40
|
+
<div class="relative mb-3">
|
|
41
|
+
<input type="text"
|
|
42
|
+
data-block-collection-target="searchInput"
|
|
43
|
+
data-action="input->block-collection#onSearchInput focus->block-collection#onSearchFocus"
|
|
44
|
+
placeholder="Search or add block..."
|
|
45
|
+
autocomplete="off"
|
|
46
|
+
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-spina focus:ring-spina" />
|
|
47
|
+
|
|
48
|
+
<div data-block-collection-target="dropdown"
|
|
49
|
+
style="display:none"
|
|
50
|
+
class="absolute z-10 w-full bg-white border border-gray-200 rounded-md shadow-lg py-1 max-h-60 overflow-y-auto">
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
37
54
|
<%# Empty state message %>
|
|
38
55
|
<div data-block-collection-target="emptyMessage"
|
|
39
56
|
class="text-sm text-gray-400 italic py-3"
|
|
40
57
|
style="<%= selected_ids.any? ? 'display:none' : '' %>">
|
|
41
|
-
No blocks selected.
|
|
58
|
+
No blocks selected. Use the search field above to add blocks.
|
|
42
59
|
</div>
|
|
43
60
|
|
|
44
61
|
<%# Sortable list of selected blocks (rendered by Stimulus) %>
|
|
@@ -47,24 +64,6 @@
|
|
|
47
64
|
<%# Hidden fields container (rendered by Stimulus) %>
|
|
48
65
|
<div data-block-collection-target="hiddenFields"></div>
|
|
49
66
|
|
|
50
|
-
<%# Add block button + dropdown %>
|
|
51
|
-
<div class="relative inline-block">
|
|
52
|
-
<button type="button"
|
|
53
|
-
data-action="block-collection#toggleDropdown"
|
|
54
|
-
data-block-collection-target="addButton"
|
|
55
|
-
class="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-spina">
|
|
56
|
-
<svg class="w-4 h-4 mr-1.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
57
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
58
|
-
</svg>
|
|
59
|
-
Add block
|
|
60
|
-
</button>
|
|
61
|
-
|
|
62
|
-
<div data-block-collection-target="dropdown"
|
|
63
|
-
style="display:none"
|
|
64
|
-
class="absolute z-10 mt-1 w-64 bg-white border border-gray-200 rounded-md shadow-lg py-1 max-h-60 overflow-y-auto">
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
67
|
<%# ===== Templates for Stimulus controller ===== %>
|
|
69
68
|
|
|
70
69
|
<%# Selected block list item %>
|
|
@@ -110,6 +109,19 @@
|
|
|
110
109
|
|
|
111
110
|
<%# Dropdown empty state %>
|
|
112
111
|
<template data-block-collection-target="dropdownEmptyTemplate">
|
|
113
|
-
<div class="px-3 py-2 text-sm text-gray-400 italic">
|
|
112
|
+
<div class="px-3 py-2 text-sm text-gray-400 italic">No blocks found</div>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<%# Dropdown "New block" link %>
|
|
116
|
+
<template data-block-collection-target="newBlockTemplate">
|
|
117
|
+
<div class="border-t border-gray-200 mt-1 pt-1">
|
|
118
|
+
<a class="w-full text-left px-3 py-2 text-sm text-spina-primary hover:bg-gray-100 flex items-center font-medium"
|
|
119
|
+
href="" data-turbo-frame="modal" data-role="new-block-link">
|
|
120
|
+
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
121
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
122
|
+
</svg>
|
|
123
|
+
<%= t('spina.blocks.new') %>
|
|
124
|
+
</a>
|
|
125
|
+
</div>
|
|
114
126
|
</template>
|
|
115
127
|
</div>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="text-gray-400 text-sm"><%= f.object.hint %></div>
|
|
5
5
|
<% end %>
|
|
6
6
|
<div class="mt-1">
|
|
7
|
-
<% blocks =
|
|
7
|
+
<% blocks = f.object.available_blocks %>
|
|
8
8
|
<%= f.select :block_id,
|
|
9
9
|
blocks.map { |b| [b.name, b.id] },
|
|
10
10
|
{include_blank: t('spina.blocks.select_block')},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%# Modal variant of the new block form. Used when creating a block from BlockCollection.
|
|
2
|
+
After successful creation, the modal transitions to the block edit form.
|
|
3
|
+
Expects: @block, @block_templates %>
|
|
4
|
+
<%= turbo_frame_tag dom_id(@block, :new_block_modal_form) do %>
|
|
5
|
+
<%= form_with model: @block, url: spina.blocks_admin_blocks_path, data: { turbo_frame: "modal" } do |f| %>
|
|
6
|
+
<%= hidden_field_tag :modal, 1 %>
|
|
7
|
+
|
|
8
|
+
<div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
9
|
+
<div class="sm:flex sm:items-start">
|
|
10
|
+
<div class="w-full">
|
|
11
|
+
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
|
12
|
+
<%= t('spina.blocks.new') %>
|
|
13
|
+
</h3>
|
|
14
|
+
|
|
15
|
+
<div class="mt-4">
|
|
16
|
+
<label for="block_block_template" class="block text-sm font-medium text-gray-700 mb-1">
|
|
17
|
+
<%= t('spina.blocks.block_type') %>
|
|
18
|
+
</label>
|
|
19
|
+
<%= f.select :block_template,
|
|
20
|
+
@block_templates.map { |bt| [bt[:title], bt[:name]] },
|
|
21
|
+
{ include_blank: t('spina.blocks.select_block_type') },
|
|
22
|
+
class: "block w-full rounded-md border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 text-sm",
|
|
23
|
+
required: true %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="mt-3">
|
|
27
|
+
<%= render Spina::Forms::TextFieldComponent.new(f, :name, size: 'lg', autofocus: true) %>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="px-4 pb-5 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
33
|
+
<button type="submit" class="btn btn-primary w-full sm:w-auto sm:ml-3">
|
|
34
|
+
<%= t('spina.blocks.create') %>
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
<button type="button" class="btn btn-default w-full sm:w-auto mt-2 sm:mt-0" data-action="modal#close">
|
|
38
|
+
<%= t('spina.ui.cancel') %>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% end %>
|
data/config/routes.rb
CHANGED
data/lib/spina/blocks/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spina-blocks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Kanashchuk
|
|
@@ -94,6 +94,7 @@ files:
|
|
|
94
94
|
- app/controllers/spina/blocks/admin/categories_controller.rb
|
|
95
95
|
- app/controllers/spina/blocks/admin/page_blocks_controller.rb
|
|
96
96
|
- app/helpers/spina/blocks/blocks_helper.rb
|
|
97
|
+
- app/models/concerns/spina/parts/block_filterable.rb
|
|
97
98
|
- app/models/spina/blocks/account_extension.rb
|
|
98
99
|
- app/models/spina/blocks/application_record.rb
|
|
99
100
|
- app/models/spina/blocks/block.rb
|
|
@@ -114,6 +115,7 @@ files:
|
|
|
114
115
|
- app/views/spina/blocks/admin/blocks/_form_block_settings.html.erb
|
|
115
116
|
- app/views/spina/blocks/admin/blocks/_modal_form.html.erb
|
|
116
117
|
- app/views/spina/blocks/admin/blocks/_new_block_form.html.erb
|
|
118
|
+
- app/views/spina/blocks/admin/blocks/_new_block_modal_form.html.erb
|
|
117
119
|
- app/views/spina/blocks/admin/blocks/edit.html.erb
|
|
118
120
|
- app/views/spina/blocks/admin/blocks/edit_content.html.erb
|
|
119
121
|
- app/views/spina/blocks/admin/blocks/edit_modal.html.erb
|