Skip to content

Commit

Permalink
fix: Enable access to block settings within UBE (#48435)
Browse files Browse the repository at this point in the history
* fix: Enable access to block settings within UBE

The "Show more settings" menu item is no longer included in the block
toolbar after #46709 merged. Rather than relying upon that menu item,
this conditionally displays the sidebar toggle button whenever a block
is selected.

* fix: Disable white space stripping that breaks nested CSS selectors

CSS selectors rely upon a single white space between selectors to
represent an ancestor relationship. Globally removing white space in the
stylesheet breaks this functionality, as it transforms the selector to
target a single element with all the selectors.

The white space stripping should likely be replaced with a proper CSS
minification long term.

* fix: Hide block actions unrelated to editing a single block

Hide the entire "block settings" drop-down menu now that we no longer
rely upon it to access the "Show more settings" menu option that was
removed entirely.

* refactor: Relocate script toggling block settings visibility

This relates more to editor behavior than the post content.

* fix: Apply styles and script to editor canvas iframe

The editor canvas now relies upon an iframe. It is not possible to style
elements within an iframe from the parent context. This copies the
styles from the parent conext to the iframe.

Additionally, the logic selecting the first block also failed due to the
block not existing when it was invoked. This relocates that logic until
after the iframe is ready.

* fix: Expand conditional checks for partial DOM trees

On Android, there were times where the iframe was present, but the
nested window was not yet ready.

* refactor: Rename for brevity

* fix: Avoid React removing appended iframe styles

Append the styles to the `document` element, as React will remove the
mutation to the `head` element.

* docs: Add change log entry
  • Loading branch information
dcalhoun authored Mar 1, 2023
1 parent ce49c68 commit aefb9c8
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();";
Expand Down Expand Up @@ -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"));
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 );
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 = [
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Loading

0 comments on commit aefb9c8

Please sign in to comment.