Skip to content

Commit

Permalink
feat: add preventClickOnDrag option to PanInput (#199)
Browse files Browse the repository at this point in the history
* feat: add preventClickOnDrag option to PanInput

* skip: apply review

* skip: apply review

* fix: set preventClick capture to true

* chore: throw error when target element does not exist

* skip: apply review
  • Loading branch information
malangfox committed Aug 30, 2022
1 parent 18b3070 commit b66481d
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 31 deletions.
21 changes: 14 additions & 7 deletions packages/axes/src/inputType/MoveKeyInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface MoveKeyInputOption {
* @example
* ```js
* const moveKey = new eg.Axes.MoveKeyInput("#area", {
* scale: [1, 1]
* scale: [1, 1]
* });
*
* // Connect 'x', 'y' axes when the moveKey is pressed.
Expand Down Expand Up @@ -199,17 +199,24 @@ export class MoveKeyInput implements InputType {
}

private _attachEvent(observer: InputTypeObserver) {
const element = this.element;
if (!element) {
throw new Error("Element to connect input does not exist.");
}
this._observer = observer;
this.element.addEventListener("keydown", this._onKeydown, false);
this.element.addEventListener("keypress", this._onKeydown, false);
this.element.addEventListener("keyup", this._onKeyup, false);
element.addEventListener("keydown", this._onKeydown, false);
element.addEventListener("keypress", this._onKeydown, false);
element.addEventListener("keyup", this._onKeyup, false);
this._enabled = true;
}

private _detachEvent() {
this.element.removeEventListener("keydown", this._onKeydown, false);
this.element.removeEventListener("keypress", this._onKeydown, false);
this.element.removeEventListener("keyup", this._onKeyup, false);
const element = this.element;
if (element) {
element.removeEventListener("keydown", this._onKeydown, false);
element.removeEventListener("keypress", this._onKeydown, false);
element.removeEventListener("keyup", this._onKeyup, false);
}
this._enabled = false;
this._observer = null;
}
Expand Down
43 changes: 35 additions & 8 deletions packages/axes/src/inputType/PanInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface PanInputOption {
scale?: number[];
thresholdAngle?: number;
threshold?: number;
preventClickOnDrag?: boolean;
iOSEdgeSwipeThreshold?: number;
releaseOnScroll?: boolean;
touchAction?: string;
Expand Down Expand Up @@ -77,6 +78,7 @@ export const getDirectionByAngle = (
* @param {Number} [scale[1]=1] vertical axis scale <ko>수직축 배율</ko>
* @param {Number} [thresholdAngle=45] The threshold value that determines whether user action is horizontal or vertical (0~90) <ko>사용자의 동작이 가로 방향인지 세로 방향인지 판단하는 기준 각도(0~90)</ko>
* @param {Number} [threshold=0] Minimal pan distance required before recognizing <ko>사용자의 Pan 동작을 인식하기 위해산 최소한의 거리</ko>
* @param {Boolean} [preventClickOnDrag=false] Whether to cancel the {@link https://developer.mozilla.org/en/docs/Web/API/Element/click_event click} event when the user finishes dragging more than 1 pixel <ko>사용자가 1픽셀 이상 드래그를 마쳤을 때 {@link https://developer.mozilla.org/ko/docs/Web/API/Element/click_event click} 이벤트 취소 여부/ko>
* @param {Number} [iOSEdgeSwipeThreshold=30] Area (px) that can go to the next page when swiping the right edge in iOS safari <ko>iOS Safari에서 오른쪽 엣지를 스와이프 하는 경우 다음 페이지로 넘어갈 수 있는 영역(px)</ko>
* @param {String} [touchAction=null] Value that overrides the element's "touch-action" css property. If set to null, it is automatically set to prevent scrolling in the direction of the connected axis. <ko>엘리먼트의 "touch-action" CSS 속성을 덮어쓰는 값. 만약 null로 설정된 경우, 연결된 축 방향으로의 스크롤을 방지하게끔 자동으로 설정된다.</ko>
**/
Expand Down Expand Up @@ -115,6 +117,7 @@ export class PanInput implements InputType {
private _originalCssProps: { [key: string]: string };
private _atRightEdge = false;
private _rightEdgeTimer = 0;
private _dragged = false;

/**
*
Expand All @@ -127,6 +130,7 @@ export class PanInput implements InputType {
scale: [1, 1],
thresholdAngle: 45,
threshold: 0,
preventClickOnDrag: false,
iOSEdgeSwipeThreshold: IOS_EDGE_THRESHOLD,
releaseOnScroll: false,
touchAction: null,
Expand Down Expand Up @@ -228,6 +232,7 @@ export class PanInput implements InputType {
if (panEvent.srcEvent.cancelable !== false) {
const edgeThreshold = this.options.iOSEdgeSwipeThreshold;

this._dragged = false;
this._observer.hold(this, panEvent);
this._atRightEdge =
IS_IOS_SAFARI && panEvent.center.x > window.innerWidth - edgeThreshold;
Expand Down Expand Up @@ -299,6 +304,7 @@ export class PanInput implements InputType {
}
panEvent.preventSystemEvent = prevent;
if (prevent) {
this._dragged = true;
this._observer.change(this, panEvent, toAxis(this.axes, offset));
}
activeEvent.prevEvent = panEvent;
Expand Down Expand Up @@ -356,32 +362,53 @@ export class PanInput implements InputType {

private _attachElementEvent(observer: InputTypeObserver) {
const activeEvent = convertInputType(this.options.inputType);
const element = this.element;
if (!activeEvent) {
return;
}
if (!element) {
throw new Error("Element to connect input does not exist.");
}
this._observer = observer;
this._enabled = true;
this._activeEvent = activeEvent;
if (this.options.preventClickOnDrag) {
element.addEventListener("click", this._preventClickWhenDragged, true);
}
activeEvent.start.forEach((event) => {
this.element?.addEventListener(event, this._onPanstart);
element.addEventListener(event, this._onPanstart);
});
// adding event listener to element prevents invalid behavior in iOS Safari
activeEvent.move.forEach((event) => {
this.element?.addEventListener(event, this._voidFunction);
element.addEventListener(event, this._voidFunction);
});
}

private _detachElementEvent() {
const activeEvent = this._activeEvent;
activeEvent?.start.forEach((event) => {
this.element?.removeEventListener(event, this._onPanstart);
});
activeEvent?.move.forEach((event) => {
this.element?.removeEventListener(event, this._voidFunction);
});
const element = this.element;
if (element) {
if (this.options.preventClickOnDrag) {
element.removeEventListener("click", this._preventClickWhenDragged, true);
}
activeEvent?.start.forEach((event) => {
element.removeEventListener(event, this._onPanstart);
});
activeEvent?.move.forEach((event) => {
element.removeEventListener(event, this._voidFunction);
});
}
this._enabled = false;
this._observer = null;
}

private _preventClickWhenDragged = (e: PointerEvent | MouseEvent) => {
if (this._dragged) {
e.preventDefault();
e.stopPropagation();
}
this._dragged = false;
};

private _voidFunction = () => {};
}
33 changes: 20 additions & 13 deletions packages/axes/src/inputType/PinchInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface PinchInputOption {
* @example
* ```js
* const pinch = new eg.Axes.PinchInput("#area", {
* scale: 1
* scale: 1
* });
*
* // Connect 'something' axis when two pointers are moving toward (zoom-in) or away from each other (zoom-out).
Expand Down Expand Up @@ -191,34 +191,41 @@ export class PinchInput implements InputType {

private _attachEvent(observer: InputTypeObserver) {
const activeEvent = convertInputType(this.options.inputType);
const element = this.element;
if (!activeEvent) {
return;
}
if (!element) {
throw new Error("Element to connect input does not exist.");
}
this._observer = observer;
this._enabled = true;
this._activeEvent = activeEvent;
activeEvent.start.forEach((event) => {
this.element.addEventListener(event, this._onPinchStart, false);
element.addEventListener(event, this._onPinchStart, false);
});
activeEvent.move.forEach((event) => {
this.element.addEventListener(event, this._onPinchMove, false);
element.addEventListener(event, this._onPinchMove, false);
});
activeEvent.end.forEach((event) => {
this.element.addEventListener(event, this._onPinchEnd, false);
element.addEventListener(event, this._onPinchEnd, false);
});
}

private _detachEvent() {
const activeEvent = this._activeEvent;
activeEvent?.start.forEach((event) => {
this.element.removeEventListener(event, this._onPinchStart, false);
});
activeEvent?.move.forEach((event) => {
this.element.removeEventListener(event, this._onPinchMove, false);
});
activeEvent?.end.forEach((event) => {
this.element.removeEventListener(event, this._onPinchEnd, false);
});
const element = this.element;
if (element) {
activeEvent?.start.forEach((event) => {
element.removeEventListener(event, this._onPinchStart, false);
});
activeEvent?.move.forEach((event) => {
element.removeEventListener(event, this._onPinchMove, false);
});
activeEvent?.end.forEach((event) => {
element.removeEventListener(event, this._onPinchEnd, false);
});
}
this._enabled = false;
this._observer = null;
}
Expand Down
13 changes: 10 additions & 3 deletions packages/axes/src/inputType/WheelInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface WheelInputOption {
* @example
* ```js
* const wheel = new eg.Axes.WheelInput("#area", {
* scale: 1
* scale: 1
* });
*
* // Connect only one 'something1' axis to the vertical mouse wheel.
Expand Down Expand Up @@ -186,13 +186,20 @@ export class WheelInput implements InputType {
}

private _attachEvent(observer: InputTypeObserver) {
const element = this.element;
if (!element) {
throw new Error("Element to connect input does not exist.");
}
this._observer = observer;
this.element.addEventListener("wheel", this._onWheel);
element.addEventListener("wheel", this._onWheel);
this._enabled = true;
}

private _detachEvent() {
this.element.removeEventListener("wheel", this._onWheel);
const element = this.element;
if (element) {
this.element.removeEventListener("wheel", this._onWheel);
}
this._enabled = false;
this._observer = null;

Expand Down
61 changes: 61 additions & 0 deletions packages/axes/test/unit/inputType/PanInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,67 @@ describe("PanInput", () => {
cleanup();
});

describe("preventClickOnDrag", () => {
it("should prevent click event when preventClickOnDrag is true", (done) => {
// Given
const click = sinon.spy();
el.addEventListener("click", click);
input = new PanInput(el, {
inputType: ["touch", "mouse"],
preventClickOnDrag: true,
});
inst.connect(["x", "y"], input);

// When
Simulator.gestures.pan(
el,
{
pos: [0, 0],
deltaX: 50,
deltaY: 50,
duration: 200,
easing: "linear",
},
() => {
el.click();
// Then
expect(click.called).to.be.false;
done();
}
);
});

it("shouldn't bother click event when input is disabled", (done) => {
// Given
const click = sinon.spy();
el.addEventListener("click", click);
input = new PanInput(el, {
inputType: ["touch", "mouse"],
preventClickOnDrag: true,
});
inst.connect(["x", "y"], input);
input.disable();

// When
Simulator.gestures.pan(
el,
{
pos: [0, 0],
deltaX: 50,
deltaY: 50,
duration: 200,
easing: "linear",
},
() => {
el.click();
// Then
expect(click.called).to.be.true;
done();
}
);
});
});

describe("inputButton", () => {
["left", "middle", "right"].forEach((button) => {
it("should check only the button set in inputButton is available", (done) => {
Expand Down

0 comments on commit b66481d

Please sign in to comment.