preface

React-grid-layout is a react-based grid layout system that supports dragging and zooming views.

Online experience.

In my work, a project module used react-grid-layout, so I took a look at the implementation of core functions.

In fact, this article is also a part of the internal series, and I will share my experience of doing the series separately when I have time.

I have to say, the writer’s mind is very clever, a series of nesting dolls.

Today we will look at the core functionality of the library, including grid layout calculation, dragging, and scaling.

More things, optional reading.

The overall structure diagram and the realization principle of core functions are as follows:

The basic use

As you can see, all you need to do is pass a Layout array with layout information

import React from 'react';
import GridLayout from 'react-grid-layout';
export default class App extends React.PureComponent {
  render() {
    // layout is an array of objects
    // static means not able to drag or zoom
    // Key is required
    const layout = [
      { i: 'a'.x: 0.y: 1.w: 1.h: 1.static: true },
      { i: 'b'.x: 1.y: 0.w: 3.h: 2 },
      { i: 'c'.x: 4.y: 0.w: 1.h: 2},];return (
      <GridLayout layout={layout} width={1200}>
        <div key="a">a</div>
        <div key="b">b</div>
        <div key="c">c</div>
      </GridLayout>); }}Copy the code

Grid layout

Next comes the most critical part of React-Grid-Layout, grid layout generation and calculation. In simple terms, according to the layout given by the user, calculate the specific style with PX, and finally display on the page. Let’s look at the render function in ReactGridLayout:

render() {
    const { className, style, isDroppable, innerRef } = this.props;
    // Merge the class name
    const mergedClassName = classNames(layoutClassName, className);
    / / merge style
    const mergedStyle = {
      height: this.containerHeight(),// Calculate the container height. style, };// Bind the drag and drop events, where noop is an empty function
    // export const noop = () => {};
    return (
      <div
        ref={innerRef}
        className={mergedClassName}
        style={mergedStyle}// Drag and drop some related callbacks, if business scenarios do not need to set // defaultisDroppableisfalse
        onDrop={isDroppable ? this.onDrop : noop}
        onDragLeave={isDroppable ? this.onDragLeave : noop}
        onDragEnter={isDroppable ? this.onDragEnter : noop}
        onDragOver={isDroppable ? this.onDragOver : noop}
      >{react.children.map (this.props. Children, child => this.processGridItem(child))} // Render node {react.children.map (this.props. Children, child => this.processGridItem(child))} Default isDroppable is false {isDroppable && enclosing state. DroppingDOMNode && enclosing processGridItem (this. State. DroppingDOMNode, {this.placeholder()} {this.placeholder()}</div>
    );
  }
Copy the code

Render does three key things:

  • Merge style and class name
  • Bind drag events
  • Rendering the Children

Rendering the Children

ProcessGridItem wraps the React element with a GridItem component and returns it. The GridItem is the presentation component of the grid cell, and it receives the props for layout, drag, scaling, and so on. More details about the GridItem are covered below.

processGridItem(
    child: ReactElement<any>, isDroppingItem? :boolean) :? ReactElement<any> {
   // If the child is passed without a key, it will be returned and not displayed on the page.
    if(! child || ! child.key)return;
    // The layout is relevant
    const l = getLayoutItem(this.state.layout, String(child.key));
    if(! l)return null;
     // xxx... 
    return (
      <GridItem
    //.Layout drag scaling relatedprops
      >
        {child}
      </GridItem>
    );
  }
Copy the code

Next, let’s look at layout and related things. The getLayoutItem function above takes a layout argument from internal state.

  state = {
    activeDrag: null.layout: synchronizeLayoutWithChildren(
      this.props.layout,// An array object containing layout information
      this.props.children,/ / the react elements
      this.props.cols,// The default layout column count is 12
      // Control the horizontal/vertical layout
      compactType(this.props)
    ),
    mounted: false.oldDragItem: null.oldLayout: null.oldResizeItem: null.droppingDOMNode: null.children: []};Copy the code

Do to layout a processing in the state, involves synchronizeLayoutWithChildren function.

synchronizeLayoutWithChildren

This function is used to synchronize Layout and children, generating a grid layout cell for each child. For existing layouts (where the I and child keys of each entry in the incoming layout match), use. If the layout parameter is not available, check whether the _grid and data-grid attributes are available on the child. If none of the layout parameters mentioned above are available, a default layout is created and added below the existing layout.

 function synchronizeLayoutWithChildren(
  initialLayout: Layout,
  children: ReactChildren,
  cols: number,
  compactType: CompactType
) :Layout {
  initialLayout = initialLayout || [];
  const layout: LayoutItem[] = [];
  React.Children.forEach(children, (child: ReactElement<any>, i: number) = > {
    // The existing layout is reused directly, which is actually a find operation
    const exists = getLayoutItem(initialLayout, String(child.key));
    if (exists) {
      layout[i] = cloneLayoutItem(exists);
    } else {
      if(! isProduction && child.props._grid) {// Discard warning for _grid. Use layout or data-grid to pass layout information
     // xxx..
      }
      const g = child.props["data-grid"] || child.props._grid;
      // If the child has a data-grid or _grid attribute, use it directly
      if (g) {
        if(! isProduction) { validateLayout([g],"ReactGridLayout.children"); } layout[i] = cloneLayoutItem({ ... g,i: child.key });
      } else {
        // Create a default layout
        layout[i] = cloneLayoutItem({
          w: 1.h: 1.x: 0.y: bottom(layout),
          i: String(child.key) }); }}});// border processing/stack prevention
  const correctedLayout = correctBounds(layout, { cols: cols });
  // Space compression
  return compact(correctedLayout, compactType, cols);
}
Copy the code

Layouts passed in by props, or artificially dragged/zoomed layouts, can cause minor collisions such as stacking and crossing boundaries. So at the end of the day, you need to do some extra work on the layout: out-of-bounds correction, anti-stacking, and compressing extra space to make the layout compact.

correctBounds

Boundary control functions, for a given layout, ensure that each is within its boundary limits. If the right side is out of bounds, the new x coordinate = layout column number – column width. If the left side is out of bounds, the new x-coordinate is 0 and column width = number of layout columns.

// Cols grid column number defaults to 12
function correctBounds(layout: Layout, bounds: { cols: number }) :Layout {
 // Get static item, static =true
  const collidesWith = getStatics(layout);
  for (let i = 0, len = layout.length; i < len; i++) {
    const l = layout[i];
    // Right overflow processing
    if (l.x + l.w > bounds.cols) {
      l.x = bounds.cols - l.w;
    }
    // Left overflow processing
    if (l.x < 0) {
      l.x = 0;
      l.w = bounds.cols;
    }
    if(! l.static) { collidesWith.push(l); }else {
   // If static elements collide, move the first item down to avoid stacking
      while(getFirstCollision(collidesWith, l)) { l.y++; }}}return layout;
}


function getFirstCollision(layout: Layout, layoutItem: LayoutItem): ?LayoutItem {
  for (let i = 0, len = layout.length; i < len; i++) {
    if (collides(layout[i], layoutItem)) returnlayout[i]; }}Copy the code

Collision detection function

function collides(l1, l2){
  if (l1.i === l2.i) return false; // same element
  if (l1.x + l1.w <= l2.x) return false; // l1 is left of l2
  if (l1.x >= l2.x + l2.w) return false; // l1 is right of l2
  if (l1.y + l1.h <= l2.y) return false; // l1 is above l2
  if (l1.y >= l2.y + l2.h) return false; // l1 is below l2
  return true; // boxes overlap
}
Copy the code

compact

This function is used to compress the layout space to make the layout more compact.

 function compact(layout, compactType, cols) {
   // Get static layout static =true
  const compareWith = getStatics(layout);
  // Sort according to the compression method passed in
  / / horizontal or vertical 'horizontal' | 'vertical';
  const sorted = sortLayoutItems(layout, compactType);
  // An array to place the new layout
  const out = Array(layout.length);
  for (let i = 0, len = sorted.length; i < len; i++) {
    let l = cloneLayoutItem(sorted[i]);
    // Static elements are not moved
    if(! l.static) {// Compress the space
      l = compactItem(compareWith, l, compactType, cols, sorted);
      compareWith.push(l);
    }
    // Add to output array 
    // to make sure they still come out in the right order.
    out[layout.indexOf(sorted[i])] = l;
    // Clear moved flag, if it exists.
    l.moved = false;
  }

  return out;
}


// Compression handlers
function compactItem(
  compareWith: Layout,
  l: LayoutItem,
  compactType: CompactType,
  cols: number,
  fullLayout: Layout
) :LayoutItem {
  const compactV = compactType === "vertical";
  const compactH = compactType === "horizontal";
  if (compactV) {
     // Compress the y coordinate vertically without collision
    l.y = Math.min(bottom(compareWith), l.y);
    while (l.y > 0&&! getFirstCollision(compareWith, l)) { l.y--; }}else if (compactH) {
    // Compress the x coordinate horizontally without collision
    while (l.x > 0&&! getFirstCollision(compareWith, l)) { l.x--; }}// If there is a collision, move down or left
  let collides;
  while ((collides = getFirstCollision(compareWith, l))) {
    if (compactH) {
      resolveCompactionCollision(fullLayout, l, collides.x + collides.w, "x");
    } else {
      resolveCompactionCollision(fullLayout, l, collides.y + collides.h, "y");
    }
    // Control infinite growth in horizontal direction.
    if(compactH && l.x + l.w > cols) { l.x = cols - l.w; l.y++; }}// Make sure there are no negative values for y--,x--
  l.y = Math.max(l.y, 0);
  l.x = Math.max(l.x, 0);
  
  return l;
}
Copy the code

The correntBounds and Compact functions produce a compact, overflow – free, stack-free grid layout cell.

Container height calculation

With layout generation behind us, let’s look at the handling of class names and styles in the render function of the entry component. There’s nothing special about classname merge, just use classnames for merge.

// Basic use of classnames
var classNames = require('classnames');
classNames('foo'.'bar'); // => 'foo bar'
// react-grid-layout
const { className, style, isDroppable, innerRef } = this.props;
// Merge the class name
const mergedClassName = classNames(layoutClassName, className);
Copy the code

The style merge involves containerHeight, a function used to calculate the height of the container, but there are some interesting points here. The height of a container must accommodate at least the highest space occupying layout (height H and position Y), so it is necessary to find the item with the largest h+y from the given layout as the container reference height. As shown in the figure below, for easy observation, height H of each layout item is 1, the maximum Y-axis coordinate is 2, and the container reference height is 3.But the full height is not only the base height, but also the margin between grid-items and the container padding.

containerHeight() {
    // The default autoSize is true
    if (!this.props.autoSize) {
      return;
    }
    // Get the bottom coordinates
    // The layout here is modified, different from this.props. Layout
    const nbRow = bottom(this.state.layout);
    const containerPaddingY = this.props.containerPadding 
    ? this.props.containerPadding[1] 
    : this.props.margin[1];
    
   // Calculate the specific px
   // rowHeight default 150 margin default [10,10]
    return `
    ${nbRow * this.props.rowHeight +
     (nbRow - 1) * this.props.margin[1] + 
     containerPaddingY * 2 }
     px`;
  }
  
  // Get the maximum value of y+h in the layout
 function bottom(layout: Layout) :number {
  let max = 0;
  let bottomY;
  for (let i = 0, len = layout.length; i < len; i++) {
    bottomY = layout[i].y + layout[i].h;
    if(bottomY > max) { max = bottomY; }}return max;
}
Copy the code

Layout calculation: 30(rowHeight)*3(base height)+20(two margins)+20(upper and lower padding)=130px. It is important to note that when calculating the container height, the base height refers to the coordinate value compressed by the Compact function. Consider a specific height calculation example:

export default class App extends React.PureComponent {
  render() {
    const layout = [
      { i: 'a'.x: 0.y: 100.w: 1.h: 1},];return (
      <div style={{ width: 600.border: '1px solid #ccc', margin: 10}} >
        <GridLayout layout={layout} width={600}>
          <div key="a">a</div>
        </GridLayout>
      </div>); }}Copy the code

If you print inside containerHeight, you’ll see that y is not passed in as 100, but a compact compressed 0. So the base height of the container is h+y=1+0=1. Container height = 150(rowHeight)*1(base height)+0(margin)+20(upper and lower container padding)=170px.

GridItem

The container layout calculation above, the grid cell calculation is done in the GridItem component component. This component accepts a lot of props, which can be roughly divided into three categories: layout, drag, and scaling.

 processGridItem(child: any, isDroppingItem? :boolean) :any {
    if(! child || ! child.key) {return;
    }
    const l = getLayoutItem(this.state.layout, String(child.key));
    if(! l) {return null;
    }
const {
  width,// The container width
  cols, // The default layout column count is 12
  margin, // Margin between items [x, y] in px
  containerPadding, // Padding inside the container [x, y] in px
  rowHeight, // Height of a single grid-item
  maxRows,// The maximum number of rows is infinite vertical Growth by default
  isDraggable, // Whether it can be dragged defaults to true
  isResizable, // Whether it is scalable defaults to true
  isBounded,  // Controls whether to move within container limits by default false
  useCSSTransforms,// Using transforms left/top transforms this function replaces left/top with true by default for a 6-fold improvement in rendering performance
  transformScale, Transform: scale(n)
  draggableCancel, // Undrag the handle CSS class name selector
  draggableHandle,// Drag the handle CSS class name selector
  resizeHandles,// Zoom orientation defaults to the lower right corner of se
  resizeHandle, // Zoom handle
} = this.props;
    
const { mounted, droppingPosition } = this.state;
// Determine whether it can be dragged/scaled
const draggable = typeof l.isDraggable === 'boolean'? l.isDraggable : ! l.static && isDraggable;const resizable = typeof l.isResizable === 'boolean'? l.isResizable : ! l.static && isResizable;// Determine the scaling direction by default se
const resizeHandlesOptions = l.resizeHandles || resizeHandles;

// Determine whether to restrict movement within the container
constbounded = draggable && isBounded && l.isBounded ! = =false;

    return (
      <GridItem
        containerWidth={width}
        cols={cols}
        margin={margin}
        containerPadding={containerPadding || margin}
        maxRows={maxRows}
        rowHeight={rowHeight}
        cancel={draggableCancel}
        handle={draggableHandle}
        onDragStop={this.onDragStop}
        onDragStart={this.onDragStart}
        onDrag={this.onDrag}
        onResizeStart={this.onResizeStart}
        onResize={this.onResize}
        onResizeStop={this.onResizeStop}
        isDraggable={draggable}
        isResizable={resizable}
        isBounded={bounded}
        useCSSTransforms={useCSSTransforms && mounted}
        usePercentages={! mounted}
        transformScale={transformScale}
        w={l.w}
        h={l.h}
        x={l.x}
        y={l.y}
        i={l.i}
        minH={l.minH}
        minW={l.minW}
        maxH={l.maxH}
        maxW={l.maxW}
        static={l.static}
        droppingPosition={isDroppingItem ? droppingPosition : undefined}
        resizeHandles={resizeHandlesOptions}
        resizeHandle={resizeHandle}
      >
        {child}
      </GridItem>
    );
  }
Copy the code

Render

Next, let’s take a look at what the render function of this component does.

render() {
    const { 
    x, y, w, h, 
    isDraggable, 
    isResizable, 
    droppingPosition,
    useCSSTransforms 
   } = this.props;
    // Position calculation, also recalculated when dragging and scaling are triggered
    const pos =calcGridItemPosition(
     this.getPositionParams(), 
     x, y, w, h,
     this.state
     );
   / / for the child
    const child= React.Children.only(this.props.children);

    // Modify the class name and style of child
    let newChild = React.cloneElement(child, {
      ref: this.elementRef,
      // Change the class name
      className: classNames(
      'react-grid-item', 
       child.props.className, 
       this.props.className, {
        static: this.props.static,
        resizing: Boolean(this.state.resizing),
        'react-draggable': isDraggable,
        'react-draggable-dragging': Boolean(this.state.dragging),
        dropping: Boolean(droppingPosition),
        cssTransforms: useCSSTransforms,
      }),
      // Modify the style
      // Actually replace the grid elements W, H, x, y with the specific dimensions of PX
      style: {... this.props.style, ... child.props.style, ... this.createStyle(pos), }, });// Add zoom support
    newChild = this.mixinResizable(newChild, pos, isResizable);
    // Add drag support
    newChild = this.mixinDraggable(newChild, isDraggable);

    return newChild;
  }
  
  
 getPositionParams(props: Props = this.props): PositionParams {
    return {
      cols: props.cols,
      containerPadding: props.containerPadding,
      containerWidth: props.containerWidth,
      margin: props.margin,
      maxRows: props.maxRows,
      rowHeight: props.rowHeight
    };
  }
Copy the code

calcGridItemPosition

This function takes layout parameters and returns the final result after a series of calculations. Given the following parameters:

} Container width 600, intermesh margin10, container paadding10, column number cols12Copy the code

Calculation principle

The column width is calculated in the same way as the height before, and the margin between the grids and the padding of the container should also be considered. ColWidth = (containerwidth-margin [0] * (cols-1) -containerpadding [0] * 2)/cols But this is based on layout units. If the gridItem is being scaled, the width,height recorded by state at scaling is used. If the gridItem is being dragged, use the position of the state record at the time of the drag (left,top). Note: in react-grid-layout, margin is stored in [x,y] form, which is the opposite of CSS when margin is set to two values.

function calcGridItemPosition(positionParams, x, y, w, h, state){
  const { margin, containerPadding, rowHeight } = positionParams;
  // Calculate the column width
  const colWidth = calcGridColWidth(positionParams);
  const out = {};

  // If the gridItem is being scaled, use the width,height recorded by state at the time of scaling.
  // Get the layout information through the callback function
  if (state && state.resizing) {
    out.width = Math.round(state.resizing.width);
    out.height = Math.round(state.resizing.height);
  }
  // Instead, calculate based on grid cells
  else {
    out.width = calcGridItemWHPx(w, colWidth, margin[0]);
    out.height = calcGridItemWHPx(h, rowHeight, margin[1]);
  }

  // If the gridItem is being dragged, use the position of the state record at the time of the drag (left,top)
  // Get the layout information through the callback function
  if (state && state.dragging) {
    out.top = Math.round(state.dragging.top);
    out.left = Math.round(state.dragging.left);
  }
  // Instead, calculate based on grid cells
  else {
    out.top = Math.round((rowHeight + margin[1]) * y + containerPadding[1]);
    out.left = Math.round((colWidth + margin[0]) * x + containerPadding[0]);
  }

  return out;
}
// Calculate the column width
function calcGridColWidth(positionParams: PositionParams) :number {
  const { margin, containerPadding, containerWidth, cols } = positionParams;
  return (
    (containerWidth - margin[0] * (cols - 1) - containerPadding[0] * 2) / cols
  );
}
// gridUnits Grid layout base units
function calcGridItemWHPx(gridUnits, colOrRowSize, marginPx){
  // 0 * Infinity === NaN, which causes problems with resize contraints
  if (!Number.isFinite(gridUnits)) return gridUnits;
  return Math.round(
  colOrRowSize * gridUnits + Math.max(0, gridUnits - 1) * marginPx);
}
Copy the code

createStyle

With layout width, height and position computed, let’s look at styling. GridItem style merge uses the function createStyle, which converts the calculated layout into a CSS style with px.

createStyle(pos) {
    const { usePercentages, containerWidth, useCSSTransforms } = this.props;
    let style;
    // CSS Transforms are supported by default
    // Skip layout and drawing directly, and do not occupy the main thread resources, relatively fast
    if (useCSSTransforms) {
      style = setTransform(pos);
    } else {
      // Use top,left display, will be slow
      style = setTopLeft(pos);
      // Server render related
      if(usePercentages) { style.left = perc(pos.left / containerWidth); style.width = perc(pos.width / containerWidth); }}return style;
  }
  
  // Take translate and add compatible processing and unit px
  function setTransform({ top, left, width, height }) {
      const translate = `translate(${left}px,${top}px)`;
      return {
        transform: translate,
        WebkitTransform: translate,
        MozTransform: translate,
        msTransform: translate,
        OTransform: translate,
        width: `${width}px`.height: `${height}px`.position: "absolute"
      };
}

// Use the left top form and add the unit px
function setTopLeft({ top, left, width, height } {
  return {
    top: `${top}px`,
    left: `${left}px`,
    width: `${width}px`,
    height: `${height}px`,
    position: "absolute"
  };
}
Copy the code

Drag and zoom

mixinDraggable

The mixinDraggable function adds drag support for child and relies on react-draggable for implementation.

Drag and drop the principle

Inside the DraggableCore component of the React-Draggable library, useful information such as coordinates and the current node is generated when the corresponding drag event is triggered. This information is encapsulated as an object and passed as a parameter to the corresponding external callback function. In this way, the external callback can get useful information from this object, reset setState, and set the dragging value to a new {left,top}. This value is then processed by calcGridItemPosition and createStyle as a CSS style attached to the child for drag and drop.

 import { DraggableCore } from 'react-draggable';
  function mixinDraggable(child, isDraggable) {
  // The following drag-and-drop callback functions are used to receive additional location information and calculate the layout
    return (
      <DraggableCore
        disabled={! isDraggable}
        onStart={this.onDragStart}
        onDrag={this.onDrag}
        onStop={this.onDragStop}
        handle={this.props.handle}
        cancel={`.react-resizable-handleThe ${this.props.cancel? `, ${this.props.cancel} `:"'} `}scale={this.props.transformScale}
        nodeRef={this.elementRef}
      >
        {child}
      </DraggableCore>
    );
  }
  
Copy the code

DraggableCore

In react-Grid-Layout, both mixinDraggable and mixinResizable depend on DraggableCore. This is because dragging and zooming involve the same mouse events (touch events aside), for which the component also encapsulates the corresponding event handler functions. Within these three functions, the callback functions onStart, onDrag, and onStop passed in props are called.

  • HandleDragStart: Records the initial position of the drag
  • In handleDrag: Monitor the distance and direction of the drag and move the real DOM
  • HandleDragStop End of drag: Cancel event listening in drag
render() {
    return React.cloneElement(React.Children.only(this.props.children), {
      onMouseDown: this.onMouseDown,
      onMouseUp: this.onMouseUp,
      // xxx.. Touch related events
    });
  }
  
  
 // dragEventFor is a global variable used to identify the trigger event type mouse or touch
 onMouseDown = (e) = > {
  // Mouse related events
    dragEventFor ={
         start: 'mousedown'.move: 'mousemove'.stop: 'mouseup'
    }
    return this.handleDragStart(e);
 };
 
handleDragStart(){
/ /...
this.props.onStart()
}

handleDrag(){
/ /...
this.props.onDrag()
}

handleDragStop(){
/ /...
this.props.onStop()
}
Copy the code

Let’s take a look at the inner workings of each of these event handlers.

handleDragStart

  handleDragStart = (e) = > {
    // Support the mouse-down callback function
    this.props.onMouseDown(e);

    // Only accept left-clicks.
    //xxx...
    
    // Make sure you get the document
   // https://developer.mozilla.org/zh-CN/docs/Web/API/Node/ownerDocument
    const thisNode = this.findDOMNode();
    if(! thisNode || ! thisNode.ownerDocument || ! thisNode.ownerDocument.body) {throw new Error('
      
        not mounted on DragStart! '
      );
    }
    const {ownerDocument} = thisNode;

   
if (this.props.disabled || (! (e.targetinstanceof ownerDocument.defaultView.Node)) ||
  (this.props.handle && ! matchesSelectorAndParentsTo(e.target,this.props.handle, thisNode)) ||
  (this.props.cancel && 
  matchesSelectorAndParentsTo(e.target, this.props.cancel, thisNode))) {
  return;
}

/** Handle example <! <Draggable handle=".handle"> <div> <div className="handle">Click me to drag</div> <div>This is some other content</div> </div> </Draggable>*/


    // Touch related operations...
    // For non-touch devices, getControlPosition the second function is undefined
    // Get the coordinates when the mouse is pressed
    const position = getControlPosition(e, undefined.this);
    if (position == null) return; 
    const {x, y} = position;
    
   // An object containing the node itself, coordinates, and other information
    const coreEvent = createCoreData(this, x, y);
    // Call the callback onStart passed to props
    const shouldUpdate = this.props.onStart(e, coreEvent);
    if (shouldUpdate === false || this.mounted === false) return;
    // Update the drag state and store offsets
    this.setState({
      dragging: true.lastX: x,
      lastY: y
    });

   // Bind the move event to the document to expand the response
   // The event is guaranteed to get a response even if the current gridItem is removed.
   // Touchable devices and non-touchable devices respond to the end of the drag differently. Here two events are required
    addEvent(ownerDocument, dragEventFor.move, this.handleDrag);
    addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop);
  };
Copy the code

handleDrag

Both handleDrag and handleDragStop will be easier to understand after looking at the internal details of the handleDragStart function. The main thing handleDrag does is update location information as you drag.

  handleDrag=(e) = > {
    // Get the current drag point from the event. This is used as the offset.
    const position = getControlPosition(e, null.this);
    if (position == null) return;
    let {x, y} = position;
    const coreEvent = createCoreData(this, x, y);
    // Call event handler. If it returns explicit false, trigger end.
    const shouldUpdate = this.props.onDrag(e, coreEvent);
    if (shouldUpdate === false || this.mounted === false) {
      try {
        this.handleDragStop(new MouseEvent('mouseup'));
      } catch (err) {
        // Old browsers
        //xxx... Some compatibility handling of older browsers
      }
      return;
    }

    this.setState({
      lastX: x,
      lastY: y
    });
  };
Copy the code

handleDropStop

The drag ends, resets the location information, and deletes the bound event handler.


  handleDragStop= (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();
    // Reset the el.
    this.setState({
      dragging: false.lastX: NaN.lastY: NaN
    });

    if (thisNode) {
      // Remove event handlers
      removeEvent(thisNode.ownerDocument, dragEventFor.move, this.handleDrag);
      removeEvent(thisNode.ownerDocument, dragEventFor.stop, this.handleDragStop); }};Copy the code

mixinResizable

The mixinResizable function adds scaling support for child, which relies on react-Resizable implementation. The implementation of react-resizable relies on react- Draggable.

Scaling principle

Scaling and dragging rely on the same library at the bottom, which means that the implementation is similar, with the help of callback functions. The DraggableCore component internally passes the event object containing the location information to the external callback function, which resets the state and sets the resizing value to the new {width,height}. Finally, the new width and height will be applied to the Grid-Item using CSS styles to enable scaling.

function mixinResizable(child,position,isResizable) {
    const {
      cols,
      x,
      minW,
      minH,
      maxW,
      maxH,
      transformScale,
      resizeHandles,
      resizeHandle
    } = this.props;
    const positionParams = this.getPositionParams();
    // Maximum width
    const maxWidth = calcGridItemPosition(positionParams, 0.0, cols - x, 0)
      .width;

    // Calculate the minimum and maximum grid layouts and corresponding container sizes
    const mins = calcGridItemPosition(positionParams, 0.0, minW, minH);
    const maxes = calcGridItemPosition(positionParams, 0.0, maxW, maxH);
    const minConstraints = [mins.width, mins.height];
    const maxConstraints = [
      Math.min(maxes.width, maxWidth),
      Math.min(maxes.height, Infinity)];return (
      <Resizable
        draggableOpts={{
          disabled: !isResizable,}}className={isResizable ? undefined : "react-resizable-hide"}
        width={position.width}
        height={position.height}
        minConstraints={minConstraints}
        maxConstraints={maxConstraints}
        onResizeStop={this.onResizeStop}
        onResizeStart={this.onResizeStart}
        onResize={this.onResize}
        transformScale={transformScale}
        resizeHandles={resizeHandles}
        handle={resizeHandle}
      >
        {child}
      </Resizable>
    );
  }
Copy the code

Resizable

The Resizable component does three main things:

  • Pass the resizable internal callback function to the DraggableCore component to retrieve the event information object.
  • Passing the retrieved event information object to the external callback for the final style update in the resizable internal callback function effectively covers two layers
  • Render control handle
 render() {
    returncloneElement(children, { ... p,className: `${className ? `${className} ` : ' '}react-resizable`.children: [
        ...[].concat(children.props.children),
        // handleAxis is an array that stores manipulation directions. resizeHandles.map((handleAxis) = > {
          // Mount a node for manipulation
          const ref = this.handleRefs[handleAxis] ?
           this. HandleRefs [handleAxis] : the React. CreateRef ();return (
            <DraggableCore
              {. draggableOpts}
              nodeRef={ref}
              key={`resizableHandle-The ${handleAxis} `}onStop={this.resizeHandler('onResizeStop', handleAxis)}
              onStart={this.resizeHandler('onResizeStart', handleAxis)}
              onDrag={this.resizeHandler('onResize', handleAxis)}
            >Se {this.renderResizeHandle(handleAxis, ref)}</DraggableCore>); ]}})); }Copy the code

Generic event function encapsulation

The three event handlers for scaling do only simple triggering externally and share a common set of processing logic internally (onResizeHandler).

  // Stop scaling
  onResizeStop: (Event, { node: HTMLElement, size: Position }) = > void = (e, callbackData) = > {
    this.onResizeHandler(e, callbackData, "onResizeStop");
  };

// Start scaling
  onResizeStart: (Event, { node: HTMLElement, size: Position }) = > void = (e, callbackData) = > {
    this.onResizeHandler(e, callbackData, "onResizeStart");
  };

 / / zoom in
  onResize: (Event, { node: HTMLElement, size: Position }) = > void = (e, callbackData) = > {
    this.onResizeHandler(e, callbackData, "onResize");
  };
Copy the code

onResizeHandler

This function is used to calculate the regenerated grid cell information after scaling and store the changed width and height on State’s Resizing.

 onResizeHandler(
    e: Event,
    { node, size }: { node: HTMLElement, size: Position },
    handlerName: string) :void {
   // Get the corresponding event handler based on the handler name passed in
    const handler = this.props[handlerName];
    if(! handler)return;
    const { cols, x, y, i, maxH, minH } = this.props;
    let { minW, maxW } = this.props;

    // Calculate the grid elements w,h according to the width and height
    // Since scaling changes the size, the grid cell should change as well
    let { w, h } = calcWH(
      this.getPositionParams(),
      size.width,
      size.height, 
      x,
      y
    );

    // Keep the layout of one cell at a minimum
    minW = Math.max(minW, 1);
    // Maximum (cols-x)
    maxW = Math.min(maxW, cols - x);

    // Limit width height between min Max, can be equal to min Max
    w = clamp(w, minW, maxW);
    h = clamp(h, minH, maxH);

    // Update the reszing value, similar to the dragging function, for the final style calculation
    // The difference is that the edge only stores width/height
    // Dragging will store left/top
    this.setState({ resizing: handlerName === "onResizeStop" ? null : size });

    handler.call(this, i, w, h, { e, node, size });
  }
  
  
// Restrict target values between upper and lower boundaries
function clamp(
  num: number,
  lowerBound: number,
  upperBound: number
) :number {
  return Math.max(Math.min(num, upperBound), lowerBound);
}
Copy the code

resizeHandler

In fact, resizaHandle acts as a transfer station and obtains nodes and location information objects from DraggableCore first. Then calculate the width and height after scaling according to the obtained object information, and use it as a parameter to trigger the corresponding callback.

 resizeHandler(handlerName: 'onResize' | 'onResizeStart' | 'onResizeStop', axis): Function {
    return (e, { node, deltaX, deltaY }) = > {
      // Reset data in case it was left over somehow (should not be possible)
      if (handlerName === 'onResizeStart') this.resetData();

      // Axis restrictions
      const canDragX = (this.props.axis === 'both' || this.props.axis === 'x') && axis ! = ='n'&& axis ! = ='s';
      const canDragY = (this.props.axis === 'both' || this.props.axis === 'y') && axis ! = ='e'&& axis ! = ='w';
      // No dragging possible.
      if(! canDragX && ! canDragY)return;

      // Decompose axis for later use
      const axisV = axis[0];
      const axisH = axis[axis.length - 1]; // intentionally not axis[1], so that this catches axis === 'w' for example

      // Track the element being dragged to account for changes in position.
      // If a handle's position is changed between callbacks, we need to factor this in to the next callback.
      // Failure to do so will cause the element to "skip" when resized upwards or leftwards.
      const handleRect = node.getBoundingClientRect();
      if (this.lastHandleRect ! =null) {
        // If the handle has repositioned on either axis since last render,
        // we need to increase our callback values by this much.
        // Only checking 'n', 'w' since resizing by 's', 'w' won't affect the overall position on page,
        if (axisH === 'w') {
          const deltaLeftSinceLast = handleRect.left - this.lastHandleRect.left;
          deltaX += deltaLeftSinceLast;
        }
        if (axisV === 'n') {
          const deltaTopSinceLast = handleRect.top - this.lastHandleRect.top; deltaY += deltaTopSinceLast; }}// Storage of last rect so we know how much it has really moved.
      this.lastHandleRect = handleRect;

      // Reverse delta if using top or left drag handles.
      if (axisH === 'w') deltaX = -deltaX;
      if (axisV === 'n') deltaY = -deltaY;

      // Calculate the width and height after scaling
      let width = this.props.width + (canDragX ? deltaX / this.props.transformScale : 0);
      let height = this.props.height + (canDragY ? deltaY / this.props.transformScale : 0);

      // Run user-provided constraints.
      [width, height] = this.runConstraints(width, height);

      constdimensionsChanged = width ! = =this.props.width || height ! = =this.props.height;

      // Call user-supplied callback if present.
      const cb = typeof this.props[handlerName] === 'function' ? this.props[handlerName] : null;
      // Don't call 'onResize' if dimensions haven't changed.
      const shouldSkipCb = handlerName === 'onResize' && !dimensionsChanged;
      if(cb && ! shouldSkipCb) { e.persist? . (); cb(e, { node,size: { width, height }, handle: axis });
      }

      // Reset internal data
      if (handlerName === 'onResizeStop') this.resetData();
    };
  }
Copy the code

farewell

Love is fickle,

Is a move that hurt.

Thank you so much for reading my article,

I’m Cold Moon Heart. See you next time.