⚠️ this article for nuggets community first contract article, not authorized to forbid reprint
Designers on most low-code platforms support component drag and drop capabilities, which greatly improves the user design experience. Another common dragging scene is file uploading. By dragging, users can easily upload files. With drag-and-drop, we can also share data across browser boundaries.
So how do you share data across browser boundaries? This article describes transmat, an open source project from Google that makes this possible. Not only that, but the project also allows us to implement some interesting features, such as different responses to different releasable goals.
Here’s a look at four giFs to get a feel for the magic and fun drag-and-drop functionality that Transmat has developed.
Figure 1 (Drag draggable elements to a rich text editor)
Figure 2 (Drag draggable elements to Chrome and other browsers)
Figure 3 (Drag draggable elements to custom release targets)
Figure 4 (Drag draggable elements to Chrome Developer Tools)
Chrome 91.0.4472.114 (official version) (x86_64)
The draggable element in the above four diagrams is the same element, and when it is placed on different releasable objects, it produces different effects. At the same time, we are sharing data across the boundaries of the browser. After looking at the above 4 GIFs, do you think it is quite magical? In addition to drag and drop, the example also supports copy and paste operations. However, before going into the details of how to use Transmat to do this, let’s take a quick look at the Transmat library.
Introduction to Transmat
Transmat is a small library around the DataTransfer API that uses drag-drop and copy-paste interactions to simplify transferring and receiving data in Web applications. The DataTransfer API can transfer different types of data to other applications on the user’s device. The common data types supported by the API are text/plain, Text/HTML, and Application/JSON.
(photo: Google. Making. IO/transmat /)
Now that we know what Transmat is, let’s look at its application scenarios:
- Want to integrate with external applications in a convenient way.
- You want to give users the ability to share data with other applications, even those you don’t know about.
- You want the external application to be deeply integrated with your Web application.
- You want your application to better fit your user’s existing workflow.
Now that you’re familiar with Transmat, let’s analyze how to use Transmat to implement the four giFs above.
Second, Transmat actual combat
2.1 transmat – source
html
In the following code, we add a draggable attribute to the div#source element. This attribute identifies whether the element is allowed to be dragged, and has a value of true or false.
<script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
<div id="source" draggable="true" tabindex="0">Hello, I'm Brother Po</div>
Copy the code
css
#source {
background: #eef;
border: solid 1px rgba(0.0.255.0.2);
border-radius: 8px;
cursor: move;
display: inline-block;
margin: 1em;
padding: 4em 5em;
}
Copy the code
js
const { Transmat, addListeners, TransmatObserver } = transmat;
const source = document.getElementById("source");
addListeners(source, "transmit".(event) = > {
const transmat = new Transmat(event);
transmat.setData({
"text/plain": "Everybody is good, I am a treasure elder brother!"."text/html": visit my home page < / a >!
`."text/uri-list": "https://juejin.cn/user/764915822103079"."application/json": {
name: "My brother".wechat: "semlinker",}}); });Copy the code
In the above code, we use the addListeners function provided by the transmat library to add the event listener for transmit to the div#source element. In the corresponding event handler, we first create a Transmat object and then call the setData method on the object to set the data of the different MIME types.
Here’s a quick review of the MIME types used in the example:
text/plain
: represents the default value for a text file. A text file should be human-readable and contain no binary data.text/html
: represents an HTML file type that some rich text editors preferdataTransfer
Objecttext/html
Type of data, if it does not existtext/plain
Type of data.text/uri-list
: indicates the URI link type. Most browsers preferentially read this type of data. If a valid URI link is found, the browser opens the link directly. If it’s not a valid URI link, for Chrome, it reads ittext/plain
Type of data and use that data as a keyword for content retrieval.application/json
Said:JSONType, this type should be familiar to front-end developers.
After covering Transmat-source, let’s look at the implementation code for Figure 3, Transmat-Target.
2.2 transmat – target
html
<script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
<div id="target" tabindex="0">Put it here!</div>
Copy the code
css
body {
text-align: center;
font: 1.2 em Helvetia, Arial, sans-serif;
}
#target {
border: dashed 1px rgba(0.0.0.0.5);
border-radius: 8px;
margin: 1em;
padding: 4em;
}
.drag-active {
background: rgba(255.255.0.0.1);
}
.drag-over {
background: rgba(255.255.0.0.5);
}
Copy the code
js
const { Transmat, addListeners, TransmatObserver } = transmat;
const target = document.getElementById("target");
addListeners(target, "receive".(event) = > {
const transmat = new Transmat(event);
// Check whether there is data of type "application/json"
// And whether the event type is drop or paste event
if (transmat.hasType("application/json")
&& transmat.accept()
) {
const jsonString = transmat.getData("application/json");
const data = JSON.parse(jsonString); target.textContent = jsonString; }});Copy the code
In the above code, we use the addListeners function provided by the transmat library to add the receive event listener to the div#target element. As the name suggests, the receive event indicates that the message is received. In the corresponding event handler, we filtered the application/ JSON messages through the hasType method of the Transmat object, and then deserialized the corresponding data through the json. parse method. The contents of the corresponding jsonString are also displayed in the div#target element.
In Figure 3, when we drag a draggable element to a custom release target, we get a highlighting effect, as shown below:
This effect is achieved by using the Transmat library’s TransmatObserver class, which helps us respond to users’ drag-and-drop behavior, as follows:
const obs = new TransmatObserver((entries) = > {
for (const entry of entries) {
const transmat = new Transmat(entry.event);
if (transmat.hasType("application/json")) {
entry.target.classList.toggle("drag-active", entry.isActive);
entry.target.classList.toggle("drag-over", entry.isTarget); }}}); obs.observe(target);Copy the code
After seeing TransmatObserver for the first time, Abgo immediately thought of the MutationObserver API because they are both observers and have similar apis. Using the MutationObserver API, we can monitor DOM changes. Any changes to the DOM, such as the addition or removal of nodes, changes in attributes, and changes in text content, are notified through the API. If you’re interested in the API, you can read who touched my DOM? This article.
Now that we know how the Transmat library works, Apogo takes a look at how it works.
Transmat Example: Transmat Demo
Gist.github.com/semlinker/c…
3. Transmat source code analysis
In transmat source code analysis, we used three “functions”, addListeners, Transmat and TransmatObserver, to implement core functions in the previous practical part, so we will focus on them in the following source code analysis. Let’s start with the addListeners function.
3.1 addListeners function
The addListeners function is used to set listeners and returns a function to remove event listeners. When analyzing a function, It is customary to analyze the signature of the function first:
// src/transmat.ts
function addListeners<T extends Node> (
target: T,
type: TransferEventType,
listener: (event: DataTransferEvent, target: T) => void,
options = {dragDrop: true, copyPaste: true}
) : () = >void
Copy the code
By observing the above function signature, we can intuitively understand the input and output of the function. This function supports the following four arguments:
target
: indicates the target to be monitored. Its type isNode
Type.type
: indicates the type of the listenerTransferEventType
Is a union type'transmit' | 'receive'
.listener
: indicates an event listener. It supports the following event typesDataTransferEvent
, which is also a union type —DragEvent | ClipboardEvent
, which supports drag and drop events and clipboard events.options
: indicates a configuration object that is used to set whether to allow drag and drop, copy and paste operations.
The addListeners function body consists of the following three steps:
- Step 1: According to
isTransmitEvent
和options.copyPaste
To register clipboard-related events. - Step 2: According to
isTransmitEvent
和options.dragDrop
To register drag-and-drop related events. - Step ③ : Return the function object used to remove the registered event listener.
// src/transmat.ts
export function addListeners<T extends Node> (
target: T,
type: TransferEventType, // 'transmit' | 'receive'
listener: (event: DataTransferEvent, target: T) => void,
options = {dragDrop: true, copyPaste: true}
) : () = >void {
const isTransmitEvent = type= = ='transmit';
let unlistenCopyPaste: undefined | (() = > void);
let unlistenDragDrop: undefined | (() = > void);
if (options.copyPaste) {
// You can drag the source listener cut and copy events to release the target listener paste event
const events = isTransmitEvent ? ['cut'.'copy'] : ['paste'];
constparentElement = target.parentElement! ; unlistenCopyPaste = addEventListeners(parentElement, events,event= > {
if(! target.contains(document.activeElement)) {
return;
}
listener(event as DataTransferEvent, target);
if (event.type === 'copy' || event.type === 'cut') { event.preventDefault(); }}); }if (options.dragDrop) {
// ② The source can be dragged to listen for dragStart events, and the target can be freed to listen for dragover and drop events
const events = isTransmitEvent ? ['dragstart'] : ['dragover'.'drop'];
unlistenDragDrop = addEventListeners(target, events, event= > {
listener(event as DataTransferEvent, target);
});
}
// return the function object used to remove the registered event listener
return () = > {
unlistenCopyPaste && unlistenCopyPaste();
unlistenDragDrop && unlistenDragDrop();
};
}
Copy the code
Event listeners in the above code are ultimately implemented by calling the addEventListeners function, which loops through the addEventListener method to addEventListeners. Take the previous example of using Transmat as an example. In the corresponding event processing callback function, we will call the Transmat constructor to create a Transmat instance with the event object as the parameter. So what does this instance do? To understand what it does, we need to understand the Transmat class.
3.2 Transmat class
The Transmat class is defined in the SRC /transmat.ts file. The constructor of this class contains an argument event of type DataTransferEvent:
// src/transmat.ts
export class Transmat {
public readonly event: DataTransferEvent;
public readonly dataTransfer: DataTransfer;
// type DataTransferEvent = DragEvent | ClipboardEvent;
constructor(event: DataTransferEvent) {
this.event = event;
this.dataTransfer = getDataTransfer(event); }}Copy the code
The getDataTransfer function is also used inside the Transmat constructor to retrieve the DataTransfer object and assign it to the internal DataTransfer property. The DataTransfer object is used to hold data during drag and drop. It can hold one or more items of data, which can be of one or more data types.
Let’s look at the implementation of the getDataTransfer function:
// src/data_transfer.ts
export function getDataTransfer(event: DataTransferEvent) :DataTransfer {
const dataTransfer =
(event as ClipboardEvent).clipboardData ??
(event as DragEvent).dataTransfer;
if(! dataTransfer) {throw new Error('No DataTransfer available at this event.');
}
return dataTransfer;
}
Copy the code
In the above code, the null merge operator?? . The characteristic of this operator is that if the left-hand operand is null or undefined, it returns the right-hand operand; otherwise, it returns the left-hand operand. If it is a clipboard event, the DataTransfer object is fetched from the clipboardData property. Otherwise, it is fetched from the dataTransfer property.
For drag-and-drop sources, after creating the Transmat object, we can call the setData method on that object to hold one or more pieces of data. For example, in the following code, we set a number of different types of data:
transmat.setData({
"text/plain": "Everybody is good, I am a treasure elder brother!"."text/html":
... `."text/uri-list": "https://juejin.cn/user/764915822103079"."application/json": {
name: "My brother".wechat: "semlinker",}});Copy the code
Now that we know how to use setData, let’s look at its implementation:
// src/transmat.ts
setData(
typeOrEntries: string | {[type: string]: unknown}, data? : unknown ):void {
if (typeof typeOrEntries === 'string') {
this.setData({[typeOrEntries]: data});
} else {
// Handle multiple types of data
for (const [type, data] of Object.entries(typeOrEntries)) {
const stringData =
typeof data === 'object' ? JSON.stringify(data) : `${data}`;
this.dataTransfer.setData(normalizeType(type), stringData); }}}Copy the code
As you can see from the above code, the datatransfer.setdata method is eventually called inside the setData method to hold the data. The setData method of the dataTransfer object supports two string type arguments: format and data. They represent the data format to be saved and the actual data, respectively. If the given data format does not exist, the corresponding data is saved to the end. If the given data format already exists, the old data is replaced with the new data.
The following figure shows the compatibility of the datatransfer.setdata method, which is supported by most modern browsers.
(Photo credit: caniuse.com/mdn-api_dat…
In addition to having the setData method, the Transmat class also contains a getData method for retrieving the saved data. The getData method supports a string parameter type that represents the type of the data. Before fetching the data, the hasType method is called to determine whether there is data of that type. If there are, the data for that type is retrieved through the getData method of the dataTransfer object.
// src/transmat.ts
getData(type: string) :string | undefined {
return this.hasType(type)?this.dataTransfer.getData(normalizeType(type))
: undefined;
}
Copy the code
In addition, before the getData method is called, the normalizeType function is called to standardize the type parameter passed in. The details are as follows:
// src/data_transfer.ts
export function normalizeType(input: string) {
const result = input.toLowerCase();
switch (result) {
case 'text':
return 'text/plain';
case 'url':
return 'text/uri-list';
default:
returnresult; }}Copy the code
Similarly, let’s look at the compatibility of the datatransfer.getData method:
(Photo credit: caniuse.com/mdn-api_dat…
Ok, so that’s the core setData and getData methods of Transmat. Next, let’s introduce another class, TransmatObserver.
3.3 TransmatObserver class
The Purpose of the TransmatObserver class is to help us respond to the user’s drag-and-drop behavior, which can be used to highlight the drop area during drag-and-drop. For example, in the previous example, we achieved the highlight effect of the drop area by:
const obs = new TransmatObserver((entries) = > {
for (const entry of entries) {
const transmat = new Transmat(entry.event);
if (transmat.hasType("application/json")) {
entry.target.classList.toggle("drag-active", entry.isActive);
entry.target.classList.toggle("drag-over", entry.isTarget); }}}); obs.observe(target);Copy the code
Again, let’s first examine the TransmatObserver constructor:
// src/transmat_observer.ts
export class TransmatObserver {
private readonly targets = new Set<Element>(); // The set of objects to observe
private prevRecords: ReadonlyArray<TransmatObserverEntry> = []; // Save the previous record
private removeEventListeners = () = > {};
constructor(private readonly callback: TransmatObserverCallback){}}Copy the code
The TransmatObserver constructor supports a parameter callback of type TransmatObserverCallback, which is defined as follows:
// src/transmat_observer.ts
export type TransmatObserverCallback = (entries: ReadonlyArray
, observer: TransmatObserver
) = > void;
Copy the code
The TransmatObserverCallback function type takes two parameters: Entries and observer. The type of the entries parameter is one
ReadonlyArray. Each item in the array is of type TransmatObserverEntry. The corresponding type is defined as follows:
// src/transmat_observer.ts
export interface TransmatObserverEntry {
target: Element;
/** type DataTransferEvent = DragEvent | ClipboardEvent */
event: DataTransferEvent;
/** Whether a transfer operation is active in this window. */
isActive: boolean;
/** Whether the element is the active target (dragover). */
isTarget: boolean;
}
Copy the code
In the previous Transmat-Target example, after creating a TransmatObserver instance, the instance’s observe method is called and the object to be observed is passed in. The implementation of the observe method is not complicated, as follows:
// src/transmat_observer.ts
observe(target: Element) {
/** private readonly targets = new Set
(); * /
this.targets.add(target);
if (this.targets.size === 1) {
this.addEventListeners(); }}Copy the code
Inside the Observe method, the element to be observed is saved to the Targets Set Set. When the size of the Targets collection is equal to 1, the current instance’s addEventListeners method is called to add the event listeners:
// src/transmat_observer.ts
private addEventListeners() {
const listener = this.onTransferEvent as EventListener;
this.removeEventListeners = addEventListeners(
document['dragover'.'dragend'.'dragleave'.'drop'],
listener,
true
);
}
Copy the code
Inside the private addEventListeners method, the addEventListeners function we described earlier is used to batch add drag-and-drop event listeners to document elements. The corresponding events are described as follows:
dragover
: Fired when an element or selected text is dragged over a releasable target;dragend
: Triggered when the drag operation is over (e.g. release of the mouse button);dragleave
: Fired when dragging an element or selected text away from a releasable target;drop
: Fired when an element or selected text is released on a releasable target.
There are more than four drag-and-drop related events. If you are interested in the full event, read the HTML Drag-and-drop API on MDN. Let’s focus on the onTransferEvent event listener:
private onTransferEvent = (event: DataTransferEvent) = > {
const records: TransmatObserverEntry[] = [];
for (const target of this.targets) {
// When the cursor leaves the browser, the corresponding event is dispatched to the body or HTML node
const isLeavingDrag =
event.type === 'dragleave' &&
(event.target === document.body ||
event.target === document.body.parentElement);
// Whether there is drag and drop on the page
// Triggers the dragend event when the drag operation ends
// Raises the drop event when an element or selected text is released on a releasable target
constisActive = event.type ! = ='drop'&& event.type ! = ='dragend' && !isLeavingDrag;
// Determine whether the dragable element has been dragged to the target element
const isTargetNode = target.contains(event.target as Node);
const isTarget = isActive && isTargetNode
&& event.type === 'dragover';
records.push({
target,
event,
isActive,
isTarget,
});
}
// The callback function is called only when the record has changed
if(! entryStatesEqual(records,this.prevRecords)) {
this.prevRecords = records as ReadonlyArray<TransmatObserverEntry>;
this.callback(records, this); }}Copy the code
In the above code, the node.contains(otherNode) method is used to determine whether a draggable element is being dragged onto the target element. Return true if otherNode is a descendant of node or node itself, false otherwise. In addition, to avoid firing the callback function frequently, the entryStatesEqual function is called to detect whether the record has changed before the callback function is called. The implementation of the entryStatesEqual function is relatively simple, as follows:
// src/transmat_observer.ts
function entryStatesEqual(a: ReadonlyArray
, b: ReadonlyArray
) :boolean {
if(a.length ! == b.length) {return false;
}
// If one of the entries does not match, return false immediately.
return a.every((av, index) = > {
const bv = b[index];
return av.isActive === bv.isActive && av.isTarget === bv.isTarget;
});
}
Copy the code
Like the MutationObserver, the TransmatObserver provides a takeRecords method for retrieving the most recently triggered records and a Disconnect method for “disconnecting” the connection:
// Return the most recently triggered record
takeRecords() {
return this.prevRecords;
}
// Remove all target and event listeners
disconnect() {
this.targets.clear();
this.removeEventListeners();
}
Copy the code
The Transmat source code analysis is covered here, but if you are interested in the project, you can read the full source code for yourself. This project is developed in TypeScript. Those who have already started TypeScript can use this project to consolidate their knowledge of TS and OOP object-oriented design.
Four,
This article introduces the application scenario, usage mode and related source code of Google Transmat open source project. In the source code analysis section, we reviewed drag-and-drop related events and the DataTransfer API. In addition, we’ve analyzed the TransmatObserver class that helps us respond to the user’s drag-and-drop behavior. Hopefully, after analyzing this class, you’ll have a better understanding of the MutationObserver API. At the same time, in the future work, if you encounter similar scenarios, you can refer to TransmatObserver class to implement their own Observer class.
While a custom payload (custom JSON data) is useful for communication between applications that you control, it also limits the ability to transfer data to external applications. To solve this problem, consider using the lightweight JSON-LD (Linked Data) Data format, which corresponds to the MIME type ‘Application/LD + JSON’. With this data format, you can better organize and link data to create better Web applications. If you are interested in this Data format and want to learn more about JSON-LD (Linked Data), you can read this article.
5. Reference Resources
- MDN – MIME type
- MDN — DataTransfer
- MDN – HTML drag-and-drop API
- Who moved my DOM?