summaryrefslogtreecommitdiff
path: root/public/javascripts
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/javascripts
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/javascripts')
-rw-r--r--public/javascripts/admin_interface.js114
-rw-r--r--public/javascripts/related_assets.js19
2 files changed, 130 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 }