spina-blocks 0.1.1 → 0.1.2
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/app/assets/javascripts/spina/controllers/block_collection_controller.js +209 -0
- data/app/controllers/spina/blocks/admin/blocks_controller.rb +2 -0
- data/app/models/spina/parts/block_collection.rb +5 -0
- data/app/views/spina/admin/parts/block_collections/_form.html.erb +102 -18
- data/app/views/spina/blocks/admin/blocks/_new_block_form.html.erb +11 -3
- data/app/views/spina/blocks/admin/blocks/index.html.erb +50 -35
- data/config/locales/en.yml +2 -0
- data/lib/spina/blocks/engine.rb +12 -0
- data/lib/spina/blocks/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8080a1b60cf27e9ce3dedd84b24c0eea3bd9059071f5a8983e09281168ab701a
|
|
4
|
+
data.tar.gz: 4e4e314f77217684b7608810db9c3c98db9955cdcb30dda6a1b3480724f57ae6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d958eaac09962f67d5cf861e27123da4b43e26332533e06575e8adc6ec32e0cbf4855ace4dfc8f9ecc549de3ad712a36b46f60e1e392a7623539dc6c515b9e0b
|
|
7
|
+
data.tar.gz: 1e4bf92608f2ab86fc28de5ea7e4f220b46d987a4914901dc925d80392136532e8cf43632102599aa0613a24e7e2b9fad8e72d6d8838d8b9a9e425d53e3f1fa6
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import Sortable from "libraries/sortablejs";
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static get targets() {
|
|
6
|
+
return [
|
|
7
|
+
"list",
|
|
8
|
+
"hiddenFields",
|
|
9
|
+
"dropdown",
|
|
10
|
+
"addButton",
|
|
11
|
+
"emptyMessage",
|
|
12
|
+
"listItemTemplate",
|
|
13
|
+
"groupHeaderTemplate",
|
|
14
|
+
"dropdownOptionTemplate",
|
|
15
|
+
"dropdownEmptyTemplate",
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static get values() {
|
|
20
|
+
return {
|
|
21
|
+
blocks: Array, // [{id, title, templateName, templateTitle}]
|
|
22
|
+
selectedIds: Array, // [id, id, ...]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connect() {
|
|
27
|
+
// Sanitize: filter out any null/NaN/0 values from previously corrupted data
|
|
28
|
+
this.selectedIdsValue = this.selectedIdsValue.filter((id) => {
|
|
29
|
+
return id !== null && id !== undefined && !isNaN(id) && id > 0;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.sortable = Sortable.create(this.listTarget, {
|
|
33
|
+
handle: "[data-sortable-handle]",
|
|
34
|
+
animation: 150,
|
|
35
|
+
onEnd: this.reorderHiddenFields.bind(this),
|
|
36
|
+
});
|
|
37
|
+
this.render();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnect() {
|
|
41
|
+
if (this.sortable) this.sortable.destroy();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Actions ---
|
|
45
|
+
|
|
46
|
+
add(event) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
const id = parseInt(event.currentTarget.dataset.blockId);
|
|
49
|
+
if (this.selectedIdsValue.indexOf(id) !== -1) return;
|
|
50
|
+
|
|
51
|
+
this.selectedIdsValue = this.selectedIdsValue.concat([id]);
|
|
52
|
+
this.render();
|
|
53
|
+
this.closeDropdown();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
remove(event) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
const id = parseInt(event.currentTarget.dataset.blockId);
|
|
59
|
+
this.selectedIdsValue = this.selectedIdsValue.filter((sid) => sid !== id);
|
|
60
|
+
this.render();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toggleDropdown(event) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
const dropdown = this.dropdownTarget;
|
|
67
|
+
if (dropdown.style.display === "none" || dropdown.style.display === "") {
|
|
68
|
+
this.openDropdown();
|
|
69
|
+
} else {
|
|
70
|
+
this.closeDropdown();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
closeOnOutsideClick(event) {
|
|
75
|
+
if (!this.element.contains(event.target)) {
|
|
76
|
+
this.closeDropdown();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Rendering ---
|
|
81
|
+
|
|
82
|
+
render() {
|
|
83
|
+
this.renderList();
|
|
84
|
+
this.renderHiddenFields();
|
|
85
|
+
this.renderDropdown();
|
|
86
|
+
this.renderEmptyMessage();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
renderList() {
|
|
90
|
+
this.listTarget.innerHTML = "";
|
|
91
|
+
this.selectedIdsValue.forEach((id) => {
|
|
92
|
+
const block = this.findBlock(id);
|
|
93
|
+
if (!block) return;
|
|
94
|
+
this.listTarget.appendChild(this.buildListItem(block));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
renderHiddenFields() {
|
|
99
|
+
const fieldName = this.element.dataset.fieldName;
|
|
100
|
+
this.hiddenFieldsTarget.innerHTML = "";
|
|
101
|
+
if (this.selectedIdsValue.length === 0) {
|
|
102
|
+
// Empty sentinel: ensures the parameter is sent so Rails clears the array
|
|
103
|
+
const input = document.createElement("input");
|
|
104
|
+
input.type = "hidden";
|
|
105
|
+
input.name = fieldName;
|
|
106
|
+
input.value = "";
|
|
107
|
+
this.hiddenFieldsTarget.appendChild(input);
|
|
108
|
+
} else {
|
|
109
|
+
this.selectedIdsValue.forEach((id) => {
|
|
110
|
+
const input = document.createElement("input");
|
|
111
|
+
input.type = "hidden";
|
|
112
|
+
input.name = fieldName;
|
|
113
|
+
input.value = id;
|
|
114
|
+
this.hiddenFieldsTarget.appendChild(input);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
renderDropdown() {
|
|
120
|
+
const availableBlocks = this.blocksValue.filter((b) => {
|
|
121
|
+
return this.selectedIdsValue.indexOf(b.id) === -1;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.dropdownTarget.innerHTML = "";
|
|
125
|
+
|
|
126
|
+
if (availableBlocks.length === 0) {
|
|
127
|
+
const empty = this.dropdownEmptyTemplateTarget.content.cloneNode(true);
|
|
128
|
+
this.dropdownTarget.appendChild(empty);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Group by templateTitle
|
|
133
|
+
const groups = {};
|
|
134
|
+
availableBlocks.forEach((b) => {
|
|
135
|
+
const key = b.templateTitle || b.templateName || "Other";
|
|
136
|
+
if (!groups[key]) groups[key] = [];
|
|
137
|
+
groups[key].push(b);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
Object.keys(groups)
|
|
141
|
+
.sort()
|
|
142
|
+
.forEach((groupName) => {
|
|
143
|
+
const header = this.groupHeaderTemplateTarget.content.cloneNode(true);
|
|
144
|
+
header.querySelector("[data-role='group-name']").textContent =
|
|
145
|
+
groupName;
|
|
146
|
+
this.dropdownTarget.appendChild(header);
|
|
147
|
+
|
|
148
|
+
groups[groupName].forEach((block) => {
|
|
149
|
+
const option =
|
|
150
|
+
this.dropdownOptionTemplateTarget.content.cloneNode(true);
|
|
151
|
+
const button = option.querySelector("button");
|
|
152
|
+
button.dataset.blockId = block.id;
|
|
153
|
+
option.querySelector("[data-role='title']").textContent = block.title;
|
|
154
|
+
this.dropdownTarget.appendChild(option);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
renderEmptyMessage() {
|
|
160
|
+
if (this.hasEmptyMessageTarget) {
|
|
161
|
+
this.emptyMessageTarget.style.display =
|
|
162
|
+
this.selectedIdsValue.length === 0 ? "" : "none";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Helpers ---
|
|
167
|
+
|
|
168
|
+
buildListItem(block) {
|
|
169
|
+
const fragment = this.listItemTemplateTarget.content.cloneNode(true);
|
|
170
|
+
const root = fragment.querySelector("[data-block-id]");
|
|
171
|
+
root.dataset.blockId = block.id;
|
|
172
|
+
fragment.querySelector("[data-role='title']").textContent = block.title;
|
|
173
|
+
fragment.querySelector("[data-role='template-label']").textContent =
|
|
174
|
+
"(" + (block.templateTitle || block.templateName) + ")";
|
|
175
|
+
fragment.querySelector(
|
|
176
|
+
"[data-action='block-collection#remove']",
|
|
177
|
+
).dataset.blockId = block.id;
|
|
178
|
+
return fragment;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
reorderHiddenFields() {
|
|
182
|
+
// :scope > selects only direct children, not nested buttons that also have data-block-id
|
|
183
|
+
const items = this.listTarget.querySelectorAll(":scope > [data-block-id]");
|
|
184
|
+
const ids = [];
|
|
185
|
+
items.forEach((el) => {
|
|
186
|
+
ids.push(parseInt(el.dataset.blockId));
|
|
187
|
+
});
|
|
188
|
+
this.selectedIdsValue = ids;
|
|
189
|
+
this.renderHiddenFields();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
openDropdown() {
|
|
193
|
+
this.dropdownTarget.style.display = "block";
|
|
194
|
+
this._outsideClickHandler = this.closeOnOutsideClick.bind(this);
|
|
195
|
+
document.addEventListener("click", this._outsideClickHandler, true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
closeDropdown() {
|
|
199
|
+
this.dropdownTarget.style.display = "none";
|
|
200
|
+
if (this._outsideClickHandler) {
|
|
201
|
+
document.removeEventListener("click", this._outsideClickHandler, true);
|
|
202
|
+
this._outsideClickHandler = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
findBlock(id) {
|
|
207
|
+
return this.blocksValue.find((b) => b.id === id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -27,6 +27,7 @@ module Spina
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def new
|
|
30
|
+
@block_templates = current_theme.try(:block_templates) || []
|
|
30
31
|
@block = Spina::Blocks::Block.new(block_template: params[:block_template])
|
|
31
32
|
end
|
|
32
33
|
|
|
@@ -35,6 +36,7 @@ module Spina
|
|
|
35
36
|
if @block.save
|
|
36
37
|
redirect_to spina.edit_blocks_admin_block_url(@block)
|
|
37
38
|
else
|
|
39
|
+
@block_templates = current_theme.try(:block_templates) || []
|
|
38
40
|
render turbo_stream: turbo_stream.update(
|
|
39
41
|
helpers.dom_id(@block, :new_block_form),
|
|
40
42
|
partial: 'new_block_form'
|
|
@@ -7,6 +7,11 @@ module Spina
|
|
|
7
7
|
|
|
8
8
|
attr_accessor :options
|
|
9
9
|
|
|
10
|
+
# Defense: strip nils, zeros, and blanks that may sneak in from form submissions
|
|
11
|
+
def block_ids=(value)
|
|
12
|
+
super(Array(value).reject(&:blank?).map(&:to_i).reject(&:zero?).uniq)
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
def content
|
|
11
16
|
return [] if block_ids.blank?
|
|
12
17
|
|
|
@@ -1,24 +1,108 @@
|
|
|
1
|
-
|
|
1
|
+
<%
|
|
2
|
+
blocks = Spina::Blocks::Block.active.sorted
|
|
3
|
+
selected_ids = f.object.block_ids || []
|
|
4
|
+
field_name = "#{f.object_name}[block_ids][]"
|
|
5
|
+
|
|
6
|
+
# Build block_templates title lookup from current theme
|
|
7
|
+
template_titles = {}
|
|
8
|
+
if defined?(current_theme) && current_theme.respond_to?(:block_templates)
|
|
9
|
+
current_theme.block_templates.each do |bt|
|
|
10
|
+
template_titles[bt[:name].to_s] = bt[:title].to_s
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Build JSON data for Stimulus controller
|
|
15
|
+
blocks_json = blocks.map { |b|
|
|
16
|
+
{
|
|
17
|
+
id: b.id,
|
|
18
|
+
title: b.title,
|
|
19
|
+
templateName: b.block_template.to_s,
|
|
20
|
+
templateTitle: template_titles[b.block_template.to_s] || b.block_template.to_s.titleize
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
%>
|
|
24
|
+
|
|
25
|
+
<div class="mt-6"
|
|
26
|
+
data-controller="block-collection"
|
|
27
|
+
data-block-collection-blocks-value="<%= blocks_json.to_json %>"
|
|
28
|
+
data-block-collection-selected-ids-value="<%= selected_ids.to_json %>"
|
|
29
|
+
data-field-name="<%= field_name %>">
|
|
30
|
+
|
|
2
31
|
<label class="block text-sm leading-5 font-medium text-gray-700"><%= f.object.title %></label>
|
|
3
32
|
<% if f.object.hint.present? %>
|
|
4
|
-
<div class="text-gray-400 text-sm"><%= f.object.hint %></div>
|
|
33
|
+
<div class="text-gray-400 text-sm mb-2"><%= f.object.hint %></div>
|
|
5
34
|
<% end %>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
|
|
36
|
+
<%# Empty state message %>
|
|
37
|
+
<div data-block-collection-target="emptyMessage"
|
|
38
|
+
class="text-sm text-gray-400 italic py-3"
|
|
39
|
+
style="<%= selected_ids.any? ? 'display:none' : '' %>">
|
|
40
|
+
No blocks selected. Click “Add block” to get started.
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<%# Sortable list of selected blocks (rendered by Stimulus) %>
|
|
44
|
+
<div data-block-collection-target="list" class="space-y-1 mb-3"></div>
|
|
45
|
+
|
|
46
|
+
<%# Hidden fields container (rendered by Stimulus) %>
|
|
47
|
+
<div data-block-collection-target="hiddenFields"></div>
|
|
48
|
+
|
|
49
|
+
<%# Add block button + dropdown %>
|
|
50
|
+
<div class="relative inline-block">
|
|
51
|
+
<button type="button"
|
|
52
|
+
data-action="block-collection#toggleDropdown"
|
|
53
|
+
data-block-collection-target="addButton"
|
|
54
|
+
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">
|
|
55
|
+
<svg class="w-4 h-4 mr-1.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
56
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
|
57
|
+
</svg>
|
|
58
|
+
Add block
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<div data-block-collection-target="dropdown"
|
|
62
|
+
style="display:none"
|
|
63
|
+
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">
|
|
21
64
|
</div>
|
|
22
|
-
<input type="hidden" name="<%= f.object_name %>[block_ids][]" value="">
|
|
23
65
|
</div>
|
|
66
|
+
|
|
67
|
+
<%# ===== Templates for Stimulus controller ===== %>
|
|
68
|
+
|
|
69
|
+
<%# Selected block list item %>
|
|
70
|
+
<template data-block-collection-target="listItemTemplate">
|
|
71
|
+
<div class="flex items-center bg-white border border-gray-200 rounded-md px-3 py-2 shadow-sm group" data-block-id="">
|
|
72
|
+
<button type="button" data-sortable-handle class="mr-2 text-gray-300 hover:text-gray-500 cursor-grab" title="Drag to reorder">
|
|
73
|
+
<svg class="w-4 h-4" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
74
|
+
<path d="M432 288H16c-8.8 0-16 7.2-16 16v16c0 8.8 7.2 16 16 16h416c8.8 0 16-7.2 16-16v-16c0-8.8-7.2-16-16-16zm0-112H16c-8.8 0-16 7.2-16 16v16c0 8.8 7.2 16 16 16h416c8.8 0 16-7.2 16-16v-16c0-8.8-7.2-16-16-16z"/>
|
|
75
|
+
</svg>
|
|
76
|
+
</button>
|
|
77
|
+
<div class="flex-1 min-w-0">
|
|
78
|
+
<span class="text-sm font-medium text-gray-800 truncate" data-role="title"></span>
|
|
79
|
+
<span class="ml-2 text-xs text-gray-400" data-role="template-label"></span>
|
|
80
|
+
</div>
|
|
81
|
+
<button type="button" class="ml-2 text-gray-300 hover:text-red-500 transition-colors"
|
|
82
|
+
data-action="block-collection#remove" data-block-id="" title="Remove">
|
|
83
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
85
|
+
</svg>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<%# Dropdown group header %>
|
|
91
|
+
<template data-block-collection-target="groupHeaderTemplate">
|
|
92
|
+
<div class="px-3 py-1.5 text-xs font-semibold text-gray-400 uppercase tracking-wider" data-role="group-name"></div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<%# Dropdown option (available block) %>
|
|
96
|
+
<template data-block-collection-target="dropdownOptionTemplate">
|
|
97
|
+
<button type="button"
|
|
98
|
+
class="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
|
|
99
|
+
data-action="block-collection#add" data-block-id="">
|
|
100
|
+
<span data-role="title"></span>
|
|
101
|
+
</button>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<%# Dropdown empty state %>
|
|
105
|
+
<template data-block-collection-target="dropdownEmptyTemplate">
|
|
106
|
+
<div class="px-3 py-2 text-sm text-gray-400 italic">All blocks added</div>
|
|
107
|
+
</template>
|
|
24
108
|
</div>
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
<%= turbo_frame_tag dom_id(@block, :new_block_form) do %>
|
|
2
2
|
<%= form_with model: @block, url: spina.blocks_admin_blocks_path, data: {turbo_frame: "_top"} do |f| %>
|
|
3
|
-
<%= f.hidden_field :block_template %>
|
|
4
|
-
|
|
5
3
|
<div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
6
4
|
<div class="sm:flex sm:items-start">
|
|
7
5
|
<div class="w-full">
|
|
8
6
|
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
|
|
9
7
|
<%= t('spina.blocks.new') %>
|
|
10
|
-
<span class="text-gray-400 text-sm">(<%= f.object.block_template %>)</span>
|
|
11
8
|
</h3>
|
|
12
9
|
|
|
10
|
+
<div class="mt-4">
|
|
11
|
+
<label for="block_block_template" class="block text-sm font-medium text-gray-700 mb-1">
|
|
12
|
+
<%= t('spina.blocks.block_type') %>
|
|
13
|
+
</label>
|
|
14
|
+
<%= f.select :block_template,
|
|
15
|
+
@block_templates.map { |bt| [bt[:title], bt[:name]] },
|
|
16
|
+
{ include_blank: t('spina.blocks.select_block_type') },
|
|
17
|
+
class: "block w-full rounded-md border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 text-sm",
|
|
18
|
+
required: true %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
13
21
|
<div class="mt-3">
|
|
14
22
|
<%= render Spina::Forms::TextFieldComponent.new(f, :title, size: 'lg', autofocus: true) %>
|
|
15
23
|
</div>
|
|
@@ -2,48 +2,63 @@
|
|
|
2
2
|
<% header.with_actions do %>
|
|
3
3
|
<% block_templates = current_theme.try(:block_templates) || [] %>
|
|
4
4
|
<% if block_templates.any? %>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
<%= link_to spina.new_blocks_admin_block_path,
|
|
6
|
+
class: "btn btn-primary",
|
|
7
|
+
data: {turbo_frame: "modal"} do %>
|
|
8
|
+
<%= heroicon('plus', style: :mini, class: 'w-4 h-4 mr-1 -ml-1') %>
|
|
9
|
+
<%= t('spina.blocks.new') %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<% header.with_navigation do %>
|
|
15
|
+
<% if @block_templates.any? %>
|
|
16
|
+
<nav class="-mb-1 md:-mb-3 mt-4">
|
|
17
|
+
<div class="relative inline-block" data-controller="reveal" data-reveal-away-value>
|
|
18
|
+
<button type="button"
|
|
19
|
+
class="inline-flex items-center px-3 py-1.5 rounded-md text-sm font-medium bg-white border border-gray-300 text-gray-700 shadow-xs hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
20
|
+
data-action="reveal#toggle">
|
|
21
|
+
<% if @current_block_template.present? %>
|
|
22
|
+
<% current_bt = @block_templates.find { |bt| bt[:name].to_s == @current_block_template } %>
|
|
23
|
+
<%= current_bt ? current_bt[:title] : @current_block_template %>
|
|
24
|
+
<% else %>
|
|
25
|
+
<%= heroicon('squares-2x2', class: 'h-4 w-4 mr-1.5 -ml-0.5 opacity-75') %>
|
|
26
|
+
<%= t('spina.blocks.all') %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<%= heroicon('chevron-down', style: :mini, class: 'w-4 h-4 ml-1.5 -mr-0.5 opacity-50') %>
|
|
29
|
+
</button>
|
|
10
30
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<%=
|
|
18
|
-
<% if
|
|
19
|
-
|
|
31
|
+
<div hidden data-reveal data-transition class="origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg border border-gray-200 z-30">
|
|
32
|
+
<div class="rounded-md bg-white shadow-xs py-1">
|
|
33
|
+
<%= link_to spina.blocks_admin_blocks_path,
|
|
34
|
+
class: "flex items-center px-4 py-2 text-sm font-medium leading-5 #{@current_block_template.nil? ? 'text-blue-600 bg-blue-50' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'}",
|
|
35
|
+
data: {action: "reveal#hide"} do %>
|
|
36
|
+
<%= heroicon('squares-2x2', class: 'h-4 w-4 mr-2 opacity-75') %>
|
|
37
|
+
<%= t('spina.blocks.all') %>
|
|
38
|
+
<% if @current_block_template.nil? %>
|
|
39
|
+
<%= heroicon('check', style: :mini, class: 'w-4 h-4 ml-auto opacity-75') %>
|
|
20
40
|
<% end %>
|
|
21
41
|
<% end %>
|
|
22
|
-
|
|
42
|
+
|
|
43
|
+
<div class="border-t border-gray-100 my-1"></div>
|
|
44
|
+
|
|
45
|
+
<% @block_templates.each do |bt| %>
|
|
46
|
+
<% active = @current_block_template == bt[:name].to_s %>
|
|
47
|
+
<%= link_to spina.blocks_admin_blocks_path(block_template: bt[:name]),
|
|
48
|
+
class: "flex items-center px-4 py-2 text-sm font-medium leading-5 #{active ? 'text-blue-600 bg-blue-50' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'}",
|
|
49
|
+
data: {action: "reveal#hide"} do %>
|
|
50
|
+
<span class="flex-1"><%= bt[:title] %></span>
|
|
51
|
+
<% if active %>
|
|
52
|
+
<%= heroicon('check', style: :mini, class: 'w-4 h-4 ml-2 opacity-75') %>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
23
57
|
</div>
|
|
24
58
|
</div>
|
|
25
|
-
</
|
|
59
|
+
</nav>
|
|
26
60
|
<% end %>
|
|
27
61
|
<% end %>
|
|
28
|
-
|
|
29
|
-
<% header.with_navigation do %>
|
|
30
|
-
<nav class="-mb-1 md:-mb-3 mt-4">
|
|
31
|
-
<ul class="inline-flex flex-wrap w-auto rounded-md bg-white">
|
|
32
|
-
<%= render Spina::UserInterface::TabLinkComponent.new(spina.blocks_admin_blocks_path, active: @current_block_template.nil?) do %>
|
|
33
|
-
<%= heroicon('squares-2x2', class: 'h-4 w-4 mr-1 -ml-1 opacity-75') %>
|
|
34
|
-
<%= t('spina.blocks.all') %>
|
|
35
|
-
<% end %>
|
|
36
|
-
|
|
37
|
-
<% @block_templates.each do |bt| %>
|
|
38
|
-
<%= render Spina::UserInterface::TabLinkComponent.new(
|
|
39
|
-
spina.blocks_admin_blocks_path(block_template: bt[:name]),
|
|
40
|
-
active: @current_block_template == bt[:name].to_s) do %>
|
|
41
|
-
<%= bt[:title] %>
|
|
42
|
-
<% end %>
|
|
43
|
-
<% end %>
|
|
44
|
-
</ul>
|
|
45
|
-
</nav>
|
|
46
|
-
<% end %>
|
|
47
62
|
<% end %>
|
|
48
63
|
|
|
49
64
|
<% if @blocks.any? %>
|
data/config/locales/en.yml
CHANGED
|
@@ -17,6 +17,8 @@ en:
|
|
|
17
17
|
category: "Category"
|
|
18
18
|
no_category: "— No category —"
|
|
19
19
|
select_block: "— Select block —"
|
|
20
|
+
block_type: "Block type"
|
|
21
|
+
select_block_type: "— Select block type —"
|
|
20
22
|
no_blocks_yet: "No blocks yet. Create your first block to get started."
|
|
21
23
|
inactive: "Inactive"
|
|
22
24
|
uncategorized: "Uncategorized"
|
data/lib/spina/blocks/engine.rb
CHANGED
|
@@ -34,6 +34,17 @@ module Spina
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
initializer 'spina.blocks.importmap', before: 'spina.blocks.register_parts' do
|
|
38
|
+
Spina.config.importmap.draw do
|
|
39
|
+
pin_all_from Spina::Blocks::Engine.root.join('app/assets/javascripts/spina/controllers'),
|
|
40
|
+
under: 'controllers', to: 'spina/controllers'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
initializer 'spina.blocks.assets.precompile' do |app|
|
|
45
|
+
app.config.assets.precompile += %w[spina/controllers/block_collection_controller.js] if defined?(Sprockets)
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
initializer 'spina.blocks.register_parts' do
|
|
38
49
|
config.to_prepare do
|
|
39
50
|
::Spina::Part.register(
|
|
@@ -54,6 +65,7 @@ module Spina
|
|
|
54
65
|
initializer 'spina.blocks.tailwind_content' do
|
|
55
66
|
::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/views/**/*.*"
|
|
56
67
|
::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/helpers/**/*.*"
|
|
68
|
+
::Spina.config.tailwind_content << "#{Spina::Blocks::Engine.root}/app/assets/javascripts/**/*.js"
|
|
57
69
|
end
|
|
58
70
|
|
|
59
71
|
initializer 'spina.blocks.i18n' do
|
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.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Kanashchuk
|
|
@@ -34,6 +34,7 @@ extra_rdoc_files: []
|
|
|
34
34
|
files:
|
|
35
35
|
- README.md
|
|
36
36
|
- Rakefile
|
|
37
|
+
- app/assets/javascripts/spina/controllers/block_collection_controller.js
|
|
37
38
|
- app/controllers/spina/blocks/admin/blocks_controller.rb
|
|
38
39
|
- app/controllers/spina/blocks/admin/categories_controller.rb
|
|
39
40
|
- app/controllers/spina/blocks/admin/page_blocks_controller.rb
|