diff --git a/packages/block-editor/src/components/alignment-toolbar/index.native.js b/packages/block-editor/src/components/alignment-toolbar/index.native.js deleted file mode 100644 index fdd842d037514..0000000000000 --- a/packages/block-editor/src/components/alignment-toolbar/index.native.js +++ /dev/null @@ -1,5 +0,0 @@ -const AlignmentToolbar = () => { - return null; -}; - -export default AlignmentToolbar; diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index 2e1c3a9fcd801..e25885c73503b 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -3,11 +3,9 @@ */ import React from 'react'; import { - requestMediaPickFromMediaLibrary, - requestMediaPickFromDeviceLibrary, - requestMediaPickFromDeviceCamera, getOtherMediaOptions, - requestOtherMediaPickFrom, + requestMediaPicker, + mediaSources, } from 'react-native-gutenberg-bridge'; /** @@ -19,23 +17,52 @@ import { Picker } from '@wordpress/components'; export const MEDIA_TYPE_IMAGE = 'image'; export const MEDIA_TYPE_VIDEO = 'video'; -export const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE = 'choose_from_device'; -export const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA = 'take_media'; -export const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_media_library'; - export const OPTION_TAKE_VIDEO = __( 'Take a Video' ); export const OPTION_TAKE_PHOTO = __( 'Take a Photo' ); export const OPTION_TAKE_PHOTO_OR_VIDEO = __( 'Take a Photo or Video' ); +const cameraImageSource = { + id: mediaSources.deviceCamera, // ID is the value sent to native + value: mediaSources.deviceCamera + '-IMAGE', // This is needed to diferenciate image-camera from video-camera sources. + label: __( 'Take a Photo' ), + types: [ MEDIA_TYPE_IMAGE ], + icon: 'camera', +}; + +const cameraVideoSource = { + id: mediaSources.deviceCamera, + value: mediaSources.deviceCamera, + label: __( 'Take a Video' ), + types: [ MEDIA_TYPE_VIDEO ], + icon: 'camera', +}; + +const deviceLibrarySource = { + id: mediaSources.deviceLibrary, + value: mediaSources.deviceLibrary, + label: __( 'Choose from device' ), + types: [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ], +}; + +const siteLibrarySource = { + id: mediaSources.siteMediaLibrary, + value: mediaSources.siteMediaLibrary, + label: __( 'WordPress Media Library' ), + types: [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ], + icon: 'wordpress-alt', +}; + +const internalSources = [ deviceLibrarySource, cameraImageSource, cameraVideoSource, siteLibrarySource ]; + export class MediaUpload extends React.Component { constructor( props ) { super( props ); this.onPickerPresent = this.onPickerPresent.bind( this ); - this.onPickerChange = this.onPickerChange.bind( this ); this.onPickerSelect = this.onPickerSelect.bind( this ); + this.getAllSources = this.getAllSources.bind( this ); this.state = { - otherMediaOptions: undefined, + otherMediaOptions: [], }; } @@ -44,9 +71,9 @@ export class MediaUpload extends React.Component { getOtherMediaOptions( allowedTypes, ( otherMediaOptions ) => { const otherMediaOptionsWithIcons = otherMediaOptions.map( ( option ) => { return { - icon: this.getChooseFromDeviceIcon(), - value: option.value, - label: option.label, + ...option, + types: allowedTypes, + id: option.value, }; } ); @@ -54,26 +81,21 @@ export class MediaUpload extends React.Component { } ); } - getTakeMediaLabel() { - const { allowedTypes = [] } = this.props; - - const isOneType = allowedTypes.length === 1; - const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); - const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); - - if ( isImage ) { - return OPTION_TAKE_PHOTO; - } else if ( isVideo ) { - return OPTION_TAKE_VIDEO; - } return OPTION_TAKE_PHOTO_OR_VIDEO; + getAllSources() { + return internalSources.concat( this.state.otherMediaOptions ); } getMediaOptionsItems() { - return [ - { icon: this.getChooseFromDeviceIcon(), value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, label: __( 'Choose from device' ) }, - { icon: this.getTakeMediaIcon(), value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA, label: this.getTakeMediaLabel() }, - { icon: this.getWordPressLibraryIcon(), value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, label: __( 'WordPress Media Library' ) }, - ]; + const { allowedTypes = [] } = this.props; + + return this.getAllSources().filter( ( source ) => { + return allowedTypes.filter( ( allowedType ) => source.types.includes( allowedType ) ).length > 0; + } ).map( ( source ) => { + return { + ...source, + icon: source.icon || this.getChooseFromDeviceIcon(), + }; + } ); } getChooseFromDeviceIcon() { @@ -90,59 +112,30 @@ export class MediaUpload extends React.Component { } } - getTakeMediaIcon() { - return 'camera'; - } - - getWordPressLibraryIcon() { - return 'wordpress-alt'; - } - onPickerPresent() { if ( this.picker ) { this.picker.presentPicker(); } } - onPickerSelect( requestFunction ) { + onPickerSelect( value ) { const { allowedTypes = [], onSelect, multiple = false } = this.props; - requestFunction( allowedTypes, multiple, ( media ) => { + const mediaSource = this.getAllSources().filter( ( source ) => source.value === value ).shift(); + const types = allowedTypes.filter( ( type ) => mediaSource.types.includes( type ) ); + requestMediaPicker( mediaSource.id, types, multiple, ( media ) => { if ( ( multiple && media ) || ( media && media.id ) ) { onSelect( media ); } } ); } - onPickerChange( value ) { - if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { - this.onPickerSelect( requestMediaPickFromDeviceLibrary ); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA ) { - this.onPickerSelect( requestMediaPickFromDeviceCamera ); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { - this.onPickerSelect( requestMediaPickFromMediaLibrary ); - } else { - const { onSelect, multiple = false } = this.props; - requestOtherMediaPickFrom( value, multiple, ( media ) => { - if ( ( multiple && media ) || ( media && media.id ) ) { - onSelect( media ); - } - } ); - } - } - render() { - let mediaOptions = this.getMediaOptionsItems(); - - if ( this.state.otherMediaOptions ) { - mediaOptions = [ ...mediaOptions, ...this.state.otherMediaOptions ]; - } - const getMediaOptions = () => ( this.picker = instance } - options={ mediaOptions } - onChange={ this.onPickerChange } + options={ this.getMediaOptionsItems() } + onChange={ this.onPickerSelect } /> ); diff --git a/packages/block-editor/src/components/media-upload/test/index.native.js b/packages/block-editor/src/components/media-upload/test/index.native.js index 85b63b098ca92..337b038cab949 100644 --- a/packages/block-editor/src/components/media-upload/test/index.native.js +++ b/packages/block-editor/src/components/media-upload/test/index.native.js @@ -4,9 +4,8 @@ import { shallow } from 'enzyme'; import { TouchableWithoutFeedback } from 'react-native'; import { - requestMediaPickFromMediaLibrary, - requestMediaPickFromDeviceLibrary, - requestMediaPickFromDeviceCamera, + requestMediaPicker, + mediaSources, } from 'react-native-gutenberg-bridge'; /** @@ -14,9 +13,6 @@ import { */ import { MediaUpload, - MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, - MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA, - MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, OPTION_TAKE_VIDEO, @@ -67,7 +63,7 @@ describe( 'MediaUpload component', () => { } ); const expectMediaPickerForOption = ( option, allowMultiple, requestFunction ) => { - requestFunction.mockImplementation( ( mediaTypes, multiple, callback ) => { + requestFunction.mockImplementation( ( source, mediaTypes, multiple, callback ) => { expect( mediaTypes[ 0 ] ).toEqual( MEDIA_TYPE_VIDEO ); if ( multiple ) { callback( [ { id: MEDIA_ID, url: MEDIA_URL } ] ); @@ -101,22 +97,22 @@ describe( 'MediaUpload component', () => { }; it( 'can select media from device library', () => { - expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, false, requestMediaPickFromDeviceLibrary ); + expectMediaPickerForOption( mediaSources.deviceLibrary, false, requestMediaPicker ); } ); it( 'can select media from WP media library', () => { - expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, false, requestMediaPickFromMediaLibrary ); + expectMediaPickerForOption( mediaSources.siteMediaLibrary, false, requestMediaPicker ); } ); it( 'can select media by capturig', () => { - expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA, false, requestMediaPickFromDeviceCamera ); + expectMediaPickerForOption( mediaSources.deviceCamera, false, requestMediaPicker ); } ); it( 'can select multiple media from device library', () => { - expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, true, requestMediaPickFromDeviceLibrary ); + expectMediaPickerForOption( mediaSources.deviceLibrary, true, requestMediaPicker ); } ); it( 'can select multiple media from WP media library', () => { - expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, true, requestMediaPickFromMediaLibrary ); + expectMediaPickerForOption( mediaSources.siteMediaLibrary, true, requestMediaPicker ); } ); } ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 53292f1402d5d..e433c89eafd24 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -348,6 +348,7 @@ class RichTextWrapper extends Component { // To do: find a better way to implicitly inherit props. start, reversed, + style, // From experimental filter. To do: pick props instead. ...experimentalProps } = this.props; @@ -396,6 +397,7 @@ class RichTextWrapper extends Component { __unstableMarkAutomaticChange={ markAutomaticChange } __unstableDidAutomaticChange={ didAutomaticChange } __unstableUndo={ undo } + style={ style } > { ( { isSelected, value, onChange, Editable } ) => <> diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 1d9274c375469..edcfff4ff2a07 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -2,12 +2,13 @@ * External dependencies */ import React from 'react'; -import { View, ImageBackground, Text, TouchableWithoutFeedback, Dimensions } from 'react-native'; +import { View, ImageBackground, Text, TouchableWithoutFeedback, Dimensions, Platform } from 'react-native'; import { requestMediaImport, mediaUploadSync, requestImageFailedRetryDialog, requestImageUploadCancelDialog, + requestImageFullscreenPreview, } from 'react-native-gutenberg-bridge'; import { isEmpty, map } from 'lodash'; @@ -142,6 +143,8 @@ export class ImageEdit extends React.Component { requestImageUploadCancelDialog( attributes.id ); } else if ( attributes.id && ! isURL( attributes.url ) ) { requestImageFailedRetryDialog( attributes.id ); + } else if ( Platform.OS === 'android' ) { + requestImageFullscreenPreview( attributes.url ); } this.setState( { @@ -334,6 +337,7 @@ export class ImageEdit extends React.Component { }; const imageContainerHeight = Dimensions.get( 'window' ).width / IMAGE_ASPECT_RATIO; + const getImageComponent = ( openMediaOptions, getMediaOptions ) => ( { const opacity = isUploadInProgress ? 0.3 : 1; const icon = this.getIcon( isUploadFailed ); + const imageBorderOnSelectedStyle = isSelected && ! ( isUploadInProgress || isUploadFailed ) ? styles.imageBorder : ''; const iconContainer = ( @@ -378,7 +383,7 @@ export class ImageEdit extends React.Component { accessibilityLabel={ alt } accessibilityHint={ __( 'Double tap and hold to edit' ) } accessibilityRole={ 'imagebutton' } - style={ { width: finalWidth, height: finalHeight, opacity } } + style={ [ imageBorderOnSelectedStyle, { width: finalWidth, height: finalHeight, opacity } ] } resizeMethod="scale" source={ { uri: url } } key={ url } diff --git a/packages/block-library/src/image/styles.native.scss b/packages/block-library/src/image/styles.native.scss index 24b20de69dc8f..e153fae404737 100644 --- a/packages/block-library/src/image/styles.native.scss +++ b/packages/block-library/src/image/styles.native.scss @@ -6,6 +6,12 @@ background-color: $gray-lighten-30; } +.imageBorder { + border-color: $blue-medium; + border-width: 2px; + border-style: solid; +} + .uploadFailedText { color: #fff; font-size: 14; diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index c5485580538d1..98903cad72919 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -1,3 +1,7 @@ +/** + * External dependencies + */ +import { Platform } from 'react-native'; /** * WordPress dependencies */ @@ -143,6 +147,8 @@ export const registerCoreBlocks = () => { quote, mediaText, // eslint-disable-next-line no-undef + ( ( Platform.OS === 'ios' ) || ( !! __DEV__ ) ) ? preformatted : null, + // eslint-disable-next-line no-undef !! __DEV__ ? group : null, // eslint-disable-next-line no-undef !! __DEV__ ? spacer : null, diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 6dbcaf8a1459d..9d018a428040e 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -9,7 +9,7 @@ import { View } from 'react-native'; import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { createBlock } from '@wordpress/blocks'; -import { RichText } from '@wordpress/block-editor'; +import { AlignmentToolbar, BlockControls, RichText } from '@wordpress/block-editor'; /** * Internal dependencies @@ -47,12 +47,22 @@ class ParagraphEdit extends Component { } = this.props; const { - placeholder, + align, content, + placeholder, } = attributes; return ( + + { + setAttributes( { align: nextAlign } ); + } } + /> + onReplace( [] ) : undefined } placeholder={ placeholder || __( 'Start writing…' ) } + textAlign={ align } /> ); diff --git a/packages/block-library/src/preformatted/edit.js b/packages/block-library/src/preformatted/edit.js index 620543d090004..415afea843ef3 100644 --- a/packages/block-library/src/preformatted/edit.js +++ b/packages/block-library/src/preformatted/edit.js @@ -4,12 +4,13 @@ import { __ } from '@wordpress/i18n'; import { RichText } from '@wordpress/block-editor'; -export default function PreformattedEdit( { attributes, mergeBlocks, setAttributes, className } ) { +export default function PreformattedEdit( { attributes, mergeBlocks, setAttributes, className, style } ) { const { content } = attributes; return ( ' ) } onChange={ ( nextContent ) => { @@ -22,6 +23,7 @@ export default function PreformattedEdit( { attributes, mergeBlocks, setAttribut } } placeholder={ __( 'Write preformatted text…' ) } className={ className } + style={ style } onMerge={ mergeBlocks } /> ); diff --git a/packages/block-library/src/preformatted/edit.native.js b/packages/block-library/src/preformatted/edit.native.js new file mode 100644 index 0000000000000..43d5200dba875 --- /dev/null +++ b/packages/block-library/src/preformatted/edit.native.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; +/** + * WordPress dependencies + */ +import { withPreferredColorScheme } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import WebPreformattedEdit from './edit.js'; +import styles from './styles.scss'; + +function PreformattedEdit( props ) { + const { getStylesFromColorScheme } = props; + const richTextStyle = getStylesFromColorScheme( styles.wpRichTextLight, styles.wpRichTextDark ); + const wpBlockPreformatted = getStylesFromColorScheme( styles.wpBlockPreformattedLight, styles.wpBlockPreformattedDark ); + const propsWithStyle = { + ...props, + style: richTextStyle, + }; + return ( + + + + ); +} + +export default withPreferredColorScheme( PreformattedEdit ); diff --git a/packages/block-library/src/preformatted/styles.native.scss b/packages/block-library/src/preformatted/styles.native.scss new file mode 100644 index 0000000000000..c3d4cf129f2ba --- /dev/null +++ b/packages/block-library/src/preformatted/styles.native.scss @@ -0,0 +1,25 @@ +%wpBlockPreformattedLightColor { + background-color: $gray-light; +} + +%wpBlockPreformattedDarkColor { + background-color: $gray-100; +} + +.wpBlockPreformattedLight { + @extend %wpBlockPreformattedLightColor; + padding: 12px 16px; + border-radius: 4px; +} + +.wpRichTextLight { + @extend %wpBlockPreformattedLightColor; +} + +.wpBlockPreformattedDark { + @extend %wpBlockPreformattedDarkColor; +} + +.wpRichTextDark { + @extend %wpBlockPreformattedDarkColor; +} diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index a47de19cfaa1c..13bee48224509 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -8,7 +8,7 @@ import { isEmpty } from 'lodash'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { __experimentalRichText as RichText } from '@wordpress/rich-text'; +import { __experimentalRichText as RichText, create, insert } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withDispatch, withSelect } from '@wordpress/data'; import { withFocusOutside } from '@wordpress/components'; @@ -43,6 +43,19 @@ class PostTitle extends Component { this.props.onSelect(); } + onPaste( { value, onChange, plainText } ) { + const content = pasteHandler( { + plainText, + mode: 'INLINE', + tagName: 'p', + } ); + + if ( typeof content === 'string' ) { + const valueToInsert = create( { html: content } ); + onChange( insert( value, valueToInsert ) ); + } + } + render() { const { placeholder, @@ -84,13 +97,14 @@ class PostTitle extends Component { onChange={ ( value ) => { this.props.onUpdate( value ); } } + onPaste={ this.onPaste } placeholder={ decodedPlaceholder } value={ title } onSelectionChange={ () => { } } onEnter={ this.props.onEnterPress } disableEditingMenu={ true } - __unstablePasteHandler={ pasteHandler } __unstableIsSelected={ this.props.isSelected } + __unstableOnCreateUndoLevel={ () => { } } > diff --git a/packages/element/src/platform.android.js b/packages/element/src/platform.android.js index 4f0da0cab9c68..9f9bd4c0d9e4c 100644 --- a/packages/element/src/platform.android.js +++ b/packages/element/src/platform.android.js @@ -5,6 +5,7 @@ import { Platform as OriginalPlatform } from 'react-native'; const Platform = { ...OriginalPlatform, + OS: 'native', select: ( spec ) => { if ( 'android' in spec ) { return spec.android; diff --git a/packages/element/src/platform.ios.js b/packages/element/src/platform.ios.js index bbadff1f19331..f5833141e3302 100644 --- a/packages/element/src/platform.ios.js +++ b/packages/element/src/platform.ios.js @@ -5,6 +5,7 @@ import { Platform as OriginalPlatform } from 'react-native'; const Platform = { ...OriginalPlatform, + OS: 'native', select: ( spec ) => { if ( 'ios' in spec ) { return spec.ios; diff --git a/test/native/setup.js b/test/native/setup.js index f1e507f38ba39..bdef966c7ff00 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -16,11 +16,13 @@ jest.mock( 'react-native-gutenberg-bridge', () => { editorDidMount: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), - requestMediaPickFromMediaLibrary: jest.fn(), - requestMediaPickFromDeviceLibrary: jest.fn(), - requestMediaPickFromDeviceCamera: jest.fn(), getOtherMediaOptions: jest.fn(), - requestOtherMediaPickFrom: jest.fn(), + requestMediaPicker: jest.fn(), + mediaSources: { + deviceLibrary: 'DEVICE_MEDIA_LIBRARY', + deviceCamera: 'DEVICE_CAMERA', + siteMediaLibrary: 'SITE_MEDIA_LIBRARY', + }, }; } );