From 8847db139bef9dd8f9465c31438f16af13593267 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 14 Jul 2026 17:06:59 +0200 Subject: 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 wrapping an , 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. --- public/javascripts/admin_interface.js | 114 +++++++++++++++++++++++++++++++++- public/javascripts/related_assets.js | 19 ++++++ 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 () { promotion: false, menubar: false, plugins: 'code link lists visualblocks', - toolbar: 'bold italic underline | bullist numlist | link unlink | blocks | code', - extended_valid_elements: 'aggregate[children|tags|limit|order_by|order_direction|partial|conditions]', + toolbar: 'bold italic underline | bullist numlist | link unlink | insertpageimage | blocks | code', + 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]', relative_urls: false, entity_encoding: 'raw', - valid_classes: { '*': '' }, + 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; }', + valid_classes: { + '*': '', + 'img': 'inline-image inline-image--full inline-image--half inline-image--left inline-image--right', + 'a': 'glightbox' + }, setup: function(editor) { editor.on('init', function() { cccms.setup_autosave(); }); + + editor.ui.registry.addButton('insertpageimage', { + icon: 'image', + text: 'Insert image', + tooltip: "Insert one of this page's attached images", + onAction: function() { + cccms.inline_images.open(editor); + } + }); } }); @@ -172,6 +186,10 @@ cccms = { options = options || {}; if (options.force || page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) { + elements.body.find('a.glightbox').each(function() { + if ($(this).find('img').length === 0) { $(this).remove(); } + }); + tinymce.triggerSave(); page.cached_title = elements.title.val(); page.cached_abstract = elements.abstract.val(); @@ -252,6 +270,96 @@ cccms = { refresh_if_open : function() { if (cccms.preview.is_open) cccms.preview.refresh(); } + }, + + inline_images: { + ensure_overlay: function() { + if ($('#inline_image_picker').length) return; + + var overlay = $( + '' + ); + $('body').append(overlay); + + overlay.on('click', '#inline_image_picker_grid img', function() { + overlay.find('#inline_image_picker_grid img').removeClass('selected'); + $(this).addClass('selected'); + cccms.inline_images.selected_index = $(this).data('index'); + }); + + overlay.on('click', '[data-placement]', function() { + cccms.inline_images.insert($(this).data('placement')); + }); + + overlay.on('click', '#inline_image_picker_cancel', function() { + overlay.hide(); + }); + }, + + open: function(editor) { + cccms.inline_images.ensure_overlay(); + cccms.inline_images.editor = editor; + + var showUrl = $("#page_editor > form").attr("data-show-url") || ""; + var match = showUrl.match(/(\d+)$/); + cccms.inline_images.node_id = match ? match[1] : ""; + + var items = $('#related_asset_list li').map(function() { + return { + thumb: $(this).find('img').attr('src'), + large: $(this).data('large-url'), + original: $(this).data('original-url'), + name: $(this).data('name') + }; + }).get(); + cccms.inline_images.items = items; + + var grid = $('#inline_image_picker_grid').empty(); + if (items.length === 0) { + grid.append('

No images are attached to this page yet -- attach one from the Images section first.

'); + cccms.inline_images.selected_index = null; + } else { + items.forEach(function(item, i) { + var wrapper = $('
'); + wrapper.append($('').attr('src', item.thumb).attr('data-index', i)); + if (i === 0) { + wrapper.append($('', { "class": "inline_image_picker_headline_badge" }).text("Headline")); + } + grid.append(wrapper); + }); + cccms.inline_images.selected_index = 0; + grid.find('img').first().addClass('selected'); + } + + $('#inline_image_picker').show(); + }, + + escape_attr: function(str) { + return String(str || '').replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); + }, + + insert: function(placement) { + var item = cccms.inline_images.items[cccms.inline_images.selected_index]; + if (!item) return; + + var classes = placement === 'full' + ? 'inline-image inline-image--full' + : 'inline-image inline-image--half inline-image--' + placement; + + var html = '
' + + '' + cccms.inline_images.escape_attr(item.name) + ''; + + cccms.inline_images.editor.insertContent(html); + $('#inline_image_picker').hide(); + } } } 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 = { var createUrl = container.data("create-url"); var list = $("#related_asset_list"); + related_assets.update_headline_badge(list); + initSearchPicker({ inputSelector: "#related_asset_search_term", resultsSelector: "#related_asset_search_results", @@ -45,6 +47,7 @@ related_assets = { url: ui.item.data("url"), data: "position=" + newPosition }); + related_assets.update_headline_badge(list); } }); @@ -55,11 +58,23 @@ related_assets = { url: item.data("url"), success: function() { item.remove(); + related_assets.update_headline_badge(list); } }); }); }, + update_headline_badge: function(list) { + list.find(".related_asset_headline_badge").remove(); + var first = list.children("li").first(); + if (first.length) { + $("", { + "class": "related_asset_headline_badge", + "title": "Shown automatically as this page's headline image" + }).text("Headline").insertBefore(first.find(".related_asset_remove")); + } + }, + attach: function(assetId, createUrl, list) { $.ajax({ type: "POST", @@ -69,9 +84,13 @@ related_assets = { success: function(related) { var item = $($("#related_asset_template").html().trim()); item.attr("data-url", related.url); + item.attr("data-large-url", related.large_url); + item.attr("data-original-url", related.original_url); + item.attr("data-name", related.name); item.find("img").attr("src", related.thumb_url); list.append(item); list.sortable("refresh"); + related_assets.update_headline_badge(list); $("#related_asset_search_term").val(""); $("#related_asset_search_results").slideUp().empty(); } 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 { display: block; } +.node_content .inline-image--full, +.node_content .inline-image--half { + max-width: 300px; + width: auto; + float: none; + display: block; + margin: 0.5rem 0; +} + .action_button, form.button_to input[type="submit"], form.button_to button[type="submit"] { @@ -1292,6 +1301,15 @@ div#image_browser ul li { height: 1rem; } +.related_asset_headline_badge { + background: #fff3cd; + color: #6b5900; + border-radius: 999px; + padding: 0.1rem 0.6rem; + font-size: 0.75em; + white-space: nowrap; +} + .related_asset_remove { display: flex; align-items: center; @@ -1392,3 +1410,68 @@ div#image_browser ul li { border-color: #a9c6e8; color: #1a4d8f; } + +/* ============================================================ + Image picker + ============================================================ */ + +#inline_image_picker { + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + padding: 1rem; + max-width: 480px; + max-height: 70vh; + overflow-y: auto; + z-index: 1000; + box-shadow: 0 4px 16px rgba(0,0,0,0.2); +} + +#inline_image_picker_grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 1rem; +} + +.inline_image_picker_item { + position: relative; +} + +#inline_image_picker_grid img { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 4px; + cursor: pointer; + border: 2px solid transparent; + display: block; +} + +.inline_image_picker_headline_badge { + position: absolute; + bottom: 2px; + left: 2px; + right: 2px; + background: rgba(255, 243, 205, 0.95); + color: #6b5900; + font-size: 0.65em; + text-align: center; + border-radius: 3px; + padding: 1px 0; + pointer-events: none; +} + +#inline_image_picker_grid img.selected { + border-color: #1a4d8f; +} + +#inline_image_picker_actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} -- cgit v1.3