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

Edit unsupported blocks in a RN web view #21832

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 84 additions & 22 deletions packages/block-library/src/missing/edit.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,29 @@ import { Platform, View, Text, TouchableWithoutFeedback } from 'react-native';
* WordPress dependencies
*/
import { BottomSheet, Icon } from '@wordpress/components';
import { withPreferredColorScheme } from '@wordpress/compose';
import { withPreferredColorScheme, compose } from '@wordpress/compose';
import { coreBlocks } from '@wordpress/block-library';
import { normalizeIconObject } from '@wordpress/blocks';
import { normalizeIconObject, parse } from '@wordpress/blocks';
import { Component } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { help, plugins } from '@wordpress/icons';
import { withDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import styles from './style.scss';
import UnsupportedBlockModal from '../unsupported-block-modal/index.native.js';

export class UnsupportedBlockEdit extends Component {
constructor( props ) {
super( props );
this.state = { showHelp: false };
this.state = { showHelp: false, showUnsupportedBlockModal: false };
this.toggleSheet = this.toggleSheet.bind( this );
this.requestFallback = this.requestFallback.bind( this );
this.closeUnsupportedBlockModal = this.closeUnsupportedBlockModal.bind(
this
);
}

toggleSheet() {
Expand All @@ -31,6 +38,12 @@ export class UnsupportedBlockEdit extends Component {
} );
}

componentWillUnmount() {
if ( this.timeout ) {
clearTimeout( this.timeout );
}
}

renderHelpIcon() {
const infoIconStyle = this.props.getStylesFromColorScheme(
styles.infoIcon,
Expand All @@ -42,7 +55,7 @@ export class UnsupportedBlockEdit extends Component {
accessibilityLabel={ __( 'Help icon' ) }
accessibilityRole={ 'button' }
accessibilityHint={ __( 'Tap here to show help' ) }
onPress={ this.toggleSheet.bind( this ) }
onPress={ this.toggleSheet }
>
<View style={ styles.helpIconContainer }>
<Icon
Expand All @@ -56,6 +69,17 @@ export class UnsupportedBlockEdit extends Component {
);
}

requestFallback() {
// this.setState( { showHelp: false } );
this.setState( { showUnsupportedBlockModal: true } );
}

closeUnsupportedBlockModal( updatedContent ) {
this.setState( { showUnsupportedBlockModal: false } );
const updatedBlock = parse( updatedContent );
this.props.replaceBlockWithHTML( updatedBlock );
}

renderSheet( title ) {
const { getStylesFromColorScheme } = this.props;
const infoTextStyle = getStylesFromColorScheme(
Expand Down Expand Up @@ -87,7 +111,7 @@ export class UnsupportedBlockEdit extends Component {
<BottomSheet
isVisible={ this.state.showHelp }
hideHeader
onClose={ this.toggleSheet.bind( this ) }
onClose={ this.toggleSheet }
>
<View style={ styles.infoContainer }>
<Icon
Expand All @@ -104,12 +128,32 @@ export class UnsupportedBlockEdit extends Component {
) }
</Text>
</View>
{ // eslint-disable-next-line no-undef
__DEV__ && (
<>
<BottomSheet.Cell
label={ __( 'Edit post on Web Browser' ) }
separatorType="topFullWidth"
onPress={ () => {
this.toggleSheet();
this.timeout = setTimeout( () => {
this.requestFallback();
}, 1000 );
} }
/>
<BottomSheet.Cell
label={ __( 'Dismiss' ) }
separatorType="topFullWidth"
onPress={ this.toggleSheet }
/>
</>
) }
</BottomSheet>
);
}

render() {
const { originalName } = this.props.attributes;
const { originalName, originalContent } = this.props.attributes;
const { getStylesFromColorScheme, preferredColorScheme } = this.props;
const blockType = coreBlocks[ originalName ];

Expand All @@ -136,24 +180,42 @@ export class UnsupportedBlockEdit extends Component {
);
const iconClassName = 'unsupported-icon' + '-' + preferredColorScheme;
return (
<View
style={ getStylesFromColorScheme(
styles.unsupportedBlock,
styles.unsupportedBlockDark
) }
>
{ this.renderHelpIcon() }
<Icon
className={ iconClassName }
icon={ icon && icon.src ? icon.src : icon }
color={ iconStyle.color }
<>
<UnsupportedBlockModal
visible={ this.state.showUnsupportedBlockModal }
blockHTML={ originalContent }
closeModal={ ( updatedContent ) =>
this.closeUnsupportedBlockModal( updatedContent )
}
/>
<Text style={ titleStyle }>{ title }</Text>
{ subtitle }
{ this.renderSheet( title ) }
</View>
<View
style={ getStylesFromColorScheme(
styles.unsupportedBlock,
styles.unsupportedBlockDark
) }
>
{ this.renderHelpIcon() }
<Icon
className={ iconClassName }
icon={ icon && icon.src ? icon.src : icon }
color={ iconStyle.color }
/>
<Text style={ titleStyle }>{ title }</Text>
{ subtitle }
{ this.renderSheet( title ) }
</View>
</>
);
}
}

export default withPreferredColorScheme( UnsupportedBlockEdit );
export default compose( [
withDispatch( ( dispatch, { clientId } ) => {
return {
replaceBlockWithHTML( html ) {
dispatch( 'core/block-editor' ).replaceBlock( clientId, html );
},
};
} ),
withPreferredColorScheme,
] )( UnsupportedBlockEdit );
Empty file.
140 changes: 140 additions & 0 deletions packages/block-library/src/unsupported-block-modal/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* External dependencies
*/
import { Button, Modal, SafeAreaView } from 'react-native';
import WebView from 'react-native-webview';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';

export class UnsupportedBlockModal extends Component {
render() {
const { blockHTML } = this.props;

// Escape new-lines
const escapedBlockHTML = blockHTML.replace( /\n/g, '\\n' );
const scriptToInject = `
window.onSave = () => {
const blocks = window.wp.data.select( 'core/block-editor' ).getBlocks();
const HTML = window.wp.blocks.serialize( blocks );
window.ReactNativeWebView.postMessage( HTML );
};

window.insertBlock = () => {
window.setTimeout( () => {
const blockHTML = '${ escapedBlockHTML }';
const blocks = window.wp.blocks.parse( blockHTML );
window.wp.data.dispatch( 'core/block-editor' ).resetBlocks( blocks );
}, 0 );
};

window.onload = () => {
const wpAdminBar = document.getElementById( 'wpadminbar' );
const wpToolbar = document.getElementById( 'wp-toolbar' );
if ( wpAdminBar ) {
wpAdminBar.style.display = 'none';
}
if ( wpToolbar ) {
wpToolbar.style.display = 'none';
}

const content = document.getElementById( 'wpbody-content' );
if ( content ) {
const callback = function( mutationsList, observer ) {
const header = document.getElementsByClassName(
'edit-post-header'
)[ 0 ];
const postTitle = document.getElementById( 'post-title-0' );
if ( postTitle && header.id === '' ) {
header.id = 'gb-header';
header.style.height = 0;
postTitle.style.display = 'none';
Array.from( header.children ).forEach( ( child ) => {
child.style.display = 'none';
} );

const headerToolbar = header.getElementsByClassName(
'edit-post-header-toolbar'
)[ 0 ];
headerToolbar.style.display = null;
headerToolbar.parentNode.style.display = null;
const inserterToggle = header.getElementsByClassName(
'block-editor-inserter__toggle'
)[ 0 ];
inserterToggle.style.display = 'none';

const blockToolbar = header.getElementsByClassName(
'edit-post-header-toolbar__block-toolbar'
)[ 0 ];
blockToolbar.style.top = '0px';

// const skeleton = document.getElementsByClassName(
// 'block-editor-editor-skeleton'
// )[ 0 ];
// skeleton.style.top = '0px';

const appender = document.getElementsByClassName(
'block-list-appender'
)[ 0 ];
if ( appender && appender.id === '' ) {
appender.id = 'appender';
appender.style.display = 'none';
}

window.insertBlock();
observer.disconnect();
}
};
/* eslint-disable-next-line no-undef */
const observer = new MutationObserver( callback );

const config = { attributes: true, childList: true, subtree: true };
observer.observe( content, config );
}
};

/* eslint-disable-next-line no-unused-expressions */
true; // react-native-webview asks for it
`;

return (
<Modal
animationType="slide"
presentationStyle="fullScreen"
visible={ this.props.visible }
onRequestClose={ () => this.props.closeModal() }
>
<SafeAreaView style={ { flex: 1, backgroundColor: 'black' } }>
<Button
onPress={ () => this.props.closeModal() }
title="Close"
color="red"
/>
<Button
onPress={ () =>
this.webref.injectJavaScript( 'window.onSave()' )
}
title="Save"
color="red"
/>
<WebView
ref={ ( r ) => ( this.webref = r ) }
source={ {
uri:
'https://cruisinglamb.wordpress.com/wp-admin/post-new.php',
} }
injectedJavaScript={ scriptToInject }
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have more control over when can we inject JS code?

I have noticed that to avoid some flickering of UI or CSS overrides we will need to inject some snippets as early as possible and others as late as possible.

There's for example this little flash of the WP Admin Bar while the page is loading. So we should inject the "hide WP Admin Bar" CSS earlier. But then the Editor CSS will override the Gutenberg top bar changes since it loads later. And to avoid that, we need to inject the Editor CSS as late as possible (when everything is loaded).

flash

onMessage={ ( event ) => {
const html = event.nativeEvent.data;
this.props.closeModal( html );
} }
/>
</SafeAreaView>
</Modal>
);
}
}

export default UnsupportedBlockModal;
1 change: 1 addition & 0 deletions test/native/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jest.mock( 'react-native-gutenberg-bridge', () => {
subscribeMediaUpload: jest.fn(),
getOtherMediaOptions: jest.fn(),
requestMediaPicker: jest.fn(),
requestUnsupportedBlockFallback: jest.fn(),
mediaSources: {
deviceLibrary: 'DEVICE_MEDIA_LIBRARY',
deviceCamera: 'DEVICE_CAMERA',
Expand Down