Skip to content

Commit 7df9d39

Browse files
authored
Improve performance of drag & drop, esp. in Chrome (#502)
1 parent 578b0a3 commit 7df9d39

File tree

3 files changed

+147
-98
lines changed

3 files changed

+147
-98
lines changed

packages/dragdrop/src/index.ts

+94-24
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,7 @@ export class Drag implements IDisposable {
231231
return;
232232
}
233233
let style = this.dragImage.style;
234-
style.top = `${clientY}px`;
235-
style.left = `${clientX}px`;
234+
style.transform = `translate(${clientX}px, ${clientY}px)`;
236235
}
237236

238237
/**
@@ -371,7 +370,7 @@ export class Drag implements IDisposable {
371370
let prevElem = this._currentElement;
372371

373372
// Find the current indicated element at the given position.
374-
let currElem = this.document.elementFromPoint(event.clientX, event.clientY);
373+
let currElem = Private.findElementBehidBackdrop(event, this.document);
375374

376375
// Update the current element reference.
377376
this._currentElement = currElem;
@@ -415,8 +414,7 @@ export class Drag implements IDisposable {
415414
let style = this.dragImage.style;
416415
style.pointerEvents = 'none';
417416
style.position = 'fixed';
418-
style.top = `${clientY}px`;
419-
style.left = `${clientX}px`;
417+
style.transform = `translate(${clientX}px, ${clientY}px)`;
420418
const body =
421419
this.document instanceof Document
422420
? this.document.body
@@ -789,25 +787,8 @@ export namespace Drag {
789787
cursor: string,
790788
doc: Document | ShadowRoot = document
791789
): IDisposable {
792-
let id = ++overrideCursorID;
793-
const body =
794-
doc instanceof Document
795-
? doc.body
796-
: (doc.firstElementChild as HTMLElement);
797-
body.style.cursor = cursor;
798-
body.classList.add('lm-mod-override-cursor');
799-
return new DisposableDelegate(() => {
800-
if (id === overrideCursorID) {
801-
body.style.cursor = '';
802-
body.classList.remove('lm-mod-override-cursor');
803-
}
804-
});
790+
return Private.overrideCursor(cursor, doc);
805791
}
806-
807-
/**
808-
* The internal id for the active cursor override.
809-
*/
810-
let overrideCursorID = 0;
811792
}
812793

813794
/**
@@ -851,6 +832,32 @@ namespace Private {
851832
distance: number;
852833
}
853834

835+
/**
836+
* Find the event target using pointer position.
837+
*/
838+
export function findElementBehidBackdrop(
839+
event: PointerEvent,
840+
root: Document | ShadowRoot = document
841+
) {
842+
// Check if we already cached element for this event.
843+
if (lastElementSearch && event == lastElementSearch.event) {
844+
return lastElementSearch.element;
845+
}
846+
Private.cursorBackdrop.style.zIndex = '-1000';
847+
const element: Element | null = root.elementFromPoint(
848+
event.clientX,
849+
event.clientY
850+
);
851+
Private.cursorBackdrop.style.zIndex = '';
852+
lastElementSearch = { event, element };
853+
return element;
854+
}
855+
856+
let lastElementSearch: {
857+
event: PointerEvent;
858+
element: Element | null;
859+
} | null = null;
860+
854861
/**
855862
* Find the drag scroll target under the mouse, if any.
856863
*/
@@ -860,7 +867,7 @@ namespace Private {
860867
let y = event.clientY;
861868

862869
// Get the element under the mouse.
863-
let element: Element | null = document.elementFromPoint(x, y);
870+
let element: Element | null = findElementBehidBackdrop(event);
864871

865872
// Search for a scrollable target based on the mouse position.
866873
// The null assert in third clause of for-loop is required due to:
@@ -1211,4 +1218,67 @@ namespace Private {
12111218
'link-move': actionTable['link'] | actionTable['move'],
12121219
all: actionTable['copy'] | actionTable['link'] | actionTable['move']
12131220
};
1221+
1222+
/**
1223+
* Implementation of `Drag.overrideCursor`.
1224+
*/
1225+
export function overrideCursor(
1226+
cursor: string,
1227+
doc: Document | ShadowRoot = document
1228+
): IDisposable {
1229+
let id = ++overrideCursorID;
1230+
const body =
1231+
doc instanceof Document
1232+
? doc.body
1233+
: (doc.firstElementChild as HTMLElement);
1234+
if (!cursorBackdrop.isConnected) {
1235+
body.appendChild(cursorBackdrop);
1236+
document.addEventListener('pointermove', alignBackdrop, {
1237+
capture: true,
1238+
passive: true
1239+
});
1240+
}
1241+
cursorBackdrop.style.cursor = cursor;
1242+
return new DisposableDelegate(() => {
1243+
if (id === overrideCursorID && cursorBackdrop.isConnected) {
1244+
document.removeEventListener('pointermove', alignBackdrop, true);
1245+
body.removeChild(cursorBackdrop);
1246+
}
1247+
});
1248+
}
1249+
1250+
/**
1251+
* Move cursor backdrop to match cursor position.
1252+
*/
1253+
function alignBackdrop(event: PointerEvent) {
1254+
if (!cursorBackdrop) {
1255+
return;
1256+
}
1257+
cursorBackdrop.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`;
1258+
}
1259+
1260+
/**
1261+
* Create cursor backdrop node.
1262+
*/
1263+
function createCursorBackdrop(): HTMLElement {
1264+
const backdrop = document.createElement('div');
1265+
backdrop.classList.add('lm-cursor-backdrop');
1266+
return backdrop;
1267+
}
1268+
1269+
/**
1270+
* The internal id for the active cursor override.
1271+
*/
1272+
let overrideCursorID = 0;
1273+
1274+
/**
1275+
* A backdrop node overriding pointer cursor.
1276+
*
1277+
* #### Notes
1278+
* We use a backdrop node rather than setting the cursor directly on the body
1279+
* because setting it on body requires more extensive style recalculation for
1280+
* reliable application of the cursor, this is the cursor not being overriden
1281+
* when over child elements with another style like `cursor: other!important`.
1282+
*/
1283+
export const cursorBackdrop: HTMLElement = createCursorBackdrop();
12141284
}

packages/dragdrop/style/index.css

+12-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212
| The full license is in the file LICENSE, distributed with this software.
1313
|----------------------------------------------------------------------------*/
1414

15-
body.lm-mod-override-cursor * {
16-
cursor: inherit !important;
15+
.lm-cursor-backdrop {
16+
position: fixed;
17+
width: 200px;
18+
height: 200px;
19+
margin-top: -100px;
20+
margin-left: -100px;
21+
will-change: transform;
22+
z-index: 100;
23+
}
24+
25+
.lm-mod-drag-image {
26+
will-change: transform;
1727
}

packages/dragdrop/tests/src/index.spec.ts

+41-72
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,7 @@ describe('@lumino/dragdrop', () => {
215215
dragImage.style.minWidth = '10px';
216216
let drag = new Drag({ mimeData: new MimeData(), dragImage });
217217
drag.start(10, 20);
218-
expect(dragImage.style.top).to.equal('20px');
219-
expect(dragImage.style.left).to.equal('10px');
218+
expect(dragImage.style.transform).to.equal(`translate(10px, 20px)`);
220219
drag.dispose();
221220
});
222221

@@ -308,8 +307,9 @@ describe('@lumino/dragdrop', () => {
308307
})
309308
);
310309
let image = drag.dragImage!;
311-
expect(image.style.top).to.equal(`${rect.top + 1}px`);
312-
expect(image.style.left).to.equal(`${rect.left + 1}px`);
310+
expect(image.style.transform).to.equal(
311+
`translate(${rect.left + 1}px, ${rect.top + 1}px)`
312+
);
313313
});
314314
});
315315

@@ -554,93 +554,62 @@ describe('@lumino/dragdrop', () => {
554554
});
555555

556556
describe('.overrideCursor()', () => {
557-
it('should update the body `cursor` style', () => {
558-
expect(document.body.style.cursor).to.equal('');
559-
let override = Drag.overrideCursor('wait');
560-
expect(document.body.style.cursor).to.equal('wait');
561-
override.dispose();
562-
});
563-
564-
it('should add the `lm-mod-override-cursor` class to the body', () => {
565-
expect(
566-
document.body.classList.contains('lm-mod-override-cursor')
567-
).to.equal(false);
568-
let override = Drag.overrideCursor('wait');
569-
expect(
570-
document.body.classList.contains('lm-mod-override-cursor')
571-
).to.equal(true);
572-
override.dispose();
573-
});
574-
575-
it('should clear the override when disposed', () => {
576-
expect(document.body.style.cursor).to.equal('');
557+
it('should attach a backdrop with `cursor` style', () => {
558+
expect(document.querySelector('.lm-cursor-backdrop')).to.equal(null);
577559
let override = Drag.overrideCursor('wait');
578-
expect(document.body.style.cursor).to.equal('wait');
560+
const backdrop = document.querySelector(
561+
'.lm-cursor-backdrop'
562+
) as HTMLElement;
563+
expect(backdrop.style.cursor).to.equal('wait');
579564
override.dispose();
580-
expect(document.body.style.cursor).to.equal('');
581565
});
582566

583-
it('should remove the `lm-mod-override-cursor` class when disposed', () => {
584-
expect(
585-
document.body.classList.contains('lm-mod-override-cursor')
586-
).to.equal(false);
567+
it('should detach the backdrop when disposed', () => {
568+
expect(document.querySelector('.lm-cursor-backdrop')).to.equal(null);
587569
let override = Drag.overrideCursor('wait');
588-
expect(
589-
document.body.classList.contains('lm-mod-override-cursor')
590-
).to.equal(true);
570+
expect(document.querySelector('.lm-cursor-backdrop')).to.not.equal(
571+
null
572+
);
591573
override.dispose();
592-
expect(
593-
document.body.classList.contains('lm-mod-override-cursor')
594-
).to.equal(false);
574+
expect(document.querySelector('.lm-cursor-backdrop')).to.equal(null);
595575
});
596576

597577
it('should respect the most recent override', () => {
598-
expect(document.body.style.cursor).to.equal('');
599-
expect(
600-
document.body.classList.contains('lm-mod-override-cursor')
601-
).to.equal(false);
602578
let one = Drag.overrideCursor('wait');
603-
expect(document.body.style.cursor).to.equal('wait');
604-
expect(
605-
document.body.classList.contains('lm-mod-override-cursor')
606-
).to.equal(true);
579+
const backdrop = document.querySelector(
580+
'.lm-cursor-backdrop'
581+
) as HTMLElement;
582+
expect(backdrop.style.cursor).to.equal('wait');
583+
expect(backdrop.isConnected).to.equal(true);
607584
let two = Drag.overrideCursor('default');
608-
expect(document.body.style.cursor).to.equal('default');
609-
expect(
610-
document.body.classList.contains('lm-mod-override-cursor')
611-
).to.equal(true);
585+
expect(backdrop.style.cursor).to.equal('default');
586+
expect(backdrop.isConnected).to.equal(true);
612587
let three = Drag.overrideCursor('cell');
613-
expect(document.body.style.cursor).to.equal('cell');
614-
expect(
615-
document.body.classList.contains('lm-mod-override-cursor')
616-
).to.equal(true);
588+
expect(backdrop.style.cursor).to.equal('cell');
589+
expect(backdrop.isConnected).to.equal(true);
617590
two.dispose();
618-
expect(document.body.style.cursor).to.equal('cell');
619-
expect(
620-
document.body.classList.contains('lm-mod-override-cursor')
621-
).to.equal(true);
591+
expect(backdrop.style.cursor).to.equal('cell');
592+
expect(backdrop.isConnected).to.equal(true);
622593
one.dispose();
623-
expect(document.body.style.cursor).to.equal('cell');
624-
expect(
625-
document.body.classList.contains('lm-mod-override-cursor')
626-
).to.equal(true);
594+
expect(backdrop.style.cursor).to.equal('cell');
595+
expect(backdrop.isConnected).to.equal(true);
627596
three.dispose();
628-
expect(document.body.style.cursor).to.equal('');
629-
expect(
630-
document.body.classList.contains('lm-mod-override-cursor')
631-
).to.equal(false);
597+
expect(backdrop.isConnected).to.equal(false);
632598
});
633599

634-
it('should override the computed cursor for a node', () => {
635-
let div = document.createElement('div');
636-
div.style.cursor = 'cell';
637-
document.body.appendChild(div);
638-
expect(window.getComputedStyle(div).cursor).to.equal('cell');
600+
it('should move backdrop with pointer', () => {
639601
let override = Drag.overrideCursor('wait');
640-
expect(window.getComputedStyle(div).cursor).to.equal('wait');
602+
const backdrop = document.querySelector(
603+
'.lm-cursor-backdrop'
604+
) as HTMLElement;
605+
document.dispatchEvent(
606+
new PointerEvent('pointermove', {
607+
clientX: 100,
608+
clientY: 500
609+
})
610+
);
611+
expect(backdrop.style.transform).to.equal('translate(100px, 500px)');
641612
override.dispose();
642-
expect(window.getComputedStyle(div).cursor).to.equal('cell');
643-
document.body.removeChild(div);
644613
});
645614
});
646615
});

0 commit comments

Comments
 (0)