From e0f2394fd0816ef03bf4b928ab9fa5a18c32ffda Mon Sep 17 00:00:00 2001 From: erdgeist Date: Tue, 14 Jul 2026 04:10:01 +0200 Subject: Add a toggleable live preview panel to both editors Hidden by default, shares horizontal space with the body editor once opened rather than a separate section, and only requests the iframe's src on first toggle -- no preview traffic at all until someone actually wants to see it. Refreshes automatically on every successful autosave; a separate force-render button re-attempts a save-and- refresh cycle immediately, for when the normal 7-second cycle has been interrupted (a lock-lost error, a transient failure) and waiting for the next tick isn't good enough. --- app/views/nodes/edit.html.erb | 21 +++++++++- app/views/page_translations/edit.html.erb | 24 +++++++++-- public/javascripts/admin_interface.js | 43 ++++++++++++++++++- public/stylesheets/admin.css | 68 +++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 7 deletions(-) diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index e0e51d8..9506995 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -116,10 +116,27 @@
<%= d.text_area :abstract %>
Body
-
<%= d.text_area :body, :class => 'with_editor' %>
+
+ + +
+ +
+
+
<%= d.text_area :body, :class => 'with_editor' %>
+
+ +
+ <% end %> -
+
<%= f.submit 'Save Draft' %> <%= f.submit 'Save + Unlock + Exit' %>
diff --git a/app/views/page_translations/edit.html.erb b/app/views/page_translations/edit.html.erb index 981fb74..0f64d40 100644 --- a/app/views/page_translations/edit.html.erb +++ b/app/views/page_translations/edit.html.erb @@ -20,8 +20,8 @@
- <%= form_with url: node_translation_path(@node, @locale), method: :patch, local: true, id: "translation_edit_form", - data: { autosave_url: autosave_node_translation_path(@node, @locale), show_url: node_path(@node) } do |f| %> + <%= form_with url: node_translation_path(@node, @locale), method: :patch, local: true, id: "translation_edit_form", + data: { autosave_url: autosave_node_translation_path(@node, @locale), show_url: node_path(@node) } do |f| %>
Title
@@ -34,8 +34,24 @@
Body
-
- <%= text_area_tag "page[body]", @translation&.body, :class => 'with_editor' %> +
+ + +
+ +
+
+
+ <%= text_area_tag "page[body]", @translation&.body, :class => 'with_editor' %> +
+
+
<% end %> diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index e3d4ff1..9da5d28 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js @@ -67,6 +67,10 @@ $(document).ready(function () { rrule_builder.initialize(); } + if ($("#preview_panel").length != 0) { + cccms.preview.initialize(); + } + if ($('#recent_changes_toggle').length != 0) { hide_all(); $('#recent_changes_toggle').attr("class", "selected"); @@ -165,7 +169,8 @@ cccms = { } jQuery.fn.submitWithAjax = function(options) { - if (page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) { + options = options || {}; + if (options.force || page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) { tinymce.triggerSave(); page.cached_title = elements.title.val(); @@ -181,6 +186,9 @@ cccms = { type: "POST", url: this.attr("data-autosave-url"), data: data, + success: function() { + cccms.preview.refresh_if_open(); + }, error: function(xhr) { if (xhr.status === 423) { clearInterval(cccms.autosave_timer); @@ -211,6 +219,39 @@ cccms = { $banner.append("."); $flash.html($banner); + }, + + preview : { + is_open : false, + + initialize : function() { + var $panel = $('#preview_panel'); + var $toggle = $('#preview_toggle'); + var $force = $('#preview_force_render'); + + $toggle.on('click', function() { + cccms.preview.is_open = !cccms.preview.is_open; + $panel.toggle(cccms.preview.is_open); + $force.toggle(cccms.preview.is_open); + $toggle.attr('aria-pressed', cccms.preview.is_open); + if (cccms.preview.is_open) cccms.preview.refresh(); + }); + + $force.on('click', function() { + $("#page_editor > form").submitWithAjax({ force: true }); + }); + }, + + refresh : function() { + var $iframe = $('#live_preview_iframe'); + if ($iframe.length === 0) return; + var base = $iframe.data('src'); + $iframe.attr('src', base + (base.indexOf('?') === -1 ? '?' : '&') + '_t=' + Date.now()); + }, + + refresh_if_open : function() { + if (cccms.preview.is_open) cccms.preview.refresh(); + } } } diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 93cc3f5..5f7723b 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -683,6 +683,10 @@ table.user_table td.user_login { display: block; margin-bottom: 1rem; } + + .body_toolbar_row { + margin-left: 120px; + } } @media(max-width:1015px) { div.node_description { @@ -1324,3 +1328,67 @@ div#image_browser ul li { border-radius: 4px; } +/* ============================================================ + Live edit preview + ============================================================ */ + +#editor_and_preview { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +@media (min-width: 1016px) { + #editor_and_preview { + flex-direction: row; + align-items: flex-start; + } + + #editor_and_preview .preview_content { + flex: 1; + min-width: 0; + } + + #preview_panel { + flex: 1; + min-width: 0; + } +} + +#preview_panel iframe { + width: 100%; + height: 612px; + border: 0.5px solid var(--border); + border-radius: var(--radius); +} + +.body_toolbar_row { + min-height: 2rem; + margin-bottom: 1rem; + display: flex; + gap: 8px; +} + +.view_toggle { + background: #f7f7f7; + border: 1px solid #ddd; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + color: #555; + display: flex; + align-items: center; + gap: 1em; +} + +.view_toggle:hover { + background: #ececec; + border-color: #ccc; + color: #222; +} + +.view_toggle[aria-pressed="true"] { + background: #dde8f5; + border-color: #a9c6e8; + color: #1a4d8f; +} -- cgit v1.3