diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-14 17:06:59 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-14 18:30:40 +0200 |
| commit | 8847db139bef9dd8f9465c31438f16af13593267 (patch) | |
| tree | d192bd396ea74f89cd84aac22d79ef126aec6976 /public | |
| parent | ffb3c9987c2f820b99e8ad0ad56db8ae0324671c (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.js | 114 | ||||
| -rw-r--r-- | public/javascripts/related_assets.js | 19 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 83 |
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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); | ||
| 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, |
| 768 | form.button_to input[type="submit"], | 777 | form.button_to input[type="submit"], |
| 769 | form.button_to button[type="submit"] { | 778 | form.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 | } | ||
