summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-13 04:54:33 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-13 04:54:33 +0200
commit8baac265059b70da0148487458ee4077b15f155e (patch)
treeb97b07a76bec2fef602fa01c1739a89a412d812f /public
parentb138f40a6493f7c4341fba196c48440e795babb9 (diff)
Asset picker: attach/detach/reorder UI, read-only view, autosave fix
Replaces nodes#edit's old Images section -- a hidden panel dumping every image asset in the system unfiltered (#image_browser) plus a raw drag-and-drop box (#image_box) -- with a small search-and-click picker built on the endpoint from the last two commits. Attaching posts immediately and appends the new thumbnail via a cloned <template> -- icons only render correctly through the Rails helper server-side, so the template holds real, pre-rendered markup for JS to clone rather than duplicating raw SVG in a JS string. Reordering is jQuery UI sortable on the small attached list only, with a dedicated drag handle rather than the whole thumbnail. Two bugs caught while click-testing, fixed here rather than shipped and patched after: the search panel never closed after attaching an image, since the success handler re-triggered focus to keep it open for attaching several in a row -- which meant it just re-populated itself forever instead of signaling "done." Fixed to close explicitly; a click-outside-closes handler was added alongside it, matching the affordance the top-bar search already has. A real, independent, pre-existing data bug surfaced during the same testing: Node#autosave!'s first-time-creation branch never carried related assets forward from whatever page was previously current -- attach an image, let autosave fire once, and it silently landed on a fresh, assetless Page row. Long-dormant, not introduced by this work, just finally exercised by something that made it visible. Fixed inside the `unless self.autosave` guard specifically -- running this on every call, not just creation, would overwrite anything attached directly to an existing autosave in between, a worse bug than the one being fixed. nodes#show gains a read-only Images section, rendered only when a page actually has attached images, so an attachment can be confirmed present without entering the edit/lock cycle -- useful on its own, and specifically useful the next time an asset bug needs investigating. Its thumbnail CSS is shared with the edit view's picker via a class (.thumbnail_list) rather than duplicated under a second name.
Diffstat (limited to 'public')
-rw-r--r--public/javascripts/admin_interface.js4
-rw-r--r--public/javascripts/related_assets.js80
-rw-r--r--public/stylesheets/admin.css75
3 files changed, 159 insertions, 0 deletions
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js
index 3f6a0a9..619933d 100644
--- a/public/javascripts/admin_interface.js
+++ b/public/javascripts/admin_interface.js
@@ -59,6 +59,10 @@ $(document).ready(function () {
59 event_search.initialize_search(); 59 event_search.initialize_search();
60 } 60 }
61 61
62 if ($("#related_asset_search_term").length != 0) {
63 related_assets.initialize();
64 }
65
62 if ($("#rrule_builder").length != 0) { 66 if ($("#rrule_builder").length != 0) {
63 rrule_builder.initialize(); 67 rrule_builder.initialize();
64 } 68 }
diff --git a/public/javascripts/related_assets.js b/public/javascripts/related_assets.js
new file mode 100644
index 0000000..2955cbf
--- /dev/null
+++ b/public/javascripts/related_assets.js
@@ -0,0 +1,80 @@
1related_assets = {
2 initialize: function() {
3 var container = $("#related_assets");
4 var searchUrl = container.data("search-url");
5 var createUrl = container.data("create-url");
6 var list = $("#related_asset_list");
7
8 initSearchPicker({
9 inputSelector: "#related_asset_search_term",
10 resultsSelector: "#related_asset_search_results",
11 url: searchUrl,
12 loadOnFocus: true,
13 renderResults: function(data, results) {
14 var found = false;
15 data.forEach(function(asset) {
16 var item = $(
17 "<a href='#' class='related_asset_result'>" +
18 "<img src='" + asset.thumb_url + "' alt=''>" +
19 "<span>" + asset.name + "</span>" +
20 "</a>"
21 );
22 item.bind("click", function() {
23 related_assets.attach(asset.id, createUrl, list);
24 return false;
25 });
26 results.append(item);
27 found = true;
28 });
29 return found;
30 }
31 });
32
33 $(document).on("click", function(e) {
34 if (!$(e.target).closest("#related_assets").length) {
35 $("#related_asset_search_results").slideUp().empty();
36 }
37 });
38
39 list.sortable({
40 handle: ".related_asset_handle",
41 update: function(event, ui) {
42 var newPosition = ui.item.index() + 1; // acts_as_list is 1-based; jQuery UI's index() is 0-based
43 $.ajax({
44 type: "PATCH",
45 url: ui.item.data("url"),
46 data: "position=" + newPosition
47 });
48 }
49 });
50
51 list.on("click", ".related_asset_remove", function() {
52 var item = $(this).closest("li");
53 $.ajax({
54 type: "DELETE",
55 url: item.data("url"),
56 success: function() {
57 item.remove();
58 }
59 });
60 });
61 },
62
63 attach: function(assetId, createUrl, list) {
64 $.ajax({
65 type: "POST",
66 url: createUrl,
67 data: "asset_id=" + assetId,
68 dataType: "json",
69 success: function(related) {
70 var item = $($("#related_asset_template").html().trim());
71 item.attr("data-url", related.url);
72 item.find("img").attr("src", related.thumb_url);
73 list.append(item);
74 list.sortable("refresh");
75 $("#related_asset_search_term").val("");
76 $("#related_asset_search_results").slideUp().empty();
77 }
78 });
79 }
80};
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index a58a061..c3c0cb4 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -1210,3 +1210,78 @@ div#image_browser {
1210div#image_browser ul li { 1210div#image_browser ul li {
1211 list-style-type: none; 1211 list-style-type: none;
1212} 1212}
1213
1214.thumbnail_list {
1215 display: flex;
1216 flex-wrap: wrap;
1217 gap: 0.75rem;
1218 list-style: none;
1219 margin: 0 0 0.75rem;
1220 padding: 0;
1221}
1222
1223.thumbnail_list li {
1224 position: relative;
1225 display: flex;
1226 align-items: center;
1227 gap: 0.25rem;
1228 border: 1px solid #e8e8e8;
1229 border-radius: 6px;
1230 padding: 4px;
1231}
1232
1233.thumbnail_list img {
1234 display: block;
1235 width: 60px;
1236 height: 60px;
1237 object-fit: cover;
1238 border-radius: 4px;
1239}
1240
1241.related_asset_handle {
1242 display: flex;
1243 color: #969696;
1244 cursor: grab;
1245}
1246
1247.related_asset_handle svg {
1248 width: 1rem;
1249 height: 1rem;
1250}
1251
1252.related_asset_remove {
1253 display: flex;
1254 align-items: center;
1255 justify-content: center;
1256 background: none;
1257 border: none;
1258 padding: 2px;
1259 cursor: pointer;
1260 color: #969696;
1261}
1262
1263.related_asset_remove svg {
1264 width: 0.9rem;
1265 height: 0.9rem;
1266}
1267
1268.related_asset_remove:hover {
1269 color: #cc0000;
1270}
1271
1272.related_asset_result {
1273 display: flex;
1274 align-items: center;
1275 gap: 0.5rem;
1276 padding: 4px;
1277 border-bottom: 1px solid #e8e8e8;
1278 text-decoration: none;
1279}
1280
1281.related_asset_result img {
1282 width: 40px;
1283 height: 40px;
1284 object-fit: cover;
1285 border-radius: 4px;
1286}
1287