-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RNMobile] BlockDraggable
component
#39617
Changes from 31 commits
a779e95
b71b308
faea4cf
799f077
6aaae21
f0bd8c0
64708c5
778b10a
3822b27
3688013
14a9d6b
90838fd
ab337ef
ee179a9
f173ab8
95fd2e1
593644d
a910072
0ddca86
1492550
2bd4e66
9a2ae54
fd0883e
6bef485
ad016ed
7fac2ec
5c6e21a
55e62f1
258eaf3
78da359
8cac264
1612b48
b7e800a
8c79ad3
e9ce4e3
4060e1a
d16ce15
842b09b
bda7b24
b886b66
d66fb3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { View } from 'react-native'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { dragHandle } from '@wordpress/icons'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { getBlockType } from '@wordpress/blocks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import BlockIcon from '../block-icon'; | ||
import styles from './style.scss'; | ||
import { store as blockEditorStore } from '../../store'; | ||
|
||
const shadowStyle = { | ||
shadowColor: '#000', | ||
shadowOffset: { | ||
width: 0, | ||
height: 2, | ||
}, | ||
shadowOpacity: 0.25, | ||
shadowRadius: 3.84, | ||
|
||
elevation: 5, | ||
}; | ||
|
||
/** | ||
* Block draggable chip component | ||
* | ||
* @return {JSX.Element} Chip component. | ||
*/ | ||
export default function BlockDraggableChip() { | ||
const { blockIcon } = useSelect( ( select ) => { | ||
const { getBlockName, getDraggedBlockClientIds } = select( | ||
blockEditorStore | ||
); | ||
const draggedBlockClientIds = getDraggedBlockClientIds(); | ||
const blockName = getBlockName( draggedBlockClientIds[ 0 ] ); | ||
|
||
return { | ||
blockIcon: getBlockType( blockName )?.icon, | ||
}; | ||
} ); | ||
Comment on lines
+44
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought of passing the |
||
|
||
return ( | ||
<View style={ [ styles[ 'draggable-chip__container' ], shadowStyle ] }> | ||
<BlockIcon icon={ dragHandle } /> | ||
<BlockIcon icon={ blockIcon } /> | ||
</View> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import Animated, { | ||
interpolate, | ||
runOnJS, | ||
runOnUI, | ||
useAnimatedRef, | ||
useAnimatedStyle, | ||
useSharedValue, | ||
withTiming, | ||
scrollTo, | ||
useAnimatedReaction, | ||
} from 'react-native-reanimated'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Draggable } from '@wordpress/components'; | ||
import { useSelect, useDispatch } from '@wordpress/data'; | ||
import { useEffect } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import DraggableChip from './draggable-chip'; | ||
import { store as blockEditorStore } from '../../store'; | ||
import { useBlockListContext } from '../block-list/block-list-context'; | ||
import styles from './style.scss'; | ||
|
||
const CHIP_OFFSET_TO_TOUCH_POSITION = 32; | ||
const BLOCK_COLLAPSED_HEIGHT = 20; | ||
const EXTRA_OFFSET_WHEN_CLOSE_TO_TOP_EDGE = 80; | ||
const SCROLL_ANIMATION_DURATION = 350; | ||
|
||
/** | ||
* 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. | ||
* | ||
* @return {Function} Render function that passes `onScroll` event handler. | ||
*/ | ||
const BlockDraggableWrapper = ( { children } ) => { | ||
const { startDraggingBlocks, stopDraggingBlocks } = useDispatch( | ||
blockEditorStore | ||
); | ||
|
||
const { | ||
blocksLayouts, | ||
scrollRef, | ||
findBlockLayoutByPosition, | ||
} = useBlockListContext(); | ||
const animatedScrollRef = useAnimatedRef(); | ||
animatedScrollRef( scrollRef ); | ||
Comment on lines
+77
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to use Reanimated's |
||
|
||
const scroll = { | ||
offsetY: useSharedValue( 0 ), | ||
}; | ||
const chip = { | ||
x: useSharedValue( 0 ), | ||
y: useSharedValue( 0 ), | ||
width: useSharedValue( 0 ), | ||
height: useSharedValue( 0 ), | ||
scale: useSharedValue( 0 ), | ||
}; | ||
const isDragging = useSharedValue( false ); | ||
const scrollAnimation = useSharedValue( 0 ); | ||
|
||
const scrollHandler = ( event ) => { | ||
'worklet'; | ||
const { contentOffset } = event; | ||
scroll.offsetY.value = contentOffset.y; | ||
}; | ||
|
||
// Stop dragging blocks if the block draggable is unmounted. | ||
useEffect( () => { | ||
return () => { | ||
if ( isDragging.value ) { | ||
stopDraggingBlocks(); | ||
} | ||
}; | ||
}, [] ); | ||
|
||
const setupDraggingBlock = ( position ) => { | ||
const blockLayout = findBlockLayoutByPosition( blocksLayouts.current, { | ||
x: position.x, | ||
y: position.y + scroll.offsetY.value, | ||
} ); | ||
|
||
const foundClientId = blockLayout?.clientId; | ||
if ( foundClientId ) { | ||
startDraggingBlocks( [ foundClientId ] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling this Redux action will notify the dragged block that is being dragged. |
||
|
||
const isBlockOutOfScrollView = blockLayout.y < scroll.offsetY.value; | ||
// If the dragging block is out of the scroll view, we have to | ||
// scroll the block list to show the origin position of the block. | ||
if ( isBlockOutOfScrollView ) { | ||
scrollAnimation.value = scroll.offsetY.value; | ||
const scrollOffsetTarget = Math.max( | ||
0, | ||
scroll.offsetY.value - | ||
( scroll.offsetY.value - blockLayout.y ) - | ||
EXTRA_OFFSET_WHEN_CLOSE_TO_TOP_EDGE | ||
); | ||
scrollAnimation.value = withTiming( scrollOffsetTarget, { | ||
duration: SCROLL_ANIMATION_DURATION, | ||
} ); | ||
} | ||
} else { | ||
// We stop dragging If no block is found. | ||
runOnUI( stopDragging )(); | ||
} | ||
}; | ||
|
||
// This hook is used for animating the scroll via a shared value. | ||
useAnimatedReaction( | ||
() => scrollAnimation.value, | ||
( value ) => { | ||
if ( isDragging.value ) { | ||
scrollTo( animatedScrollRef, 0, value, false ); | ||
} | ||
} | ||
); | ||
|
||
const onChipLayout = ( { nativeEvent: { layout } } ) => { | ||
chip.width.value = layout.width; | ||
chip.height.value = layout.height; | ||
}; | ||
|
||
const startDragging = ( { x, y } ) => { | ||
'worklet'; | ||
const dragPosition = { x, y }; | ||
chip.x.value = dragPosition.x; | ||
chip.y.value = dragPosition.y; | ||
|
||
isDragging.value = true; | ||
|
||
chip.scale.value = withTiming( 1 ); | ||
runOnJS( setupDraggingBlock )( dragPosition ); | ||
}; | ||
|
||
const updateDragging = ( { x, y } ) => { | ||
'worklet'; | ||
const dragPosition = { x, y }; | ||
chip.x.value = dragPosition.x; | ||
chip.y.value = dragPosition.y; | ||
}; | ||
|
||
const stopDragging = () => { | ||
'worklet'; | ||
isDragging.value = false; | ||
|
||
chip.scale.value = withTiming( 0 ); | ||
runOnJS( stopDraggingBlocks )(); | ||
}; | ||
|
||
const chipStyles = useAnimatedStyle( () => { | ||
return { | ||
position: 'absolute', | ||
transform: [ | ||
{ translateX: chip.x.value - chip.width.value / 2 }, | ||
{ | ||
translateY: | ||
chip.y.value - | ||
chip.height.value - | ||
CHIP_OFFSET_TO_TOUCH_POSITION, | ||
}, | ||
{ scaleX: chip.scale.value }, | ||
{ scaleY: chip.scale.value }, | ||
], | ||
}; | ||
} ); | ||
|
||
return ( | ||
<> | ||
<Draggable | ||
onDragStart={ startDragging } | ||
onDragOver={ updateDragging } | ||
onDragEnd={ stopDragging } | ||
wrapperAnimatedStyles={ { flex: 1 } } | ||
> | ||
{ children( { onScroll: scrollHandler } ) } | ||
</Draggable> | ||
<Animated.View | ||
onLayout={ onChipLayout } | ||
style={ chipStyles } | ||
pointerEvents="none" | ||
> | ||
<DraggableChip /> | ||
</Animated.View> | ||
</> | ||
); | ||
}; | ||
|
||
/** | ||
* 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. | ||
* | ||
* @return {Function} Render function which includes the parameter `isDraggable` to determine if the block can be dragged. | ||
*/ | ||
const BlockDraggable = ( { clientId, children } ) => { | ||
const container = { | ||
height: useSharedValue( 0 ), | ||
}; | ||
const containerHeightBeforeDragging = useSharedValue( 0 ); | ||
const collapseAnimation = useSharedValue( 0 ); | ||
|
||
const onContainerLayout = ( { nativeEvent: { layout } } ) => { | ||
container.height.value = layout.height; | ||
}; | ||
|
||
const startBlockDragging = () => { | ||
'worklet'; | ||
containerHeightBeforeDragging.value = container.height.value; | ||
collapseAnimation.value = withTiming( 1 ); | ||
}; | ||
|
||
const stopBlockDragging = () => { | ||
'worklet'; | ||
collapseAnimation.value = withTiming( 0 ); | ||
}; | ||
|
||
const { isDraggable, isBeingDragged } = useSelect( | ||
( select ) => { | ||
const { | ||
getBlockRootClientId, | ||
getTemplateLock, | ||
isBlockBeingDragged, | ||
} = select( blockEditorStore ); | ||
const rootClientId = getBlockRootClientId( clientId ); | ||
const templateLock = rootClientId | ||
? getTemplateLock( rootClientId ) | ||
: null; | ||
|
||
return { | ||
isBeingDragged: isBlockBeingDragged( clientId ), | ||
isDraggable: 'all' !== templateLock, | ||
}; | ||
}, | ||
[ clientId ] | ||
); | ||
|
||
useEffect( () => { | ||
if ( isBeingDragged ) { | ||
runOnUI( startBlockDragging )(); | ||
} else { | ||
runOnUI( stopBlockDragging )(); | ||
} | ||
}, [ isBeingDragged ] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The animations applied to the block are triggered when the block detects that is being dragged. |
||
|
||
const containerStyles = useAnimatedStyle( () => { | ||
const height = interpolate( | ||
collapseAnimation.value, | ||
[ 0, 1 ], | ||
[ containerHeightBeforeDragging.value, BLOCK_COLLAPSED_HEIGHT ] | ||
); | ||
return { | ||
height: | ||
containerHeightBeforeDragging.value === 0 || | ||
collapseAnimation.value === 0 | ||
? 'auto' | ||
: height, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only set a fixed height when the block is collapsed or when the container height value hasn't been calculated (i.e. value is |
||
}; | ||
} ); | ||
|
||
const blockStyles = useAnimatedStyle( () => { | ||
return { | ||
opacity: 1 - collapseAnimation.value, | ||
}; | ||
} ); | ||
|
||
const placeholderStyles = useAnimatedStyle( () => { | ||
return { | ||
display: collapseAnimation.value === 0 ? 'none' : 'flex', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only show the placeholder component when the block is collapsed. |
||
opacity: collapseAnimation.value, | ||
}; | ||
} ); | ||
|
||
if ( ! isDraggable ) { | ||
return children( { isDraggable: false } ); | ||
} | ||
|
||
return ( | ||
<Animated.View onLayout={ onContainerLayout } style={ containerStyles }> | ||
geriux marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<Animated.View style={ blockStyles }> | ||
{ children( { isDraggable: true } ) } | ||
</Animated.View> | ||
<Animated.View | ||
style={ [ | ||
geriux marked this conversation as resolved.
Show resolved
Hide resolved
|
||
styles[ 'draggable-placeholder__container' ], | ||
placeholderStyles, | ||
] } | ||
pointerEvents="none" | ||
/> | ||
</Animated.View> | ||
); | ||
}; | ||
|
||
export { BlockDraggableWrapper }; | ||
export default BlockDraggable; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
.draggable-chip__container { | ||
flex-direction: row; | ||
padding: 16px; | ||
background-color: #f7f7f7; | ||
geriux marked this conversation as resolved.
Show resolved
Hide resolved
|
||
border-radius: 8px; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
.draggable-placeholder__container { | ||
position: absolute; | ||
top: 0; | ||
left: $solid-border-space; | ||
right: $solid-border-space; | ||
bottom: 0; | ||
z-index: 10; | ||
background-color: $gray-lighten-30; | ||
border-radius: 8px; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I defined the shadow style properties here because these properties are not recognized in SCSS.