summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-14 17:06:59 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-14 18:30:40 +0200
commit8847db139bef9dd8f9465c31438f16af13593267 (patch)
treed192bd396ea74f89cd84aac22d79ef126aec6976 /public
parentffb3c9987c2f820b99e8ad0ad56db8ae0324671c (diff)
Add inline image insertion for page bodies, plus a headline-conflict badge
Custom TinyMCE toolbar button opens a picker scoped to a page's own attached images. Selecting one and a placement (full width, left, right) inserts <a class="glightbox" data-gallery="page-N"> wrapping an <img>, sharing the existing headline-image lightbox gallery. No native TinyMCE image dialog or resize handles -- placement is fixed to the three options via CSS classes only. extended_valid_elements defines a and img explicitly and symmetrically; valid_classes allows the placement classes and glightbox specifically. content_style renders placement inside the editing iframe, since ccc.css doesn't load there. A pre-save pass removes any a.glightbox left without an img, covering in-editor deletion regardless of how it happened. Alt text comes from the asset's name, escaped. A badge marks whichever attached image sits at position 0, since _headline_image.html.erb always renders that position as the page's automatic headline. Shown in the sidebar Images list and in the picker grid.
Diffstat (limited to 'public')
-rw-r--r--public/javascripts/admin_interface.js114
-rw-r--r--public/javascripts/related_assets.js19
-rw-r--r--public/stylesheets/admin.css83
3 files changed, 213 insertions, 3 deletions
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js
index 9da5d28..0c9ec66 100644
--- a/public/javascripts/admin_interface.js
+++ b/public/javascripts/admin_interface.js
@@ -19,15 +19,29 @@ $(document).ready(function () {
19 promotion: false, 19 promotion: false,
20 menubar: false, 20 menubar: false,
21 plugins: 'code link lists visualblocks', 21 plugins: 'code link lists visualblocks',
22 toolbar: 'bold italic underline | bullist numlist | link unlink | blocks | code', 22 toolbar: 'bold italic underline | bullist numlist | link unlink | insertpageimage | blocks | code',
23 extended_valid_elements: 'aggregate[children|tags|limit|order_by|order_direction|partial|conditions]', 23 extended_valid_elements: 'aggregate[children|tags|limit|order_by|order_direction|partial|conditions],a[href|target|rel|class|data-gallery],img[class|src|alt|title|width|height|style|border]',
24 relative_urls: false, 24 relative_urls: false,
25 entity_encoding: 'raw', 25 entity_encoding: 'raw',
26 valid_classes: { '*': '' }, 26 content_style: '.inline-image--full { width: 100%; margin: 1rem 0; } .inline-image--half { width: 48%; margin-bottom: 1rem; } .inline-image--left { float: left; margin-right: 1rem; } .inline-image--right { float: right; margin-left: 1rem; }',
27 valid_classes: {
28 '*': '',
29 'img': 'inline-image inline-image--full inline-image--half inline-image--left inline-image--right',
30 'a': 'glightbox'
31 },
27 setup: function(editor) { 32 setup: function(editor) {
28 editor.on('init', function() { 33 editor.on('init', function() {
29 cccms.setup_autosave(); 34 cccms.setup_autosave();
30 }); 35 });
36
37 editor.ui.registry.addButton('insertpageimage', {
38 icon: 'image',
39 text: 'Insert image',
40 tooltip: "Insert one of this page's attached images",
41 onAction: function() {
42 cccms.inline_images.open(editor);
43 }
44 });
31 } 45 }
32 }); 46 });
33 47
@@ -172,6 +186,10 @@ cccms = {
172 options = options || {}; 186 options = options || {};
173 if (options.force || page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) { 187 if (options.force || page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) {
174 188
189 elements.body.find('a.glightbox').each(function() {
190 if ($(this).find('img').length === 0) { $(this).remove(); }
191 });
192
175 tinymce.triggerSave(); 193 tinymce.triggerSave();
176 page.cached_title = elements.title.val(); 194 page.cached_title = elements.title.val();
177 page.cached_abstract = elements.abstract.val(); 195 page.cached_abstract = elements.abstract.val();
@@ -252,6 +270,96 @@ cccms = {
252 refresh_if_open : function() { 270 refresh_if_open : function() {
253 if (cccms.preview.is_open) cccms.preview.refresh(); 271 if (cccms.preview.is_open) cccms.preview.refresh();
254 } 272 }
273 },
274
275 inline_images: {
276 ensure_overlay: function() {
277 if ($('#inline_image_picker').length) return;
278
279 var overlay = $(
280 '<div id="inline_image_picker" style="display:none">' +
281 '<div id="inline_image_picker_grid"></div>' +
282 '<div id="inline_image_picker_actions">' +
283 '<button type="button" data-placement="full">Insert full width</button>' +
284 '<button type="button" data-placement="left">Insert, align left</button>' +
285 '<button type="button" data-placement="right">Insert, align right</button>' +
286 '<button type="button" id="inline_image_picker_cancel">Cancel</button>' +
287 '</div>' +
288 '</div>'
289 );
290 $('body').append(overlay);
291
292 overlay.on('click', '#inline_image_picker_grid img', function() {
293 overlay.find('#inline_image_picker_grid img').removeClass('selected');
294 $(this).addClass('selected');
295 cccms.inline_images.selected_index = $(this).data('index');
296 });
297
298 overlay.on('click', '[data-placement]', function() {
299 cccms.inline_images.insert($(this).data('placement'));
300 });
301
302 overlay.on('click', '#inline_image_picker_cancel', function() {
303 overlay.hide();
304 });
305 },
306
307 open: function(editor) {
308 cccms.inline_images.ensure_overlay();
309 cccms.inline_images.editor = editor;
310
311 var showUrl = $("#page_editor > form").attr("data-show-url") || "";
312 var match = showUrl.match(/(\d+)$/);
313 cccms.inline_images.node_id = match ? match[1] : "";
314
315 var items = $('#related_asset_list li').map(function() {
316 return {
317 thumb: $(this).find('img').attr('src'),
318 large: $(this).data('large-url'),
319 original: $(this).data('original-url'),
320 name: $(this).data('name')
321 };
322 }).get();
323 cccms.inline_images.items = items;
324
325 var grid = $('#inline_image_picker_grid').empty();
326 if (items.length === 0) {
327 grid.append('<p>No images are attached to this page yet -- attach one from the Images section first.</p>');
328 cccms.inline_images.selected_index = null;
329 } else {
330 items.forEach(function(item, i) {
331 var wrapper = $('<div class="inline_image_picker_item"></div>');
332 wrapper.append($('<img>').attr('src', item.thumb).attr('data-index', i));
333 if (i === 0) {
334 wrapper.append($('<span>', { "class": "inline_image_picker_headline_badge" }).text("Headline"));
335 }
336 grid.append(wrapper);
337 });
338 cccms.inline_images.selected_index = 0;
339 grid.find('img').first().addClass('selected');
340 }
341
342 $('#inline_image_picker').show();
343 },
344
345 escape_attr: function(str) {
346 return String(str || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
347 },
348
349 insert: function(placement) {
350 var item = cccms.inline_images.items[cccms.inline_images.selected_index];
351 if (!item) return;
352
353 var classes = placement === 'full'
354 ? 'inline-image inline-image--full'
355 : 'inline-image inline-image--half inline-image--' + placement;
356
357 var html = '<a href="' + item.original + '" class="glightbox" data-gallery="page-' + cccms.inline_images.node_id + '">' +
358 '<img src="' + item.large + '" class="' + classes + '" alt="' + cccms.inline_images.escape_attr(item.name) + '"></a>';
359
360 cccms.inline_images.editor.insertContent(html);
361 $('#inline_image_picker').hide();
362 }
255 } 363 }
256} 364}
257 365
diff --git a/public/javascripts/related_assets.js b/public/javascripts/related_assets.js
index 2955cbf..803332f 100644
--- a/public/javascripts/related_assets.js
+++ b/public/javascripts/related_assets.js
@@ -5,6 +5,8 @@ related_assets = {
5 var createUrl = container.data("create-url"); 5 var createUrl = container.data("create-url");
6 var list = $("#related_asset_list"); 6 var list = $("#related_asset_list");
7 7
8 related_assets.update_headline_badge(list);
9
8 initSearchPicker({ 10 initSearchPicker({
9 inputSelector: "#related_asset_search_term", 11 inputSelector: "#related_asset_search_term",
10 resultsSelector: "#related_asset_search_results", 12 resultsSelector: "#related_asset_search_results",
@@ -45,6 +47,7 @@ related_assets = {
45 url: ui.item.data("url"), 47 url: ui.item.data("url"),
46 data: "position=" + newPosition 48 data: "position=" + newPosition
47 }); 49 });
50 related_assets.update_headline_badge(list);
48 } 51 }
49 }); 52 });
50 53
@@ -55,11 +58,23 @@ related_assets = {
55 url: item.data("url"), 58 url: item.data("url"),
56 success: function() { 59 success: function() {
57 item.remove(); 60 item.remove();
61 related_assets.update_headline_badge(list);
58 } 62 }
59 }); 63 });
60 }); 64 });
61 }, 65 },
62 66
67 update_headline_badge: function(list) {
68 list.find(".related_asset_headline_badge").remove();
69 var first = list.children("li").first();
70 if (first.length) {
71 $("<span>", {
72 "class": "related_asset_headline_badge",
73 "title": "Shown automatically as this page's headline image"
74 }).text("Headline").insertBefore(first.find(".related_asset_remove"));
75 }
76 },
77
63 attach: function(assetId, createUrl, list) { 78 attach: function(assetId, createUrl, list) {
64 $.ajax({ 79 $.ajax({
65 type: "POST", 80 type: "POST",
@@ -69,9 +84,13 @@ related_assets = {
69 success: function(related) { 84 success: function(related) {
70 var item = $($("#related_asset_template").html().trim()); 85 var item = $($("#related_asset_template").html().trim());
71 item.attr("data-url", related.url); 86 item.attr("data-url", related.url);
87 item.attr("data-large-url", related.large_url);
88 item.attr("data-original-url", related.original_url);
89 item.attr("data-name", related.name);
72 item.find("img").attr("src", related.thumb_url); 90 item.find("img").attr("src", related.thumb_url);
73 list.append(item); 91 list.append(item);
74 list.sortable("refresh"); 92 list.sortable("refresh");
93 related_assets.update_headline_badge(list);
75 $("#related_asset_search_term").val(""); 94 $("#related_asset_search_term").val("");
76 $("#related_asset_search_results").slideUp().empty(); 95 $("#related_asset_search_results").slideUp().empty();
77 } 96 }
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 5f7723b..b5b8707 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -764,6 +764,15 @@ table.user_table td.user_login {
764 display: block; 764 display: block;
765} 765}
766 766
767.node_content .inline-image--full,
768.node_content .inline-image--half {
769 max-width: 300px;
770 width: auto;
771 float: none;
772 display: block;
773 margin: 0.5rem 0;
774}
775
767.action_button, 776.action_button,
768form.button_to input[type="submit"], 777form.button_to input[type="submit"],
769form.button_to button[type="submit"] { 778form.button_to button[type="submit"] {
@@ -1292,6 +1301,15 @@ div#image_browser ul li {
1292 height: 1rem; 1301 height: 1rem;
1293} 1302}
1294 1303
1304.related_asset_headline_badge {
1305 background: #fff3cd;
1306 color: #6b5900;
1307 border-radius: 999px;
1308 padding: 0.1rem 0.6rem;
1309 font-size: 0.75em;
1310 white-space: nowrap;
1311}
1312
1295.related_asset_remove { 1313.related_asset_remove {
1296 display: flex; 1314 display: flex;
1297 align-items: center; 1315 align-items: center;
@@ -1392,3 +1410,68 @@ div#image_browser ul li {
1392 border-color: #a9c6e8; 1410 border-color: #a9c6e8;
1393 color: #1a4d8f; 1411 color: #1a4d8f;
1394} 1412}
1413
1414/* ============================================================
1415 Image picker
1416 ============================================================ */
1417
1418#inline_image_picker {
1419 position: fixed;
1420 top: 10%;
1421 left: 50%;
1422 transform: translateX(-50%);
1423 background: #fff;
1424 border: 1px solid #ddd;
1425 border-radius: 8px;
1426 padding: 1rem;
1427 max-width: 480px;
1428 max-height: 70vh;
1429 overflow-y: auto;
1430 z-index: 1000;
1431 box-shadow: 0 4px 16px rgba(0,0,0,0.2);
1432}
1433
1434#inline_image_picker_grid {
1435 display: flex;
1436 flex-wrap: wrap;
1437 gap: 8px;
1438 margin-bottom: 1rem;
1439}
1440
1441.inline_image_picker_item {
1442 position: relative;
1443}
1444
1445#inline_image_picker_grid img {
1446 width: 80px;
1447 height: 80px;
1448 object-fit: cover;
1449 border-radius: 4px;
1450 cursor: pointer;
1451 border: 2px solid transparent;
1452 display: block;
1453}
1454
1455.inline_image_picker_headline_badge {
1456 position: absolute;
1457 bottom: 2px;
1458 left: 2px;
1459 right: 2px;
1460 background: rgba(255, 243, 205, 0.95);
1461 color: #6b5900;
1462 font-size: 0.65em;
1463 text-align: center;
1464 border-radius: 3px;
1465 padding: 1px 0;
1466 pointer-events: none;
1467}
1468
1469#inline_image_picker_grid img.selected {
1470 border-color: #1a4d8f;
1471}
1472
1473#inline_image_picker_actions {
1474 display: flex;
1475 gap: 8px;
1476 flex-wrap: wrap;
1477}