UNPKG

@wordpress/block-editor

Version:
432 lines (419 loc) 14.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.BlockDraggableWrapper = void 0; var _reactNative = require("react-native"); var _reactNativeSafeAreaContext = require("react-native-safe-area-context"); var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); var _components = require("@wordpress/components"); var _data = require("@wordpress/data"); var _element = require("@wordpress/element"); var _blocks = require("@wordpress/blocks"); var _reactNativeBridge = require("@wordpress/react-native-bridge"); var _reactNativeAztec = _interopRequireDefault(require("@wordpress/react-native-aztec")); var _useScrollWhenDragging = _interopRequireDefault(require("./use-scroll-when-dragging")); var _draggableChip = _interopRequireDefault(require("./draggable-chip")); var _store = require("../../store"); var _droppingInsertionPoint = _interopRequireDefault(require("./dropping-insertion-point")); var _useBlockDropZone = _interopRequireDefault(require("../use-block-drop-zone")); var _style = _interopRequireDefault(require("./style.scss")); var _jsxRuntime = require("react/jsx-runtime"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ 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] = (0, _element.useState)(); const { selectBlock, startDraggingBlocks, stopDraggingBlocks } = (0, _data.useDispatch)(_store.store); const { left, right } = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)(); const { width } = (0, _reactNativeSafeAreaContext.useSafeAreaFrame)(); const safeAreaOffset = left + right; const contentWidth = width - safeAreaOffset; const scroll = { offsetY: (0, _reactNativeReanimated.useSharedValue)(0) }; const chip = { x: (0, _reactNativeReanimated.useSharedValue)(0), y: (0, _reactNativeReanimated.useSharedValue)(0), width: (0, _reactNativeReanimated.useSharedValue)(0), height: (0, _reactNativeReanimated.useSharedValue)(0) }; const currentYPosition = (0, _reactNativeReanimated.useSharedValue)(0); const isDragging = (0, _reactNativeReanimated.useSharedValue)(false); const [startScrolling, scrollOnDragOver, stopScrolling, draggingScrollHandler] = (0, _useScrollWhenDragging.default)(); const scrollHandler = event => { 'worklet'; const { contentOffset } = event; scroll.offsetY.value = contentOffset.y; draggingScrollHandler(event); }; const { onBlockDragOverWorklet, onBlockDragEnd, onBlockDrop, targetBlockIndex } = (0, _useBlockDropZone.default)(); // Stop dragging blocks if the block draggable is unmounted. (0, _element.useEffect)(() => { return () => { if (isDragging.value) { stopDraggingBlocks(); } }; }, []); const setDraggedBlockIconByClientId = clientId => { const blockName = (0, _data.select)(_store.store).getBlockName(clientId); const blockIcon = (0, _blocks.getBlockType)(blockName)?.icon; if (blockIcon) { setDraggedBlockIcon(blockIcon); } }; const onStartDragging = ({ clientId, position }) => { if (clientId) { startDraggingBlocks([clientId]); setDraggedBlockIconByClientId(clientId); (0, _reactNativeReanimated.runOnUI)(startScrolling)(position.y); (0, _reactNativeBridge.generateHapticFeedback)(); } else { // We stop dragging if no block is found. (0, _reactNativeReanimated.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; (0, _reactNativeReanimated.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(); (0, _reactNativeReanimated.runOnJS)(onStopDragging)({ clientId: id }); }; const chipDynamicStyles = (0, _reactNativeReanimated.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, _style.default['draggable-chip__wrapper']]; const exitingAnimation = ({ currentHeight, currentWidth }) => { 'worklet'; const translateX = !isRTL ? 0 : currentWidth * -1; const duration = 150; const animations = { transform: [{ translateY: (0, _reactNativeReanimated.withTiming)(currentHeight, { duration }) }, { translateX: (0, _reactNativeReanimated.withTiming)(translateX, { duration }) }, { scale: (0, _reactNativeReanimated.withTiming)(0, { duration }) }] }; const initialValues = { transform: [{ translateY: 0 }, { translateX }, { scale: 1 }] }; return { initialValues, animations }; }; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_droppingInsertionPoint.default, { scroll: scroll, currentYPosition: currentYPosition, isDragging: isDragging, targetBlockIndex: targetBlockIndex }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Draggable, { onDragStart: startDragging, onDragOver: updateDragging, onDragEnd: stopDragging, testID: "block-draggable-wrapper", children: children({ onScroll: scrollHandler }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, { onLayout: onChipLayout, style: chipStyles, pointerEvents: "none", children: draggedBlockIcon && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, { entering: _reactNativeReanimated.ZoomInEasyDown.duration(200), exiting: exitingAnimation, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_draggableChip.default, { icon: draggedBlockIcon }) }) })] }); }; exports.BlockDraggableWrapper = BlockDraggableWrapper; function useIsScreenReaderEnabled() { const [isScreenReaderEnabled, setIsScreenReaderEnabled] = (0, _element.useState)(false); (0, _element.useEffect)(() => { let mounted = true; const changeListener = _reactNative.AccessibilityInfo.addEventListener('screenReaderChanged', enabled => setIsScreenReaderEnabled(enabled)); _reactNative.AccessibilityInfo.isScreenReaderEnabled().then(screenReaderEnabled => { if (mounted && screenReaderEnabled) { setIsScreenReaderEnabled(screenReaderEnabled); } }); return () => { mounted = false; changeListener.remove(); }; }, []); return isScreenReaderEnabled; } function useIsEditingText() { const [isEditingText, setIsEditingText] = (0, _element.useState)(() => _reactNativeAztec.default.InputState.isFocused()); (0, _element.useEffect)(() => { const onFocusChangeAztec = ({ isFocused }) => { setIsEditingText(isFocused); }; _reactNativeAztec.default.InputState.addFocusChangeListener(onFocusChangeAztec); return () => { _reactNativeAztec.default.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 = (0, _element.useRef)(false); const isEditingText = useIsEditingText(); const isScreenReaderEnabled = useIsScreenReaderEnabled(); const draggingAnimation = { opacity: (0, _reactNativeReanimated.useSharedValue)(1) }; const startDraggingBlock = () => { draggingAnimation.opacity.value = (0, _reactNativeReanimated.withTiming)(0.4, BLOCK_OPACITY_ANIMATION_CONFIG); }; const stopDraggingBlock = () => { draggingAnimation.opacity.value = (0, _reactNativeReanimated.withDelay)(BLOCK_OPACITY_ANIMATION_DELAY, (0, _reactNativeReanimated.withTiming)(1, BLOCK_OPACITY_ANIMATION_CONFIG)); }; const { isDraggable, isBeingDragged, isBlockSelected } = (0, _data.useSelect)(_select => { const { getBlockRootClientId, getTemplateLock, isBlockBeingDragged, getSelectedBlockClientId } = _select(_store.store); 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]); (0, _element.useEffect)(() => { if (isBeingDragged !== wasBeingDragged.current) { if (isBeingDragged) { startDraggingBlock(); } else { stopDraggingBlock(); } } wasBeingDragged.current = isBeingDragged; }, [isBeingDragged]); const onLongPressDraggable = (0, _element.useCallback)(() => { // Ensure that no text input is focused when starting the dragging gesture in order to prevent conflicts with text editing. _reactNativeAztec.default.InputState.blurCurrentFocusedElement(); }, []); const animatedWrapperStyles = (0, _reactNativeReanimated.useAnimatedStyle)(() => { return { opacity: draggingAnimation.opacity.value }; }); const wrapperStyles = [animatedWrapperStyles, _style.default['draggable-wrapper__container']]; const canDragBlock = enabled && !isScreenReaderEnabled && (!isBlockSelected || !isEditingText); if (!isDraggable) { return children({ isDraggable: false }); } return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.DraggableTrigger, { id: draggingClientId || clientId, enabled: enabled && canDragBlock, minDuration: _element.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__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, { style: wrapperStyles, children: children({ isDraggable: true }) }) }); }; var _default = exports.default = BlockDraggable; //# sourceMappingURL=index.native.js.map