From 1629241ea7d35ac60261eb2268987c9a095b0eb0 Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler Date: Mon, 5 Aug 2019 13:48:23 +0200 Subject: [PATCH] Refactor BlockToolbar out of BlockList (#16677) * Move BlockToolbar from BlockList to Layout * Remove BlockEditorProvider from BlockList and add native version of EditorProvider to Editor. Plus support InsertionPoint and BlockListAppender * Revert BlockListAppender and InsertionPoint additions * Fix dismissing block picker * Add missing function in BlockList * Disable add block in HTML mode and show hide keyboard button only when keyboard is shown * Fix bringing back finishInsertingOrReplacingBlock * Fix inserting block in first position when post title is selected * Show insertion point before block if its replaceable * Fix missing shouldPreventAutomaticScroll props for iOS * Fix native tests * Add back bottom View to push block list up * Improve defining toolbar height * Make html view a flexbax * Quickly hide the modal to let the keyboard show up after inserting a new text based block * Let's unmount the modal instead to make sure we don't have timing errors * revert to defining the toolbar height in the component itsef * Revert "Make html view a flexbax" This reverts commit 59f0431520407282188482534d742f8a74ff9765. * Simplify layout * Fix dismiss keyboard on iOS --- .../src/components/block-list/block.native.js | 3 - .../src/components/block-list/index.native.js | 149 ++--------- .../components/block-list/style.native.scss | 8 - .../components/block-toolbar/index.native.js | 122 +++------ .../default-block-appender/index.native.js | 11 +- .../src/components/inserter/index.native.js | 127 ++++++---- .../src/components/inserter/menu.native.js | 217 ++++++++++++++++ .../src/components/inserter/style.native.scss | 7 + .../components/src/dropdown/index.native.js | 62 +++++ packages/components/src/index.native.js | 1 + .../keyboard-aware-flat-list/index.ios.js | 7 +- .../header/header-toolbar/index.native.js | 102 ++++++++ .../header/header-toolbar}/style.native.scss | 8 +- .../src/components/header/index.native.js | 51 ++++ .../src/components/layout/index.native.js | 42 +++- .../src/components/layout/style.native.scss | 7 + .../components/visual-editor/index.native.js | 37 +-- packages/edit-post/src/editor.native.js | 232 +++++++----------- packages/edit-post/src/index.native.js | 1 + .../edit-post/src/store/defaults.native.js | 14 ++ packages/edit-post/src/test/editor.native.js | 2 +- .../convert-to-group-buttons/index.native.js | 2 + .../editor/src/components/index.native.js | 3 + .../src/components/provider/index.native.js | 182 ++++++++++++++ .../reusable-blocks-buttons/index.native.js | 2 + packages/viewport/src/index.native.js | 7 + 26 files changed, 920 insertions(+), 486 deletions(-) create mode 100644 packages/block-editor/src/components/inserter/menu.native.js create mode 100644 packages/components/src/dropdown/index.native.js create mode 100644 packages/edit-post/src/components/header/header-toolbar/index.native.js rename packages/{block-editor/src/components/block-toolbar => edit-post/src/components/header/header-toolbar}/style.native.scss (75%) create mode 100644 packages/edit-post/src/components/header/index.native.js create mode 100644 packages/edit-post/src/store/defaults.native.js create mode 100644 packages/editor/src/components/convert-to-group-buttons/index.native.js create mode 100644 packages/editor/src/components/provider/index.native.js create mode 100644 packages/editor/src/components/reusable-blocks-buttons/index.native.js diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index d836627337635..b87f95445e11f 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -118,8 +118,6 @@ class BlockListBlock extends Component { const accessibilityLabel = this.getAccessibilityLabel(); return ( - // accessible prop needs to be false to access children - // https://facebook.github.io/react-native/docs/accessibility#accessible-ios-android { isSelected && } - ); } diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 5227bce63ecc6..3a193a9d02a9b 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -2,30 +2,26 @@ * External dependencies */ import { identity } from 'lodash'; -import { Text, View, Keyboard, SafeAreaView, Platform, TouchableWithoutFeedback } from 'react-native'; -import { subscribeMediaAppend } from 'react-native-gutenberg-bridge'; +import { Text, View, Platform, TouchableWithoutFeedback } from 'react-native'; /** * WordPress dependencies */ -import { Component, Fragment } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { HTMLTextInput, KeyboardAvoidingView, KeyboardAwareFlatList, ReadableContentView } from '@wordpress/components'; +import { KeyboardAwareFlatList, ReadableContentView } from '@wordpress/components'; /** * Internal dependencies */ import styles from './style.scss'; import BlockListBlock from './block'; -import BlockToolbar from '../block-toolbar'; import DefaultBlockAppender from '../default-block-appender'; -import Inserter from '../inserter'; -const blockMobileToolbarHeight = 44; -const toolbarHeight = 44; +const innerToolbarHeight = 44; export class BlockList extends Component { constructor() { @@ -34,34 +30,11 @@ export class BlockList extends Component { this.renderItem = this.renderItem.bind( this ); this.renderAddBlockSeparator = this.renderAddBlockSeparator.bind( this ); this.renderBlockListFooter = this.renderBlockListFooter.bind( this ); - this.shouldFlatListPreventAutomaticScroll = this.shouldFlatListPreventAutomaticScroll.bind( this ); this.renderDefaultBlockAppender = this.renderDefaultBlockAppender.bind( this ); - this.onBlockTypeSelected = this.onBlockTypeSelected.bind( this ); - this.keyboardDidShow = this.keyboardDidShow.bind( this ); - this.keyboardDidHide = this.keyboardDidHide.bind( this ); this.onCaretVerticalPositionChange = this.onCaretVerticalPositionChange.bind( this ); this.scrollViewInnerRef = this.scrollViewInnerRef.bind( this ); this.getNewBlockInsertionIndex = this.getNewBlockInsertionIndex.bind( this ); - - this.state = { - blockTypePickerVisible: false, - isKeyboardVisible: false, - }; - } - - // TODO: in the near future this will likely be changed to onShowBlockTypePicker and bound to this.props - // once we move the action to the toolbar - showBlockTypePicker( show ) { - this.setState( { blockTypePickerVisible: show } ); - } - - onBlockTypeSelected( itemValue ) { - this.setState( { blockTypePickerVisible: false } ); - - // create an empty block of the selected type - const newBlock = createBlock( itemValue ); - - this.finishBlockAppendingOrReplacing( newBlock ); + this.shouldFlatListPreventAutomaticScroll = this.shouldFlatListPreventAutomaticScroll.bind( this ); } finishBlockAppendingOrReplacing( newBlock ) { @@ -88,48 +61,7 @@ export class BlockList extends Component { } blockHolderBorderStyle() { - return this.state.isFullyBordered ? styles.blockHolderFullBordered : styles.blockHolderSemiBordered; - } - - componentDidMount() { - this._isMounted = true; - Keyboard.addListener( 'keyboardDidShow', this.keyboardDidShow ); - Keyboard.addListener( 'keyboardDidHide', this.keyboardDidHide ); - - this.subscriptionParentMediaAppend = subscribeMediaAppend( ( payload ) => { - // create an empty media block - const newMediaBlock = createBlock( 'core/' + payload.mediaType ); - - // now set the url and id - if ( payload.mediaType === 'image' ) { - newMediaBlock.attributes.url = payload.mediaUrl; - } else if ( payload.mediaType === 'video' ) { - newMediaBlock.attributes.src = payload.mediaUrl; - } - - newMediaBlock.attributes.id = payload.mediaId; - - // finally append or replace as appropriate - this.finishBlockAppendingOrReplacing( newMediaBlock ); - } ); - } - - componentWillUnmount() { - Keyboard.removeListener( 'keyboardDidShow', this.keyboardDidShow ); - Keyboard.removeListener( 'keyboardDidHide', this.keyboardDidHide ); - - if ( this.subscriptionParentMediaAppend ) { - this.subscriptionParentMediaAppend.remove(); - } - this._isMounted = false; - } - - keyboardDidShow() { - this.setState( { isKeyboardVisible: true } ); - } - - keyboardDidHide() { - this.setState( { isKeyboardVisible: false } ); + return this.props.isFullyBordered ? styles.blockHolderFullBordered : styles.blockHolderSemiBordered; } onCaretVerticalPositionChange( targetId, caretY, previousCaretY ) { @@ -141,7 +73,7 @@ export class BlockList extends Component { } shouldFlatListPreventAutomaticScroll() { - return this.state.blockTypePickerVisible; + return this.props.isBlockInsertionPointVisible; } renderDefaultBlockAppender() { @@ -159,7 +91,7 @@ export class BlockList extends Component { ); } - renderList() { + render() { return ( - - - - - { - this.showBlockTypePicker( true ); - } } - showKeyboardHideButton={ this.state.isKeyboardVisible } - /> - ); } - render() { - return ( - - { this.renderList() } - { this.state.blockTypePickerVisible && ( - this.showBlockTypePicker( false ) } - onValueSelected={ this.onBlockTypeSelected } - isReplacement={ this.isReplaceable( this.props.selectedBlock ) } - addExtraBottomPadding={ this.props.safeAreaBottomInset === 0 } - /> - ) } - - ); - } - isReplaceable( block ) { if ( ! block ) { return false; @@ -226,12 +125,10 @@ export class BlockList extends Component { return isUnmodifiedDefaultBlock( block ); } - renderItem( { item: clientId, index } ) { - const shouldShowAddBlockSeparator = this.state.blockTypePickerVisible && ( this.props.isBlockSelected( clientId ) || ( index === 0 && this.props.isPostTitleSelected ) ); - const shouldPutAddBlockSeparatorAboveBlock = this.isReplaceable( this.props.selectedBlock ) || this.props.isPostTitleSelected; - + renderItem( { item: clientId } ) { return ( - + + { this.props.shouldShowInsertionPoint( clientId ) && this.renderAddBlockSeparator() } - { shouldShowAddBlockSeparator && this.renderAddBlockSeparator() } ); } @@ -266,12 +162,6 @@ export class BlockList extends Component { ); } - - renderHTML() { - return ( - - ); - } } export default compose( [ @@ -284,15 +174,28 @@ export default compose( [ getSelectedBlock, getSelectedBlockClientId, isBlockSelected, + getBlockInsertionPoint, + isBlockInsertionPointVisible, } = select( 'core/block-editor' ); const selectedBlockClientId = getSelectedBlockClientId(); + const blockClientIds = getBlockOrder( rootClientId ); + const insertionPoint = getBlockInsertionPoint(); + const shouldShowInsertionPoint = ( clientId ) => { + return ( + isBlockInsertionPointVisible() && + insertionPoint.rootClientId === rootClientId && + blockClientIds[ insertionPoint.index ] === clientId + ); + }; return { - blockClientIds: getBlockOrder( rootClientId ), + blockClientIds, blockCount: getBlockCount( rootClientId ), getBlockName, isBlockSelected, + isBlockInsertionPointVisible: isBlockInsertionPointVisible(), + shouldShowInsertionPoint, selectedBlock: getSelectedBlock(), selectedBlockClientId, selectedBlockOrder: getBlockIndex( selectedBlockClientId ), diff --git a/packages/block-editor/src/components/block-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss index 884c0612f0586..5185e2099bf93 100644 --- a/packages/block-editor/src/components/block-list/style.native.scss +++ b/packages/block-editor/src/components/block-list/style.native.scss @@ -40,13 +40,6 @@ background-color: $white; } -.blockToolbarKeyboardAvoidingView { - position: absolute; - bottom: 0; - right: 0; - left: 0; -} - .blockHolderSemiBordered { border-top-width: 1px; border-bottom-width: 1px; @@ -61,7 +54,6 @@ border-right-width: 1px; } - .blockContainerFocused { background-color: $white; padding-left: 16; diff --git a/packages/block-editor/src/components/block-toolbar/index.native.js b/packages/block-editor/src/components/block-toolbar/index.native.js index 3d12312673bf8..5413e4e1f9cde 100644 --- a/packages/block-editor/src/components/block-toolbar/index.native.js +++ b/packages/block-editor/src/components/block-toolbar/index.native.js @@ -1,105 +1,41 @@ /** - * External dependencies + * WordPress dependencies */ -import { View, ScrollView, Keyboard, Platform } from 'react-native'; +import { withSelect } from '@wordpress/data'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; -import { Toolbar, ToolbarButton, Dashicon } from '@wordpress/components'; import { BlockFormatControls, BlockControls } from '@wordpress/block-editor'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import styles from './style.scss'; - -export class BlockToolbar extends Component { - constructor() { - super( ...arguments ); - this.onKeyboardHide = this.onKeyboardHide.bind( this ); +export const BlockToolbar = ( { blockClientIds, isValid, mode } ) => { + if ( blockClientIds.length === 0 ) { + return null; } - onKeyboardHide() { - this.props.clearSelectedBlock(); - if ( Platform.OS === 'android' ) { - // Avoiding extra blur calls on iOS but still needed for android. - Keyboard.dismiss(); - } - } - - render() { - const { - hasRedo, - hasUndo, - redo, - undo, - onInsertClick, - showKeyboardHideButton, - } = this.props; - - return ( - - - - ) } - onClick={ onInsertClick } - extraProps={ { hint: __( 'Double tap to add a block' ) } } - /> - - - + return ( + <> + { mode === 'visual' && isValid && ( + <> - - { showKeyboardHideButton && - - - - } - - ); - } -} - -export default compose( [ - withSelect( ( select ) => ( { - hasRedo: select( 'core/editor' ).hasEditorRedo(), - hasUndo: select( 'core/editor' ).hasEditorUndo(), - } ) ), - withDispatch( ( dispatch ) => ( { - redo: dispatch( 'core/editor' ).redo, - undo: dispatch( 'core/editor' ).undo, - clearSelectedBlock: dispatch( 'core/editor' ).clearSelectedBlock, - } ) ), -] )( BlockToolbar ); + + ) } + + ); +}; + +export default withSelect( ( select ) => { + const { + getBlockMode, + getSelectedBlockClientIds, + isBlockValid, + } = select( 'core/block-editor' ); + const blockClientIds = getSelectedBlockClientIds(); + + return { + blockClientIds, + isValid: blockClientIds.length === 1 ? isBlockValid( blockClientIds[ 0 ] ) : null, + mode: blockClientIds.length === 1 ? getBlockMode( blockClientIds[ 0 ] ) : null, + }; +} )( BlockToolbar ); diff --git a/packages/block-editor/src/components/default-block-appender/index.native.js b/packages/block-editor/src/components/default-block-appender/index.native.js index 1319fd3fd2ce9..4f361376f6d58 100644 --- a/packages/block-editor/src/components/default-block-appender/index.native.js +++ b/packages/block-editor/src/components/default-block-appender/index.native.js @@ -11,6 +11,7 @@ import { RichText } from '@wordpress/block-editor'; import { compose } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; import { withSelect, withDispatch } from '@wordpress/data'; +import { getDefaultBlockName } from '@wordpress/blocks'; /** * Internal dependencies @@ -28,7 +29,7 @@ export function DefaultBlockAppender( { return null; } - const value = decodeEntities( placeholder ) || __( 'Start writing…' ); + const value = typeof placeholder === 'string' ? decodeEntities( placeholder ) : __( 'Start writing…' ); return ( { - const { getBlockCount, getSettings, getTemplateLock } = select( 'core/block-editor' ); + const { getBlockCount, getBlockName, isBlockValid, getTemplateLock } = select( 'core/block-editor' ); const isEmpty = ! getBlockCount( ownProps.rootClientId ); - const { bodyPlaceholder } = getSettings(); + const isLastBlockDefault = getBlockName( ownProps.lastBlockClientId ) === getDefaultBlockName(); + const isLastBlockValid = isBlockValid( ownProps.lastBlockClientId ); return { - isVisible: isEmpty, + isVisible: isEmpty || ! isLastBlockDefault || ! isLastBlockValid, isLocked: !! getTemplateLock( ownProps.rootClientId ), - placeholder: bodyPlaceholder, }; } ), withDispatch( ( dispatch, ownProps ) => { diff --git a/packages/block-editor/src/components/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js index 2985d61c7811c..90009022f0bd3 100644 --- a/packages/block-editor/src/components/inserter/index.native.js +++ b/packages/block-editor/src/components/inserter/index.native.js @@ -1,12 +1,8 @@ -/** - * External dependencies - */ -import { FlatList, Text, TouchableHighlight, View } from 'react-native'; - /** * WordPress dependencies */ -import { BottomSheet, Icon } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { Dropdown, ToolbarButton, Dashicon } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -16,58 +12,85 @@ import { getUnregisteredTypeHandlerName } from '@wordpress/blocks'; * Internal dependencies */ import styles from './style.scss'; +import InserterMenu from './menu'; + +const defaultRenderToggle = ( { onToggle, disabled } ) => ( + ) } + onClick={ onToggle } + extraProps={ { hint: __( 'Double tap to add a block' ) } } + isDisabled={ disabled } + /> +); class Inserter extends Component { - calculateNumberOfColumns() { - const bottomSheetWidth = BottomSheet.getWidth(); - const { paddingLeft: itemPaddingLeft, paddingRight: itemPaddingRight } = styles.modalItem; - const { paddingLeft: containerPaddingLeft, paddingRight: containerPaddingRight } = styles.content; - const { width: itemWidth } = styles.modalIconWrapper; - const itemTotalWidth = itemWidth + itemPaddingLeft + itemPaddingRight; - const containerTotalWidth = bottomSheetWidth - ( containerPaddingLeft + containerPaddingRight ); - return Math.floor( containerTotalWidth / itemTotalWidth ); + constructor() { + super( ...arguments ); + + this.onToggle = this.onToggle.bind( this ); + this.renderToggle = this.renderToggle.bind( this ); + this.renderContent = this.renderContent.bind( this ); } - render() { - const numberOfColumns = this.calculateNumberOfColumns(); - const bottomPadding = this.props.addExtraBottomPadding && styles.contentBottomPadding; + onToggle( isOpen ) { + const { onToggle } = this.props; + // Surface toggle callback to parent component + if ( onToggle ) { + onToggle( isOpen ); + } + } + + /** + * Render callback to display Dropdown toggle element. + * + * @param {Function} options.onToggle Callback to invoke when toggle is + * pressed. + * @param {boolean} options.isOpen Whether dropdown is currently open. + * + * @return {WPElement} Dropdown toggle element. + */ + renderToggle( { onToggle, isOpen } ) { + const { + disabled, + renderToggle = defaultRenderToggle, + } = this.props; + + return renderToggle( { onToggle, isOpen, disabled } ); + } + + /** + * Render callback to display Dropdown content element. + * + * @param {Function} options.onClose Callback to invoke when dropdown is + * closed. + * + * @return {WPElement} Dropdown content element. + */ + renderContent( { onClose, isOpen } ) { + const { rootClientId, clientId, isAppender } = this.props; + + return ( + + ); + } + + render() { return ( - - - - } - keyExtractor={ ( item ) => item.name } - renderItem={ ( { item } ) => - this.props.onValueSelected( item.name ) }> - - - - - - - { item.title } - - - } - /> - + ); } } diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js new file mode 100644 index 0000000000000..bf2dd780841f9 --- /dev/null +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -0,0 +1,217 @@ +/** + * External dependencies + */ +import { FlatList, View, Text, TouchableHighlight } from 'react-native'; +import { subscribeMediaAppend } from 'react-native-gutenberg-bridge'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { + createBlock, + isUnmodifiedDefaultBlock, +} from '@wordpress/blocks'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { withInstanceId, compose } from '@wordpress/compose'; +import { BottomSheet, Icon } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +export class InserterMenu extends Component { + componentDidMount() { + this.subscriptionParentMediaAppend = subscribeMediaAppend( ( payload ) => { + this.props.onSelect( { + name: 'core/' + payload.mediaType, + initialAttributes: { + id: payload.mediaId, + [ payload.mediaType === 'image' ? 'url' : 'src' ]: payload.mediaUrl, + }, + } ); + } ); + this.onOpen(); + } + + componentWillUnmount() { + if ( this.subscriptionParentMediaAppend ) { + this.subscriptionParentMediaAppend.remove(); + } + this.onClose(); + } + + calculateNumberOfColumns() { + const bottomSheetWidth = BottomSheet.getWidth(); + const { paddingLeft: itemPaddingLeft, paddingRight: itemPaddingRight } = styles.modalItem; + const { paddingLeft: containerPaddingLeft, paddingRight: containerPaddingRight } = styles.content; + const { width: itemWidth } = styles.modalIconWrapper; + const itemTotalWidth = itemWidth + itemPaddingLeft + itemPaddingRight; + const containerTotalWidth = bottomSheetWidth - ( containerPaddingLeft + containerPaddingRight ); + return Math.floor( containerTotalWidth / itemTotalWidth ); + } + + onOpen() { + this.props.showInsertionPoint(); + } + + onClose() { + this.props.hideInsertionPoint(); + } + + render() { + const numberOfColumns = this.calculateNumberOfColumns(); + const bottomPadding = styles.contentBottomPadding; + + return ( + + + + } + keyExtractor={ ( item ) => item.name } + renderItem={ ( { item } ) => + this.props.onSelect( item ) }> + + + + + + + { item.title } + + + } + /> + + ); + /* eslint-enable jsx-a11y/no-autofocus, jsx-a11y/no-noninteractive-element-interactions */ + } +} + +export default compose( + withSelect( ( select, { clientId, isAppender, rootClientId } ) => { + const { + getInserterItems, + getBlockName, + getBlockRootClientId, + getBlockSelectionEnd, + } = select( 'core/block-editor' ); + const { + getChildBlockNames, + } = select( 'core/blocks' ); + + let destinationRootClientId = rootClientId; + if ( ! destinationRootClientId && ! clientId && ! isAppender ) { + const end = getBlockSelectionEnd(); + if ( end ) { + destinationRootClientId = getBlockRootClientId( end ) || undefined; + } + } + const destinationRootBlockName = getBlockName( destinationRootClientId ); + + return { + rootChildBlocks: getChildBlockNames( destinationRootBlockName ), + items: getInserterItems( destinationRootClientId ), + destinationRootClientId, + }; + } ), + withDispatch( ( dispatch, ownProps, { select } ) => { + const { + showInsertionPoint, + hideInsertionPoint, + } = dispatch( 'core/block-editor' ); + + // To avoid duplication, getInsertionIndex is extracted and used in two event handlers + // This breaks the withDispatch not containing any logic rule. + // Since it's a function only called when the event handlers are called, + // it's fine to extract it. + // eslint-disable-next-line no-restricted-syntax + function getInsertionIndex() { + const { + getBlock, + getBlockIndex, + getBlockSelectionEnd, + getBlockOrder, + } = select( 'core/block-editor' ); + const { + isPostTitleSelected, + } = select( 'core/editor' ); + const { clientId, destinationRootClientId, isAppender } = ownProps; + + // if post title is selected insert as first block + if ( isPostTitleSelected() ) { + return 0; + } + + // If the clientId is defined, we insert at the position of the block. + if ( clientId ) { + return getBlockIndex( clientId, destinationRootClientId ); + } + + // If there a selected block, + const end = getBlockSelectionEnd(); + if ( ! isAppender && end ) { + // and the last selected block is unmodified (empty), it will be replaced + if ( isUnmodifiedDefaultBlock( getBlock( end ) ) ) { + return getBlockIndex( end, destinationRootClientId ); + } + + // we insert after the selected block. + return getBlockIndex( end, destinationRootClientId ) + 1; + } + + // Otherwise, we insert at the end of the current rootClientId + return getBlockOrder( destinationRootClientId ).length; + } + + return { + showInsertionPoint() { + const index = getInsertionIndex(); + showInsertionPoint( ownProps.destinationRootClientId, index ); + }, + hideInsertionPoint, + onSelect( item ) { + const { + replaceBlocks, + insertBlock, + } = dispatch( 'core/block-editor' ); + const { + getSelectedBlock, + } = select( 'core/block-editor' ); + const { isAppender } = ownProps; + const { name, initialAttributes } = item; + const selectedBlock = getSelectedBlock(); + const insertedBlock = createBlock( name, initialAttributes ); + if ( ! isAppender && selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) { + replaceBlocks( selectedBlock.clientId, insertedBlock ); + } else { + insertBlock( + insertedBlock, + getInsertionIndex(), + ownProps.destinationRootClientId + ); + } + + ownProps.onSelect(); + }, + }; + } ), + withInstanceId, +)( InserterMenu ); diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss index 82d5fa5822650..e10b685dda406 100644 --- a/packages/block-editor/src/components/inserter/style.native.scss +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -55,3 +55,10 @@ font-size: 12; color: $gray-dark; } + +.addBlockButton { + color: $blue-wordpress; + border: 2px; + border-radius: 10px; + border-color: $blue-wordpress; +} diff --git a/packages/components/src/dropdown/index.native.js b/packages/components/src/dropdown/index.native.js new file mode 100644 index 0000000000000..de7c7b7839613 --- /dev/null +++ b/packages/components/src/dropdown/index.native.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +class Dropdown extends Component { + constructor() { + super( ...arguments ); + + this.toggle = this.toggle.bind( this ); + this.close = this.close.bind( this ); + + this.state = { + isOpen: false, + }; + } + + componentWillUnmount() { + const { isOpen } = this.state; + const { onToggle } = this.props; + if ( isOpen && onToggle ) { + onToggle( false ); + } + } + + componentDidUpdate( prevProps, prevState ) { + const { isOpen } = this.state; + const { onToggle } = this.props; + if ( prevState.isOpen !== isOpen && onToggle ) { + onToggle( isOpen ); + } + } + + toggle() { + this.setState( ( state ) => ( { + isOpen: ! state.isOpen, + } ) ); + } + + close() { + this.setState( { isOpen: false } ); + } + + render() { + const { isOpen } = this.state; + const { + renderContent, + renderToggle, + } = this.props; + + const args = { isOpen, onToggle: this.toggle, onClose: this.close }; + + return ( + <> + { renderToggle( args ) } + { isOpen && renderContent( args ) } + + ); + } +} + +export default Dropdown; diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 5965c13e26eae..4984cdde2fd36 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -1,6 +1,7 @@ // Components export * from './primitives'; export { default as Dashicon } from './dashicon'; +export { default as Dropdown } from './dropdown'; export { default as Toolbar } from './toolbar'; export { default as ToolbarButton } from './toolbar-button'; export { default as Icon } from './icon'; diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 341737b41bef2..fb9dab39a03f2 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -5,8 +5,7 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view import { FlatList } from 'react-native'; export const KeyboardAwareFlatList = ( { - blockToolbarHeight, - innerToolbarHeight, + extraScrollHeight, shouldPreventAutomaticScroll, innerRef, ...listProps @@ -16,9 +15,7 @@ export const KeyboardAwareFlatList = ( { keyboardDismissMode="none" enableResetScrollToCoords={ false } keyboardShouldPersistTaps="handled" - extraScrollHeight={ innerToolbarHeight } - extraBottomInset={ -listProps.safeAreaBottomInset } - inputAccessoryViewHeight={ blockToolbarHeight } + extraScrollHeight={ extraScrollHeight } extraHeight={ 0 } innerRef={ ( ref ) => { this.scrollViewRef = ref; diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js new file mode 100644 index 0000000000000..9c71845889d37 --- /dev/null +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { ScrollView, Keyboard, Platform, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { withViewportMatch } from '@wordpress/viewport'; +import { __ } from '@wordpress/i18n'; +import { + Inserter, + BlockToolbar, +} from '@wordpress/block-editor'; +import { Toolbar, ToolbarButton } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +function HeaderToolbar( { + hasFixedToolbar, + hasRedo, + hasUndo, + redo, + undo, + showInserter, + showKeyboardHideButton, + clearSelectedBlock, +} ) { + const hideKeyboard = () => { + clearSelectedBlock(); + if ( Platform.OS === 'android' ) { + // Avoiding extra blur calls on iOS but still needed for android. + Keyboard.dismiss(); + } + }; + + return ( + + + + + { /* TODO: replace with EditorHistoryRedo and EditorHistoryUndo */ } + + + + { hasFixedToolbar && + + } + + { showKeyboardHideButton && + + + + } + + ); +} + +export default compose( [ + withSelect( ( select ) => ( { + hasRedo: select( 'core/editor' ).hasEditorRedo(), + hasUndo: select( 'core/editor' ).hasEditorUndo(), + hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ), + // This setting (richEditingEnabled) should not live in the block editor's setting. + showInserter: select( 'core/edit-post' ).getEditorMode() === 'visual' && select( 'core/editor' ).getEditorSettings().richEditingEnabled, + isTextModeEnabled: select( 'core/edit-post' ).getEditorMode() === 'text', + } ) ), + withDispatch( ( dispatch ) => ( { + redo: dispatch( 'core/editor' ).redo, + undo: dispatch( 'core/editor' ).undo, + clearSelectedBlock: dispatch( 'core/editor' ).clearSelectedBlock, + } ) ), + withViewportMatch( { isLargeViewport: 'medium' } ), +] )( HeaderToolbar ); diff --git a/packages/block-editor/src/components/block-toolbar/style.native.scss b/packages/edit-post/src/components/header/header-toolbar/style.native.scss similarity index 75% rename from packages/block-editor/src/components/block-toolbar/style.native.scss rename to packages/edit-post/src/components/header/header-toolbar/style.native.scss index 43e996cd8e075..0aa03b90ed81c 100644 --- a/packages/block-editor/src/components/block-toolbar/style.native.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.native.scss @@ -1,3 +1,4 @@ + .container { height: 44px; flex-direction: row; @@ -18,10 +19,3 @@ justify-content: center; align-items: center; } - -.addBlockButton { - color: $blue-wordpress; - border: 2px; - border-radius: 10px; - border-color: $blue-wordpress; -} diff --git a/packages/edit-post/src/components/header/index.native.js b/packages/edit-post/src/components/header/index.native.js new file mode 100644 index 0000000000000..93fbf88ce7967 --- /dev/null +++ b/packages/edit-post/src/components/header/index.native.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { Keyboard } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import HeaderToolbar from './header-toolbar'; + +export default class Header extends Component { + constructor() { + super( ...arguments ); + + this.keyboardDidShow = this.keyboardDidShow.bind( this ); + this.keyboardDidHide = this.keyboardDidHide.bind( this ); + + this.state = { + isKeyboardVisible: false, + }; + } + + componentDidMount() { + Keyboard.addListener( 'keyboardDidShow', this.keyboardDidShow ); + Keyboard.addListener( 'keyboardDidHide', this.keyboardDidHide ); + } + + componentWillUnmount() { + Keyboard.removeListener( 'keyboardDidShow', this.keyboardDidShow ); + Keyboard.removeListener( 'keyboardDidHide', this.keyboardDidHide ); + } + + keyboardDidShow() { + this.setState( { isKeyboardVisible: true } ); + } + + keyboardDidHide() { + this.setState( { isKeyboardVisible: false } ); + } + + render() { + return ( + + ); + } +} diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index 6081dc7341060..84f7e5bcde21d 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -1,8 +1,9 @@ /** * External dependencies */ -import { SafeAreaView } from 'react-native'; +import { Platform, SafeAreaView, View } from 'react-native'; import SafeArea from 'react-native-safe-area'; +import { sendNativeEditorDidLayout } from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -10,12 +11,14 @@ import SafeArea from 'react-native-safe-area'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { HTMLTextInput, ReadableContentView } from '@wordpress/components'; +import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView } from '@wordpress/components'; /** * Internal dependencies */ import styles from './style.scss'; +import headerToolbarStyles from '../header/header-toolbar/style.scss'; +import Header from '../header'; import VisualEditor from '../visual-editor'; class Layout extends Component { @@ -27,7 +30,7 @@ class Layout extends Component { this.state = { rootViewHeight: 0, - safeAreaBottomInset: 0, + safeAreaInsets: { top: 0, bottom: 0, right: 0, left: 0 }, isFullyBordered: true, }; @@ -46,8 +49,8 @@ class Layout extends Component { onSafeAreaInsetsUpdate( result ) { const { safeAreaInsets } = result; - if ( this._isMounted && this.state.safeAreaBottomInset !== safeAreaInsets.bottom ) { - this.setState( { safeAreaBottomInset: safeAreaInsets.bottom } ); + if ( this._isMounted ) { + this.setState( { safeAreaInsets } ); } } @@ -60,7 +63,7 @@ class Layout extends Component { setHeightState( event ) { const { height } = event.nativeEvent.layout; - this.setState( { rootViewHeight: height }, this.props.onNativeEditorDidLayout ); + this.setState( { rootViewHeight: height }, sendNativeEditorDidLayout ); } setBorderStyleState() { @@ -72,9 +75,7 @@ class Layout extends Component { renderHTML() { return ( - + ); } @@ -90,8 +91,6 @@ class Layout extends Component { return ( ); @@ -102,9 +101,28 @@ class Layout extends Component { mode, } = this.props; + // add a margin view at the bottom for the header + const marginBottom = Platform.OS === 'android' ? headerToolbarStyles.container.height : 0; + + const toolbarKeyboardAvoidingViewStyle = { + ...styles.toolbarKeyboardAvoidingView, + left: this.state.safeAreaInsets.left, + right: this.state.safeAreaInsets.right, + }; + return ( - { mode === 'text' ? this.renderHTML() : this.renderVisual() } + + { mode === 'text' ? this.renderHTML() : this.renderVisual() } + + + + +
+ ); } diff --git a/packages/edit-post/src/components/layout/style.native.scss b/packages/edit-post/src/components/layout/style.native.scss index badc66b9b619f..e6d7e241bcd0d 100644 --- a/packages/edit-post/src/components/layout/style.native.scss +++ b/packages/edit-post/src/components/layout/style.native.scss @@ -4,3 +4,10 @@ justify-content: flex-start; background-color: #fff; } + +.toolbarKeyboardAvoidingView { + position: absolute; + bottom: 0; + right: 0; + left: 0; +} diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js index 092ea638f6802..15ca4ed9d451b 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -5,7 +5,7 @@ import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { BlockEditorProvider, BlockList } from '@wordpress/block-editor'; +import { BlockList } from '@wordpress/block-editor'; import { PostTitle } from '@wordpress/editor'; import { ReadableContentView } from '@wordpress/components'; @@ -43,30 +43,16 @@ class VisualEditor extends Component { render() { const { - blocks, isFullyBordered, - resetEditorBlocks, - resetEditorBlocksWithoutUndoLevel, - rootViewHeight, safeAreaBottomInset, } = this.props; return ( - - - + ); } } @@ -74,21 +60,16 @@ class VisualEditor extends Component { export default compose( [ withSelect( ( select ) => { const { - getEditorBlocks, getEditedPostAttribute, - isPostTitleSelected, } = select( 'core/editor' ); return { - blocks: getEditorBlocks(), title: getEditedPostAttribute( 'title' ), - isPostTitleSelected: isPostTitleSelected(), }; } ), withDispatch( ( dispatch ) => { const { editPost, - resetEditorBlocks, } = dispatch( 'core/editor' ); const { clearSelectedBlock } = dispatch( 'core/block-editor' ); @@ -98,12 +79,6 @@ export default compose( [ editTitle( title ) { editPost( { title } ); }, - resetEditorBlocks, - resetEditorBlocksWithoutUndoLevel( blocks ) { - resetEditorBlocks( blocks, { - __unstableShouldCreateUndoLevel: false, - } ); - }, }; } ), ] )( VisualEditor ); diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 71f8bbd9cb774..23e0bad6a9e60 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -1,22 +1,18 @@ /** * External dependencies */ -import RNReactNativeGutenbergBridge, { - subscribeParentGetHtml, - subscribeParentToggleHTMLMode, - subscribeUpdateHtml, - subscribeSetFocusOnTitle, - subscribeSetTitle, - sendNativeEditorDidLayout, -} from 'react-native-gutenberg-bridge'; +import memize from 'memize'; +import { size, map, without } from 'lodash'; /** * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { parse, serialize, getUnregisteredTypeHandlerName } from '@wordpress/blocks'; +import { EditorProvider } from '@wordpress/editor'; +import { parse, serialize } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; +import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies @@ -27,175 +23,119 @@ class Editor extends Component { constructor( props ) { super( ...arguments ); - this.setTitleRef = this.setTitleRef.bind( this ); - - // TODO: use EditorProvider instead - this.post = props.post || { - id: 1, - title: { - raw: props.initialTitle, - }, - content: { - raw: props.initialHtml || '', - }, - type: 'draft', - }; - - props.setupEditor( this.post ); - - // make sure the post content is in sync with gutenberg store - // to avoid marking the post as modified when simply loaded - // For now, let's assume: serialize( parse( html ) ) !== html - this.post.content.raw = serialize( props.getEditorBlocks() ); - if ( props.initialHtmlModeEnabled && props.mode === 'visual' ) { // enable html mode if the initial mode the parent wants it but we're not already in it - this.toggleMode(); + this.props.switchEditorMode( 'text' ); } - } - - componentDidMount() { - this.subscriptionParentGetHtml = subscribeParentGetHtml( () => { - this.serializeToNativeAction(); - } ); - this.subscriptionParentToggleHTMLMode = subscribeParentToggleHTMLMode( () => { - this.toggleMode(); + this.getEditorSettings = memize( this.getEditorSettings, { + maxSize: 1, } ); - - this.subscriptionParentSetTitle = subscribeSetTitle( ( payload ) => { - this.props.editTitle( payload.title ); - } ); - - this.subscriptionParentUpdateHtml = subscribeUpdateHtml( ( payload ) => { - this.updateHtmlAction( payload.html ); - } ); - - this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { - if ( this.postTitleRef ) { - this.postTitleRef.focus(); - } - } ); - } - - componentWillUnmount() { - if ( this.subscriptionParentGetHtml ) { - this.subscriptionParentGetHtml.remove(); - } - - if ( this.subscriptionParentToggleHTMLMode ) { - this.subscriptionParentToggleHTMLMode.remove(); - } - - if ( this.subscriptionParentSetTitle ) { - this.subscriptionParentSetTitle.remove(); - } - - if ( this.subscriptionParentUpdateHtml ) { - this.subscriptionParentUpdateHtml.remove(); - } - - if ( this.subscriptionParentSetFocusOnTitle ) { - this.subscriptionParentSetFocusOnTitle.remove(); - } } - serializeToNativeAction() { - if ( this.props.mode === 'text' ) { - this.updateHtmlAction( this.props.getEditedPostContent() ); - } - - const html = serialize( this.props.getEditorBlocks() ); - const title = this.props.getEditedPostAttribute( 'title' ); - - const hasChanges = title !== this.post.title.raw || html !== this.post.content.raw; - - RNReactNativeGutenbergBridge.provideToNative_Html( html, title, hasChanges ); + getEditorSettings( + settings, + hasFixedToolbar, + focusMode, + hiddenBlockTypes, + blockTypes, + ) { + settings = { + ...settings, + hasFixedToolbar, + focusMode, + }; - if ( hasChanges ) { - this.post.title.raw = title; - this.post.content.raw = html; + // Omit hidden block types if exists and non-empty. + if ( size( hiddenBlockTypes ) > 0 ) { + // Defer to passed setting for `allowedBlockTypes` if provided as + // anything other than `true` (where `true` is equivalent to allow + // all block types). + const defaultAllowedBlockTypes = ( + true === settings.allowedBlockTypes ? + map( blockTypes, 'name' ) : + ( settings.allowedBlockTypes || [] ) + ); + + settings.allowedBlockTypes = without( + defaultAllowedBlockTypes, + ...hiddenBlockTypes, + ); } - } - - updateHtmlAction( html ) { - const parsed = parse( html ); - this.props.resetEditorBlocksWithoutUndoLevel( parsed ); - } - toggleMode() { - const { mode, switchMode } = this.props; - // refresh html content first - this.serializeToNativeAction(); - switchMode( mode === 'visual' ? 'text' : 'visual' ); + return settings; } - componentDidUpdate( prevProps ) { - if ( ! prevProps.isReady && this.props.isReady ) { - const blocks = this.props.getEditorBlocks(); - const isUnsupportedBlock = ( { name } ) => name === getUnregisteredTypeHandlerName(); - const unsupportedBlockNames = blocks.filter( isUnsupportedBlock ).map( ( block ) => block.attributes.originalName ); - RNReactNativeGutenbergBridge.editorDidMount( unsupportedBlockNames ); - } - } + render() { + const { + settings, + hasFixedToolbar, + focusMode, + initialEdits, + hiddenBlockTypes, + blockTypes, + post, + ...props + } = this.props; + + const editorSettings = this.getEditorSettings( + settings, + hasFixedToolbar, + focusMode, + hiddenBlockTypes, + blockTypes, + ); - setTitleRef( titleRef ) { - this.postTitleRef = titleRef; - } + const normalizedPost = post || { + id: 1, + title: { + raw: props.initialTitle, + }, + content: { + // make sure the post content is in sync with gutenberg store + // to avoid marking the post as modified when simply loaded + // For now, let's assume: serialize( parse( html ) ) !== html + raw: serialize( parse( props.initialHtml || '' ) ), + }, + type: 'draft', + }; - render() { return ( - + + + + + ); } } export default compose( [ withSelect( ( select ) => { - const { - __unstableIsEditorReady: isEditorReady, - getEditorBlocks, - getEditedPostAttribute, - getEditedPostContent, - } = select( 'core/editor' ); - const { - getEditorMode, - } = select( 'core/edit-post' ); + const { isFeatureActive, getEditorMode, getPreference } = select( 'core/edit-post' ); + const { getBlockTypes } = select( 'core/blocks' ); return { + hasFixedToolbar: isFeatureActive( 'fixedToolbar' ), + focusMode: isFeatureActive( 'focusMode' ), mode: getEditorMode(), - isReady: isEditorReady(), - getEditorBlocks, - getEditedPostAttribute, - getEditedPostContent, + hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ), + blockTypes: getBlockTypes(), }; } ), withDispatch( ( dispatch ) => { - const { - editPost, - setupEditor, - resetEditorBlocks, - } = dispatch( 'core/editor' ); const { switchEditorMode, } = dispatch( 'core/edit-post' ); return { - editTitle( title ) { - editPost( { title } ); - }, - resetEditorBlocksWithoutUndoLevel( blocks ) { - resetEditorBlocks( blocks, { - __unstableShouldCreateUndoLevel: false, - } ); - }, - setupEditor, - switchMode( mode ) { - switchEditorMode( mode ); - }, + switchEditorMode, }; } ), ] )( Editor ); diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 1ffdcb1347733..296e37117e5a1 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -4,6 +4,7 @@ import '@wordpress/core-data'; import '@wordpress/block-editor'; import '@wordpress/editor'; +import '@wordpress/viewport'; import '@wordpress/notices'; import { registerCoreBlocks } from '@wordpress/block-library'; import { unregisterBlockType } from '@wordpress/blocks'; diff --git a/packages/edit-post/src/store/defaults.native.js b/packages/edit-post/src/store/defaults.native.js new file mode 100644 index 0000000000000..7ac420b872cad --- /dev/null +++ b/packages/edit-post/src/store/defaults.native.js @@ -0,0 +1,14 @@ +export const PREFERENCES_DEFAULTS = { + editorMode: 'visual', + isGeneralSidebarDismissed: true, + panels: { + 'post-status': { + opened: true, + }, + }, + features: { + fixedToolbar: true, + }, + pinnedPluginItems: {}, + hiddenBlockTypes: [], +}; diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index aee6c0fe50586..cedd6bf8c94b3 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -15,7 +15,7 @@ jest.mock( '../components/layout', () => () => 'Layout' ); /** * Internal dependencies */ -import '../store'; +import '..'; import Editor from '../editor'; const unsupportedBlock = ` diff --git a/packages/editor/src/components/convert-to-group-buttons/index.native.js b/packages/editor/src/components/convert-to-group-buttons/index.native.js new file mode 100644 index 0000000000000..bd0c2f440d06f --- /dev/null +++ b/packages/editor/src/components/convert-to-group-buttons/index.native.js @@ -0,0 +1,2 @@ + +export default () => null; diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 85a33d69bf251..69035455d49f1 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -4,4 +4,7 @@ export { default as PostTitle } from './post-title'; export { default as EditorHistoryRedo } from './editor-history/redo'; export { default as EditorHistoryUndo } from './editor-history/undo'; +// State Related Components +export { default as EditorProvider } from './provider'; + export * from './deprecated'; diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js new file mode 100644 index 0000000000000..d2b1458d3e2e5 --- /dev/null +++ b/packages/editor/src/components/provider/index.native.js @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import RNReactNativeGutenbergBridge, { + subscribeParentGetHtml, + subscribeParentToggleHTMLMode, + subscribeUpdateHtml, + subscribeSetFocusOnTitle, + subscribeSetTitle, +} from 'react-native-gutenberg-bridge'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { parse, serialize, getUnregisteredTypeHandlerName } from '@wordpress/blocks'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import EditorProvider from './index.js'; + +class NativeEditorProvider extends Component { + constructor( props ) { + super( ...arguments ); + + // Keep a local reference to `post` to detect changes + this.post = props.post; + + this.setTitleRef = this.setTitleRef.bind( this ); + } + + componentDidMount() { + this.subscriptionParentGetHtml = subscribeParentGetHtml( () => { + this.serializeToNativeAction(); + } ); + + this.subscriptionParentToggleHTMLMode = subscribeParentToggleHTMLMode( () => { + this.toggleMode(); + } ); + + this.subscriptionParentSetTitle = subscribeSetTitle( ( payload ) => { + this.props.editTitle( payload.title ); + } ); + + this.subscriptionParentUpdateHtml = subscribeUpdateHtml( ( payload ) => { + this.updateHtmlAction( payload.html ); + } ); + + this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { + if ( this.postTitleRef ) { + this.postTitleRef.focus(); + } + } ); + } + + componentWillUnmount() { + if ( this.subscriptionParentGetHtml ) { + this.subscriptionParentGetHtml.remove(); + } + + if ( this.subscriptionParentToggleHTMLMode ) { + this.subscriptionParentToggleHTMLMode.remove(); + } + + if ( this.subscriptionParentSetTitle ) { + this.subscriptionParentSetTitle.remove(); + } + + if ( this.subscriptionParentUpdateHtml ) { + this.subscriptionParentUpdateHtml.remove(); + } + + if ( this.subscriptionParentSetFocusOnTitle ) { + this.subscriptionParentSetFocusOnTitle.remove(); + } + } + + componentDidUpdate( prevProps ) { + if ( ! prevProps.isReady && this.props.isReady ) { + const blocks = this.props.blocks; + const isUnsupportedBlock = ( { name } ) => name === getUnregisteredTypeHandlerName(); + const unsupportedBlockNames = blocks.filter( isUnsupportedBlock ).map( ( block ) => block.attributes.originalName ); + RNReactNativeGutenbergBridge.editorDidMount( unsupportedBlockNames ); + } + } + + setTitleRef( titleRef ) { + this.postTitleRef = titleRef; + } + + serializeToNativeAction() { + if ( this.props.mode === 'text' ) { + this.updateHtmlAction( this.props.getEditedPostContent() ); + } + + const html = serialize( this.props.blocks ); + const title = this.props.title; + + const hasChanges = title !== this.post.title.raw || html !== this.post.content.raw; + + RNReactNativeGutenbergBridge.provideToNative_Html( html, title, hasChanges ); + + if ( hasChanges ) { + this.post.title.raw = title; + this.post.content.raw = html; + } + } + + updateHtmlAction( html ) { + const parsed = parse( html ); + this.props.resetEditorBlocksWithoutUndoLevel( parsed ); + } + + toggleMode() { + const { mode, switchMode } = this.props; + // refresh html content first + this.serializeToNativeAction(); + switchMode( mode === 'visual' ? 'text' : 'visual' ); + } + + render() { + const { + children, + post, // eslint-disable-line no-unused-vars + ...props + } = this.props; + + return ( + + { children } + + ); + } +} + +export default compose( [ + withSelect( ( select ) => { + const { + __unstableIsEditorReady: isEditorReady, + getEditorBlocks, + getEditedPostAttribute, + getEditedPostContent, + } = select( 'core/editor' ); + const { + getEditorMode, + } = select( 'core/edit-post' ); + + return { + mode: getEditorMode(), + isReady: isEditorReady(), + blocks: getEditorBlocks(), + title: getEditedPostAttribute( 'title' ), + getEditedPostContent, + }; + } ), + withDispatch( ( dispatch ) => { + const { + editPost, + resetEditorBlocks, + } = dispatch( 'core/editor' ); + const { + switchEditorMode, + } = dispatch( 'core/edit-post' ); + + return { + editTitle( title ) { + editPost( { title } ); + }, + resetEditorBlocksWithoutUndoLevel( blocks ) { + resetEditorBlocks( blocks, { + __unstableShouldCreateUndoLevel: false, + } ); + }, + switchMode( mode ) { + switchEditorMode( mode ); + }, + }; + } ), +] )( NativeEditorProvider ); diff --git a/packages/editor/src/components/reusable-blocks-buttons/index.native.js b/packages/editor/src/components/reusable-blocks-buttons/index.native.js new file mode 100644 index 0000000000000..bd0c2f440d06f --- /dev/null +++ b/packages/editor/src/components/reusable-blocks-buttons/index.native.js @@ -0,0 +1,2 @@ + +export default () => null; diff --git a/packages/viewport/src/index.native.js b/packages/viewport/src/index.native.js index 8b137891791fe..605b511ac9d68 100644 --- a/packages/viewport/src/index.native.js +++ b/packages/viewport/src/index.native.js @@ -1 +1,8 @@ +/** + * Internal dependencies + */ +import './store'; + +export { default as ifViewportMatches } from './if-viewport-matches'; +export { default as withViewportMatch } from './with-viewport-match';