@wordpress/block-editor
Version:
360 lines (348 loc) • 11.3 kB
JavaScript
/**
* External dependencies
*/
import { AccessibilityInfo, Platform } from 'react-native';
/**
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
import { Dropdown, ToolbarButton, Picker } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose, withPreferredColorScheme } from '@wordpress/compose';
import { isUnmodifiedDefaultBlock } from '@wordpress/blocks';
import { Icon, plus, plusCircle, plusCircleFilled, insertAfter, insertBefore } from '@wordpress/icons';
import { setBlockTypeImpressions } from '@wordpress/react-native-bridge';
/**
* Internal dependencies
*/
import styles from './style.scss';
import InserterMenu from './menu';
import BlockInsertionPoint from '../block-list/insertion-point';
import { store as blockEditorStore } from '../../store';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const VOICE_OVER_ANNOUNCEMENT_DELAY = 1000;
const defaultRenderToggle = ({
onToggle,
disabled,
iconStyle,
buttonStyle,
onLongPress
}) => {
return /*#__PURE__*/_jsx(ToolbarButton, {
title: _x('Add block', 'Generic label for block inserter button'),
icon: /*#__PURE__*/_jsx(Icon, {
icon: plus,
style: iconStyle
}),
onClick: onToggle,
extraProps: {
hint: __('Double tap to add a block'),
// testID is present to disambiguate this element for native UI tests. It's not
// usually required for components. See: https://github.com/WordPress/gutenberg/pull/18832#issuecomment-561411389.
testID: 'add-block-button',
onLongPress,
hitSlop: {
top: 10,
bottom: 10,
left: 10,
right: 10
}
},
isDisabled: disabled,
customContainerStyles: buttonStyle,
fixedRatio: false
});
};
export class Inserter extends Component {
announcementTimeout;
constructor() {
super(...arguments);
this.onToggle = this.onToggle.bind(this);
this.renderInserterToggle = this.renderInserterToggle.bind(this);
this.renderContent = this.renderContent.bind(this);
}
componentWillUnmount() {
clearTimeout(this.announcementTimeout);
}
getInsertionOptions() {
const addBeforeOption = {
value: 'before',
label: __('Add Block Before'),
icon: plusCircle
};
const replaceCurrentOption = {
value: 'replace',
label: __('Replace Current Block'),
icon: plusCircleFilled
};
const addAfterOption = {
value: 'after',
label: __('Add Block After'),
icon: plusCircle
};
const addToBeginningOption = {
value: 'start',
label: __('Add To Beginning'),
icon: insertBefore
};
const addToEndOption = {
value: 'end',
label: __('Add To End'),
icon: insertAfter
};
const {
isAnyBlockSelected,
isSelectedBlockReplaceable
} = this.props;
if (isAnyBlockSelected) {
if (isSelectedBlockReplaceable) {
return [addToBeginningOption, addBeforeOption, replaceCurrentOption, addAfterOption, addToEndOption];
}
return [addToBeginningOption, addBeforeOption, addAfterOption, addToEndOption];
}
return [addToBeginningOption, addToEndOption];
}
getInsertionIndex(insertionType) {
const {
insertionIndexDefault,
insertionIndexStart,
insertionIndexBefore,
insertionIndexAfter,
insertionIndexEnd
} = this.props;
if (insertionType === 'start') {
return insertionIndexStart;
}
if (insertionType === 'before' || insertionType === 'replace') {
return insertionIndexBefore;
}
if (insertionType === 'after') {
return insertionIndexAfter;
}
if (insertionType === 'end') {
return insertionIndexEnd;
}
return insertionIndexDefault;
}
shouldReplaceBlock(insertionType) {
const {
isSelectedBlockReplaceable
} = this.props;
if (insertionType === 'replace') {
return true;
}
if (insertionType === 'default' && isSelectedBlockReplaceable) {
return true;
}
return false;
}
onToggle(isOpen) {
const {
blockTypeImpressions,
onToggle,
updateSettings
} = this.props;
if (!isOpen) {
const impressionsRemain = Object.values(blockTypeImpressions).some(count => count > 0);
if (impressionsRemain) {
const decrementedImpressions = Object.entries(blockTypeImpressions).reduce((acc, [blockName, count]) => ({
...acc,
[blockName]: Math.max(count - 1, 0)
}), {});
// Persist block type impression to JavaScript store.
updateSettings({
impressions: decrementedImpressions
});
// Persist block type impression count to native app store.
setBlockTypeImpressions(decrementedImpressions);
}
}
// Surface toggle callback to parent component.
if (onToggle) {
onToggle(isOpen);
}
this.onInserterToggledAnnouncement(isOpen);
}
onInserterToggledAnnouncement(isOpen) {
AccessibilityInfo.isScreenReaderEnabled().then(isEnabled => {
if (isEnabled) {
const isIOS = Platform.OS === 'ios';
const announcement = isOpen ? __('Scrollable block menu opened. Select a block.') : __('Scrollable block menu closed.');
this.announcementTimeout = setTimeout(() => AccessibilityInfo.announceForAccessibility(announcement), isIOS ? VOICE_OVER_ANNOUNCEMENT_DELAY : 0);
}
});
}
/**
* Render callback to display Dropdown toggle element.
*
* @param {Object} options
* @param {Function} options.onToggle Callback to invoke when toggle is
* pressed.
* @param {boolean} options.isOpen Whether dropdown is currently open.
*
* @return {Element} Dropdown toggle element.
*/
renderInserterToggle({
onToggle,
isOpen
}) {
const {
disabled,
renderToggle = defaultRenderToggle,
getStylesFromColorScheme,
showSeparator
} = this.props;
if (showSeparator && isOpen) {
return /*#__PURE__*/_jsx(BlockInsertionPoint, {});
}
const buttonStyle = getStylesFromColorScheme(styles['inserter-menu__add-block-button'], styles['inserter-menu__add-block-button--dark']);
const iconStyle = getStylesFromColorScheme(styles['inserter-menu__add-block-button-icon'], styles['inserter-menu__add-block-button-icon--dark']);
const onPress = () => {
this.setState({
destinationRootClientId: this.props.destinationRootClientId,
shouldReplaceBlock: this.shouldReplaceBlock('default'),
insertionIndex: this.getInsertionIndex('default')
}, onToggle);
};
const onLongPress = () => {
if (this.picker) {
this.picker.presentPicker();
}
};
const onPickerSelect = insertionType => {
this.setState({
destinationRootClientId: this.props.destinationRootClientId,
shouldReplaceBlock: this.shouldReplaceBlock(insertionType),
insertionIndex: this.getInsertionIndex(insertionType)
}, onToggle);
};
return /*#__PURE__*/_jsxs(_Fragment, {
children: [renderToggle({
onToggle: onPress,
isOpen,
disabled,
iconStyle,
buttonStyle,
onLongPress
}), /*#__PURE__*/_jsx(Picker, {
ref: instance => this.picker = instance,
options: this.getInsertionOptions(),
onChange: onPickerSelect,
hideCancelButton: true
})]
});
}
/**
* Render callback to display Dropdown content element.
*
* @param {Object} options
* @param {Function} options.onClose Callback to invoke when dropdown is
* closed.
* @param {boolean} options.isOpen Whether dropdown is currently open.
*
* @return {Element} Dropdown content element.
*/
renderContent({
onClose,
isOpen
}) {
const {
clientId,
isAppender
} = this.props;
const {
destinationRootClientId,
shouldReplaceBlock,
insertionIndex
} = this.state;
return /*#__PURE__*/_jsx(InserterMenu, {
isOpen: isOpen,
onSelect: onClose,
onDismiss: onClose,
rootClientId: destinationRootClientId,
clientId: clientId,
isAppender: isAppender,
shouldReplaceBlock: shouldReplaceBlock,
insertionIndex: insertionIndex
});
}
render() {
return /*#__PURE__*/_jsx(Dropdown, {
onToggle: this.onToggle,
headerTitle: __('Add a block'),
renderToggle: this.renderInserterToggle,
renderContent: this.renderContent
});
}
}
export default compose([withDispatch(dispatch => {
const {
updateSettings
} = dispatch(blockEditorStore);
return {
updateSettings
};
}), withSelect((select, {
clientId,
isAppender,
rootClientId
}) => {
const {
getBlockRootClientId,
getBlockSelectionEnd,
getBlockOrder,
getBlockIndex,
getBlock,
getSettings: getBlockEditorSettings
} = select(blockEditorStore);
const end = getBlockSelectionEnd();
// `end` argument (id) can refer to the component which is removed
// due to pressing `undo` button, that's why we need to check
// if `getBlock( end) is valid, otherwise `null` is passed
const isAnyBlockSelected = !isAppender && end && getBlock(end);
const destinationRootClientId = isAnyBlockSelected ? getBlockRootClientId(end) : rootClientId;
const selectedBlockIndex = getBlockIndex(end);
const endOfRootIndex = getBlockOrder(rootClientId).length;
const isSelectedUnmodifiedDefaultBlock = isAnyBlockSelected ? isUnmodifiedDefaultBlock(getBlock(end)) : undefined;
function getDefaultInsertionIndex() {
const {
__experimentalShouldInsertAtTheTop: shouldInsertAtTheTop
} = getBlockEditorSettings();
// If post title is selected insert as first block.
if (shouldInsertAtTheTop) {
return 0;
}
// If the clientId is defined, we insert at the position of the block.
if (clientId) {
return getBlockIndex(clientId);
}
// If there is a selected block,
if (isAnyBlockSelected) {
// And the last selected block is unmodified (empty), it will be replaced.
if (isSelectedUnmodifiedDefaultBlock) {
return selectedBlockIndex;
}
// We insert after the selected block.
return selectedBlockIndex + 1;
}
// Otherwise, we insert at the end of the current rootClientId.
return endOfRootIndex;
}
const insertionIndexStart = 0;
const insertionIndexBefore = isAnyBlockSelected ? selectedBlockIndex : insertionIndexStart;
const insertionIndexAfter = isAnyBlockSelected ? selectedBlockIndex + 1 : endOfRootIndex;
const insertionIndexEnd = endOfRootIndex;
return {
blockTypeImpressions: getBlockEditorSettings().impressions,
destinationRootClientId,
insertionIndexDefault: getDefaultInsertionIndex(),
insertionIndexBefore,
insertionIndexAfter,
insertionIndexStart,
insertionIndexEnd,
isAnyBlockSelected: !!isAnyBlockSelected,
isSelectedBlockReplaceable: isSelectedUnmodifiedDefaultBlock
};
}), withPreferredColorScheme])(Inserter);
//# sourceMappingURL=index.native.js.map