FIX: Ensure widget hooks always call the correct instance (#15127)

Widgets instances are ephemeral - they change on every re-render. We always want to notify the 'most recent' widget instance of events. This regressed in 1b9cf1b1 because the touchStart and drag hooks would persist the widget instance from the initial render. This commit switches TouchStart and Drag back to the pattern other events use, so that the most recent instance is always called. The performance benefits of per-element event listeners are retained.
This commit is contained in:
David Taylor 2021-11-29 09:33:40 +00:00 committed by GitHub
parent a5fbb90df4
commit 0ab57975a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,5 +1,4 @@
/*eslint no-loop-func:0*/
import { bind } from "discourse-common/utils/decorators";
const CLICK_ATTRIBUTE_NAME = "_discourse_click_widget";
const DOUBLE_CLICK_ATTRIBUTE_NAME = "_discourse_double_click_widget";
@ -8,6 +7,7 @@ const MOUSE_DOWN_OUTSIDE_ATTRIBUTE_NAME =
"_discourse_mouse_down_outside_widget";
const KEY_UP_ATTRIBUTE_NAME = "_discourse_key_up_widget";
const KEY_DOWN_ATTRIBUTE_NAME = "_discourse_key_down_widget";
const DRAG_ATTRIBUTE_NAME = "_discourse_drag_widget";
const INPUT_ATTRIBUTE_NAME = "_discourse_input_widget";
const CHANGE_ATTRIBUTE_NAME = "_discourse_change_widget";
const MOUSE_DOWN_ATTRIBUTE_NAME = "_discourse_mouse_down_widget";
@ -15,6 +15,7 @@ const MOUSE_UP_ATTRIBUTE_NAME = "_discourse_mouse_up_widget";
const MOUSE_MOVE_ATTRIBUTE_NAME = "_discourse_mouse_move_widget";
const MOUSE_OVER_ATTRIBUTE_NAME = "_discourse_mouse_over_widget";
const MOUSE_OUT_ATTRIBUTE_NAME = "_discourse_mouse_out_widget";
const TOUCH_START_ATTRIBUTE_NAME = "_discourse_touch_start_widget";
const TOUCH_END_ATTRIBUTE_NAME = "_discourse_touch_end_widget";
class WidgetBaseHook {
@ -69,79 +70,88 @@ export const WidgetTouchEndHook = buildHook(TOUCH_END_ATTRIBUTE_NAME);
// listeners for these events.
// Instead, the WidgetTouchStartHook and WidgetDragHook automatically register listeners on
// the specific widget DOM elements when required.
function touchStartHandler(e) {
return e.currentTarget[TOUCH_START_ATTRIBUTE_NAME].touchStart(e);
}
export class WidgetTouchStartHook extends WidgetBaseHook {
hook(node, propertyName, previousValue) {
node[TOUCH_START_ATTRIBUTE_NAME] = this.widget;
if (!previousValue) {
// Adding to DOM
node.addEventListener("touchstart", this.callback, { passive: false });
// Element added to DOM
node.addEventListener("touchstart", touchStartHandler, {
passive: false,
});
}
}
unhook(node, propertyName, newValue) {
if (!newValue) {
node.removeEventListener("touchstart", this.callback);
// Element removed from DOM
node.removeEventListener("touchstart", touchStartHandler);
}
}
@bind
callback(e) {
this.widget.touchStart(e);
}
}
let _currentlyDraggingHook;
let _currentlyDraggingElement;
function dragStart(e) {
e.preventDefault();
e.stopPropagation();
if (_currentlyDraggingElement) {
dragEnd();
}
_currentlyDraggingElement = e.currentTarget;
document.body.classList.add("widget-dragging");
document.addEventListener("touchmove", drag, { passive: false });
document.addEventListener("mousemove", drag, { passive: false });
document.addEventListener("touchend", dragEnd);
document.addEventListener("mouseup", dragEnd);
}
function drag(e) {
const widget = _currentlyDraggingElement[DRAG_ATTRIBUTE_NAME];
if (event.type === "mousemove") {
widget.drag(e);
} else {
const tt = e.targetTouches[0];
e.preventDefault();
e.stopPropagation();
widget.drag(tt);
}
}
function dragEnd(e) {
document.body.classList.remove("widget-dragging");
document.removeEventListener("touchmove", drag);
document.removeEventListener("mousemove", drag);
document.removeEventListener("touchend", dragEnd);
document.removeEventListener("mouseup", dragEnd);
const widget = _currentlyDraggingElement[DRAG_ATTRIBUTE_NAME];
widget.dragEnd(e);
_currentlyDraggingElement = null;
}
export class WidgetDragHook extends WidgetBaseHook {
hook(node, propertyName, previousValue) {
node[DRAG_ATTRIBUTE_NAME] = this.widget;
if (!previousValue) {
// Adding to DOM
node.addEventListener("touchstart", this.startDrag, { passive: false });
node.addEventListener("mousedown", this.startDrag, { passive: false });
node.addEventListener("touchstart", dragStart, { passive: false });
node.addEventListener("mousedown", dragStart, { passive: false });
}
}
unhook(node, propertyName, newValue) {
if (!newValue) {
// Removing from DOM
node.removeEventListener("touchstart", this.startDrag);
node.removeEventListener("mousedown", this.startDrag);
if (_currentlyDraggingElement === node) {
dragEnd();
}
node.removeEventListener("touchstart", dragStart);
node.removeEventListener("mousedown", dragStart);
}
}
@bind
startDrag(e) {
e.preventDefault();
e.stopPropagation();
_currentlyDraggingHook?.dragEnd();
_currentlyDraggingHook = this;
document.body.classList.add("widget-dragging");
document.addEventListener("touchmove", this.drag, { passive: false });
document.addEventListener("mousemove", this.drag, { passive: false });
document.addEventListener("touchend", this.dragEnd);
document.addEventListener("mouseup", this.dragEnd);
}
@bind
drag(e) {
if (event.type === "mousemove") {
this.widget.drag(e);
} else {
const tt = e.targetTouches[0];
e.preventDefault();
e.stopPropagation();
this.widget.drag(tt);
}
}
@bind
dragEnd(e) {
document.body.classList.remove("widget-dragging");
document.removeEventListener("touchmove", this.drag);
document.removeEventListener("mousemove", this.drag);
document.removeEventListener("touchend", this.dragEnd);
document.removeEventListener("mouseup", this.dragEnd);
this.widget.dragEnd(e);
_currentlyDraggingHook = null;
}
}
function nodeCallback(node, attrName, cb, options = { rerender: true }) {