diff --git a/packages/block-editor/src/components/autocomplete/index.native.js b/packages/block-editor/src/components/autocomplete/index.native.js new file mode 100644 index 0000000000000..461f67a0a4bcb --- /dev/null +++ b/packages/block-editor/src/components/autocomplete/index.native.js @@ -0,0 +1 @@ +export default () => null; diff --git a/packages/block-editor/src/components/rich-text/file-paste-handler.js b/packages/block-editor/src/components/rich-text/file-paste-handler.js new file mode 100644 index 0000000000000..eceeb069b263f --- /dev/null +++ b/packages/block-editor/src/components/rich-text/file-paste-handler.js @@ -0,0 +1,11 @@ +/** + * WordPress dependencies + */ +import { createBlobURL } from '@wordpress/blob'; + +export function filePasteHandler( files ) { + return files + .filter( ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ) + .map( ( file ) => `` ) + .join( '' ); +} diff --git a/packages/block-editor/src/components/rich-text/file-paste-handler.native.js b/packages/block-editor/src/components/rich-text/file-paste-handler.native.js new file mode 100644 index 0000000000000..41402a0dcda68 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/file-paste-handler.native.js @@ -0,0 +1,3 @@ +export function filePasteHandler( files ) { + return files.map( ( url ) => `` ).join( '' ); +} diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.js new file mode 100644 index 0000000000000..fc857311706db --- /dev/null +++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.js @@ -0,0 +1,59 @@ +/** + * WordPress dependencies + */ +import { Popover } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import BlockFormatControls from '../block-format-controls'; +import FormatToolbar from './format-toolbar'; + +function getAnchorRect( anchorObj ) { + const { current } = anchorObj; + const rect = current.getBoundingClientRect(); + + // Add some space. + const buffer = 6; + + // Subtract padding if any. + let { paddingTop } = window.getComputedStyle( current ); + + paddingTop = parseInt( paddingTop, 10 ); + + return { + x: rect.left, + y: rect.top + paddingTop - buffer, + width: rect.width, + height: rect.height - paddingTop + buffer, + left: rect.left, + right: rect.right, + top: rect.top + paddingTop - buffer, + bottom: rect.bottom, + }; +} + +const FormatToolbarContainer = ( { inline, anchorObj } ) => { + if ( inline ) { + // Render in popover + return ( + getAnchorRect( anchorObj ) } + className="block-editor-rich-text__inline-format-toolbar" + > + + + ); + } + // Render regular toolbar + return ( + + + + ); +}; + +export default FormatToolbarContainer; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js new file mode 100644 index 0000000000000..d37fe5bfee0f6 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import BlockFormatControls from '../block-format-controls'; +import FormatToolbar from './format-toolbar'; + +const FormatToolbarContainer = () => { + // Render regular toolbar + return ( + + + + ); +}; + +export default FormatToolbarContainer; diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index b78e8c3a46ffc..53292f1402d5d 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -7,9 +7,9 @@ import { omit } from 'lodash'; /** * WordPress dependencies */ -import { RawHTML, Component, createRef } from '@wordpress/element'; +import { RawHTML, Component, createRef, Platform } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; -import { pasteHandler, children as childrenSource, getBlockTransforms, findTransform } from '@wordpress/blocks'; +import { pasteHandler, children as childrenSource, getBlockTransforms, findTransform, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { withInstanceId, compose } from '@wordpress/compose'; import { __experimentalRichText as RichText, @@ -25,8 +25,7 @@ import { toHTMLString, slice, } from '@wordpress/rich-text'; -import { withFilters, Popover } from '@wordpress/components'; -import { createBlobURL } from '@wordpress/blob'; +import { withFilters } from '@wordpress/components'; import deprecated from '@wordpress/deprecated'; import { isURL } from '@wordpress/url'; @@ -34,10 +33,10 @@ import { isURL } from '@wordpress/url'; * Internal dependencies */ import Autocomplete from '../autocomplete'; -import BlockFormatControls from '../block-format-controls'; -import FormatToolbar from './format-toolbar'; import { withBlockEditContext } from '../block-edit/context'; import { RemoveBrowserShortcuts } from './remove-browser-shortcuts'; +import { filePasteHandler } from './file-paste-handler'; +import FormatToolbarContainer from './format-toolbar-container'; const wrapperClasses = 'editor-rich-text block-editor-rich-text'; const classes = 'editor-rich-text__editable block-editor-rich-text__editable'; @@ -66,7 +65,6 @@ class RichTextWrapper extends Component { this.onPaste = this.onPaste.bind( this ); this.onDelete = this.onDelete.bind( this ); this.inputRule = this.inputRule.bind( this ); - this.getAnchorRect = this.getAnchorRect.bind( this ); } onEnter( { value, onChange, shiftKey } ) { @@ -124,7 +122,7 @@ class RichTextWrapper extends Component { } } - onPaste( { value, onChange, html, plainText, image } ) { + onPaste( { value, onChange, html, plainText, files } ) { const { onReplace, onSplit, @@ -134,16 +132,18 @@ class RichTextWrapper extends Component { __unstableEmbedURLOnPaste, } = this.props; - if ( image && ! html ) { - const file = image.getAsFile ? image.getAsFile() : image; + // Only process file if no HTML is present. + // Note: a pasted file may have the URL as plain text. + if ( files && files.length && ! html ) { const content = pasteHandler( { - HTML: ``, + HTML: filePasteHandler( files ), mode: 'BLOCKS', tagName, } ); // Allows us to ask for this information when we get a report. - window.console.log( 'Received item:\n\n', file ); + // eslint-disable-next-line no-console + window.console.log( 'Received items:\n\n', files ); if ( onReplace && isEmpty( value ) ) { onReplace( content ); @@ -303,30 +303,6 @@ class RichTextWrapper extends Component { return formattingControls.map( ( name ) => `core/${ name }` ); } - getAnchorRect() { - const { current } = this.ref; - const rect = current.getBoundingClientRect(); - - // Add some space. - const buffer = 6; - - // Subtract padding if any. - let { paddingTop } = window.getComputedStyle( current ); - - paddingTop = parseInt( paddingTop, 10 ); - - return { - x: rect.left, - y: rect.top + paddingTop - buffer, - width: rect.width, - height: rect.height - paddingTop + buffer, - left: rect.left, - right: rect.right, - top: rect.top + paddingTop - buffer, - bottom: rect.bottom, - }; - } - render() { const { children, @@ -424,22 +400,7 @@ class RichTextWrapper extends Component { { ( { isSelected, value, onChange, Editable } ) => <> { children && children( { value, onChange } ) } - { isSelected && ! inlineToolbar && hasFormats && ( - - - - ) } - { isSelected && inlineToolbar && hasFormats && ( - - - - ) } + { isSelected && hasFormats && ( ) } { isSelected && } ( { clientId } ) ), + withBlockEditContext( ( { clientId, onCaretVerticalPositionChange, isSelected }, ownProps ) => { + if ( Platform.OS === 'web' ) { + return { clientId }; + } + return { + clientId, + blockIsSelected: ownProps.isSelected !== undefined ? ownProps.isSelected : isSelected, + onCaretVerticalPositionChange, + }; + } ), withSelect( ( select, { clientId, instanceId, @@ -495,6 +465,7 @@ const RichTextContainer = compose( [ getSelectionEnd, getSettings, didAutomaticChange, + __unstableGetBlockWithoutInnerBlocks, } = select( 'core/block-editor' ); const selectionStart = getSelectionStart(); @@ -509,6 +480,18 @@ const RichTextContainer = compose( [ isSelected = selectionStart.clientId === clientId; } + let extraProps = {}; + if ( Platform.OS === 'native' ) { + // If the block of this RichText is unmodified then it's a candidate for replacing when adding a new block. + // In order to fix https://github.com/wordpress-mobile/gutenberg-mobile/issues/1126, let's blur on unmount in that case. + // This apparently assumes functionality the BlockHlder actually + const block = clientId && __unstableGetBlockWithoutInnerBlocks( clientId ); + const shouldBlurOnUnmount = block && isSelected && isUnmodifiedDefaultBlock( block ); + extraProps = { + shouldBlurOnUnmount, + }; + } + return { canUserUseUnfilteredHTML: __experimentalCanUserUseUnfilteredHTML, isCaretWithinFormattedText: isCaretWithinFormattedText(), @@ -516,6 +499,7 @@ const RichTextContainer = compose( [ selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, didAutomaticChange: didAutomaticChange(), + ...extraProps, }; } ), withDispatch( ( dispatch, { diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js deleted file mode 100644 index feab0f747a922..0000000000000 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { View } from 'react-native'; - -/** - * WordPress dependencies - */ -import { RawHTML } from '@wordpress/element'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { pasteHandler, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { withInstanceId, compose } from '@wordpress/compose'; -import { __experimentalRichText as RichText } from '@wordpress/rich-text'; - -/** - * Internal dependencies - */ -import Autocomplete from '../autocomplete'; -import BlockFormatControls from '../block-format-controls'; -import FormatToolbar from './format-toolbar'; -import { withBlockEditContext } from '../block-edit/context'; - -const wrapperClasses = 'editor-rich-text block-editor-rich-text'; -const classes = 'editor-rich-text__editable block-editor-rich-text__editable'; - -function RichTextWraper( { - children, - tagName, - value: originalValue, - onChange: originalOnChange, - selectionStart, - selectionEnd, - onSelectionChange, - multiline, - inlineToolbar, - wrapperClassName, - className, - autocompleters, - onReplace, - onRemove, - onMerge, - onSplit, - isCaretWithinFormattedText, - onEnterFormattedText, - onExitFormattedText, - canUserUseUnfilteredHTML, - isSelected: originalIsSelected, - onCreateUndoLevel, - placeholder, - // From experimental filter. - ...experimentalProps -} ) { - const adjustedValue = originalValue; - const adjustedOnChange = originalOnChange; - - return ( - - { ( { isSelected, value, onChange } ) => - - { children && children( { value, onChange } ) } - { isSelected && ! inlineToolbar && ( - - - - ) } - - } - - ); -} - -const RichTextContainer = compose( [ - withInstanceId, - withBlockEditContext( ( { clientId, onCaretVerticalPositionChange, isSelected }, ownProps ) => { - return { - clientId, - blockIsSelected: ownProps.isSelected !== undefined ? ownProps.isSelected : isSelected, - onCaretVerticalPositionChange, - }; - } ), - withSelect( ( select, { - clientId, - instanceId, - identifier = instanceId, - isSelected, - blockIsSelected, - } ) => { - const { getFormatTypes } = select( 'core/rich-text' ); - const { - getSelectionStart, - getSelectionEnd, - __unstableGetBlockWithoutInnerBlocks, - } = select( 'core/block-editor' ); - - const selectionStart = getSelectionStart(); - const selectionEnd = getSelectionEnd(); - - if ( isSelected === undefined ) { - isSelected = ( - selectionStart.clientId === clientId && - selectionStart.attributeKey === identifier - ); - } - - // If the block of this RichText is unmodified then it's a candidate for replacing when adding a new block. - // In order to fix https://github.com/wordpress-mobile/gutenberg-mobile/issues/1126, let's blur on unmount in that case. - // This apparently assumes functionality the BlockHlder actually - const block = clientId && __unstableGetBlockWithoutInnerBlocks( clientId ); - const shouldBlurOnUnmount = block && isSelected && isUnmodifiedDefaultBlock( block ); - - return { - formatTypes: getFormatTypes(), - selectionStart: isSelected ? selectionStart.offset : undefined, - selectionEnd: isSelected ? selectionEnd.offset : undefined, - isSelected, - blockIsSelected, - shouldBlurOnUnmount, - }; - } ), - withDispatch( ( dispatch, { - clientId, - instanceId, - identifier = instanceId, - } ) => { - const { - __unstableMarkLastChangeAsPersistent, - selectionChange, - } = dispatch( 'core/block-editor' ); - - return { - onCreateUndoLevel: __unstableMarkLastChangeAsPersistent, - onSelectionChange( start, end ) { - selectionChange( clientId, identifier, start, end ); - }, - }; - } ), -] )( RichTextWraper ); - -RichTextContainer.Content = ( { value, format, tagName: Tag, multiline, ...props } ) => { - let content; - let html = value; - let MultilineTag; - - if ( multiline === true || multiline === 'p' || multiline === 'li' ) { - MultilineTag = multiline === true ? 'p' : multiline; - } - - if ( ! html && MultilineTag ) { - html = `<${ MultilineTag }>`; - } - - switch ( format ) { - case 'string': - content = { html }; - break; - } - - if ( Tag ) { - return { content }; - } - - return content; -}; - -RichTextContainer.isEmpty = ( value = '' ) => { - // Handle deprecated `children` and `node` sources. - if ( Array.isArray( value ) ) { - return ! value || value.length === 0; - } - - return value.length === 0; -}; - -RichTextContainer.Content.defaultProps = { - format: 'string', - value: '', -}; - -/** - * @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/rich-text/README.md - */ -export default RichTextContainer; -export { RichTextShortcut } from './shortcut'; -export { RichTextToolbarButton } from './toolbar-button'; -export { __unstableRichTextInputEvent } from './input-event'; diff --git a/packages/block-editor/src/components/rich-text/remove-browser-shortcuts.native.js b/packages/block-editor/src/components/rich-text/remove-browser-shortcuts.native.js new file mode 100644 index 0000000000000..43a4b030646db --- /dev/null +++ b/packages/block-editor/src/components/rich-text/remove-browser-shortcuts.native.js @@ -0,0 +1 @@ +export const RemoveBrowserShortcuts = () => null; diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js deleted file mode 100644 index b2cfd4963d2e8..0000000000000 --- a/packages/blocks/src/api/index.native.js +++ /dev/null @@ -1,44 +0,0 @@ -export { - cloneBlock, - createBlock, - switchToBlockType, -} from './factory'; -export { - default as parse, - getBlockAttributes, - parseWithAttributeSchema, -} from './parser'; -export { - default as serialize, - getBlockContent, - getBlockDefaultClassName, - getSaveContent, -} from './serializer'; -export { - registerBlockType, - unregisterBlockType, - getFreeformContentHandlerName, - setUnregisteredTypeHandlerName, - getUnregisteredTypeHandlerName, - getBlockType, - getBlockTypes, - getBlockSupport, - hasBlockSupport, - isReusableBlock, - getChildBlockNames, - hasChildBlocks, - hasChildBlocksWithInserterSupport, - setDefaultBlockName, - getDefaultBlockName, - setGroupingBlockName, -} from './registration'; -export { - isUnmodifiedDefaultBlock, - normalizeIconObject, -} from './utils'; -export { - doBlocksMatchTemplate, - synchronizeBlocksWithTemplate, -} from './templates'; -export { pasteHandler, getPhrasingContentSchema } from './raw-handling'; -export { default as children } from './children'; diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 3fada7034deeb..e9aec0ad2fb71 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -1,5 +1,7 @@ // Components export * from './primitives'; +export { default as ColorIndicator } from './color-indicator'; +export { default as ColorPalette } from './color-palette'; export { default as Dashicon } from './dashicon'; export { default as Dropdown } from './dropdown'; export { default as Toolbar } from './toolbar'; diff --git a/packages/components/src/keyboard-shortcuts/index.native.js b/packages/components/src/keyboard-shortcuts/index.native.js new file mode 100644 index 0000000000000..fab7b99261e3a --- /dev/null +++ b/packages/components/src/keyboard-shortcuts/index.native.js @@ -0,0 +1,2 @@ +const KeyboardShortcuts = () => null; +export default KeyboardShortcuts; diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index d6a40914ea0f9..c3112d3337fb7 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -259,18 +259,32 @@ class RichText extends Component { } if ( onPaste ) { - // Only process file if no HTML is present. - // Note: a pasted file may have the URL as plain text. - const image = find( [ ...items, ...files ], ( { type } ) => - /^image\/(?:jpe?g|png|gif)$/.test( type ) - ); + files = Array.from( files ); + + Array.from( items ).forEach( ( item ) => { + if ( ! item.getAsFile ) { + return; + } + + const file = item.getAsFile(); + + if ( ! file ) { + return; + } + + const { name, type, size } = file; + + if ( ! find( files, { name, type, size } ) ) { + files.push( file ); + } + } ); onPaste( { value: this.removeEditorOnlyFormats( record ), onChange: this.onChange, html, plainText, - image, + files, } ); } } diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 09ac451ed56d2..34a1b03fe24ef 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -30,10 +30,7 @@ import { getActiveFormat } from '../get-active-format'; import { getActiveFormats } from '../get-active-formats'; import { isEmpty, isEmptyLine } from '../is-empty'; import { create } from '../create'; -import { split } from '../split'; import { toHTMLString } from '../to-html-string'; -import { insert } from '../insert'; -import { insertLineSeparator } from '../insert-line-separator'; import { removeLineSeparator } from '../remove-line-separator'; import { isCollapsed } from '../is-collapsed'; import { remove } from '../remove'; @@ -43,28 +40,6 @@ const unescapeSpaces = ( text ) => { return text.replace( / | /gi, ' ' ); }; -/** - * Calls {@link pasteHandler} with a fallback to plain text when HTML processing - * results in errors - * - * @param {Function} originalPasteHandler The original handler function - * @param {Object} [options] The options to pass to {@link pasteHandler} - * - * @return {Array|string} A list of blocks or a string, depending on - * `handlerMode`. - */ -const saferPasteHandler = ( originalPasteHandler, options ) => { - try { - return originalPasteHandler( options ); - } catch ( error ) { - window.console.log( 'Pasting HTML failed:', error ); - window.console.log( 'HTML:', options.HTML ); - window.console.log( 'Falling back to plain text.' ); - // fallback to plain text - return originalPasteHandler( { ...options, HTML: '' } ); - } -}; - const gutenbergFormatNamesToAztec = { 'core/bold': 'bold', 'core/italic': 'italic', @@ -72,7 +47,7 @@ const gutenbergFormatNamesToAztec = { }; export class RichText extends Component { - constructor( { value, __unstableMultiline: multiline, selectionStart, selectionEnd } ) { + constructor( { value, selectionStart, selectionEnd, __unstableMultilineTag: multiline } ) { super( ...arguments ); this.isMultiline = false; @@ -84,12 +59,12 @@ export class RichText extends Component { if ( this.multilineTag === 'li' ) { this.multilineWrapperTags = [ 'ul', 'ol' ]; } - this.onSplit = this.onSplit.bind( this ); + this.isIOS = Platform.OS === 'ios'; this.createRecord = this.createRecord.bind( this ); this.onChange = this.onChange.bind( this ); - this.onEnter = this.onEnter.bind( this ); - this.onBackspace = this.onBackspace.bind( this ); + this.handleEnter = this.handleEnter.bind( this ); + this.handleDelete = this.handleDelete.bind( this ); this.onPaste = this.onPaste.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); @@ -169,63 +144,6 @@ export class RichText extends Component { return { ...value, start, end }; } - /** - * Signals to the RichText owner that the block can be replaced with two - * blocks as a result of splitting the block by pressing enter, or with - * blocks as a result of splitting the block by pasting block content in the - * instance. - * - * @param {Object} record The rich text value to split. - * @param {Array} pastedBlocks The pasted blocks to insert, if any. - */ - onSplit( record, pastedBlocks = [] ) { - const { - __unstableOnReplace: onReplace, - __unstableOnSplit: onSplit, - __unstableOnSplitMiddle: onSplitMiddle, - } = this.props; - - if ( ! onReplace || ! onSplit ) { - return; - } - - const blocks = []; - const [ before, after ] = split( record ); - const hasPastedBlocks = pastedBlocks.length > 0; - - // Create a block with the content before the caret if there's no pasted - // blocks, or if there are pasted blocks and the value is not empty. - // We do not want a leading empty block on paste, but we do if split - // with e.g. the enter key. - if ( ! hasPastedBlocks || ! isEmpty( before ) ) { - blocks.push( onSplit( this.valueToFormat( before ) ) ); - } - - if ( hasPastedBlocks ) { - blocks.push( ...pastedBlocks ); - } else if ( onSplitMiddle ) { - blocks.push( onSplitMiddle() ); - } - - // If there's pasted blocks, append a block with the content after the - // caret. Otherwise, do append and empty block if there is no - // `onSplitMiddle` prop, but if there is and the content is empty, the - // middle block is enough to set focus in. - if ( hasPastedBlocks || ! onSplitMiddle || ! isEmpty( after ) ) { - blocks.push( onSplit( this.valueToFormat( after ) ) ); - } - - // If there are pasted blocks, set the selection to the last one. - // Otherwise, set the selection to the second block. - const indexToSelect = hasPastedBlocks ? blocks.length - 1 : 1; - // The onSplit event can cause a content update event for this block. Such event should - // definitely be processed by our native components, since they have no knowledge of - // how the split works. Setting lastEventCount to undefined forces the native component to - // always update when provided with new content. - this.lastEventCount = undefined; - onReplace( blocks, indexToSelect ); - } - valueToFormat( value ) { // remove the outer root tags return this.removeRootTagsProduceByAztec( toHTMLString( { @@ -245,6 +163,7 @@ export class RichText extends Component { } onFormatChange( record ) { + this.getRecord( record ); const { start, end, activeFormats = [] } = record; const changeHandlers = pickBy( this.props, ( v, key ) => key.startsWith( 'format_on_change_functions_' ) @@ -346,92 +265,67 @@ export class RichText extends Component { this.lastAztecEventType = 'content size change'; } - onEnter( event ) { - if ( this.props.onEnter ) { - this.props.onEnter(); + handleEnter( event ) { + const { onEnter } = this.props; + + if ( ! onEnter ) { return; } - const { - __unstableOnReplace: onReplace, - __unstableOnSplit: onSplit, - } = this.props; - this.lastEventCount = event.nativeEvent.eventCount; - this.comesFromAztec = true; - this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; - - const canSplit = onReplace && onSplit; - const currentRecord = this.createRecord(); - if ( this.multilineTag ) { - if ( event.shiftKey ) { - this.needsSelectionUpdate = true; - const insertedLineBreak = { ...insert( currentRecord, '\n' ) }; - this.onFormatChange( insertedLineBreak ); - } else if ( canSplit && isEmptyLine( currentRecord ) ) { - this.onSplit( currentRecord ); - } else { - this.needsSelectionUpdate = true; - const insertedLineSeparator = { ...insertLineSeparator( currentRecord ) }; - this.onFormatChange( insertedLineSeparator ); - } - } else if ( event.shiftKey || ! onSplit ) { - this.needsSelectionUpdate = true; - const insertedLineBreak = { ...insert( currentRecord, '\n' ) }; - this.onFormatChange( insertedLineBreak ); - } else { - this.onSplit( currentRecord ); - } + onEnter( { + value: this.createRecord(), + onChange: this.onFormatChange, + shiftKey: event.shiftKey, + } ); this.lastAztecEventType = 'input'; } - onBackspace( event ) { - const { - __unstableOnMerge: onMerge, - __unstableOnRemove: onRemove, - onChange, - } = this.props; - if ( ! onMerge && ! onRemove ) { - return; - } - + handleDelete( event ) { const keyCode = BACKSPACE; // TODO : should we differentiate BACKSPACE and DELETE? const isReverse = keyCode === BACKSPACE; + const { onDelete, __unstableMultilineTag: multilineTag } = this.props; + const { activeFormats = [] } = this.state; this.lastEventCount = event.nativeEvent.eventCount; this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; const value = this.createRecord(); - const { start, end } = value; + const { start, end, text } = value; let newValue; // Always handle full content deletion ourselves. - if ( start === 0 && end !== 0 && end >= value.text.length ) { - newValue = remove( value, start, end ); - onChange( newValue ); + if ( start === 0 && end !== 0 && end >= text.length ) { + newValue = remove( value ); + this.onFormatChange( newValue ); + event.preventDefault(); return; } - if ( this.multilineTag ) { - newValue = removeLineSeparator( value, keyCode === BACKSPACE ); + if ( multilineTag ) { + if ( isReverse && value.start === 0 && value.end === 0 && isEmptyLine( value ) ) { + newValue = removeLineSeparator( value, ! isReverse ); + } else { + newValue = removeLineSeparator( value, isReverse ); + } if ( newValue ) { this.onFormatChange( newValue ); + event.preventDefault(); return; } } - const empty = this.isEmpty(); - - if ( onMerge ) { - onMerge( ! isReverse ); + // Only process delete if the key press occurs at an uncollapsed edge. + if ( + ! onDelete || + ! isCollapsed( value ) || + activeFormats.length || + ( isReverse && start !== 0 ) || + ( ! isReverse && end !== text.length ) + ) { + return; } - // Only handle remove on Backspace. This serves dual-purpose of being - // an intentional user interaction distinguishing between Backspace and - // Delete to remove the empty field, but also to avoid merge & remove - // causing destruction of two fields (merge, then removed merged). - if ( onRemove && empty && isReverse ) { - onRemove( ! isReverse ); - } + onDelete( { isReverse, value } ); event.preventDefault(); this.lastAztecEventType = 'input'; @@ -444,10 +338,7 @@ export class RichText extends Component { */ onPaste( event ) { const { - tagName, - __unstablePasteHandler: pasteHandler, - __unstableOnReplace: onReplace, - __unstableOnSplit: onSplit, + onPaste, onChange, } = this.props; @@ -456,30 +347,6 @@ export class RichText extends Component { event.preventDefault(); - // Only process file if no HTML is present. - // Note: a pasted file may have the URL as plain text. - if ( files && files.length > 0 ) { - const uploadId = Number.MAX_SAFE_INTEGER; - let html = ''; - files.forEach( ( file ) => { - html += ``; - } ); - const content = pasteHandler( { - HTML: html, - mode: 'BLOCKS', - tagName, - } ); - const shouldReplace = onReplace && this.isEmpty(); - - if ( shouldReplace ) { - onReplace( content ); - } else { - this.onSplit( currentRecord, content ); - } - - return; - } - // There is a selection, check if a URL is pasted. if ( ! isCollapsed( currentRecord ) ) { const trimmedText = ( pastedHtml || pastedText ).replace( /<[^>]+>/g, '' ) @@ -503,46 +370,14 @@ export class RichText extends Component { } } - const shouldReplace = this.props.onReplace && this.isEmpty(); - - let mode = 'INLINE'; - - if ( shouldReplace ) { - mode = 'BLOCKS'; - } else if ( onSplit ) { - mode = 'AUTO'; - } - - const pastedContent = saferPasteHandler( pasteHandler, { - HTML: pastedHtml, - plainText: pastedText, - mode, - tagName: this.props.tagName, - canUserUseUnfilteredHTML: this.props.canUserUseUnfilteredHTML, - } ); - - if ( typeof pastedContent === 'string' ) { - const recordToInsert = create( { html: pastedContent } ); - const resultingRecord = insert( currentRecord, recordToInsert ); - const resultingContent = this.valueToFormat( resultingRecord ); - - this.lastEventCount = undefined; - this.value = resultingContent; - - // explicitly set selection after inline paste - this.onSelectionChange( resultingRecord.start, resultingRecord.end ); - - onChange( this.value ); - } else if ( onSplit ) { - if ( ! pastedContent.length ) { - return; - } - - if ( shouldReplace ) { - onReplace( pastedContent ); - } else { - this.onSplit( currentRecord, pastedContent ); - } + if ( onPaste ) { + onPaste( { + value: currentRecord, + onChange: this.onFormatChange, + html: pastedHtml, + plainText: pastedText, + files, + } ); } } @@ -759,7 +594,7 @@ export class RichText extends Component { this.lastEventCount = undefined; // force a refresh on the native side value = ''; } - // On android if content is empty we need to send no content or else the placeholder with not show. + // On android if content is empty we need to send no content or else the placeholder will not show. if ( ! this.isIOS && value === '' ) { return value; } @@ -852,8 +687,8 @@ export class RichText extends Component { onChange={ this.onChange } onFocus={ this.onFocus } onBlur={ this.onBlur } - onEnter={ this.onEnter } - onBackspace={ this.onBackspace } + onEnter={ this.handleEnter } + onBackspace={ this.handleDelete } onPaste={ this.onPaste } activeFormats={ this.getActiveFormatNames( record ) } onContentSizeChange={ this.onContentSizeChange } diff --git a/packages/rich-text/src/component/test/index.native.js b/packages/rich-text/src/component/test/index.native.js index 6b2bc12f855ff..22ee6b118bb2d 100644 --- a/packages/rich-text/src/component/test/index.native.js +++ b/packages/rich-text/src/component/test/index.native.js @@ -1,17 +1,8 @@ -/** - * External dependencies - */ -import { shallow } from 'enzyme'; - /** * Internal dependencies */ import { RichText } from '../index'; -const getStylesFromColorScheme = () => { - return { color: 'white' }; -}; - describe( 'RichText Native', () => { let richText; @@ -33,29 +24,4 @@ describe( 'RichText Native', () => { expect( richText.willTrimSpaces( html ) ).toBe( false ); } ); } ); - - describe( 'Adds new line on Enter', () => { - let newValue; - const wrapper = shallow( { - newValue = value; - } } - formatTypes={ [] } - onSelectionChange={ jest.fn() } - getStylesFromColorScheme={ getStylesFromColorScheme } - /> ); - - const event = { - nativeEvent: { - eventCount: 0, - }, - }; - wrapper.instance().onEnter( event ); - - it( ' Adds
tag to content after pressing Enter key', () => { - expect( newValue ).toEqual( '
' ); - } ); - } ); } );