diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergWebViewActivity.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergWebViewActivity.java index 2b3302b75e257..c4ae7e350f4cb 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergWebViewActivity.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergWebViewActivity.java @@ -40,7 +40,7 @@ public class GutenbergWebViewActivity extends AppCompatActivity { public static final String ARG_BLOCK_CONTENT = "block_content"; private static final String INJECT_LOCAL_STORAGE_SCRIPT_TEMPLATE = "localStorage.setItem('WP_DATA_USER_%d','%s')"; - private static final String INJECT_CSS_SCRIPT_TEMPLATE = "window.injectCss('%s')"; + private static final String INJECT_CSS_SCRIPT_TEMPLATE = "window.injectCss('%s', '%s')"; private static final String INJECT_GET_HTML_POST_CONTENT_SCRIPT = "window.getHTMLPostContent();"; private static final String INJECT_ON_SHOW_CONTEXT_MENU_SCRIPT = "window.onShowContextMenu();"; private static final String INJECT_ON_HIDE_CONTEXT_MENU_SCRIPT = "window.onHideContextMenu();"; @@ -327,16 +327,16 @@ private void injectCssScript() { mWebView.evaluateJavascript(injectCssScript, message -> { if (message != null) { String editorStyle = getFileContentFromAssets("gutenberg-web-single-block/editor-style-overrides.css"); - editorStyle = removeWhiteSpace(removeNewLines(editorStyle)); - evaluateJavaScript(String.format(INJECT_CSS_SCRIPT_TEMPLATE, editorStyle)); + editorStyle = removeNewLines(editorStyle); + evaluateJavaScript(String.format(INJECT_CSS_SCRIPT_TEMPLATE, editorStyle, "editor-style-overrides")); String injectWPBarsCssScript = getFileContentFromAssets("gutenberg-web-single-block/wp-bar-override.css"); injectWPBarsCssScript = removeWhiteSpace(removeNewLines(injectWPBarsCssScript)); - evaluateJavaScript(String.format(INJECT_CSS_SCRIPT_TEMPLATE, injectWPBarsCssScript)); + evaluateJavaScript(String.format(INJECT_CSS_SCRIPT_TEMPLATE, injectWPBarsCssScript, "wp-bar-override")); String injectExternalCssScript = getOnGutenbergReadyExternalStyles(); injectExternalCssScript = removeWhiteSpace(removeNewLines(injectExternalCssScript)); - evaluateJavaScript(String.format(INJECT_CSS_SCRIPT_TEMPLATE, injectExternalCssScript)); + evaluateJavaScript(String.format(INJECT_CSS_SCRIPT_TEMPLATE, injectExternalCssScript, "external-styles")); } }); } diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/content-functions.js b/packages/react-native-bridge/common/gutenberg-web-single-block/content-functions.js index d6e7d51375d44..611701c237b09 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/content-functions.js +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/content-functions.js @@ -25,19 +25,12 @@ window.getHTMLPostContent = () => { }; window.insertBlock = ( blockHTML ) => { - const { blockEditorSelect, blockEditorDispatch } = - window.getBlockEditorStore(); - // Setup the editor with the inserted block. const post = window.wp.data.select( 'core/editor' ).getCurrentPost(); window.wp.data .dispatch( 'core/editor' ) .setupEditor( post, { content: blockHTML } ); - // Select the first block. - const clientId = blockEditorSelect.getBlocks()[ 0 ].clientId; - blockEditorDispatch.selectBlock( clientId ); - window.contentIncerted = true; }; diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js index 0cfa0e9985fa0..09dcd6447824d 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-behavior-overrides.js @@ -1,59 +1,167 @@ -// Listeners for native context menu visibility changes. -let isContextMenuVisible = false; -const hideContextMenuListeners = []; - -window.onShowContextMenu = () => { - isContextMenuVisible = true; -}; -window.onHideContextMenu = () => { - isContextMenuVisible = false; - while ( hideContextMenuListeners.length > 0 ) { - const listener = hideContextMenuListeners.pop(); - listener(); - } -}; - -/* -This is a fix for a text selection quirk in the UBE. -It notifies the Android app to dismiss the text selection -context menu when certain menu items are tapped. This is -done via the 'hideTextSelectionContextMenu' method, which -is sent back to the Android app, where the dismissal is -then handle. See PR for further details: -https://github.com/WordPress/gutenberg/pull/34668 -*/ -window.addEventListener( - 'click', - ( event ) => { - const selected = document.getSelection(); - if ( ! isContextMenuVisible || ! selected || ! selected.toString() ) { - return; +/** + * Detects whether the user agent is Android. + * + * @return {boolean} Whether the user agent is Android. + */ +function isAndroid() { + return !! window.navigator.userAgent.match( /Android/ ); +} + +/** + * This is a fix for a text selection quirk in the UBE. It notifies the Android + * app to dismiss the text selection context menu when certain menu items are + * tapped. This is done via the 'hideTextSelectionContextMenu' method, which + * is sent back to the Android app, where the dismissal is then handle. + * + * @return {void} + * @see https://github.com/WordPress/gutenberg/pull/34668 + */ +function manageTextSelectonContextMenu() { + // Listeners for native context menu visibility changes. + let isContextMenuVisible = false; + const hideContextMenuListeners = []; + + window.onShowContextMenu = () => { + isContextMenuVisible = true; + }; + window.onHideContextMenu = () => { + isContextMenuVisible = false; + while ( hideContextMenuListeners.length > 0 ) { + const listener = hideContextMenuListeners.pop(); + listener(); } + }; - // Check if the event is triggered by a dropdown - // toggle button. - const dropdownToggles = document.querySelectorAll( - '.components-dropdown-menu > button' - ); - let currentToggle; - for ( const node of dropdownToggles.values() ) { - if ( node.contains( event.target ) ) { - currentToggle = node; - break; + window.addEventListener( + 'click', + ( event ) => { + const selected = document.getSelection(); + if ( + ! isContextMenuVisible || + ! selected || + ! selected.toString() + ) { + return; } - } - // Hide text selection context menu when the click - // is triggered by a dropdown toggle. - // - // NOTE: The event propagation is prevented because - // it will be dispatched after the context menu - // is hidden. - if ( currentToggle ) { - event.stopPropagation(); - hideContextMenuListeners.push( () => currentToggle.click() ); - window.wpwebkit.hideTextSelectionContextMenu(); + // Check if the event is triggered by a dropdown + // toggle button. + const dropdownToggles = document.querySelectorAll( + '.components-dropdown-menu > button' + ); + let currentToggle; + for ( const node of dropdownToggles.values() ) { + if ( node.contains( event.target ) ) { + currentToggle = node; + break; + } + } + + // Hide text selection context menu when the click + // is triggered by a dropdown toggle. + // + // NOTE: The event propagation is prevented because + // it will be dispatched after the context menu + // is hidden. + if ( currentToggle ) { + event.stopPropagation(); + hideContextMenuListeners.push( () => currentToggle.click() ); + window.wpwebkit.hideTextSelectionContextMenu(); + } + }, + true + ); +} + +if ( isAndroid() ) { + manageTextSelectonContextMenu(); +} + +const editor = document.querySelector( '#editor' ); + +function _toggleBlockSelectedClass( isBlockSelected ) { + if ( isBlockSelected ) { + editor.classList.add( 'is-block-selected' ); + } else { + editor.classList.remove( 'is-block-selected' ); + } +} + +/** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ + +/** + * Toggle the `is-block-selected` class on the editor container when a block is + * selected. This is used to hide the sidebar toggle button when a block is not + * selected. + * + * @param {WPDataRegistry} registry Data registry. + * @return {WPDataRegistry} Modified data registry. + */ +function toggleBlockSelectedStyles( registry ) { + return { + dispatch: ( namespace ) => { + const namespaceName = + typeof namespace === 'string' ? namespace : namespace.name; + const actions = { ...registry.dispatch( namespaceName ) }; + + const originalSelectBlockAction = actions.selectBlock; + actions.selectBlock = ( ...args ) => { + _toggleBlockSelectedClass( true ); + return originalSelectBlockAction( ...args ); + }; + + const originalClearSelectedBlockAction = actions.clearSelectedBlock; + actions.clearSelectedBlock = ( ...args ) => { + _toggleBlockSelectedClass( false ); + return originalClearSelectedBlockAction( ...args ); + }; + + return actions; + }, + }; +} + +window.wp.data.use( toggleBlockSelectedStyles ); + +// The editor-canvas iframe relies upon `srcdoc`, which does not trigger a +// `load` event. Thus, we must poll for the iframe to be ready. +let overrideAttempts = 0; +const overrideInterval = setInterval( () => { + overrideAttempts++; + const overrideStyles = document.querySelector( '#editor-style-overrides' ); + const canvasIframe = document.querySelector( + 'iframe[name="editor-canvas"]' + ); + + if ( + overrideStyles && + canvasIframe && + canvasIframe.contentDocument && + canvasIframe.contentDocument.documentElement + ) { + clearInterval( overrideInterval ); + + // Clone the editor styles so that they can be copied to the iframe, as + // elements within an iframe cannot be styled from the parent context. + const overrideStylesClone = overrideStyles.cloneNode( true ); + overrideStylesClone.id = 'editor-styles-overrides-2'; + // Append to document rather than the head, as React will remove this + // mutation. + canvasIframe.contentDocument.documentElement.appendChild( + overrideStylesClone + ); + + // Select the first block. + const { blockEditorSelect, blockEditorDispatch } = + window.getBlockEditorStore(); + const firstBlock = blockEditorSelect.getBlocks()[ 0 ]; + if ( firstBlock ) { + blockEditorDispatch.selectBlock( firstBlock.clientId ); } - }, - true -); + } + + // Safeguard against an infinite loop. + if ( overrideAttempts > 100 ) { + clearInterval( overrideInterval ); + } +}, 300 ); diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css index 7aa208abe5537..f8f2e8fe2b4cd 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css @@ -17,14 +17,22 @@ display: none; } -/* - Hiddes the top bar header by setting its height to 0 - We can\'t remove it since the block toolbar is a child of it. - */ +/* Right align post header children as we will only display one child */ .edit-post-header { - height: 0px; - padding: 0px; - overflow: hidden; + justify-content: flex-end; +} + +/* Hide post controls unrelated to editing a single block */ +.edit-post-header__toolbar, +.edit-post-layout .edit-post-header .edit-post-header__settings > *, +.interface-pinned-items > * { + display: none; +} + +/* Display the sidebar toggle button whenever a block is selected */ +.edit-post-layout .edit-post-header .edit-post-header__settings .interface-pinned-items, +.is-block-selected .edit-post-header__settings .interface-pinned-items > button:first-child { + display: flex; } /* Move the block toolbar to the top */ @@ -36,6 +44,11 @@ top: 0px; } +/* Hide block actions unrelated to editing a single block */ +.block-editor-block-settings-menu { + display: none; +} + /* Moves the whole editor to the top. There was an extra top margin after removing the WP Admin bar. @@ -69,38 +82,6 @@ display: none; } -/* - Load second button in component menu group but hide it from view. - This is to fix a Chrome-specific bug that occurs if this button is set to "display: none;" - For additional context, see: https://github.com/WordPress/gutenberg/pull/33740 -*/ -.components-dropdown-menu__menu - > .components-menu-group - > div - > button:nth-child( 2 ) { - display: block; - min-height: 0; - height: 0; - padding: 0; -} - -.components-menu-group > div > button:nth-child( 2 ) > span { - display: none; -} - -.components-button:focus:not( :disabled ) { - box-shadow: none; -} - -/* Remove \'delete block\' button inside \'...\' button in block toolbar */ -.components-dropdown-menu__menu > div:not(:first-child) { - display: none; -} - -.components-dropdown-menu__menu > div:first-child { - padding-bottom: 0; -} - /* Some Themes can overwrite values on \'editor-styles-wrapper\'. This will ensure that the top padding is correct on our single-block version of gutenberg web. diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/inject-css.js b/packages/react-native-bridge/common/gutenberg-web-single-block/inject-css.js index 60ac677bd20f5..483e742e780bc 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/inject-css.js +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/inject-css.js @@ -1,8 +1,9 @@ const injectCss = ` -window.injectCss = (css) => { +window.injectCss = (css, id) => { const style = document.createElement('style'); style.innerHTML = css; style.type = 'text/css'; + style.id = id; document.head.appendChild(style); } `; diff --git a/packages/react-native-bridge/ios/GutenbergWebFallback/FallbackJavascriptInjection.swift b/packages/react-native-bridge/ios/GutenbergWebFallback/FallbackJavascriptInjection.swift index 13ae9dd1f0573..ee75dc0968a58 100644 --- a/packages/react-native-bridge/ios/GutenbergWebFallback/FallbackJavascriptInjection.swift +++ b/packages/react-native-bridge/ios/GutenbergWebFallback/FallbackJavascriptInjection.swift @@ -19,6 +19,7 @@ public struct FallbackJavascriptInjection { public let preventAutosavesScript: WKUserScript public let getHtmlContentScript = "window.getHTMLPostContent()".toJsScript() public let gutenbergObserverScript: WKUserScript + public let editorBehaviorScript: WKUserScript /// Init an instance of GutenbergWebJavascriptInjection or throws if any of the required sources doesn't exist. /// This helps to cach early any possible error due to missing source files. @@ -31,7 +32,7 @@ public struct FallbackJavascriptInjection { } func getInjectCssScript(with source: SourceFile) throws -> WKUserScript { - "window.injectCss(`\(try source.getContent())`)".toJsScript() + "window.injectCss(`\(try source.getContent())`, `\(source.getName())`)".toJsScript() } userContentScripts = [ @@ -44,6 +45,7 @@ public struct FallbackJavascriptInjection { injectEditorCssScript = try getInjectCssScript(with: .editorStyle) preventAutosavesScript = try script(with: .preventAutosaves) gutenbergObserverScript = try script(with: .gutenbergObserver) + editorBehaviorScript = try script(with: .editorBehavior) let localStorageJsonString = try SourceFile.localStorage.getContent().removingSpacesAndNewLines() let scriptString = String(format: injectLocalStorageScriptTemplate, userId, localStorageJsonString) diff --git a/packages/react-native-bridge/ios/GutenbergWebFallback/GutenbergWebSingleBlockViewController.swift b/packages/react-native-bridge/ios/GutenbergWebFallback/GutenbergWebSingleBlockViewController.swift index aa83095f058f1..8d11028728a89 100644 --- a/packages/react-native-bridge/ios/GutenbergWebFallback/GutenbergWebSingleBlockViewController.swift +++ b/packages/react-native-bridge/ios/GutenbergWebFallback/GutenbergWebSingleBlockViewController.swift @@ -76,6 +76,7 @@ open class GutenbergWebSingleBlockViewController: UIViewController { onGutenbergReadyScripts().forEach(evaluateJavascript) evaluateJavascript(jsInjection.preventAutosavesScript) evaluateJavascript(jsInjection.insertBlockScript) + evaluateJavascript(jsInjection.editorBehaviorScript) DispatchQueue.main.async { [weak self] in self?.removeCoverViewAnimated() } diff --git a/packages/react-native-bridge/ios/SourceFile.swift b/packages/react-native-bridge/ios/SourceFile.swift index 2f75e22278c0f..2386f84580464 100644 --- a/packages/react-native-bridge/ios/SourceFile.swift +++ b/packages/react-native-bridge/ios/SourceFile.swift @@ -29,6 +29,10 @@ public struct SourceFile { } extension SourceFile { + public func getName() -> String { + return self.name + } + public func jsScript(with argument: String? = nil) throws -> WKUserScript { let content = try getContent() let formatted = String(format: content, argument ?? []) @@ -53,4 +57,5 @@ extension SourceFile { static let preventAutosaves = SourceFile(name: "prevent-autosaves", type: .js) static let gutenbergObserver = SourceFile(name: "gutenberg-observer", type: .js) static let supportedBlocks = SourceFile(name: "supported-blocks", type: .json) + static let editorBehavior = SourceFile(name: "editor-behavior-overrides", type: .js) } diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 8b8c395a67d9e..e34f5bfd753ee 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,9 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Add metadata parameter to media upload events [#48103] +## 1.89.1 +- [*] Fix inaccessible block settings within the unsupported block editor [#48435] + ## 1.89.0 * No User facing changes *