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 +++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) (limited to 'public/javascripts/admin_interface.js') 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(); + } } } -- cgit v1.3