WebBox

Oxygen Gutenberg Integration - iframed canvas fix.

Aug 21st, 2026
82
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.51 KB | None | 0 0
  1. /**
  2. * Oxygen Gutenberg Integration - iframed canvas fix.
  3. *
  4. * Oxygen Gutenberg Integration 1.4.6 predates the iframed block-editor canvas.
  5. * It binds its contenteditable/click handlers to the top document, and it feeds
  6. * its CSS in via absolute-URL add_editor_style() calls plus an inline <style>
  7. * in the admin footer. Since the canvas became an iframe, none of that reaches
  8. * the block. This restores both halves against the canvas document.
  9. */
  10.  
  11. /* -------------------------------------------------------------------------
  12. * 1. CSS - hand Oxygen's stylesheets to the editor directly.
  13. *
  14. * Absolute-URL editor styles are fetched by WordPress over a loopback HTTP
  15. * request (get_block_editor_theme_styles()), which fails silently on local /
  16. * self-signed setups. Per-block CSS is never registered as an editor style at
  17. * all. Read both off disk instead.
  18. * ---------------------------------------------------------------------- */
  19. add_filter( 'block_editor_settings_all', function ( $settings, $context ) {
  20.  
  21. if ( ! class_exists( 'Oxygen_Gutenberg' ) || ! defined( 'CT_FW_URI' ) ) {
  22. return $settings;
  23. }
  24.  
  25. // Post editor only - the site editor has no Oxygen blocks.
  26. if ( empty( $context->post ) ) {
  27. return $settings;
  28. }
  29.  
  30. $files = array();
  31.  
  32. // Component framework base stylesheet.
  33. $files[] = str_replace(
  34. trailingslashit( plugins_url() ),
  35. trailingslashit( WP_PLUGIN_DIR ),
  36. CT_FW_URI
  37. ) . '/oxygen.css';
  38.  
  39. $upload = wp_upload_dir();
  40. $css_dir = trailingslashit( $upload['basedir'] ) . 'oxygen/css/';
  41.  
  42. // Oxygen's generated universal stylesheet (flex containers, sections...).
  43. $files[] = $css_dir . 'universal.css';
  44.  
  45. // Per-post stylesheet for every post registered as a block, so any block
  46. // that can be inserted is styled once inserted.
  47. $block_ids = get_posts( array(
  48. 'post_type' => 'oxy_user_library',
  49. 'post_status' => 'publish',
  50. 'posts_per_page' => -1,
  51. 'fields' => 'ids',
  52. ) );
  53.  
  54. $full_page_ids = get_posts( array(
  55. 'post_type' => 'page',
  56. 'post_status' => 'publish',
  57. 'posts_per_page' => -1,
  58. 'fields' => 'ids',
  59. 'meta_key' => '_ct_oxygenberg_full_page_block',
  60. 'meta_value' => '1',
  61. ) );
  62.  
  63. foreach ( array_merge( $block_ids, $full_page_ids ) as $id ) {
  64. $files[] = $css_dir . $id . '.css';
  65. }
  66.  
  67. $css = '';
  68. foreach ( $files as $file ) {
  69. if ( is_readable( $file ) ) {
  70. $css .= file_get_contents( $file ) . "\n";
  71. }
  72. }
  73.  
  74. if ( '' === trim( $css ) ) {
  75. return $settings;
  76. }
  77.  
  78. if ( ! isset( $settings['styles'] ) || ! is_array( $settings['styles'] ) ) {
  79. $settings['styles'] = array();
  80. }
  81.  
  82. $settings['styles'][] = array(
  83. 'css' => $css,
  84. '__unstableType' => 'theme',
  85. 'isGlobalStyles' => false,
  86. );
  87.  
  88. return $settings;
  89.  
  90. }, 10, 2 );
  91.  
  92.  
  93. /* -------------------------------------------------------------------------
  94. * 2. Behaviour - re-implement in-canvas editing against the iframe document,
  95. * and mirror Oxygen's webfonts into it.
  96. * ---------------------------------------------------------------------- */
  97. add_action( 'admin_enqueue_scripts', function () {
  98.  
  99. if ( ! wp_script_is( 'oxygen-vsb-gutenberg-blocks-generator', 'enqueued' ) ) {
  100. return;
  101. }
  102.  
  103. $js = <<<'JS'
  104. ( function () {
  105.  
  106. if ( ! window.wp || ! wp.data || ! wp.data.select ) { return; }
  107. if ( window.__oxyCanvasFixLoaded ) { return; }
  108. window.__oxyCanvasFixLoaded = true;
  109.  
  110. // Oxygen attribute keys are "<elementId>_<type>". Only these types are
  111. // edited in the canvas; link/icon/alt/background come from the sidebar,
  112. // which lives in the top document and still works.
  113. var CANVAS_TYPES = [ 'string', 'richtext', 'image' ];
  114.  
  115. var stats = { docs: 0, candidates: 0, resolved: 0, bound: 0, unresolved: [] };
  116.  
  117. function canvasDocument() {
  118. var frame = document.querySelector( 'iframe[name="editor-canvas"]' );
  119. if ( ! frame ) { return null; }
  120. try {
  121. var doc = frame.contentDocument;
  122. return ( doc && doc.body ) ? doc : null;
  123. } catch ( e ) {
  124. return null;
  125. }
  126. }
  127.  
  128. function getBlock( clientId ) {
  129. return wp.data.select( 'core/block-editor' ).getBlock( clientId );
  130. }
  131.  
  132. // The integration rewrites the "oxygenberg-<selector>" class to
  133. // "oxygenberg-<clientId>" when it renders the block, so the class list
  134. // tells us which block instance an element belongs to.
  135. function resolve( el ) {
  136. var elementId = el.getAttribute( 'id' );
  137. if ( ! elementId ) { return null; }
  138.  
  139. for ( var i = 0; i < el.classList.length; i++ ) {
  140. var cls = el.classList[ i ];
  141. if ( cls === 'oxygenberg-element' || cls.indexOf( 'oxygenberg-' ) !== 0 ) { continue; }
  142.  
  143. var clientId = cls.slice( 'oxygenberg-'.length );
  144. var block = getBlock( clientId );
  145. if ( ! block || ! block.attributes ) { continue; }
  146.  
  147. for ( var t = 0; t < CANVAS_TYPES.length; t++ ) {
  148. var key = elementId + '_' + CANVAS_TYPES[ t ];
  149. if ( Object.prototype.hasOwnProperty.call( block.attributes, key ) ) {
  150. return { clientId: clientId, key: key, type: CANVAS_TYPES[ t ] };
  151. }
  152. }
  153. }
  154. return null;
  155. }
  156.  
  157. function makeTextEditable( el, info ) {
  158. // Mirrors the integration's own guard: skip elements that still hold
  159. // un-substituted markers for nested editable elements.
  160. if ( el.innerHTML.indexOf( '<oxygenberg' ) !== -1 ) { return; }
  161.  
  162. el.setAttribute( 'contenteditable', 'true' );
  163. el.style.cursor = 'text';
  164. el.style.pointerEvents = 'auto';
  165. el.style.webkitUserSelect = 'text';
  166. el.style.userSelect = 'text';
  167.  
  168. // The block editor's own mousedown handler selects the block and can
  169. // preventDefault, which stops the browser placing a caret inside our
  170. // contenteditable. Take the event, then select the block ourselves so
  171. // the sidebar still follows along.
  172. el.addEventListener( 'mousedown', function ( e ) {
  173. e.stopPropagation();
  174. wp.data.dispatch( 'core/block-editor' ).selectBlock( info.clientId );
  175. if ( el.ownerDocument.activeElement !== el ) { el.focus(); }
  176. } );
  177.  
  178. el.addEventListener( 'blur', function () {
  179. var attrs = {};
  180. attrs[ info.key ] = el.innerHTML;
  181. wp.data.dispatch( 'core/block-editor' ).updateBlockAttributes( info.clientId, attrs );
  182. } );
  183.  
  184. // Keep Backspace/Delete/Enter from reaching the block editor, which
  185. // would otherwise remove or split the block while you type in it.
  186. el.addEventListener( 'keydown', function ( e ) {
  187. if ( e.key === 'Backspace' || e.key === 'Delete' || e.key === 'Enter' ) {
  188. e.stopPropagation();
  189. }
  190. } );
  191.  
  192. // Paste as plain text, as the integration does on non-iframed canvases.
  193. el.addEventListener( 'paste', function ( e ) {
  194. e.preventDefault();
  195. var text = ( e.clipboardData || window.clipboardData ).getData( 'text' );
  196. el.ownerDocument.execCommand( 'insertHTML', false, text );
  197. } );
  198. }
  199.  
  200. function makeImageEditable( el, info ) {
  201. if ( ! wp.media ) { return; }
  202.  
  203. el.removeAttribute( 'srcset' );
  204. el.style.cursor = 'pointer';
  205. el.title = 'Click to change the image';
  206.  
  207. el.addEventListener( 'click', function ( e ) {
  208. e.preventDefault();
  209. e.stopPropagation();
  210.  
  211. var frame = wp.media( {
  212. title: 'Select a replacement image',
  213. button: { text: 'Use this image' },
  214. multiple: false
  215. } );
  216.  
  217. frame.on( 'select', function () {
  218. var attachment = frame.state().get( 'selection' ).first().toJSON();
  219. var attrs = {};
  220. attrs[ info.key ] = attachment.url;
  221.  
  222. // Keep the matching alt attribute in sync when there is one.
  223. var altKey = info.key.slice( 0, -'image'.length ) + 'alt';
  224. var block = getBlock( info.clientId );
  225. if ( block && Object.prototype.hasOwnProperty.call( block.attributes, altKey ) ) {
  226. attrs[ altKey ] = attachment.alt || '';
  227. }
  228.  
  229. wp.data.dispatch( 'core/block-editor' ).updateBlockAttributes( info.clientId, attrs );
  230. } );
  231.  
  232. frame.open();
  233. } );
  234. }
  235.  
  236. function mirrorFonts( doc ) {
  237. // Oxygen prints its Google Fonts <link>s into the admin footer of the
  238. // top document, where the canvas cannot see them.
  239. var sources = document.querySelectorAll( 'link[rel="stylesheet"][href*="fonts.googleapis.com"]' );
  240. for ( var i = 0; i < sources.length; i++ ) {
  241. var href = sources[ i ].getAttribute( 'href' );
  242. if ( ! href ) { continue; }
  243. if ( doc.querySelector( 'link[href="' + href.replace( /"/g, '\\"' ) + '"]' ) ) { continue; }
  244.  
  245. var link = doc.createElement( 'link' );
  246. link.rel = 'stylesheet';
  247. link.href = href;
  248. doc.head.appendChild( link );
  249. }
  250. }
  251.  
  252. function enhance( doc ) {
  253. if ( ! doc.__oxyLinkGuard ) {
  254. doc.__oxyLinkGuard = true;
  255. stats.docs++;
  256. // Anchors inside Oxygen blocks must not navigate the canvas away.
  257. doc.addEventListener( 'click', function ( e ) {
  258. var link = e.target && e.target.closest ? e.target.closest( '.oxygenberg-element a, a.oxygenberg-element' ) : null;
  259. if ( link ) { e.preventDefault(); }
  260. }, true );
  261. }
  262.  
  263. var candidates = doc.querySelectorAll( '[class*="oxygenberg-"]' );
  264. stats.candidates = candidates.length;
  265.  
  266. for ( var i = 0; i < candidates.length; i++ ) {
  267. var el = candidates[ i ];
  268. if ( el.__oxyCanvasBound ) { continue; }
  269.  
  270. var info = resolve( el );
  271. if ( ! info ) {
  272. var id = el.getAttribute( 'id' );
  273. if ( id && stats.unresolved.indexOf( id ) === -1 ) { stats.unresolved.push( id ); }
  274. continue;
  275. }
  276.  
  277. el.__oxyCanvasBound = true;
  278. stats.resolved++;
  279.  
  280. if ( info.type === 'string' || info.type === 'richtext' ) {
  281. makeTextEditable( el, info );
  282. stats.bound++;
  283. } else if ( info.type === 'image' ) {
  284. makeImageEditable( el, info );
  285. stats.bound++;
  286. }
  287. }
  288. }
  289.  
  290. // The canvas iframe is created, and its contents replaced, at unpredictable
  291. // times (block re-renders, device preview switches, editor remounts), so
  292. // poll rather than hooking a single lifecycle event.
  293. setInterval( function () {
  294. var doc = canvasDocument();
  295. if ( ! doc ) { return; }
  296. mirrorFonts( doc );
  297. enhance( doc );
  298. }, 500 );
  299.  
  300. // Type oxyFixReport() in the browser console to see what the shim found.
  301. window.oxyFixReport = function () {
  302. var doc = canvasDocument();
  303. console.log( 'canvas iframe found:', !! doc );
  304. console.log( 'elements with an oxygenberg- class:', stats.candidates );
  305. console.log( 'resolved to a block attribute:', stats.resolved );
  306. console.log( 'made editable:', stats.bound );
  307. console.log( 'unresolved element ids:', stats.unresolved );
  308. if ( doc ) {
  309. console.log( 'contenteditable in canvas:', doc.querySelectorAll( '[contenteditable="true"]' ).length );
  310. }
  311. return stats;
  312. };
  313.  
  314. } )();
  315. JS;
  316.  
  317. wp_add_inline_script( 'oxygen-vsb-gutenberg-blocks-generator', $js );
  318.  
  319. }, 100 );
  320.  
Advertisement
Add Comment
Please, Sign In to add comment