@wordpress/block-library
Version:
Block library for the WordPress editor.
567 lines (502 loc) • 14.9 kB
JavaScript
/**
* External dependencies
*/
import { View, AccessibilityInfo, Platform, Text } from 'react-native';
/**
* WordPress dependencies
*/
import { useCallback, useEffect, useState, useRef } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { __, _x } from '@wordpress/i18n';
import {
RichText,
InspectorControls,
BlockControls,
store as blockEditorStore,
getColorObjectByAttributeValues,
getGradientValueBySlug,
__experimentalGetColorClassesAndStyles as getColorClassesAndStyles,
} from '@wordpress/block-editor';
import {
PanelBody,
ToolbarGroup,
ToolbarButton,
LinkSettingsNavigation,
UnitControl,
getValueAndUnit,
BottomSheetSelectControl,
CSS_UNITS,
filterUnitsWithSettings,
useMobileGlobalStylesColors,
} from '@wordpress/components';
import { link } from '@wordpress/icons';
import { store as editPostStore } from '@wordpress/edit-post';
/**
* Internal dependencies
*/
import richTextStyle from './rich-text.scss';
import styles from './editor.scss';
import ColorBackground from './color-background';
const MIN_BORDER_RADIUS_VALUE = 0;
const MAX_BORDER_RADIUS_VALUE = 50;
const INITIAL_MAX_WIDTH = 108;
const MIN_WIDTH = 40;
// Map of the percentage width to pixel subtraction that make the buttons fit nicely into columns.
const MIN_WIDTH_MARGINS = {
100: 0,
75: styles.button75?.marginLeft,
50: styles.button50?.marginLeft,
25: styles.button25?.marginLeft,
};
function WidthPanel( { selectedWidth, setAttributes } ) {
function handleChange( newWidth ) {
// Check if we are toggling the width off
let width = selectedWidth === newWidth ? undefined : newWidth;
if ( newWidth === 'auto' ) {
width = undefined;
}
// Update attributes.
setAttributes( { width } );
}
const options = [
{ value: 'auto', label: __( 'Auto' ) },
{ value: 25, label: '25%' },
{ value: 50, label: '50%' },
{ value: 75, label: '75%' },
{ value: 100, label: '100%' },
];
if ( ! selectedWidth ) {
selectedWidth = 'auto';
}
return (
<PanelBody title={ __( 'Width Settings' ) }>
<BottomSheetSelectControl
label={ __( 'Button width' ) }
value={ selectedWidth }
onChange={ handleChange }
options={ options }
/>
</PanelBody>
);
}
function ButtonEdit( props ) {
const { isSelected, parentWidth } = props;
const initialBorderRadius = props?.attributes?.style?.border?.radius;
const { valueUnit = 'px' } = getValueAndUnit( initialBorderRadius ) || {};
const { editorSidebarOpened, numOfButtons } = useSelect(
( select ) => {
const { isEditorSidebarOpened } = select( editPostStore );
const { getBlockCount, getBlockRootClientId } =
select( blockEditorStore );
const parentId = getBlockRootClientId( clientId );
const blockCount = getBlockCount( parentId );
const currentIsEditorSidebarOpened = isEditorSidebarOpened();
return {
editorSidebarOpened: isSelected && currentIsEditorSidebarOpened,
numOfButtons: blockCount,
};
},
[ clientId, isSelected ]
);
const { closeGeneralSidebar } = useDispatch( editPostStore );
const [ maxWidth, setMaxWidth ] = useState( INITIAL_MAX_WIDTH );
const [ isLinkSheetVisible, setIsLinkSheetVisible ] = useState( false );
const [ isButtonFocused, setIsButtonFocused ] = useState( true );
const [ placeholderTextWidth, setPlaceholderTextWidth ] = useState( 0 );
const [ borderRadiusUnit, setBorderRadiusUnit ] = useState( valueUnit );
const richTextRef = useRef();
const colors = useMobileGlobalStylesColors();
const gradients = useMobileGlobalStylesColors( 'gradients' );
useEffect( () => {
if ( isSelected ) {
onToggleButtonFocus( true );
}
}, [ isSelected ] );
useEffect( () => {
onSetMaxWidth( null, true );
}, [ parentWidth ] );
useEffect( () => {
// Blur `RichText` on Android when link settings sheet or button settings sheet is opened,
// to avoid flashing caret after closing one of them
const richText = richTextRef?.current;
if ( Platform.OS === 'android' && richText ) {
if ( editorSidebarOpened || isLinkSheetVisible ) {
richText.blur();
onToggleButtonFocus( false );
} else {
onToggleButtonFocus( true );
}
}
}, [ editorSidebarOpened, isLinkSheetVisible ] );
useEffect( () => {
if ( richTextRef?.current ) {
if ( ! isSelected && isButtonFocused ) {
onToggleButtonFocus( false );
}
if ( isSelected && ! isButtonFocused ) {
AccessibilityInfo.isScreenReaderEnabled().then( ( enabled ) => {
if ( enabled ) {
onToggleButtonFocus( true );
richTextRef?.current.focus();
}
} );
}
}
}, [ isSelected, isButtonFocused ] );
const linkSettingsActions = [
{
label: __( 'Remove link' ),
onPress: onClearSettings,
},
];
const linkSettingsOptions = {
url: {
label: __( 'Button Link URL' ),
placeholder: __( 'Add URL' ),
autoFocus: true,
autoFill: false,
},
openInNewTab: {
label: __( 'Open in new tab' ),
},
linkRel: {
label: __( 'Link Rel' ),
placeholder: _x( 'None', 'Link rel attribute value placeholder' ),
},
};
const noFocusLinkSettingOptions = {
...linkSettingsOptions,
url: {
...linkSettingsOptions.url,
autoFocus: false,
},
};
function getBackgroundColor() {
const { attributes, style } = props;
const { backgroundColor, gradient } = attributes;
// Return named gradient value if available.
const gradientValue = getGradientValueBySlug( gradients, gradient );
if ( gradientValue ) {
return gradientValue;
}
const colorProps = getColorClassesAndStyles( attributes );
// Retrieve named color object to force inline styles for themes that
// do not load their color stylesheets in the editor.
const colorObject = getColorObjectByAttributeValues(
colors,
backgroundColor
);
return (
colorObject?.color ||
colorProps.style?.backgroundColor ||
colorProps.style?.background ||
style?.backgroundColor ||
styles.defaultButton.backgroundColor
);
}
function getTextColor() {
const { attributes, style } = props;
const colorProps = getColorClassesAndStyles( attributes );
// Retrieve named color object to force inline styles for themes that
// do not load their color stylesheets in the editor.
const colorObject = getColorObjectByAttributeValues(
colors,
attributes.textColor
);
return (
colorObject?.color ||
colorProps.style?.color ||
style?.color ||
styles.defaultButton.color
);
}
function onChangeText( value ) {
const { setAttributes } = props;
setAttributes( { text: value } );
}
function onChangeBorderRadius( newRadius ) {
const { setAttributes, attributes } = props;
const { style } = attributes;
const newStyle = getNewStyle( style, newRadius, borderRadiusUnit );
setAttributes( { style: newStyle } );
}
function onChangeBorderRadiusUnit( newRadiusUnit ) {
const { setAttributes, attributes } = props;
const { style } = attributes;
const newBorderRadius = getBorderRadiusValue(
attributes?.style?.border?.radius
);
const newStyle = getNewStyle( style, newBorderRadius, newRadiusUnit );
setAttributes( { style: newStyle } );
setBorderRadiusUnit( newRadiusUnit );
}
function getNewStyle( style, radius, radiusUnit ) {
return {
...style,
border: {
...style?.border,
radius: `${ radius }${ radiusUnit }`, // Store the value with the unit so that it works as expected.
},
};
}
function onShowLinkSettings() {
setIsLinkSheetVisible( true );
}
function onHideLinkSettings() {
setIsLinkSheetVisible( false );
}
function onToggleButtonFocus( value ) {
if ( value !== isButtonFocused ) {
setIsButtonFocused( value );
}
}
function onClearSettings() {
const { setAttributes } = props;
setAttributes( {
url: '',
rel: '',
linkTarget: '',
} );
onHideLinkSettings();
}
function onLayout( { nativeEvent } ) {
const { width } = nativeEvent.layout;
onSetMaxWidth( width );
}
const onSetMaxWidth = useCallback(
( width, isParentWidthDidChange = false ) => {
const { marginRight: spacing } = styles.defaultButton;
const isParentWidthChanged = isParentWidthDidChange
? isParentWidthDidChange
: maxWidth !== parentWidth;
const isWidthChanged = maxWidth !== width;
if ( parentWidth && ! width && isParentWidthChanged ) {
setMaxWidth( parentWidth - spacing );
} else if ( ! parentWidth && width && isWidthChanged ) {
setMaxWidth( width - spacing );
}
},
[ maxWidth, parentWidth ]
);
function onRemove() {
const { onDeleteBlock, onReplace } = props;
if ( numOfButtons === 1 ) {
onDeleteBlock();
} else {
onReplace( [] );
}
}
function onPlaceholderTextWidth( { nativeEvent } ) {
const textWidth =
nativeEvent.lines[ 0 ] && nativeEvent.lines[ 0 ].width;
if ( textWidth && textWidth !== placeholderTextWidth ) {
setPlaceholderTextWidth( Math.min( textWidth, maxWidth ) );
}
}
const onSetRef = useCallback(
( ref ) => {
richTextRef.current = ref;
},
[ richTextRef ]
);
const onUnstableOnFocus = useCallback( () => {
onToggleButtonFocus( true );
}, [] );
const onBlur = useCallback( () => {
onSetMaxWidth();
}, [] );
function dismissSheet() {
onHideLinkSettings();
closeGeneralSidebar();
}
function getLinkSettings( isCompatibleWithSettings ) {
const { attributes, setAttributes } = props;
return (
<LinkSettingsNavigation
isVisible={ isLinkSheetVisible }
url={ attributes.url }
rel={ attributes.rel }
linkTarget={ attributes.linkTarget }
onClose={ dismissSheet }
setAttributes={ setAttributes }
withBottomSheet={ ! isCompatibleWithSettings }
hasPicker
actions={ linkSettingsActions }
options={
isCompatibleWithSettings
? linkSettingsOptions
: noFocusLinkSettingOptions
}
showIcon={ ! isCompatibleWithSettings }
/>
);
}
// Render `Text` with `placeholderText` styled as a placeholder
// to calculate its width which then is set as a `minWidth`
function getPlaceholderWidth( placeholderText ) {
return (
<Text
style={ styles.placeholder }
onTextLayout={ onPlaceholderTextWidth }
>
{ placeholderText }
</Text>
);
}
function getBorderRadiusValue( currentBorderRadius, defaultBorderRadius ) {
const valueAndUnit = getValueAndUnit( currentBorderRadius );
if ( Number.isInteger( parseInt( valueAndUnit?.valueToConvert ) ) ) {
return parseFloat( valueAndUnit.valueToConvert );
}
return defaultBorderRadius;
}
const {
attributes,
clientId,
onReplace,
mergeBlocks,
setAttributes,
style,
} = props;
const {
placeholder,
text,
style: buttonStyle,
url,
align = 'center',
width,
} = attributes;
const { paddingTop: spacing, borderWidth } = styles.defaultButton;
if ( parentWidth === 0 ) {
return null;
}
const currentBorderRadius = buttonStyle?.border?.radius;
const borderRadiusValue = getBorderRadiusValue(
currentBorderRadius,
styles.defaultButton.borderRadius
);
const buttonBorderRadiusValue =
borderRadiusUnit === 'px' || borderRadiusUnit === '%'
? borderRadiusValue
: Math.floor( 14 * borderRadiusValue ); // Lets assume that the font size is set to 14px; TO get a nicer preview.
const outlineBorderRadius =
buttonBorderRadiusValue > 0
? buttonBorderRadiusValue + spacing + borderWidth
: 0;
// To achieve proper expanding and shrinking `RichText` on iOS, there is a need to set a `minWidth`
// value at least on 1 when `RichText` is focused or when is not focused, but `RichText` value is
// different than empty string.
let minWidth =
isButtonFocused || ( ! isButtonFocused && text && text !== '' )
? MIN_WIDTH
: placeholderTextWidth;
if ( width ) {
// Set the width of the button.
minWidth = Math.floor(
maxWidth * ( width / 100 ) - MIN_WIDTH_MARGINS[ width ]
);
}
// To achieve proper expanding and shrinking `RichText` on Android, there is a need to set
// a `placeholder` as an empty string when `RichText` is focused,
// because `AztecView` is calculating a `minWidth` based on placeholder text.
const placeholderText =
isButtonFocused || ( ! isButtonFocused && text && text !== '' )
? ''
: placeholder || __( 'Add text…' );
const backgroundColor = getBackgroundColor();
const textColor = getTextColor();
const isFixedWidth = !! width;
const outLineStyles = [
styles.outline,
{
borderRadius: outlineBorderRadius,
borderColor: backgroundColor,
},
];
const textStyles = {
...richTextStyle.richText,
paddingLeft: isFixedWidth ? 0 : richTextStyle.richText.paddingLeft,
paddingRight: isFixedWidth ? 0 : richTextStyle.richText.paddingRight,
color: textColor,
};
return (
<View onLayout={ onLayout }>
{ getPlaceholderWidth( placeholderText ) }
<ColorBackground
borderRadiusValue={ buttonBorderRadiusValue }
backgroundColor={ backgroundColor }
isSelected={ isSelected }
>
{ isSelected && (
<View pointerEvents="none" style={ outLineStyles } />
) }
<RichText
setRef={ onSetRef }
placeholder={ placeholderText }
value={ text }
onChange={ onChangeText }
style={ textStyles }
textAlign={ align }
placeholderTextColor={
style?.color || styles.placeholderTextColor.color
}
identifier="text"
tagName="p"
minWidth={ minWidth } // The minimum Button size.
maxWidth={ isFixedWidth ? minWidth : maxWidth } // The width of the screen.
id={ clientId }
isSelected={ isButtonFocused }
withoutInteractiveFormatting
unstableOnFocus={ onUnstableOnFocus }
__unstableMobileNoFocusOnMount={ ! isSelected }
selectionColor={ textColor }
onBlur={ onBlur }
onReplace={ onReplace }
onRemove={ onRemove }
onMerge={ mergeBlocks }
fontSize={ style?.fontSize }
/>
</ColorBackground>
{ isSelected && (
<>
<BlockControls>
<ToolbarGroup>
<ToolbarButton
title={ __( 'Edit link' ) }
icon={ link }
onClick={ onShowLinkSettings }
isActive={ url }
/>
</ToolbarGroup>
</BlockControls>
{ getLinkSettings( false ) }
<InspectorControls>
<PanelBody title={ __( 'Border Settings' ) }>
<UnitControl
label={ __( 'Border Radius' ) }
min={ MIN_BORDER_RADIUS_VALUE }
max={ MAX_BORDER_RADIUS_VALUE }
value={ borderRadiusValue }
onChange={ onChangeBorderRadius }
onUnitChange={ onChangeBorderRadiusUnit }
unit={ borderRadiusUnit }
units={ filterUnitsWithSettings(
[ 'px', 'em', 'rem' ],
CSS_UNITS
) }
/>
</PanelBody>
<WidthPanel
selectedWidth={ width }
setAttributes={ setAttributes }
/>
<PanelBody title={ __( 'Link Settings' ) }>
{ getLinkSettings( true ) }
</PanelBody>
</InspectorControls>
</>
) }
</View>
);
}
export default ButtonEdit;