macron-cms 3.0.0 → 5.0.0
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 +338 -25
- data/app/assets/stylesheets/macron/cms/application.css +1 -1
- data/app/controllers/concerns/macron/cms/api_serialization.rb +79 -0
- data/app/controllers/concerns/macron/cms/slug_redirectable.rb +27 -0
- data/app/controllers/macron/cms/api/v1/article_contents_controller.rb +221 -0
- data/app/controllers/macron/cms/api/v1/articles_controller.rb +140 -66
- data/app/controllers/macron/cms/api/v1/content_types_controller.rb +3 -1
- data/app/controllers/macron/cms/article_contents_controller.rb +44 -10
- data/app/controllers/macron/cms/articles_controller.rb +4 -3
- data/app/controllers/macron/cms/content_types_controller.rb +2 -0
- data/app/controllers/macron/cms/dashboard_controller.rb +1 -1
- data/app/helpers/macron/cms/content_helper.rb +1 -1
- data/app/helpers/macron/cms/fields_helper.rb +14 -0
- data/app/models/macron/cms/article.rb +45 -15
- data/app/models/macron/cms/article_content.rb +24 -0
- data/app/models/macron/cms/content.rb +27 -0
- data/app/services/macron/cms/inline_asset_attacher.rb +70 -0
- data/app/services/macron/cms/remote_asset_attacher.rb +16 -6
- data/app/views/macron/cms/articles/_content_blocks_section.html.erb +494 -199
- data/app/views/macron/cms/articles/_form.html.erb +10 -2
- data/app/views/macron/cms/articles/_table.html.erb +3 -3
- data/app/views/macron/cms/articles/show.html.erb +3 -1
- data/app/views/macron/cms/content_types/index.html.erb +30 -0
- data/app/views/macron/cms/content_types/show.html.erb +34 -0
- data/config/routes.rb +12 -1
- data/db/migrate/20261029000001_add_convertible_to_to_macron_cms_contents.rb +5 -0
- data/db/migrate/20261029120001_add_featured_image_to_macron_cms_contents.rb +5 -0
- data/db/migrate/20261029120002_add_featured_image_blob_to_macron_cms_articles.rb +8 -0
- data/db/migrate/20261029140000_create_friendly_id_slugs.rb +26 -0
- data/db/migrate/20261029150000_remove_name_from_macron_cms_articles.rb +6 -0
- data/db/migrate/20261029160000_add_published_at_to_macron_cms_articles.rb +14 -0
- data/db/seeds.rb +3 -2
- data/lib/macron/cms/version.rb +1 -1
- data/lib/tasks/macron/cms_tasks.rake +6 -0
- metadata +11 -1
|
@@ -2,51 +2,91 @@
|
|
|
2
2
|
<% content_types ||= [] %>
|
|
3
3
|
<% newly_added_id ||= nil %>
|
|
4
4
|
<% updated_id ||= nil %>
|
|
5
|
+
<% compact = local_assigns.key?(:compact) ? !!local_assigns[:compact] : (params[:view] == "compact") %>
|
|
5
6
|
|
|
6
7
|
<div class="space-y-6">
|
|
7
8
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-zinc-200 pb-4">
|
|
8
9
|
<div>
|
|
9
|
-
<div class="flex items-center gap-3">
|
|
10
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
10
11
|
<h2 class="text-xl font-bold tracking-tight text-zinc-900">Article Content Blocks</h2>
|
|
11
12
|
<span class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-semibold text-zinc-800 ring-1 ring-inset ring-zinc-300">
|
|
12
13
|
<%= pluralize(article_contents.size, "block") %>
|
|
13
14
|
</span>
|
|
15
|
+
<% if compact && article_contents.any? %>
|
|
16
|
+
<span id="selection-status-badge" class="hidden inline-flex items-center gap-1.5 rounded-full bg-zinc-900 px-2.5 py-0.5 text-xs font-semibold text-white">
|
|
17
|
+
<span id="selected-count">0</span> selected
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onclick="deselectAllBlocks();"
|
|
21
|
+
class="text-zinc-400 hover:text-white cursor-pointer ml-1 text-[11px] underline"
|
|
22
|
+
>
|
|
23
|
+
Clear
|
|
24
|
+
</button>
|
|
25
|
+
</span>
|
|
26
|
+
<% end %>
|
|
14
27
|
</div>
|
|
15
|
-
<p class="text-xs text-zinc-500 mt-1">
|
|
28
|
+
<p class="text-xs text-zinc-500 mt-1">
|
|
29
|
+
<% if compact %>
|
|
30
|
+
Re-order blocks using drag-and-drop or Move buttons. Select multiple checkboxes (Shift+Click for range) to drag multiple blocks together.
|
|
31
|
+
<% else %>
|
|
32
|
+
Re-order blocks using drag-and-drop or the Move Up/Down buttons.
|
|
33
|
+
<% end %>
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- View Mode Switcher -->
|
|
38
|
+
<div class="inline-flex items-center rounded-lg bg-zinc-100 p-1 border border-zinc-200 text-xs font-medium self-start sm:self-auto shadow-2xs">
|
|
39
|
+
<%= link_to article_path(article),
|
|
40
|
+
class: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-colors #{!compact ? 'bg-white text-zinc-900 font-semibold shadow-2xs ring-1 ring-zinc-900/5' : 'text-zinc-600 hover:text-zinc-900'}" do %>
|
|
41
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor">
|
|
42
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
43
|
+
</svg>
|
|
44
|
+
Expanded
|
|
45
|
+
<% end %>
|
|
46
|
+
<%= link_to article_path(article, view: "compact"),
|
|
47
|
+
class: "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md transition-colors #{compact ? 'bg-white text-zinc-900 font-semibold shadow-2xs ring-1 ring-zinc-900/5' : 'text-zinc-600 hover:text-zinc-900'}" do %>
|
|
48
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor">
|
|
49
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5" />
|
|
50
|
+
</svg>
|
|
51
|
+
Compact
|
|
52
|
+
<% end %>
|
|
16
53
|
</div>
|
|
17
54
|
</div>
|
|
18
55
|
|
|
19
56
|
<!-- Content Blocks List -->
|
|
20
57
|
<div
|
|
21
58
|
id="content-blocks-list"
|
|
22
|
-
data-reorder-url="<%= reorder_article_article_contents_path(article) %>"
|
|
59
|
+
data-reorder-url="<%= reorder_article_article_contents_path(article, view: (compact ? 'compact' : nil)) %>"
|
|
23
60
|
class="space-y-2"
|
|
24
61
|
>
|
|
25
62
|
<% if article_contents.any? %>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<div class="block-inserter-
|
|
29
|
-
<div class="
|
|
30
|
-
<div class="
|
|
63
|
+
<% unless compact %>
|
|
64
|
+
<!-- Inserter above first block (Position 1) -->
|
|
65
|
+
<div id="inserter-zone-top" class="block-inserter-zone py-0.5" data-position="1">
|
|
66
|
+
<div class="block-inserter-divider relative py-0.5 flex items-center justify-center group">
|
|
67
|
+
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
|
68
|
+
<div class="w-full border-t border-dashed border-zinc-200 group-hover:border-zinc-300 transition-colors"></div>
|
|
69
|
+
</div>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onclick="toggleInsertForm(1, this);"
|
|
73
|
+
aria-label="Add block at position 1"
|
|
74
|
+
class="inserter-toggle-btn relative z-10 inline-flex items-center gap-1 rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-zinc-500 shadow-2xs border border-zinc-200 opacity-60 hover:opacity-100 hover:text-zinc-900 hover:border-zinc-400 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-900 group-hover:opacity-100 transition-all cursor-pointer"
|
|
75
|
+
>
|
|
76
|
+
<svg class="w-3.5 h-3.5 transition-transform duration-150" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
77
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
78
|
+
</svg>
|
|
79
|
+
<span class="text-[11px] font-medium">Add block</span>
|
|
80
|
+
</button>
|
|
31
81
|
</div>
|
|
32
|
-
<
|
|
33
|
-
type="button"
|
|
34
|
-
onclick="toggleInsertForm(1, this);"
|
|
35
|
-
aria-label="Add block at position 1"
|
|
36
|
-
class="inserter-toggle-btn relative z-10 inline-flex items-center gap-1 rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-zinc-500 shadow-2xs border border-zinc-200 opacity-60 hover:opacity-100 hover:text-zinc-900 hover:border-zinc-400 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-900 group-hover:opacity-100 transition-all cursor-pointer"
|
|
37
|
-
>
|
|
38
|
-
<svg class="w-3.5 h-3.5 transition-transform duration-150" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
39
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
40
|
-
</svg>
|
|
41
|
-
<span class="text-[11px] font-medium">Add block</span>
|
|
42
|
-
</button>
|
|
82
|
+
<div id="insert-slot-1" class="insert-slot-target my-3 hidden"></div>
|
|
43
83
|
</div>
|
|
44
|
-
|
|
45
|
-
</div>
|
|
84
|
+
<% end %>
|
|
46
85
|
|
|
47
86
|
<% article_contents.each_with_index do |ac, index| %>
|
|
48
87
|
<% content_type = ac.content %>
|
|
49
88
|
<% fields = content_type ? content_type.normalized_fields : [] %>
|
|
89
|
+
<% convertible_targets = content_type ? content_type.convertible_targets(content_types) : [] %>
|
|
50
90
|
<% is_new = (ac.id == newly_added_id) %>
|
|
51
91
|
<% is_updated = (ac.id == updated_id) %>
|
|
52
92
|
<div
|
|
@@ -56,8 +96,17 @@
|
|
|
56
96
|
class="content-block-item group rounded-xl border border-zinc-200 bg-white shadow-2xs transition-all duration-300 hover:shadow-xs <%= 'ring-2 ring-emerald-500/80 bg-emerald-50/15' if is_new %> <%= 'ring-2 ring-blue-500/80 bg-blue-50/15' if is_updated %>"
|
|
57
97
|
>
|
|
58
98
|
<!-- Card Header & Actions -->
|
|
59
|
-
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-100 bg-zinc-50/70 px-5 py-3.5 rounded-t-xl">
|
|
99
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-100 bg-zinc-50/70 <%= compact ? 'px-4 py-2' : 'px-5 py-3.5' %> rounded-t-xl">
|
|
60
100
|
<div class="flex items-center gap-3">
|
|
101
|
+
<% if compact %>
|
|
102
|
+
<input
|
|
103
|
+
type="checkbox"
|
|
104
|
+
class="block-select-checkbox w-4 h-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900 cursor-pointer"
|
|
105
|
+
data-id="<%= ac.id %>"
|
|
106
|
+
aria-label="Select block #<%= index + 1 %>"
|
|
107
|
+
/>
|
|
108
|
+
<% end %>
|
|
109
|
+
|
|
61
110
|
<!-- Drag Handle -->
|
|
62
111
|
<div class="cursor-grab active:cursor-grabbing text-zinc-400 group-hover:text-zinc-600 transition-colors" title="Drag to reorder">
|
|
63
112
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
@@ -77,19 +126,62 @@
|
|
|
77
126
|
<!-- Action Buttons -->
|
|
78
127
|
<div class="flex items-center gap-1.5">
|
|
79
128
|
<!-- Move Up -->
|
|
80
|
-
<%= button_to move_article_article_content_path(article, ac, direction: "up"), method: :patch, disabled: (index == 0), class: "inline-flex items-center justify-center w-8 h-8 rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors cursor-pointer" do %>
|
|
129
|
+
<%= button_to move_article_article_content_path(article, ac, direction: "up", view: (compact ? "compact" : nil)), method: :patch, disabled: (index == 0), class: "inline-flex items-center justify-center w-8 h-8 rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors cursor-pointer" do %>
|
|
81
130
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
82
131
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
|
83
132
|
</svg>
|
|
84
133
|
<% end %>
|
|
85
134
|
|
|
86
135
|
<!-- Move Down -->
|
|
87
|
-
<%= button_to move_article_article_content_path(article, ac, direction: "down"), method: :patch, disabled: (index == article_contents.size - 1), class: "inline-flex items-center justify-center w-8 h-8 rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors cursor-pointer" do %>
|
|
136
|
+
<%= button_to move_article_article_content_path(article, ac, direction: "down", view: (compact ? "compact" : nil)), method: :patch, disabled: (index == article_contents.size - 1), class: "inline-flex items-center justify-center w-8 h-8 rounded-lg border border-zinc-200 bg-white text-zinc-600 hover:bg-zinc-50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors cursor-pointer" do %>
|
|
88
137
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
89
138
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
90
139
|
</svg>
|
|
91
140
|
<% end %>
|
|
92
141
|
|
|
142
|
+
<% if convertible_targets.any? %>
|
|
143
|
+
<!-- Convert Dropdown -->
|
|
144
|
+
<div class="relative convert-dropdown-container" id="convert-dropdown-<%= ac.id %>">
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onclick="toggleConvertMenu(<%= ac.id %>, event);"
|
|
148
|
+
aria-haspopup="true"
|
|
149
|
+
aria-expanded="false"
|
|
150
|
+
aria-controls="convert-menu-<%= ac.id %>"
|
|
151
|
+
class="convert-menu-trigger inline-flex items-center gap-1 rounded-lg border border-zinc-200 bg-white px-2.5 py-1.5 text-xs font-semibold text-zinc-700 hover:bg-zinc-50 transition-colors cursor-pointer"
|
|
152
|
+
>
|
|
153
|
+
<svg class="w-3.5 h-3.5 text-zinc-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
154
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
|
155
|
+
</svg>
|
|
156
|
+
Convert
|
|
157
|
+
<svg class="w-3 h-3 text-zinc-400" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
158
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
159
|
+
</svg>
|
|
160
|
+
</button>
|
|
161
|
+
|
|
162
|
+
<div
|
|
163
|
+
id="convert-menu-<%= ac.id %>"
|
|
164
|
+
role="menu"
|
|
165
|
+
class="convert-dropdown-menu hidden absolute right-0 z-20 mt-1.5 min-w-44 origin-top-right rounded-lg border border-zinc-200 bg-white py-1 shadow-lg ring-1 ring-zinc-900/5"
|
|
166
|
+
>
|
|
167
|
+
<div class="px-3 py-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">
|
|
168
|
+
Convert to
|
|
169
|
+
</div>
|
|
170
|
+
<% convertible_targets.each do |target_ct| %>
|
|
171
|
+
<%= button_to convert_article_article_content_path(article, ac, target_content_id: target_ct.id, view: (compact ? "compact" : nil)),
|
|
172
|
+
method: :patch,
|
|
173
|
+
form: { class: "block w-full" },
|
|
174
|
+
class: "w-full text-left px-3 py-1.5 text-xs font-mono font-medium text-zinc-700 hover:bg-zinc-100 hover:text-zinc-900 flex items-center justify-between gap-2 transition-colors cursor-pointer" do %>
|
|
175
|
+
<span><%= target_ct.name %></span>
|
|
176
|
+
<svg class="w-3.5 h-3.5 text-zinc-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="currentColor">
|
|
177
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
|
178
|
+
</svg>
|
|
179
|
+
<% end %>
|
|
180
|
+
<% end %>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<% end %>
|
|
184
|
+
|
|
93
185
|
<!-- Edit Toggle Button -->
|
|
94
186
|
<button
|
|
95
187
|
type="button"
|
|
@@ -104,7 +196,7 @@
|
|
|
104
196
|
|
|
105
197
|
<!-- Delete Block Button -->
|
|
106
198
|
<% del_block_confirm = "Are you sure you want to remove this #{content_type ? content_type.name : 'content'} block?" %>
|
|
107
|
-
<%= button_to article_article_content_path(article, ac
|
|
199
|
+
<%= button_to article_article_content_path(article, ac, view: (compact ? "compact" : nil)), method: :delete, data: { turbo_confirm: del_block_confirm }, class: "inline-flex items-center justify-center w-8 h-8 rounded-lg border border-rose-200 bg-rose-50 text-rose-600 hover:bg-rose-100 transition-colors cursor-pointer" do %>
|
|
108
200
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
109
201
|
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
|
110
202
|
</svg>
|
|
@@ -113,70 +205,125 @@
|
|
|
113
205
|
</div>
|
|
114
206
|
|
|
115
207
|
<!-- View Payload Mode -->
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
<% if compact %>
|
|
209
|
+
<div id="view-payload-<%= ac.id %>" class="px-4 py-2.5">
|
|
210
|
+
<% top_field = cms_top_field_definition(ac) %>
|
|
211
|
+
<% if top_field %>
|
|
212
|
+
<% fname = top_field["name"] %>
|
|
213
|
+
<% fkind = top_field["kind"] %>
|
|
214
|
+
<% fval = ac.payload[fname] %>
|
|
215
|
+
<div class="text-xs text-zinc-700">
|
|
216
|
+
<% if fkind == 'markdown' %>
|
|
217
|
+
<% plain = cms_strip_markdown(fval) %>
|
|
218
|
+
<% if plain.present? %>
|
|
219
|
+
<div class="line-clamp-2 break-words leading-relaxed font-sans" title="<%= plain %>"><%= plain %></div>
|
|
220
|
+
<% else %>
|
|
221
|
+
<span class="italic text-zinc-400">Empty</span>
|
|
222
|
+
<% end %>
|
|
223
|
+
<% elsif fkind == 'latex' %>
|
|
224
|
+
<% if fval.present? %>
|
|
225
|
+
<div class="font-mono text-[11px] text-zinc-800 line-clamp-2 break-words leading-relaxed" title="<%= fval %>"><%= fval %></div>
|
|
226
|
+
<% else %>
|
|
227
|
+
<span class="italic text-zinc-400">Empty</span>
|
|
228
|
+
<% end %>
|
|
229
|
+
<% elsif fkind == 'file' && ac.files.attached? %>
|
|
230
|
+
<% att = ac.files.first %>
|
|
231
|
+
<div class="flex items-center gap-2">
|
|
232
|
+
<% if att.image? %>
|
|
233
|
+
<div class="w-6 h-6 rounded overflow-hidden bg-zinc-100 border border-zinc-200 shrink-0">
|
|
234
|
+
<%= image_tag main_app.rails_blob_path(att), class: "w-full h-full object-cover" %>
|
|
235
|
+
</div>
|
|
236
|
+
<% else %>
|
|
237
|
+
<div class="w-6 h-6 rounded bg-zinc-100 border border-zinc-200 flex items-center justify-center shrink-0 text-zinc-500">
|
|
238
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
239
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
|
240
|
+
</svg>
|
|
241
|
+
</div>
|
|
242
|
+
<% end %>
|
|
243
|
+
<span class="font-mono text-zinc-900 truncate max-w-xs"><%= att.filename %></span>
|
|
244
|
+
<span class="text-[10px] text-zinc-400">(<%= number_to_human_size(att.byte_size) %>)</span>
|
|
245
|
+
</div>
|
|
246
|
+
<% elsif fkind == 'color' && fval.present? %>
|
|
247
|
+
<div class="flex items-center gap-1.5 font-mono text-zinc-800">
|
|
248
|
+
<span class="w-3.5 h-3.5 rounded-full border border-zinc-300 shrink-0" style="background-color: <%= fval %>;"></span>
|
|
249
|
+
<span><%= fval %></span>
|
|
250
|
+
</div>
|
|
251
|
+
<% elsif fval.present? %>
|
|
252
|
+
<div class="line-clamp-2 break-words leading-relaxed font-sans" title="<%= fval %>"><%= fval %></div>
|
|
253
|
+
<% else %>
|
|
254
|
+
<span class="italic text-zinc-400">Empty</span>
|
|
255
|
+
<% end %>
|
|
256
|
+
</div>
|
|
257
|
+
<% else %>
|
|
258
|
+
<p class="text-xs text-zinc-400 italic">No fields defined for this content type.</p>
|
|
259
|
+
<% end %>
|
|
260
|
+
</div>
|
|
261
|
+
<% else %>
|
|
262
|
+
<div id="view-payload-<%= ac.id %>" class="p-5">
|
|
263
|
+
<% if fields.any? %>
|
|
264
|
+
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
265
|
+
<% fields.each do |f| %>
|
|
266
|
+
<% fname = f["name"] %>
|
|
267
|
+
<% fkind = f["kind"] %>
|
|
268
|
+
<% fval = ac.payload[fname] %>
|
|
269
|
+
<div class="<%= fkind.in?(%w[text markdown latex]) ? 'sm:col-span-2' : '' %>">
|
|
270
|
+
<dt class="text-xs font-mono font-medium text-zinc-500 uppercase tracking-wide">
|
|
271
|
+
<%= fname %>
|
|
272
|
+
<span class="text-[10px] text-zinc-400 lowercase font-normal">(<%= fkind %>)</span>
|
|
273
|
+
</dt>
|
|
274
|
+
<dd class="mt-1 text-sm text-zinc-900 bg-zinc-50/80 rounded-lg p-2.5 border border-zinc-100 font-sans">
|
|
275
|
+
<% if fval.present? || ac.files.attached? %>
|
|
276
|
+
<% if fkind == 'markdown' %>
|
|
277
|
+
<div class="prose prose-zinc prose-sm max-w-none">
|
|
278
|
+
<%= cms_render_markdown(fval) %>
|
|
279
|
+
</div>
|
|
280
|
+
<% elsif fkind == 'latex' %>
|
|
281
|
+
<div class="py-2 overflow-x-auto text-center" data-cms-latex="<%= fval %>">
|
|
282
|
+
<span class="font-mono text-xs text-zinc-600"><%= fval %></span>
|
|
283
|
+
</div>
|
|
284
|
+
<% elsif fkind == 'text' %>
|
|
285
|
+
<div class="whitespace-pre-wrap"><%= fval %></div>
|
|
286
|
+
<% elsif fkind == 'file' && ac.files.attached? %>
|
|
287
|
+
<div class="space-y-2">
|
|
288
|
+
<% ac.files.each do |att| %>
|
|
289
|
+
<div class="flex items-center gap-2.5">
|
|
290
|
+
<% if att.image? %>
|
|
291
|
+
<div class="w-10 h-10 rounded-md overflow-hidden bg-zinc-100 border border-zinc-200 shrink-0">
|
|
292
|
+
<%= image_tag main_app.rails_blob_path(att), class: "w-full h-full object-cover" %>
|
|
293
|
+
</div>
|
|
294
|
+
<% else %>
|
|
295
|
+
<div class="w-8 h-8 rounded-md bg-zinc-100 border border-zinc-200 flex items-center justify-center shrink-0 text-zinc-500">
|
|
296
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
297
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
|
298
|
+
</svg>
|
|
299
|
+
</div>
|
|
300
|
+
<% end %>
|
|
301
|
+
<div class="min-w-0">
|
|
302
|
+
<p class="text-xs font-medium font-mono text-zinc-900 truncate"><%= att.filename %></p>
|
|
303
|
+
<p class="text-[10px] text-zinc-400"><%= number_to_human_size(att.byte_size) %></p>
|
|
153
304
|
</div>
|
|
154
|
-
<% end %>
|
|
155
|
-
<div class="min-w-0">
|
|
156
|
-
<p class="text-xs font-medium font-mono text-zinc-900 truncate"><%= att.filename %></p>
|
|
157
|
-
<p class="text-[10px] text-zinc-400"><%= number_to_human_size(att.byte_size) %></p>
|
|
158
305
|
</div>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
306
|
+
<% end %>
|
|
307
|
+
</div>
|
|
308
|
+
<% else %>
|
|
309
|
+
<span><%= fval %></span>
|
|
310
|
+
<% end %>
|
|
162
311
|
<% else %>
|
|
163
|
-
<span
|
|
312
|
+
<span class="italic text-zinc-400 text-xs">Empty</span>
|
|
164
313
|
<% end %>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<% end %>
|
|
175
|
-
</div>
|
|
314
|
+
</dd>
|
|
315
|
+
</div>
|
|
316
|
+
<% end %>
|
|
317
|
+
</dl>
|
|
318
|
+
<% else %>
|
|
319
|
+
<p class="text-xs text-zinc-500 italic">No fields defined for this content type.</p>
|
|
320
|
+
<% end %>
|
|
321
|
+
</div>
|
|
322
|
+
<% end %>
|
|
176
323
|
|
|
177
324
|
<!-- Inline Edit Form -->
|
|
178
325
|
<div id="edit-form-<%= ac.id %>" class="hidden p-5 border-t border-zinc-100 bg-zinc-50/30">
|
|
179
|
-
<%= form_with(model: [article, ac], multipart: true, class: "space-y-4") do |f| %>
|
|
326
|
+
<%= form_with(model: [article, ac], url: article_article_content_path(article, ac, view: (compact ? "compact" : nil)), multipart: true, class: "space-y-4") do |f| %>
|
|
180
327
|
<div class="space-y-4">
|
|
181
328
|
<% fields.each do |field_def| %>
|
|
182
329
|
<% fname = field_def["name"] %>
|
|
@@ -203,26 +350,28 @@
|
|
|
203
350
|
</div>
|
|
204
351
|
</div>
|
|
205
352
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<div class="block-inserter-
|
|
209
|
-
<div class="
|
|
210
|
-
<div class="
|
|
353
|
+
<% unless compact %>
|
|
354
|
+
<!-- Inserter after block #<%= index + 1 %> (Position <%= index + 2 %>) -->
|
|
355
|
+
<div class="block-inserter-zone py-0.5" data-position="<%= index + 2 %>" data-block-id="<%= ac.id %>">
|
|
356
|
+
<div class="block-inserter-divider relative py-0.5 flex items-center justify-center group">
|
|
357
|
+
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
|
358
|
+
<div class="w-full border-t border-dashed border-zinc-200 group-hover:border-zinc-300 transition-colors"></div>
|
|
359
|
+
</div>
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
onclick="toggleInsertForm(<%= index + 2 %>, this);"
|
|
363
|
+
aria-label="Add block at position <%= index + 2 %>"
|
|
364
|
+
class="inserter-toggle-btn relative z-10 inline-flex items-center gap-1 rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-zinc-500 shadow-2xs border border-zinc-200 opacity-60 hover:opacity-100 hover:text-zinc-900 hover:border-zinc-400 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-900 group-hover:opacity-100 transition-all cursor-pointer"
|
|
365
|
+
>
|
|
366
|
+
<svg class="w-3.5 h-3.5 transition-transform duration-150" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
367
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
368
|
+
</svg>
|
|
369
|
+
<span class="text-[11px] font-medium">Add block</span>
|
|
370
|
+
</button>
|
|
211
371
|
</div>
|
|
212
|
-
<
|
|
213
|
-
type="button"
|
|
214
|
-
onclick="toggleInsertForm(<%= index + 2 %>, this);"
|
|
215
|
-
aria-label="Add block at position <%= index + 2 %>"
|
|
216
|
-
class="inserter-toggle-btn relative z-10 inline-flex items-center gap-1 rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-zinc-500 shadow-2xs border border-zinc-200 opacity-60 hover:opacity-100 hover:text-zinc-900 hover:border-zinc-400 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-zinc-900 group-hover:opacity-100 transition-all cursor-pointer"
|
|
217
|
-
>
|
|
218
|
-
<svg class="w-3.5 h-3.5 transition-transform duration-150" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
219
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
220
|
-
</svg>
|
|
221
|
-
<span class="text-[11px] font-medium">Add block</span>
|
|
222
|
-
</button>
|
|
372
|
+
<div id="insert-slot-<%= index + 2 %>" class="insert-slot-target my-3 hidden"></div>
|
|
223
373
|
</div>
|
|
224
|
-
|
|
225
|
-
</div>
|
|
374
|
+
<% end %>
|
|
226
375
|
<% end %>
|
|
227
376
|
<% else %>
|
|
228
377
|
<div class="rounded-xl border border-dashed border-zinc-300 bg-white p-8 text-center">
|
|
@@ -249,100 +398,102 @@
|
|
|
249
398
|
<% end %>
|
|
250
399
|
</div>
|
|
251
400
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<div id="
|
|
255
|
-
<div class="
|
|
256
|
-
<div class="flex items-center
|
|
257
|
-
<div class="
|
|
258
|
-
<
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
<div>
|
|
263
|
-
<div class="flex items-center gap-2">
|
|
264
|
-
<h3 class="text-sm font-semibold text-zinc-900">Add Content Block</h3>
|
|
265
|
-
<span id="insert-form-position-badge" class="inline-flex items-center rounded-md bg-zinc-200/80 px-2 py-0.5 text-xs font-mono font-semibold text-zinc-800">
|
|
266
|
-
Position #1
|
|
267
|
-
</span>
|
|
401
|
+
<% unless compact %>
|
|
402
|
+
<!-- Reusable In-Place Add Content Block Card (hidden staging storage) -->
|
|
403
|
+
<div id="form-dock-storage" class="hidden">
|
|
404
|
+
<div id="reusable-add-block-card" class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-md ring-1 ring-zinc-900/5 transition-all">
|
|
405
|
+
<div class="px-6 py-4 border-b border-zinc-200 bg-zinc-50 flex items-center justify-between">
|
|
406
|
+
<div class="flex items-center gap-2.5">
|
|
407
|
+
<div class="w-6 h-6 rounded-md bg-zinc-900 text-white flex items-center justify-center shrink-0">
|
|
408
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
409
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
410
|
+
</svg>
|
|
268
411
|
</div>
|
|
269
|
-
<p class="text-xs text-zinc-500 mt-0.5">Select a Content Type and populate its fields.</p>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
<button
|
|
273
|
-
type="button"
|
|
274
|
-
onclick="closeInsertForm();"
|
|
275
|
-
class="text-zinc-400 hover:text-zinc-700 transition-colors rounded-lg p-1 hover:bg-zinc-100 cursor-pointer"
|
|
276
|
-
title="Close (Esc)"
|
|
277
|
-
>
|
|
278
|
-
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
279
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
280
|
-
</svg>
|
|
281
|
-
</button>
|
|
282
|
-
</div>
|
|
283
|
-
|
|
284
|
-
<div class="p-6">
|
|
285
|
-
<% if content_types.any? %>
|
|
286
|
-
<div class="space-y-4">
|
|
287
412
|
<div>
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
>
|
|
296
|
-
<option value="">-- Choose a Content Type --</option>
|
|
297
|
-
<% content_types.each do |ct| %>
|
|
298
|
-
<option value="<%= ct.id %>"><%= ct.name %> (<%= pluralize(ct.normalized_fields.size, "field") %>)</option>
|
|
299
|
-
<% end %>
|
|
300
|
-
</select>
|
|
413
|
+
<div class="flex items-center gap-2">
|
|
414
|
+
<h3 class="text-sm font-semibold text-zinc-900">Add Content Block</h3>
|
|
415
|
+
<span id="insert-form-position-badge" class="inline-flex items-center rounded-md bg-zinc-200/80 px-2 py-0.5 text-xs font-mono font-semibold text-zinc-800">
|
|
416
|
+
Position #1
|
|
417
|
+
</span>
|
|
418
|
+
</div>
|
|
419
|
+
<p class="text-xs text-zinc-500 mt-0.5">Select a Content Type and populate its fields.</p>
|
|
301
420
|
</div>
|
|
421
|
+
</div>
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onclick="closeInsertForm();"
|
|
425
|
+
class="text-zinc-400 hover:text-zinc-700 transition-colors rounded-lg p-1 hover:bg-zinc-100 cursor-pointer"
|
|
426
|
+
title="Close (Esc)"
|
|
427
|
+
>
|
|
428
|
+
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
429
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
430
|
+
</svg>
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
302
433
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<div
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<% end %>
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
<div class="flex items-center gap-3 pt-2">
|
|
324
|
-
<%= f.submit "Add #{ct.name.titleize} Block", class: "inline-flex items-center gap-1.5 rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white shadow-2xs hover:bg-zinc-800 transition-colors cursor-pointer" %>
|
|
325
|
-
<button
|
|
326
|
-
type="button"
|
|
327
|
-
onclick="closeInsertForm();"
|
|
328
|
-
class="rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-xs font-medium text-zinc-700 hover:bg-zinc-50 transition-colors cursor-pointer"
|
|
329
|
-
>
|
|
330
|
-
Cancel
|
|
331
|
-
</button>
|
|
332
|
-
</div>
|
|
333
|
-
<% end %>
|
|
434
|
+
<div class="p-6">
|
|
435
|
+
<% if content_types.any? %>
|
|
436
|
+
<div class="space-y-4">
|
|
437
|
+
<div>
|
|
438
|
+
<label for="new_content_type_select" class="block text-xs font-semibold text-zinc-900 mb-1.5">
|
|
439
|
+
Select Content Type
|
|
440
|
+
</label>
|
|
441
|
+
<select
|
|
442
|
+
id="new_content_type_select"
|
|
443
|
+
class="block w-full sm:w-80 rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-sm text-zinc-900 shadow-2xs focus:border-zinc-900 focus:outline-none focus:ring-1 focus:ring-zinc-900"
|
|
444
|
+
onchange="switchNewContentType(this.value);"
|
|
445
|
+
>
|
|
446
|
+
<option value="">-- Choose a Content Type --</option>
|
|
447
|
+
<% content_types.each do |ct| %>
|
|
448
|
+
<option value="<%= ct.id %>"><%= ct.name %> (<%= pluralize(ct.normalized_fields.size, "field") %>)</option>
|
|
449
|
+
<% end %>
|
|
450
|
+
</select>
|
|
334
451
|
</div>
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
452
|
+
|
|
453
|
+
<!-- Dynamic Form per Content Type -->
|
|
454
|
+
<% content_types.each do |ct| %>
|
|
455
|
+
<% ct_fields = ct.normalized_fields %>
|
|
456
|
+
<div id="new-content-type-form-<%= ct.id %>" class="new-content-type-form hidden pt-4 border-t border-zinc-100">
|
|
457
|
+
<%= form_with(url: article_article_contents_path(article, view: (compact ? "compact" : nil)), scope: :article_content, multipart: true, class: "space-y-4") do |f| %>
|
|
458
|
+
<%= f.hidden_field :content_id, value: ct.id %>
|
|
459
|
+
<%= f.hidden_field :insert_at, value: 1, class: "insert-at-field" %>
|
|
460
|
+
|
|
461
|
+
<div class="space-y-4">
|
|
462
|
+
<% ct_fields.each do |field_def| %>
|
|
463
|
+
<% fname = field_def["name"] %>
|
|
464
|
+
<%= cms_render_field(
|
|
465
|
+
field_def,
|
|
466
|
+
value: nil,
|
|
467
|
+
input_name: "article_content[payload][#{fname}]",
|
|
468
|
+
input_id: "new_content_#{ct.id}_payload_#{fname}"
|
|
469
|
+
) %>
|
|
470
|
+
<% end %>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<div class="flex items-center gap-3 pt-2">
|
|
474
|
+
<%= f.submit "Add #{ct.name.titleize} Block", class: "inline-flex items-center gap-1.5 rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white shadow-2xs hover:bg-zinc-800 transition-colors cursor-pointer" %>
|
|
475
|
+
<button
|
|
476
|
+
type="button"
|
|
477
|
+
onclick="closeInsertForm();"
|
|
478
|
+
class="rounded-lg border border-zinc-300 bg-white px-3.5 py-2 text-xs font-medium text-zinc-700 hover:bg-zinc-50 transition-colors cursor-pointer"
|
|
479
|
+
>
|
|
480
|
+
Cancel
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
<% end %>
|
|
484
|
+
</div>
|
|
485
|
+
<% end %>
|
|
486
|
+
</div>
|
|
487
|
+
<% else %>
|
|
488
|
+
<div class="text-center py-4">
|
|
489
|
+
<p class="text-sm text-zinc-500">No content types defined yet.</p>
|
|
490
|
+
<p class="text-xs text-zinc-400 mt-1">Run <code class="font-mono bg-zinc-100 px-1 py-0.5 rounded">bin/rails macron:cms:import_content_types</code> to import defined schemas.</p>
|
|
491
|
+
</div>
|
|
492
|
+
<% end %>
|
|
493
|
+
</div>
|
|
343
494
|
</div>
|
|
344
495
|
</div>
|
|
345
|
-
|
|
496
|
+
<% end %>
|
|
346
497
|
</div>
|
|
347
498
|
|
|
348
499
|
<script>
|
|
@@ -420,10 +571,55 @@
|
|
|
420
571
|
window.activeInsertPosition = null;
|
|
421
572
|
};
|
|
422
573
|
|
|
574
|
+
window.closeAllConvertMenus = function(exceptId) {
|
|
575
|
+
document.querySelectorAll('.convert-dropdown-menu').forEach(function(menu) {
|
|
576
|
+
if (exceptId && menu.id === 'convert-menu-' + exceptId) return;
|
|
577
|
+
menu.classList.add('hidden');
|
|
578
|
+
var container = menu.closest('.convert-dropdown-container');
|
|
579
|
+
if (container) {
|
|
580
|
+
var btn = container.querySelector('.convert-menu-trigger');
|
|
581
|
+
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
window.toggleConvertMenu = function(blockId, event) {
|
|
587
|
+
if (event) event.stopPropagation();
|
|
588
|
+
var menu = document.getElementById('convert-menu-' + blockId);
|
|
589
|
+
if (!menu) return;
|
|
590
|
+
|
|
591
|
+
var willOpen = menu.classList.contains('hidden');
|
|
592
|
+
window.closeAllConvertMenus(blockId);
|
|
593
|
+
|
|
594
|
+
if (willOpen) {
|
|
595
|
+
menu.classList.remove('hidden');
|
|
596
|
+
var container = menu.closest('.convert-dropdown-container');
|
|
597
|
+
if (container) {
|
|
598
|
+
var btn = container.querySelector('.convert-menu-trigger');
|
|
599
|
+
if (btn) btn.setAttribute('aria-expanded', 'true');
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
menu.classList.add('hidden');
|
|
603
|
+
var container = menu.closest('.convert-dropdown-container');
|
|
604
|
+
if (container) {
|
|
605
|
+
var btn = container.querySelector('.convert-menu-trigger');
|
|
606
|
+
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
423
611
|
if (!window._macronEscapeBound) {
|
|
424
612
|
document.addEventListener('keydown', function(e) {
|
|
425
|
-
if (e.key === 'Escape'
|
|
426
|
-
window.
|
|
613
|
+
if (e.key === 'Escape') {
|
|
614
|
+
if (window.activeInsertPosition !== null) {
|
|
615
|
+
window.closeInsertForm();
|
|
616
|
+
}
|
|
617
|
+
window.closeAllConvertMenus();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
document.addEventListener('click', function(e) {
|
|
621
|
+
if (!e.target.closest('.convert-dropdown-container')) {
|
|
622
|
+
window.closeAllConvertMenus();
|
|
427
623
|
}
|
|
428
624
|
});
|
|
429
625
|
window._macronEscapeBound = true;
|
|
@@ -436,39 +632,136 @@
|
|
|
436
632
|
});
|
|
437
633
|
}, 2500);
|
|
438
634
|
|
|
635
|
+
// Multi-Selection State & Handlers
|
|
636
|
+
window.lastCheckedBlockIndex = null;
|
|
637
|
+
|
|
638
|
+
window.deselectAllBlocks = function() {
|
|
639
|
+
var checkboxes = document.querySelectorAll('.block-select-checkbox');
|
|
640
|
+
checkboxes.forEach(function(cb) {
|
|
641
|
+
cb.checked = false;
|
|
642
|
+
var card = cb.closest('.content-block-item');
|
|
643
|
+
if (card) {
|
|
644
|
+
card.classList.remove('ring-2', 'ring-zinc-900', 'bg-zinc-50/70');
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
window.lastCheckedBlockIndex = null;
|
|
648
|
+
window.updateBlockSelectionState();
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
window.updateBlockSelectionState = function() {
|
|
652
|
+
var checkboxes = document.querySelectorAll('.block-select-checkbox');
|
|
653
|
+
var selectedCount = 0;
|
|
654
|
+
checkboxes.forEach(function(cb) {
|
|
655
|
+
var card = cb.closest('.content-block-item');
|
|
656
|
+
if (cb.checked) {
|
|
657
|
+
selectedCount++;
|
|
658
|
+
if (card) {
|
|
659
|
+
card.classList.add('ring-2', 'ring-zinc-900', 'bg-zinc-50/70');
|
|
660
|
+
}
|
|
661
|
+
} else if (card) {
|
|
662
|
+
card.classList.remove('ring-2', 'ring-zinc-900', 'bg-zinc-50/70');
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
var badge = document.getElementById('selection-status-badge');
|
|
667
|
+
var countEl = document.getElementById('selected-count');
|
|
668
|
+
if (badge && countEl) {
|
|
669
|
+
countEl.textContent = selectedCount;
|
|
670
|
+
if (selectedCount > 0) {
|
|
671
|
+
badge.classList.remove('hidden');
|
|
672
|
+
} else {
|
|
673
|
+
badge.classList.add('hidden');
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
(function initCheckboxListeners() {
|
|
679
|
+
var container = document.getElementById('content-blocks-list');
|
|
680
|
+
if (!container) return;
|
|
681
|
+
|
|
682
|
+
container.addEventListener('click', function(e) {
|
|
683
|
+
var cb = e.target.closest('.block-select-checkbox');
|
|
684
|
+
if (!cb) return;
|
|
685
|
+
|
|
686
|
+
var allCheckboxes = Array.from(container.querySelectorAll('.block-select-checkbox'));
|
|
687
|
+
var currentIndex = allCheckboxes.indexOf(cb);
|
|
688
|
+
|
|
689
|
+
if (e.shiftKey && window.lastCheckedBlockIndex !== null && window.lastCheckedBlockIndex !== currentIndex) {
|
|
690
|
+
var start = Math.min(window.lastCheckedBlockIndex, currentIndex);
|
|
691
|
+
var end = Math.max(window.lastCheckedBlockIndex, currentIndex);
|
|
692
|
+
var state = cb.checked;
|
|
693
|
+
for (var i = start; i <= end; i++) {
|
|
694
|
+
allCheckboxes[i].checked = state;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
window.lastCheckedBlockIndex = currentIndex;
|
|
699
|
+
window.updateBlockSelectionState();
|
|
700
|
+
});
|
|
701
|
+
})();
|
|
702
|
+
|
|
439
703
|
// Initialize Drag & Drop
|
|
440
704
|
(function initDragAndDrop() {
|
|
441
705
|
var container = document.getElementById('content-blocks-list');
|
|
442
706
|
if (!container) return;
|
|
443
707
|
|
|
444
|
-
var
|
|
708
|
+
var draggedItems = [];
|
|
445
709
|
|
|
446
710
|
container.addEventListener('dragstart', function(e) {
|
|
447
711
|
var item = e.target.closest('.content-block-item');
|
|
448
712
|
if (!item) return;
|
|
449
713
|
window.closeInsertForm();
|
|
450
|
-
|
|
714
|
+
window.closeAllConvertMenus();
|
|
715
|
+
|
|
716
|
+
var cb = item.querySelector('.block-select-checkbox');
|
|
717
|
+
var allCards = Array.from(container.querySelectorAll('.content-block-item'));
|
|
718
|
+
|
|
719
|
+
if (cb && cb.checked) {
|
|
720
|
+
draggedItems = allCards.filter(function(card) {
|
|
721
|
+
var cardCb = card.querySelector('.block-select-checkbox');
|
|
722
|
+
return cardCb && cardCb.checked;
|
|
723
|
+
});
|
|
724
|
+
} else {
|
|
725
|
+
draggedItems = [item];
|
|
726
|
+
}
|
|
727
|
+
|
|
451
728
|
e.dataTransfer.effectAllowed = 'move';
|
|
452
|
-
|
|
729
|
+
draggedItems.forEach(function(dItem) {
|
|
730
|
+
dItem.classList.add('opacity-50', 'border-zinc-400');
|
|
731
|
+
});
|
|
453
732
|
});
|
|
454
733
|
|
|
455
734
|
container.addEventListener('dragend', function(e) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
735
|
+
if (draggedItems && draggedItems.length > 0) {
|
|
736
|
+
draggedItems.forEach(function(dItem) {
|
|
737
|
+
dItem.classList.remove('opacity-50', 'border-zinc-400');
|
|
738
|
+
});
|
|
459
739
|
}
|
|
460
|
-
|
|
740
|
+
draggedItems = [];
|
|
461
741
|
sendNewOrder();
|
|
462
742
|
});
|
|
463
743
|
|
|
464
744
|
container.addEventListener('dragover', function(e) {
|
|
465
745
|
e.preventDefault();
|
|
466
746
|
e.dataTransfer.dropEffect = 'move';
|
|
747
|
+
|
|
748
|
+
if (!draggedItems || draggedItems.length === 0) return;
|
|
749
|
+
|
|
467
750
|
var targetItem = e.target.closest('.content-block-item');
|
|
468
|
-
if (targetItem && targetItem
|
|
751
|
+
if (targetItem && draggedItems.indexOf(targetItem) === -1) {
|
|
469
752
|
var rect = targetItem.getBoundingClientRect();
|
|
470
753
|
var next = (e.clientY - rect.top) / (rect.bottom - rect.top) > 0.5;
|
|
471
|
-
|
|
754
|
+
|
|
755
|
+
if (next) {
|
|
756
|
+
var ref = targetItem.nextSibling;
|
|
757
|
+
draggedItems.forEach(function(dItem) {
|
|
758
|
+
container.insertBefore(dItem, ref);
|
|
759
|
+
});
|
|
760
|
+
} else {
|
|
761
|
+
draggedItems.forEach(function(dItem) {
|
|
762
|
+
container.insertBefore(dItem, targetItem);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
472
765
|
}
|
|
473
766
|
});
|
|
474
767
|
|
|
@@ -502,6 +795,9 @@
|
|
|
502
795
|
}
|
|
503
796
|
|
|
504
797
|
function refreshInserterSlots() {
|
|
798
|
+
var allZones = container.querySelectorAll('.block-inserter-zone');
|
|
799
|
+
if (!allZones || allZones.length === 0) return;
|
|
800
|
+
|
|
505
801
|
var items = container.querySelectorAll('.content-block-item');
|
|
506
802
|
var topZone = document.getElementById('inserter-zone-top');
|
|
507
803
|
if (topZone && container.firstElementChild !== topZone) {
|
|
@@ -516,7 +812,6 @@
|
|
|
516
812
|
}
|
|
517
813
|
});
|
|
518
814
|
|
|
519
|
-
var allZones = container.querySelectorAll('.block-inserter-zone');
|
|
520
815
|
allZones.forEach(function(zone, idx) {
|
|
521
816
|
var pos = idx + 1;
|
|
522
817
|
zone.setAttribute('data-position', pos);
|