Front end drag and drop introduction and practice

preface

I haven’t updated the article recently, because I went to byte practice for a while, I really have no energy to write things, so I coo. Now back to school, you can continue to update, because the byte business is more related to graph visualization and drag, so I will write drag related content this time.

HTML5 Drag and Drop interface

Html5 provides a series of Drag and Drop interfaces, mainly including four parts: DragEvent, DataTansfer, DataTransferItem and DataTransferItemList.

DragEvent

Source elements and target elements

** source element: ** is the element being dragged.

** Target element: ** is a legal releasable element.

The event body of every event is one of the two.

Drag and drop event

The event Event handler Events subject trigger
dragstart ondragstart A source element When the source element starts to be dragged.
drag ondrag A source element When the source element is dragged (continuous firing).
dragend ondragend A source element When the source element is dragged out (mouse release or downescKey)
dragenter ondragenter The target element When an element is dragged into that element.
dragover ondragover The target element When the dragged element stays on the element (continuous firing).
dragleave ondragleave The target element When an element is dragged away from the element.
drop ondrop The target element When the drag event is released on a legitimate target element.

Trigger sequence and times

We bind the related events and drag and drop them once to see how they fire.

We have the corresponding event handler print the name of the event and who the subject that the event triggers is, as shown in the excerpt below.

We can see that for dragged elements, the event firing sequence is dragstart->drag->dragend; For target elements, events fire in the order dragenter-> Dragover -> Drop/Dropleave.

Where drag and dragover fire repeatedly on the source and target elements, respectively. The entire process must be dragstart first and dragend last.

One other caveat here is that if an element has both dragover and DROP listening set, then the default behavior of dragover must be blocked or the drop will not be triggered.

DataTansfer

Let’s use a picture to give you an intuition:

As we can see, DataTransfer, as its name, is used to transfer data in the drag-and-drop process, wherein setData is used to store data, getData is used to obtain data. For security reasons, data can only be obtained when drop. Effectalhoward and dropEffect influence the way the mouse is displayed. Here’s an example:

sourceElem.addEventListener('dragstart'.(event) = > {
    event.dataTransfer.effectAllowed = 'move';
    event.dataTransfer.setData('text/plain'.'Let it in.');
});
targetElem.addEventListener('dragover'.(event) = > {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move';
});
targetElem.addEventListener('drop'.(event) = > {
    event.target.innerHTML = event.dataTransfer.getData('text/plain');
});
Copy the code

You can see that the data set in the blue box was successfully obtained.

DataTransferItemList

attribute

Length: number of drag items in the list.

methods

Add () : Adds a new item (File object or String) to the drag item list. This method returns a DataTransferItem object.

Remove () : Removes objects from the drag item list by index.

Clear () : Clears the list of drag items.

DataTransferItem() : Value method: Returns the DataTransferItem object with the given subscript.

DataTransferItem

attribute

Kind: Type of drag item, string or file.

Type: The type of the drag item, usually a MIME type.

methods

GetAsString: Executes the specified callback function using the drag item’s string as an argument.

GetAsFile: Returns a File object associated with the drag item (null if the drag item is not a File).

practice

Having learned the basics above, let’s start with a few common application scenarios to practice the above knowledge

Placable component

With the above events in mind, we are going to create a simple, placable component. For your convenience, we are not going to use any frame here, so as not to increase the learning cost of those who do not know how to frame.

To make a component draggable, you need to be able to change its position in two ways:

  • Pos: ABS changes the position of elements directly by top/left etc
  • Use translate in the TRANSFORM property of the CSS to change the position of the element

First, translate is based on its own movement, so its own coordinate is used as the origin (0,0). However, the first method, the element’s own top/left, etc., may not be 0, so it is more complicated to calculate. Secondly, the first one is calculated by CPU, while the second one is calculated by GPU, and it will be promoted to a new layer, which is very good for page performance. The reason is that Chrome turns the DOM into a screen image like this:

  1. Take the DOM and split it into layers

  2. Upload layers as textures to the GPU

  3. Compound multiple layers to produce the final screen image.

But newer frames can take shortcuts that don’t have to go through all the process:

There is no need to redraw if certain CSS properties change. Chrome can recompose using layers that already exist in the GPU as textures, but with different composite properties (e.g., appear in different locations, have different transparency, etc.).

If one element of the layer needs to be redrawn, the entire layer needs to be redrawn. So upgrading to a new layer can reduce the number of redraws. Because you only change the position, you can reuse the texture to improve performance.

More details can be found in my other article: Browser event loops and rendering mechanisms – Nuggets (juejin. Cn)

Have an idea so let’s start!

First we need to know what the drag vector is, because DragEvent inherits from MouseEvent, so we can use the offsetX and offsetY properties of the MouseEvent interface to get the current position of the mouse relative to the object. Transform sets multiple property values, and the effect can be superimposed, so we need to obtain the previous movement effect, plus the current movement effect, the previous movement effect can be obtained by using window.getComputedStyle(e.target).transform.

sourceElem.addEventListener('dragend'.(e) = > {
    const startPosition = window.getComputedStyle(e.target).transform;
    e.target.style.transform = `${startPosition} translate(${e.offsetX}px, ${e.offsetY}px)`;
}, true);
Copy the code

We add this handler to the element we want to drag and we seem to be done.

But when we actually use it, instead of stopping at the preview position, we move the upper left corner of the element to the mouse position, which is obviously not what we expected. As you can guess, we’ve lost track of the mouse position on the element. The initial position of the mouse can also be obtained through the offsetX and offsetY properties (dragStart) of the MouseEvent interface. The improvements are as follows:

function enableDrag(element) {
    let mouseDiff = null;
    element.addEventListener('dragstart'.(e) = > {
        // The initial mouse position is off the element
        mouseDiff = `translate(${-e.offsetX}px, ${-e.offsetY}px)`
    }, true);
    element.addEventListener('dragend'.(e) = > {
        // The initial position of the element
        const startPosition = window.getComputedStyle(e.target).transform;
        // Cursor position
        const mouseMove = `translate(${e.offsetX}px, ${e.offsetY}px)`;
        e.target.style.transform = `${mouseDiff} ${startPosition} ${mouseMove}`;
    }, true);
}
enableDrag(souceElement);
Copy the code

The connection to the figure

Nodes use DOM rendering, wires we use SVG to render, frames use React, but try to use less frame-specific except state, in case non-react stack students don’t understand.

First of all, we need to organize our state. As a graph, it should obviously be composed of nodes and edges. We use array storage, and give each node a unique ID. Edge active end and terminal ID, through THE ID to obtain the corresponding coordinates.

Assuming that the node can do all the work, consider only the wires. You can define the following components

const Edge = ({nodes:[sourceNode,targetNode]}) = >(
    <svg key={sourceNode.id + targetNode.id| | '}style={{position:'absolute',
        overflow:'visible',
        zIndex:'- 1',
        transform:'translate(15px.15px) '}} >
        <path d={`MThe ${sourceNode.position[0]} ${sourceNode.position[1]} 
            CThe ${(targetNode.position[0]  + sourceNode.position[0) /2} ${sourceNode.position[1]}
            ${(targetNode.position[0]  + sourceNode.position[0) /2} ${targetNode.position[1]}
            ${targetNode.position[0]} ${targetNode.position[1`]}}strokeWidth={6}
            stroke={'red'}
            fill='none'
        ></path>
    </svg>
)
Copy the code

At what point should we first generate a wire, obviously a dragstart, but there are no corresponding endpoints yet, so instead of lofting around adding edges, we should render one separately. In DragStart we set up a virtual node temNode and record the ID of the start node.

And render a preview edge if temNode is present.

temNode && (<Edge nodes = {[sourceNode,temNode]}></Edge>)
Copy the code

Then after adding the edge, we removed the virtual nodes and changed to a circular rendering to show all the edges.

edges.map(([sourceId,targetId]) = >{
    const sourceNode = getNode(sourceId);
    const targetNode = getNode(targetId);
    return (
        <Edge nodes = {[sourceNode,targetNode]}></Edge>);
})
Copy the code

So we just consider the edge of the display, how should the function of the node be improved?

First, dragStart, where we set up the start node and the virtual node

onDragStart={() = >{
    setStartNodeId(uid);
    setTemNode({position:[x,y]})
}}
Copy the code

Then, since edges can follow, we must dynamically change the position of virtual nodes in drag

onDrag={(event) = >{
    position=[x+event.nativeEvent.offsetX,y+event.nativeEvent.offsetY];
    setTemNode({position})
}}
Copy the code

And then when we drop, we add a new edge

onDrop={
    (event) = >{
        event.preventDefault();
        setEdges(edges.concat([[startNodeId,uid]]))
    }
}
Copy the code

Most importantly, no matter where the event ends, delete the virtual node

onDragEnd={
    () = >{
        setTemNode(null); }}Copy the code

Here is the final result:

reference

  • HTML Drag and Drop API – Web APIs | MDN (mozilla.org)