From 67d9c6fec58e80f4a46317059e3fa79b7eb05e7a Mon Sep 17 00:00:00 2001 From: rivanuff <16398970+rivanuff@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:12:04 +0100 Subject: [PATCH 1/3] fix: guard tinymce caret restore in unrendered editors --- config/core.php | 1 + js/editor-selection-guard.js | 64 +++++++++++++++++++++ src/Base/Foundation/Plugin.php | 8 +++ src/Base/TinyMce/TinyMceServiceProvider.php | 21 +++++++ 4 files changed, 94 insertions(+) create mode 100644 js/editor-selection-guard.js create mode 100644 src/Base/TinyMce/TinyMceServiceProvider.php diff --git a/config/core.php b/config/core.php index 8f0b8f3..e106bad 100644 --- a/config/core.php +++ b/config/core.php @@ -24,6 +24,7 @@ 'admin' => [ OWC\OpenPub\Base\Settings\SettingsServiceProvider::class, OWC\OpenPub\Base\Expired\ExpiredServiceProvider::class, + OWC\OpenPub\Base\TinyMce\TinyMceServiceProvider::class, ], 'cli' => [ diff --git a/js/editor-selection-guard.js b/js/editor-selection-guard.js new file mode 100644 index 0000000..2315e0d --- /dev/null +++ b/js/editor-selection-guard.js @@ -0,0 +1,64 @@ +/** + * The block editor moves the meta box forms into its own layout, and moving an + * iframe reloads it. When that happens after TinyMCE wrote its document (Firefox, + * or a slow load elsewhere), TinyMCE keeps the old one: the editor shows blank, + * getSelection() is null, and core's caret restore after a Visual/Code switch + * (focusHTMLBookmarkInVisualEditor() in wp-admin/js/editor.js) throws from + * select()/setRng(), taking marker cleanup and editor.save() with it. + * See https://core.trac.wordpress.org/ticket/62450. + * + * Rebuild the editor on its textarea when its iframe reloads, and skip + * select()/setRng() while there is no selection. + */ +tinymce.PluginManager.add('owcselectionguard', function (editor) { + // PreInit, not init: the wordpress plugin's init handler fires tinymce-editor-init, which can + // already restore the caret, and external plugins register their handlers after it. An iframe + // that reloads before init also leaves TinyMCE waiting on stylesheets in the dead document, so + // init never fires. + editor.on('PreInit', function () { + var selection = editor.selection; + var select = selection.select.bind(selection); + var setRng = selection.setRng.bind(selection); + var iframe = editor.iframeElement; + + selection.select = function (node, content) { + return selection.getSel() === null ? node : select(node, content); + }; + + selection.setRng = function (rng, forward) { + if (selection.getSel() !== null) { + setRng(rng, forward); + } + }; + + if (!iframe) { + return; + } + + iframe.addEventListener('load', function () { + if (editor.removed || iframe.contentDocument === editor.getDoc()) { + return; + } + + var id = editor.id; + var settings = editor.settings; + var wrap = document.getElementById('wp-' + id + '-wrap'); + var mode = wrap && wrap.classList.contains('html-active') ? 'html' : 'tmce'; + + // remove() saves first; for a hidden editor the wordpress plugin keeps the textarea value. + editor.remove(); + tinymce.init(settings); + + var rebuilt = tinymce.get(id); + + if (rebuilt) { + rebuilt.on('init', function () { + // After init_instance_callback, where Meta Box switches to the mode the field rendered in. + setTimeout(function () { + switchEditors.go(id, mode); + }); + }); + } + }); + }); +}); diff --git a/src/Base/Foundation/Plugin.php b/src/Base/Foundation/Plugin.php index 9285464..0ba43d0 100644 --- a/src/Base/Foundation/Plugin.php +++ b/src/Base/Foundation/Plugin.php @@ -157,4 +157,12 @@ public function getRootPath(): string { return $this->rootPath; } + + /** + * Return the url of the plugin. + */ + public function getPluginUrl(): string + { + return \plugins_url($this->getName()); + } } diff --git a/src/Base/TinyMce/TinyMceServiceProvider.php b/src/Base/TinyMce/TinyMceServiceProvider.php new file mode 100644 index 0000000..c4c62de --- /dev/null +++ b/src/Base/TinyMce/TinyMceServiceProvider.php @@ -0,0 +1,21 @@ +plugin->loader->addFilter('mce_external_plugins', $this, 'addSelectionGuard'); + } + + public function addSelectionGuard(array $plugins): array + { + // A TinyMCE plugin rides along in tinyMCEPreInit.mceInit, so Meta Box clones get it too. See the script. + $plugins['owcselectionguard'] = $this->plugin->getPluginUrl() . '/js/editor-selection-guard.js?ver=' . $this->plugin->getVersion(); + + return $plugins; + } +} From db10e34ad9a2651f0ab54d970747fefd1fe1dbde Mon Sep 17 00:00:00 2001 From: rivanuff <16398970+rivanuff@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:12:04 +0100 Subject: [PATCH 2/3] fix: keep default block editor styles for classic themes --- src/Base/Admin/AdminServiceProvider.php | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Base/Admin/AdminServiceProvider.php b/src/Base/Admin/AdminServiceProvider.php index d1abc89..05ffb10 100644 --- a/src/Base/Admin/AdminServiceProvider.php +++ b/src/Base/Admin/AdminServiceProvider.php @@ -14,6 +14,32 @@ public function register(): void $this->plugin->loader->addFilter('post_type_link', $this, 'filterPostLink', 10, 4); $this->plugin->loader->addFilter('preview_post_link', $this, 'filterPreviewLink', 10, 2); $this->plugin->loader->addAction('rest_prepare_openpub-item', $this, 'filterPreviewInNewTabLink', 10, 2); + $this->plugin->loader->addFilter('block_editor_settings_all', $this, 'filterEditorStyles', 20); + } + + /** + * Keep the block editor's default styles for classic themes with editor styles (WP 7.0). + */ + public function filterEditorStyles(array $settings): array + { + // Block themes manage their own editor styles. + if (function_exists('wp_theme_has_theme_json') && wp_theme_has_theme_json()) { + return $settings; + } + + if (empty($settings['styles']) || ! is_array($settings['styles'])) { + return $settings; + } + + // While any style is typed 'theme' (and "Use theme styles" is on), the editor drops its default and layout styles. + foreach ($settings['styles'] as &$style) { + if ('theme' === ($style['__unstableType'] ?? null)) { + $style['__unstableType'] = 'base-layout'; + } + } + unset($style); + + return $settings; } /** From ea4b521b44e49e875399449b38cef7cb9929215a Mon Sep 17 00:00:00 2001 From: rivanuff <16398970+rivanuff@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:13:09 +0100 Subject: [PATCH 3/3] release: v3.9.6 --- CHANGELOG.md | 6 ++++++ openpub-base.php | 2 +- src/Base/Foundation/Plugin.php | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3cbc0..7b469de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v3.9.6] - 2026-11-10 + +- Fix: keep the block editor's default styles for classic themes with editor styles (WP 7.0) +- Fix: rebuild TinyMCE editors whose iframe reloads when the block editor moves the meta boxes (blank editors in Firefox) +- Fix: skip the TinyMCE caret restore when the editor has no selection + ## [v3.9.5] - 2026-10-30 - Fix: don't propagate all query params to paginator params diff --git a/openpub-base.php b/openpub-base.php index ddea235..328d2bc 100644 --- a/openpub-base.php +++ b/openpub-base.php @@ -4,7 +4,7 @@ * Plugin Name: Yard | OpenPub Base * Plugin URI: https://www.openwebconcept.nl/ * Description: Acts as foundation for other OpenPub related content plugins. This plugin implements actions to allow for other plugins to add and/or change Custom Posttypes, Metaboxes, Taxonomies, en Posts 2 posts relations. - * Version: 3.9.5 + * Version: 3.9.6 * Author: Yard | Digital Agency * Author URI: https://www.yard.nl/ * License: GPL-3.0 diff --git a/src/Base/Foundation/Plugin.php b/src/Base/Foundation/Plugin.php index 0ba43d0..dc15c0b 100644 --- a/src/Base/Foundation/Plugin.php +++ b/src/Base/Foundation/Plugin.php @@ -8,7 +8,7 @@ class Plugin { public const NAME = 'openpub-base'; - public const VERSION = '3.9.5'; + public const VERSION = '3.9.6'; protected string $rootPath; public Config $config;