This is the sixth day of my participation in Gwen Challenge

preface

React – Draggable is a react draggable component with a high degree of freedom. It has received 6.8K stars on Github. Here’s what I learned from the project’s source code.

One, source code analysis

1.1 Implementation principle of drag and drop

Drag-and-drop functions are implemented in

, source click here

Drag can be divided into drag start, drag in, drag end process, in these three drag process do the following processing

  • Drag start: Records the initial position of the drag
  • Dragging: Monitor the distance and direction of the drag and move the real DOM
  • Drag end: Cancel event listening in drag

HandleDragStart, handleDrag, handleDragStop, handleDragStop

handleDragStart: EventHandler<MouseTouchEvent> = (e) => { // Make it possible to attach event handlers on top of this one. this.props.onMouseDown(e); // Only accept left-clicks. if (! this.props.allowAnyClick && typeof e.button === 'number' && e.button ! == 0) return false; // Get nodes. Be sure to grab relative document (could be iframed) const thisNode = this.findDOMNode(); if (! thisNode || ! thisNode.ownerDocument || ! thisNode.ownerDocument.body) { throw new Error('<DraggableCore> not mounted on DragStart! '); } const {ownerDocument} = thisNode; // Short circuit if handle or cancel prop was provided and selector doesn't match. if (this.props.disabled || (! (e.target instanceof ownerDocument.defaultView.Node)) || (this.props.handle && ! matchesSelectorAndParentsTo(e.target, this.props.handle, thisNode)) || (this.props.cancel && matchesSelectorAndParentsTo(e.target, this.props.cancel, thisNode))) { return; } // Prevent scrolling on mobile devices, like ipad/iphone. // Important that this is after handle/cancel. if (e.type === 'touchstart') e.preventDefault(); // Set touch identifier in component state if this is a touch event. This allows us to // distinguish between individual  touches on multitouch screens by identifying which // touchpoint was set to this element. const touchIdentifier = getTouchIdentifier(e); this.setState({touchIdentifier}); // Get the current drag point from the event. This is used as the offset. const position = getControlPosition(e, touchIdentifier, this); if (position == null) return; // not possible but satisfies flow const {x, y} = position; // Create an event object with all the data parents need to make a decision here. const coreEvent = createCoreData(this,  x, y); log('DraggableCore: handleDragStart: %j', coreEvent); // Call event handler. If it returns explicit false, cancel. log('calling', this.props.onStart); const shouldUpdate = this.props.onStart(e, coreEvent); if (shouldUpdate === false || this.mounted === false) return; // Add a style to the body to disable user-select. This prevents text from // being selected all over the page. if (this.props.enableUserSelectHack) addUserSelectStyles(ownerDocument); // Initiate dragging. Set the current x and y as offsets // so we know how much we've moved during the drag. This allows  us // to drag elements around even if they have been moved, without issue. this.setState({ dragging: true, lastX: x, lastY: y }); // Add events to the document directly so we catch when the user's mouse/touch moves outside of // this element. We use different events depending on whether or not we have detected that this // is a touch-capable device. addEvent(ownerDocument, dragEventFor.move, this.handleDrag); addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop); }; handleDrag: EventHandler<MouseTouchEvent> = (e) => { // Get the current drag point from the event. This is used as the offset. const  position = getControlPosition(e, this.state.touchIdentifier, this); if (position == null) return; let {x, y} = position; // Snap to grid if prop has been provided if (Array.isArray(this.props.grid)) { let deltaX = x - this.state.lastX, deltaY = y - this.state.lastY; [deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY); if (! deltaX && ! deltaY) return; // skip useless drag x = this.state.lastX + deltaX, y = this.state.lastY + deltaY; } const coreEvent = createCoreData(this, x, y); log('DraggableCore: handleDrag: %j', coreEvent); // Call event handler. If it returns explicit false, trigger end. const shouldUpdate = this.props.onDrag(e, coreEvent); if (shouldUpdate === false || this.mounted === false) { try { // $FlowIgnore this.handleDragStop(new MouseEvent('mouseup')); } catch (err) { // Old browsers const event = ((document.createEvent('MouseEvents'): any): MouseTouchEvent); // I see why this insanity was deprecated // $FlowIgnore event.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); this.handleDragStop(event); } return; } this.setState({ lastX: x, lastY: y }); }; handleDragStop: EventHandler<MouseTouchEvent> = (e) => { if (! this.state.dragging) return; const position = getControlPosition(e, this.state.touchIdentifier, this); if (position == null) return; const {x, y} = position; const coreEvent = createCoreData(this, x, y); // Call event handler const shouldContinue = this.props.onStop(e, coreEvent); if (shouldContinue === false || this.mounted === false) return false; const thisNode = this.findDOMNode(); if (thisNode) { // Remove user-select hack if (this.props.enableUserSelectHack) removeUserSelectStyles(thisNode.ownerDocument); } log('DraggableCore: handleDragStop: %j', coreEvent); // Reset the el. this.setState({ dragging: false, lastX: NaN, lastY: NaN }); if (thisNode) { // Remove event handlers log('DraggableCore: Removing handlers'); removeEvent(thisNode.ownerDocument, dragEventFor.move, this.handleDrag); removeEvent(thisNode.ownerDocument, dragEventFor.stop, this.handleDragStop); }};Copy the code

In addition, react-Draggable also does something on the mobile side to trigger these three methods.

1.2 Unified Drag and Drop event parameters

React-draggable encapsulates handleDragStart, handleDrag, and handleDragStop. There are also three methods onStart, onDrag and onStop for DraggableCoreProps. In addition, the function parameters of the three props are consistent, which is very convenient for secondary encapsulation.

export type DraggableEvent = React.MouseEvent<HTMLElement | SVGElement> | React.TouchEvent<HTMLElement | SVGElement> | MouseEvent | TouchEvent export type DraggableEventHandler = ( e: DraggableEvent, data: DraggableData ) => void | false; export interface DraggableData { node: HTMLElement, x: number, y: number, deltaX: number, deltaY: number, lastX: number, lastY: number } export interface DraggableCoreProps { allowAnyClick: boolean, cancel: string, disabled: boolean, enableUserSelectHack: boolean, offsetParent: HTMLElement, grid: [number, number], handle: string, nodeRef? : React.RefObject<HTMLElement>, onStart: DraggableEventHandler, onDrag: DraggableEventHandler, onStop: DraggableEventHandler, onMouseDown: (e: MouseEvent) => void, scale: number }Copy the code

1.3 Application of controlled components and uncontrolled components

React-draggable provides < draggable > to encapsulate

export interface DraggableProps extends DraggableCoreProps { axis: 'both' | 'x' | 'y' | 'none', bounds: DraggableBounds | string | false , defaultClassName: string, defaultClassNameDragging: string, defaultClassNameDragged: string, defaultPosition: ControlPosition, positionOffset: PositionOffsetControlPosition, position: ControlPosition } class Draggable extends React.Component<DraggableProps, DraggableState> { render(): ReactElement<any> { const { axis, bounds, children, defaultPosition, defaultClassName, defaultClassNameDragging, defaultClassNameDragged, position, positionOffset, scale, ... draggableCoreProps } = this.props; let style = {}; let svgTransform = null; // If this is controlled, we don't want to move it - unless it's dragging. const controlled = Boolean(position); const draggable = ! controlled || this.state.dragging; const validPosition = position || defaultPosition; const transformOpts = { // Set left if horizontal drag is enabled x: canDragX(this) && draggable ? this.state.x : validPosition.x, // Set top if vertical drag is enabled y: canDragY(this) && draggable ? this.state.y : validPosition.y }; // If this element was SVG, we use the `transform` attribute. if (this.state.isElementSVG) { svgTransform = createSVGTransform(transformOpts, positionOffset); } else { // Add a CSS transform to move the element around. This allows us to move the element around // without worrying about whether or not it is relatively or absolutely positioned. // If the item you are dragging already has a transform set, wrap it in a <span> so <Draggable> // has a clean slate. style = createCSSTransform(transformOpts, positionOffset); } // Mark with class while dragging const className = classNames((children.props.className || ''), defaultClassName, { [defaultClassNameDragging]: this.state.dragging, [defaultClassNameDragged]: this.state.dragged }); // Reuse the child provided // This makes it flexible to use whatever element is wanted (div, ul, etc) return ( <DraggableCore {... draggableCoreProps} onStart={this.onDragStart} onDrag={this.onDrag} onStop={this.onDragStop}> {React.cloneElement(React.Children.only(children), { className: className, style: {... children.props.style, ... style}, transform: svgTransform })} </DraggableCore> ); }}Copy the code

DraggableProps. Position

is an uncontrolled component if it is not passed; conversely, it is a controlled component if xy coordinates are passed. This kind of encapsulation also provides the space of choice for the user.

1.4 How does a parent elegantly encapsulate its children

React.cloneElement(React.Children.only(children), { className: className, style: {... children.props.style, ... style}, transform: svgTransform })Copy the code

This code also uses the react. cloneElement and react.children. Only apis, which allow you to verify that the component to be dragged is a separate DOM and to easily add a style to change position.

  1. The React. Children. Only: validationchildrenWhether there is only one child node (a React element) and if so return it, otherwise this method throws an error.
  2. React. CloneElement: Clones the child element and returns the new React element. The props of the element is the result of superficially merging the new props with the props of the original element. The new child element replaces the existing child element and comes from the original elementkeyrefWill be preserved

Second, the summary

Finally, a few highlights of React-Draggable in my opinion

  1. <DraggableCore>Responsible for drag encapsulation drag events,<Draggable>For the former encapsulation, use decorator-like patterns for drag locations to enhance functionality (such as limiting drag boundaries, raster drag, and so on). This separate encapsulation is very scalable.
  2. Abstract thehandleDragStarthandleDraghandleDragStopThree methods, so that in different runtime environments as long as the implementation is consistent to do a good match.
  3. Allows users to freely choose controlled/uncontrolled, high degree of freedom

The authors of the React-Draggable project went on to develop the following two repositories based on it, which are also very useful:

  1. Github.com/react-grid-…
  2. Github.com/react-grid-…