Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Enable access to block settings within UBE #48435

Merged
merged 10 commits into from
Mar 1, 2023
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));
Copy link
Member Author

@dcalhoun dcalhoun Feb 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rudimentary stripping of whitespace inadvertently modifies the CSS selectors, causing them to be interpreted as completely different selectors. E.g. .ancestor .descendent is different from .sibling-1.sibling-2. Ideally, a proper CSS minification is utilized long term.

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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This often threw an error where the first block was undefined. This logic was relocated to editor-behavior-overrides to await the iframe render.

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( () => {
Comment on lines +126 to +129
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explored a few different approaches for awaiting the iframe render. Unfortunately, none of them succeeded.

  • iframe.onload does not work when the iframe uses srcdoc instead of src.
  • Awaiting a "ready Gutenberg" via subscribing to the store runs before the iframe renders.

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(
Comment on lines +148 to +150
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iframe implementation "manages" the head element. So, we must avoid mutating that element, otherwise React may remove any changes we make.

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