UNPKG

@wordpress/block-editor

Version:
422 lines (411 loc) 12.6 kB
/** * External dependencies */ import { AccessibilityInfo } from 'react-native'; import { useSafeAreaInsets, useSafeAreaFrame } from 'react-native-safe-area-context'; import Animated, { runOnJS, runOnUI, useAnimatedStyle, useSharedValue, withDelay, withTiming, ZoomInEasyDown } from 'react-native-reanimated'; /** * WordPress dependencies */ import { Draggable, DraggableTrigger } from '@wordpress/components'; import { select, useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useEffect, useRef, useState, Platform } from '@wordpress/element'; import { getBlockType } from '@wordpress/blocks'; import { generateHapticFeedback } from '@wordpress/react-native-bridge'; import RCTAztecView from '@wordpress/react-native-aztec'; /** * Internal dependencies */ import useScrollWhenDragging from './use-scroll-when-dragging'; import DraggableChip from './draggable-chip'; import { store as blockEditorStore } from '../../store'; import DroppingInsertionPoint from './dropping-insertion-point'; import useBlockDropZone from '../use-block-drop-zone'; import styles from './style.scss'; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; const CHIP_OFFSET_TO_TOUCH_POSITION = 32; const BLOCK_OPACITY_ANIMATION_CONFIG = { duration: 350 }; const BLOCK_OPACITY_ANIMATION_DELAY = 250; const DEFAULT_LONG_PRESS_MIN_DURATION = 500; const DEFAULT_IOS_LONG_PRESS_MIN_DURATION = DEFAULT_LONG_PRESS_MIN_DURATION - 50; /** * Block draggable wrapper component * * This component handles all the interactions for dragging blocks. * It relies on the block list and its context for dragging, hence it * should be rendered between the `BlockListProvider` component and the * block list rendering. It also requires listening to scroll events, * therefore for this purpose, it returns the `onScroll` event handler * that should be attached to the list that renders the blocks. * * * @param {Object} props Component props. * @param {JSX.Element} props.children Children to be rendered. * @param {boolean} props.isRTL Check if current locale is RTL. * * @return {Function} Render function that passes `onScroll` event handler. */ const BlockDraggableWrapper = ({ children, isRTL }) => { const [draggedBlockIcon, setDraggedBlockIcon] = useState(); const { selectBlock, startDraggingBlocks, stopDraggingBlocks } = useDispatch(blockEditorStore); const { left, right } = useSafeAreaInsets(); const { width } = useSafeAreaFrame(); const safeAreaOffset = left + right; const contentWidth = width - safeAreaOffset; const scroll = { offsetY: useSharedValue(0) }; const chip = { x: useSharedValue(0), y: useSharedValue(0), width: useSharedValue(0), height: useSharedValue(0) }; const currentYPosition = useSharedValue(0); const isDragging = useSharedValue(false); const [startScrolling, scrollOnDragOver, stopScrolling, draggingScrollHandler] = useScrollWhenDragging(); const scrollHandler = event => { 'worklet'; const { contentOffset } = event; scroll.offsetY.value = contentOffset.y; draggingScrollHandler(event); }; const { onBlockDragOverWorklet, onBlockDragEnd, onBlockDrop, targetBlockIndex } = useBlockDropZone(); // Stop dragging blocks if the block draggable is unmounted. useEffect(() => { return () => { if (isDragging.value) { stopDraggingBlocks(); } }; }, []); const setDraggedBlockIconByClientId = clientId => { const blockName = select(blockEditorStore).getBlockName(clientId); const blockIcon = getBlockType(blockName)?.icon; if (blockIcon) { setDraggedBlockIcon(blockIcon); } }; const onStartDragging = ({ clientId, position }) => { if (clientId) { startDraggingBlocks([clientId]); setDraggedBlockIconByClientId(clientId); runOnUI(startScrolling)(position.y); generateHapticFeedback(); } else { // We stop dragging if no block is found. runOnUI(stopDragging)(); } }; const onStopDragging = ({ clientId }) => { if (clientId) { onBlockDrop({ // Dropping is only allowed at root level srcRootClientId: '', srcClientIds: [clientId], type: 'block' }); selectBlock(clientId); setDraggedBlockIcon(undefined); } onBlockDragEnd(); stopDraggingBlocks(); }; const onChipLayout = ({ nativeEvent: { layout } }) => { if (layout.width > 0) { chip.width.value = layout.width; } if (layout.height > 0) { chip.height.value = layout.height; } }; const startDragging = ({ x, y, id }) => { 'worklet'; const dragPosition = { x, y }; chip.x.value = dragPosition.x; chip.y.value = dragPosition.y; currentYPosition.value = dragPosition.y; isDragging.value = true; runOnJS(onStartDragging)({ clientId: id, position: dragPosition }); }; const updateDragging = ({ x, y }) => { 'worklet'; const dragPosition = { x, y }; chip.x.value = dragPosition.x; chip.y.value = dragPosition.y; currentYPosition.value = dragPosition.y; onBlockDragOverWorklet({ x, y: y + scroll.offsetY.value }); // Update scrolling velocity scrollOnDragOver(dragPosition.y); }; const stopDragging = ({ id }) => { 'worklet'; isDragging.value = false; stopScrolling(); runOnJS(onStopDragging)({ clientId: id }); }; const chipDynamicStyles = useAnimatedStyle(() => { const chipOffset = chip.width.value / 2; const translateX = !isRTL ? chip.x.value - chipOffset : -(contentWidth - (chip.x.value + chipOffset)); return { transform: [{ translateX }, { translateY: chip.y.value - chip.height.value - CHIP_OFFSET_TO_TOUCH_POSITION }] }; }); const chipStyles = [chipDynamicStyles, styles['draggable-chip__wrapper']]; const exitingAnimation = ({ currentHeight, currentWidth }) => { 'worklet'; const translateX = !isRTL ? 0 : currentWidth * -1; const duration = 150; const animations = { transform: [{ translateY: withTiming(currentHeight, { duration }) }, { translateX: withTiming(translateX, { duration }) }, { scale: withTiming(0, { duration }) }] }; const initialValues = { transform: [{ translateY: 0 }, { translateX }, { scale: 1 }] }; return { initialValues, animations }; }; return /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(DroppingInsertionPoint, { scroll: scroll, currentYPosition: currentYPosition, isDragging: isDragging, targetBlockIndex: targetBlockIndex }), /*#__PURE__*/_jsx(Draggable, { onDragStart: startDragging, onDragOver: updateDragging, onDragEnd: stopDragging, testID: "block-draggable-wrapper", children: children({ onScroll: scrollHandler }) }), /*#__PURE__*/_jsx(Animated.View, { onLayout: onChipLayout, style: chipStyles, pointerEvents: "none", children: draggedBlockIcon && /*#__PURE__*/_jsx(Animated.View, { entering: ZoomInEasyDown.duration(200), exiting: exitingAnimation, children: /*#__PURE__*/_jsx(DraggableChip, { icon: draggedBlockIcon }) }) })] }); }; function useIsScreenReaderEnabled() { const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false); useEffect(() => { let mounted = true; const changeListener = AccessibilityInfo.addEventListener('screenReaderChanged', enabled => setIsScreenReaderEnabled(enabled)); AccessibilityInfo.isScreenReaderEnabled().then(screenReaderEnabled => { if (mounted && screenReaderEnabled) { setIsScreenReaderEnabled(screenReaderEnabled); } }); return () => { mounted = false; changeListener.remove(); }; }, []); return isScreenReaderEnabled; } function useIsEditingText() { const [isEditingText, setIsEditingText] = useState(() => RCTAztecView.InputState.isFocused()); useEffect(() => { const onFocusChangeAztec = ({ isFocused }) => { setIsEditingText(isFocused); }; RCTAztecView.InputState.addFocusChangeListener(onFocusChangeAztec); return () => { RCTAztecView.InputState.removeFocusChangeListener(onFocusChangeAztec); }; }, []); return isEditingText; } /** * Block draggable component * * This component serves for animating the block when it is being dragged. * Hence, it should be wrapped around the rendering of a block. * * @param {Object} props Component props. * @param {JSX.Element} props.children Children to be rendered. * @param {string} props.clientId Client id of the block. * @param {string} [props.draggingClientId] Client id to use for dragging. If not defined, the value from `clientId` will be used. * @param {boolean} [props.enabled] Enables the draggable trigger. * @param {string} [props.testID] Id used for querying the long-press gesture handler in tests. * * @return {Function} Render function which includes the parameter `isDraggable` to determine if the block can be dragged. */ const BlockDraggable = ({ clientId, children, draggingClientId, enabled = true, testID }) => { const wasBeingDragged = useRef(false); const isEditingText = useIsEditingText(); const isScreenReaderEnabled = useIsScreenReaderEnabled(); const draggingAnimation = { opacity: useSharedValue(1) }; const startDraggingBlock = () => { draggingAnimation.opacity.value = withTiming(0.4, BLOCK_OPACITY_ANIMATION_CONFIG); }; const stopDraggingBlock = () => { draggingAnimation.opacity.value = withDelay(BLOCK_OPACITY_ANIMATION_DELAY, withTiming(1, BLOCK_OPACITY_ANIMATION_CONFIG)); }; const { isDraggable, isBeingDragged, isBlockSelected } = useSelect(_select => { const { getBlockRootClientId, getTemplateLock, isBlockBeingDragged, getSelectedBlockClientId } = _select(blockEditorStore); const rootClientId = getBlockRootClientId(clientId); const templateLock = rootClientId ? getTemplateLock(rootClientId) : null; const selectedBlockClientId = getSelectedBlockClientId(); return { isBeingDragged: isBlockBeingDragged(clientId), isDraggable: 'all' !== templateLock, isBlockSelected: selectedBlockClientId && selectedBlockClientId === clientId }; }, [clientId]); useEffect(() => { if (isBeingDragged !== wasBeingDragged.current) { if (isBeingDragged) { startDraggingBlock(); } else { stopDraggingBlock(); } } wasBeingDragged.current = isBeingDragged; }, [isBeingDragged]); const onLongPressDraggable = useCallback(() => { // Ensure that no text input is focused when starting the dragging gesture in order to prevent conflicts with text editing. RCTAztecView.InputState.blurCurrentFocusedElement(); }, []); const animatedWrapperStyles = useAnimatedStyle(() => { return { opacity: draggingAnimation.opacity.value }; }); const wrapperStyles = [animatedWrapperStyles, styles['draggable-wrapper__container']]; const canDragBlock = enabled && !isScreenReaderEnabled && (!isBlockSelected || !isEditingText); if (!isDraggable) { return children({ isDraggable: false }); } return /*#__PURE__*/_jsx(DraggableTrigger, { id: draggingClientId || clientId, enabled: enabled && canDragBlock, minDuration: Platform.select({ // On iOS, using a lower min duration than the default // value prevents the long-press gesture from being // triggered in underneath elements. This is required to // prevent enabling text editing when dragging is available. ios: canDragBlock ? DEFAULT_IOS_LONG_PRESS_MIN_DURATION : DEFAULT_LONG_PRESS_MIN_DURATION, android: DEFAULT_LONG_PRESS_MIN_DURATION }), onLongPress: onLongPressDraggable, testID: testID, children: /*#__PURE__*/_jsx(Animated.View, { style: wrapperStyles, children: children({ isDraggable: true }) }) }); }; export { BlockDraggableWrapper }; export default BlockDraggable; //# sourceMappingURL=index.native.js.map