UNPKG

@wordpress/block-editor

Version:
360 lines (348 loc) 11.3 kB
/** * 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