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.