diff --git a/.github/workflows/run-e2e.yml b/.github/workflows/run-e2e.yml
new file mode 100644
index 00000000..d11c7211
--- /dev/null
+++ b/.github/workflows/run-e2e.yml
@@ -0,0 +1,21 @@
+name: Run tests
+on: [push, pull_request]
+jobs:
+ unit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2.1.5
+ with:
+ node-version: 14.15.4
+ - name: install
+ run: npm install
+ working-directory: ./packages/axes
+ - name: test
+ run: npm run coverage
+ working-directory: ./packages/axes
+ - name: Coveralls GitHub Action
+ uses: coverallsapp/github-action@v1.1.2
+ with:
+ path-to-lcov: ./packages/axes/coverage/lcov.info
+ github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/lerna.json b/lerna.json
index 19cfa8cc..823a83bd 100644
--- a/lerna.json
+++ b/lerna.json
@@ -10,7 +10,7 @@
{
"basePath": "packages/axes/dist",
"dists": [
- "demo/dist"
+ "packages/demo/dist"
]
}
],
diff --git a/package.json b/package.json
index 10ededef..2fe9a50f 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"description": "A module used to change the information of user action entered by various input devices such as touch screen or mouse into the logical virtual coordinates. You can easily create a UI that responds to user actions.",
"private": true,
"scripts": {
- "lint": "eslint ./packages/axes/src/**/*.ts",
+ "lint": "eslint ./packages/axes/src/**/*.ts",
"packages": "npm run packages:update && npm run packages:build && npm run packages:publish",
"packages:update": "lerna-helper version",
"packages:build": "lerna run build --ignore demo",
@@ -11,8 +11,8 @@
"docs:build": "rm -rf ./packages/demo/docs/api && jsdoc-to-mdx -c ./jsdoc-to-mdx.json",
"demo:build": "npm run docs:build && npm run build --prefix packages/demo",
"demo:build-docusaurus": "npm run build --prefix demo",
- "demo:deploy": "lerna-helper deploy --base @egjs/axes --remote upstream",
- "demo:deploy-origin": "lerna-helper deploy --base @egjs/axes --remote origin",
+ "demo:deploy": "lerna-helper deploy --base @egjs/axes --src packages/demo/build/ --remote upstream",
+ "demo:deploy-origin": "lerna-helper deploy --base @egjs/axes --src packages/demo/build/ --remote origin",
"release": "lerna-helper release --base @egjs/axes --remote upstream --branch master",
"prepush": "npm run lint",
"commitmsg": "node config/validate-commit-msg.js"
@@ -33,16 +33,16 @@
"jsdoc-to-mdx": "^1.1.2",
"lerna": "^5.1.4",
"typescript": "^4.6.2",
- "eslint": "^7.32.0",
- "eslint-config-prettier": "^8.3.0",
- "eslint-plugin-import": "^2.25.4",
- "eslint-plugin-jsdoc": "^37.8.0",
- "eslint-plugin-prefer-arrow": "^1.2.3",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-react": "^7.22.0",
- "@typescript-eslint/eslint-plugin": "^5.11.0",
- "@typescript-eslint/eslint-plugin-tslint": "^5.11.0",
- "@typescript-eslint/parser": "^5.11.0"
+ "eslint": "^7.32.0",
+ "eslint-config-prettier": "^8.3.0",
+ "eslint-plugin-import": "^2.25.4",
+ "eslint-plugin-jsdoc": "^37.8.0",
+ "eslint-plugin-prefer-arrow": "^1.2.3",
+ "eslint-plugin-prettier": "^4.0.0",
+ "eslint-plugin-react": "^7.22.0",
+ "@typescript-eslint/eslint-plugin": "^5.11.0",
+ "@typescript-eslint/eslint-plugin-tslint": "^5.11.0",
+ "@typescript-eslint/parser": "^5.11.0"
},
"workspaces": {
"packages": [
diff --git a/packages/axes/src/InputObserver.ts b/packages/axes/src/InputObserver.ts
index edf8f2b6..d4724ea5 100644
--- a/packages/axes/src/InputObserver.ts
+++ b/packages/axes/src/InputObserver.ts
@@ -68,7 +68,13 @@ export class InputObserver implements InputTypeObserver {
this._moveDistance = this._axisManager.get(input.axes);
}
- public change(input: InputType, event, offset: Axis, useAnimation?: boolean) {
+ public change(
+ input: InputType,
+ event,
+ offset: Axis,
+ useAnimation?: boolean,
+ velocity?: number[]
+ ) {
if (
this._isStopped ||
!this._interruptManager.isInterrupting() ||
@@ -80,7 +86,10 @@ export class InputObserver implements InputTypeObserver {
if (nativeEvent.__childrenAxesAlreadyChanged) {
return;
}
- let depaPos: Axis = this._moveDistance || this._axisManager.get(input.axes);
+ let depaPos: Axis =
+ !this._moveDistance || velocity
+ ? this._axisManager.get(input.axes)
+ : this._moveDistance;
let destPos: Axis;
// for outside logic
@@ -113,8 +122,7 @@ export class InputObserver implements InputTypeObserver {
event,
};
if (useAnimation) {
- const duration = this._animationManager.getDuration(destPos, depaPos);
- this._animationManager.animateTo(destPos, duration, changeOption);
+ this._animationManager.changeTo(destPos, changeOption);
} else {
const isCanceled = !this._eventManager.triggerChange(
destPos,
@@ -126,6 +134,13 @@ export class InputObserver implements InputTypeObserver {
this._isStopped = true;
this._moveDistance = null;
this._animationManager.finish(false);
+ } else if (velocity) {
+ const displacement = this._animationManager.getDisplacement(velocity);
+ const nextOffset = toAxis(input.axes, displacement);
+ destPos = this._atOutside(
+ map(destPos, (v, k) => v + (nextOffset[k] || 0))
+ );
+ this._animationManager.changeTo(destPos, changeOption);
}
}
}
@@ -181,6 +196,7 @@ export class InputObserver implements InputTypeObserver {
destPos,
duration,
delta: this._axisManager.getDelta(depaPos, destPos),
+ triggerAnimationEvent: true,
inputEvent: event,
input,
isTrusted: true,
diff --git a/packages/axes/src/animation/AnimationManager.ts b/packages/axes/src/animation/AnimationManager.ts
index 90024d2b..77fba493 100644
--- a/packages/axes/src/animation/AnimationManager.ts
+++ b/packages/axes/src/animation/AnimationManager.ts
@@ -111,12 +111,10 @@ export abstract class AnimationManager {
if (!every(pos, (v, k) => orgPos[k] === v)) {
this.eventManager.triggerChange(pos, orgPos, option, !!option);
}
- this._animateParam = null;
- if (this._raf) {
- cancelAnimationFrame(this._raf);
+ if (this._animateParam.triggerAnimationEvent) {
+ this.eventManager.triggerAnimationEnd(!!option?.event);
}
- this._raf = null;
- this.eventManager.triggerAnimationEnd(!!option?.event);
+ this._removeAnimationParam();
}
}
@@ -189,6 +187,27 @@ export abstract class AnimationManager {
return userWish;
}
+ public changeTo(
+ destPos: Axis,
+ option: ChangeEventOption
+ ): void {
+ const depaPos = this.axisManager.get(option.input.axes);
+ if (this._raf) {
+ cancelAnimationFrame(this._raf);
+ }
+
+ if (!equal(destPos, depaPos)) {
+ const newParam = {
+ depaPos,
+ destPos: destPos,
+ duration: this.getDuration(destPos, depaPos),
+ delta: this.axisManager.getDelta(depaPos, destPos),
+ triggerAnimationEvent: false,
+ };
+ this._animateLoop(newParam, () => this._removeAnimationParam());
+ }
+ }
+
public animateTo(
destPos: Axis,
duration: number,
@@ -226,6 +245,7 @@ export abstract class AnimationManager {
destPos: userWish.destPos,
duration: userWish.duration,
delta: this.axisManager.getDelta(depaPos, userWish.destPos),
+ triggerAnimationEvent: true,
isTrusted: !!inputEvent,
inputEvent,
input: option?.input || null,
@@ -297,6 +317,7 @@ export abstract class AnimationManager {
this._options.maximumDuration
),
delta: this.axisManager.getDelta(depaPos, destPos),
+ triggerAnimationEvent: true,
inputEvent,
input: option?.input || null,
isTrusted: !!inputEvent,
@@ -304,6 +325,14 @@ export abstract class AnimationManager {
};
}
+ private _removeAnimationParam() {
+ this._animateParam = null;
+ if (this._raf) {
+ cancelAnimationFrame(this._raf);
+ }
+ this._raf = null;
+ }
+
private _animateLoop(param: AnimationParam, complete: () => void): void {
if (param.duration) {
this._animateParam = {
diff --git a/packages/axes/src/inputType/InputType.ts b/packages/axes/src/inputType/InputType.ts
index 2c79a32d..73f7117d 100644
--- a/packages/axes/src/inputType/InputType.ts
+++ b/packages/axes/src/inputType/InputType.ts
@@ -29,7 +29,7 @@ export interface InputType {
export interface InputTypeObserver {
options: AxesOption;
get(inputType: InputType): Axis;
- change(inputType: InputType, event, offset: Axis, useAnimation?: boolean);
+ change(inputType: InputType, event, offset: Axis, useAnimation?: boolean, velocity?: number[]);
hold(inputType: InputType, event);
release(
inputType: InputType,
diff --git a/packages/axes/src/inputType/PanInput.ts b/packages/axes/src/inputType/PanInput.ts
index 29c39812..209fdfa0 100644
--- a/packages/axes/src/inputType/PanInput.ts
+++ b/packages/axes/src/inputType/PanInput.ts
@@ -21,7 +21,12 @@ import {
MOUSE_LEFT,
ANY,
} from "../const";
-import { ActiveEvent, ElementType, InputEventType } from "../types";
+import {
+ ActiveEvent,
+ ElementType,
+ ExtendedEvent,
+ InputEventType,
+} from "../types";
import {
convertInputType,
@@ -40,6 +45,7 @@ export interface PanInputOption {
preventClickOnDrag?: boolean;
iOSEdgeSwipeThreshold?: number;
releaseOnScroll?: boolean;
+ useAcceleration?: boolean;
touchAction?: string;
}
@@ -93,8 +99,9 @@ export const getDirectionByAngle = (
* @param {Number} [scale[1]=1] vertical axis scale 수직축 배율
* @param {Number} [thresholdAngle=45] The threshold value that determines whether user action is horizontal or vertical (0~90) 사용자의 동작이 가로 방향인지 세로 방향인지 판단하는 기준 각도(0~90)
* @param {Number} [threshold=0] Minimal pan distance required before recognizing 사용자의 Pan 동작을 인식하기 위해산 최소한의 거리
- * @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 사용자가 1픽셀 이상 드래그를 마쳤을 때 {@link https://developer.mozilla.org/ko/docs/Web/API/Element/click_event click} 이벤트 취소 여부
+ * @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 threshold 사용자가 threshold 이상 드래그를 마쳤을 때 {@link https://developer.mozilla.org/ko/docs/Web/API/Element/click_event click} 이벤트 취소 여부
* @param {Number} [iOSEdgeSwipeThreshold=30] Area (px) that can go to the next page when swiping the right edge in iOS safari iOS Safari에서 오른쪽 엣지를 스와이프 하는 경우 다음 페이지로 넘어갈 수 있는 영역(px)
+ * @param {Boolean} [useAcceleration=false] Whether to apply the dragging speed to coordinate changes. 사용자의 드래그 속도를 좌표 변화에 반영할지 여부
* @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. 엘리먼트의 "touch-action" CSS 속성을 덮어쓰는 값. 만약 null로 설정된 경우, 연결된 축 방향으로의 스크롤을 방지하게끔 자동으로 설정된다.
**/
/**
@@ -150,6 +157,7 @@ export class PanInput implements InputType {
preventClickOnDrag: false,
iOSEdgeSwipeThreshold: IOS_EDGE_THRESHOLD,
releaseOnScroll: false,
+ useAcceleration: false,
touchAction: null,
...options,
};
@@ -267,6 +275,7 @@ export class PanInput implements InputType {
const {
iOSEdgeSwipeThreshold,
releaseOnScroll,
+ useAcceleration,
inputKey,
inputButton,
threshold,
@@ -339,9 +348,18 @@ export class PanInput implements InputType {
}
panEvent.preventSystemEvent = prevent;
if (prevent && (this._isOverThreshold || distance >= threshold)) {
+ const velocity = useAcceleration
+ ? this._getVelocity(panEvent)
+ : undefined;
this._dragged = true;
this._isOverThreshold = true;
- this._observer.change(this, panEvent, toAxis(this.axes, offset));
+ this._observer.change(
+ this,
+ panEvent,
+ toAxis(this.axes, offset),
+ false,
+ velocity
+ );
}
activeEvent.prevEvent = panEvent;
}
@@ -356,16 +374,7 @@ export class PanInput implements InputType {
this._detachWindowEvent(activeEvent);
clearTimeout(this._rightEdgeTimer);
const prevEvent = activeEvent.prevEvent;
- const velocity = this._isOverThreshold ? this._getOffset(
- [
- Math.abs(prevEvent.velocityX) * (prevEvent.offsetX < 0 ? -1 : 1),
- Math.abs(prevEvent.velocityY) * (prevEvent.offsetY < 0 ? -1 : 1),
- ],
- [
- useDirection(DIRECTION_HORIZONTAL, this._direction),
- useDirection(DIRECTION_VERTICAL, this._direction),
- ]
- ) : [0, 0];
+ const velocity = this._getVelocity(prevEvent);
activeEvent.onRelease();
this._observer.release(this, prevEvent, velocity);
}
@@ -396,6 +405,21 @@ export class PanInput implements InputType {
];
}
+ private _getVelocity(event: ExtendedEvent): number[] {
+ return this._isOverThreshold
+ ? this._getOffset(
+ [
+ Math.abs(event.velocityX) * (event.offsetX < 0 ? -1 : 1),
+ Math.abs(event.velocityY) * (event.offsetY < 0 ? -1 : 1),
+ ],
+ [
+ useDirection(DIRECTION_HORIZONTAL, this._direction),
+ useDirection(DIRECTION_VERTICAL, this._direction),
+ ]
+ )
+ : [0, 0];
+ }
+
private _getDistance(delta: number[], direction: boolean[]): number {
return Math.sqrt(
Number(direction[0]) * Math.pow(delta[0], 2) +
@@ -432,7 +456,11 @@ export class PanInput implements InputType {
const element = this.element;
if (element) {
if (this.options.preventClickOnDrag) {
- element.removeEventListener("click", this._preventClickWhenDragged, true);
+ element.removeEventListener(
+ "click",
+ this._preventClickWhenDragged,
+ true
+ );
}
activeEvent?.start.forEach((event) => {
element.removeEventListener(event, this._onPanstart);
diff --git a/packages/axes/src/types.ts b/packages/axes/src/types.ts
index 64e18172..e0c3f603 100644
--- a/packages/axes/src/types.ts
+++ b/packages/axes/src/types.ts
@@ -47,6 +47,7 @@ export interface AnimationParam {
destPos: Axis;
duration: number;
delta: Axis;
+ triggerAnimationEvent: boolean;
isTrusted?: boolean;
stop?: () => void;
setTo?: (
diff --git a/packages/axes/test/unit/Axes.spec.js b/packages/axes/test/unit/Axes.spec.js
index 74acd015..10ec3338 100644
--- a/packages/axes/test/unit/Axes.spec.js
+++ b/packages/axes/test/unit/Axes.spec.js
@@ -456,7 +456,7 @@ describe("Axes", () => {
});
});
- describe("Nested Axes Test", () => {
+ describe("nested", () => {
beforeEach(() => {
inst = new Axes({
x: {
@@ -600,6 +600,7 @@ describe("Axes", () => {
});
input = new MockPanInputInjector.PanInput(el, {
iOSEdgeSwipeThreshold,
+ inputKey: ["any"],
inputType: ["touch"],
});
inst
@@ -649,8 +650,8 @@ describe("Axes", () => {
// Then
// for test animation event
setTimeout(() => {
- const releaseEvent = releaseHandler.getCall(0).args[0];
expect(releaseHandler.calledOnce).to.be.true;
+ const releaseEvent = releaseHandler.getCall(0).args[0];
// expect(releaseEvent.inputEvent.isFinal).to.be.false;
expect(releaseEvent.isTrusted).to.be.true;
@@ -678,8 +679,8 @@ describe("Axes", () => {
// Then
// for test animation event
setTimeout(() => {
- const releaseEvent = releaseHandler.getCall(0).args[0];
expect(releaseHandler.calledOnce).to.be.true;
+ const releaseEvent = releaseHandler.getCall(0).args[0];
// expect(releaseEvent.inputEvent.isFinal).to.be.false;
expect(releaseEvent.isTrusted).to.be.true;
diff --git a/packages/axes/test/unit/inputType/PanInput.spec.js b/packages/axes/test/unit/inputType/PanInput.spec.js
index 56ed4960..d3a43d2a 100644
--- a/packages/axes/test/unit/inputType/PanInput.spec.js
+++ b/packages/axes/test/unit/inputType/PanInput.spec.js
@@ -527,6 +527,53 @@ describe("PanInput", () => {
});
});
+ describe("useAcceleration", () => {
+ it(`should apply drag velocity to coordinate change if useAcceleration is true`, (done) => {
+ // Given
+ const animationStart = sinon.spy();
+ const animationEnd = sinon.spy();
+ input = new PanInput(el, {
+ inputType: ["touch", "mouse"],
+ scale: [10, 10],
+ useAcceleration: true,
+ });
+ const result = [];
+ inst = new Axes({
+ x: {
+ range: [0, 3000],
+ },
+ });
+
+ inst.connect(["x"], input);
+ inst.on("animationStart", animationStart);
+ inst.on("animationEnd", animationEnd);
+
+ // When
+ Simulator.gestures.pan(el, {
+ pos: [0, 0],
+ deltaX: 200,
+ duration: 300,
+ easing: "linear",
+ });
+
+ inst.on({
+ change: (e) => {
+ result.push(e.pos.x);
+ },
+ // Then
+ release: () => {
+ // If animation from drag velocity are played in meanwhile, some value of the coordinate will not be divisible by 10
+ expect(result.every((x) => x % 10 === 0)).to.be.equal(false);
+ },
+ finish: () => {
+ expect(animationStart.calledOnce).to.be.equals(true);
+ expect(animationEnd.calledOnce).to.be.equals(true);
+ done();
+ },
+ });
+ });
+ });
+
describe("touchAction", () => {
["auto", "none", "manipulation", "pan-x", "pan-y"].forEach(
(touchAction) => {
diff --git a/packages/axes/test/unit/inputType/WheelInput.spec.js b/packages/axes/test/unit/inputType/WheelInput.spec.js
index 3e8b6c65..5123f707 100644
--- a/packages/axes/test/unit/inputType/WheelInput.spec.js
+++ b/packages/axes/test/unit/inputType/WheelInput.spec.js
@@ -343,7 +343,7 @@ describe("WheelInput", () => {
TestHelper.dispatchWheel(el, { deltaY, shiftKey: true }, () => {
// Then
expect(inst.axisManager.get().x).to.be.equal(10);
- done();
+ done();
});
});
@@ -357,23 +357,12 @@ describe("WheelInput", () => {
TestHelper.dispatchWheel(el, { deltaY }, () => {
// Then
expect(inst.axisManager.get().x).to.be.equal(0);
- done();
+ done();
});
});
});
describe("useAnimation", () => {
- let animationStartHandler;
- let animationEndHandler;
- beforeEach(() => {
- animationStartHandler = sinon.spy();
- animationEndHandler = sinon.spy();
- inst.on({
- animationStart: animationStartHandler,
- animationEnd: animationEndHandler,
- });
- });
-
it("should change coordinate smoothly by animation when useAnimation is true", (done) => {
// Given
const deltaY = 1;
@@ -385,8 +374,6 @@ describe("WheelInput", () => {
// Then
expect(inst.axisManager.get().x).to.be.not.equal(10);
setTimeout(() => {
- expect(animationStartHandler.calledOnce).to.be.true;
- expect(animationEndHandler.calledOnce).to.be.true;
expect(inst.axisManager.get().x).to.be.equal(10);
done();
}, 200);
@@ -403,11 +390,7 @@ describe("WheelInput", () => {
TestHelper.dispatchWheel(el, { deltaY }, () => {
// Then
expect(inst.axisManager.get().x).to.be.equal(10);
- setTimeout(() => {
- expect(animationStartHandler.called).to.be.false;
- expect(animationEndHandler.called).to.be.false;
- done();
- }, 200);
+ done();
});
});
});
diff --git a/packages/demo/docusaurus.config.js b/packages/demo/docusaurus.config.js
index 196c4fb6..86656385 100644
--- a/packages/demo/docusaurus.config.js
+++ b/packages/demo/docusaurus.config.js
@@ -44,6 +44,10 @@ const config = {
})
]
],
+ i18n: {
+ defaultLocale: "en",
+ locales: ["en", "ko"]
+ },
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
@@ -78,6 +82,10 @@ const config = {
label: "Demos",
position: "left"
},
+ {
+ type: "localeDropdown",
+ position: "right"
+ },
{
href: "https://www.npmjs.com/package/@egjs/axes",
className: "header-npm-link",
@@ -132,7 +140,12 @@ const config = {
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme
- }
+ },
+ algolia: {
+ appId: 'XFF4246J3M',
+ apiKey: '637cea2eb4f4992a666d77239ba5cfc2',
+ indexName: 'egjs-axes',
+ },
})
};
diff --git a/packages/demo/src/pages/Options.mdx b/packages/demo/src/pages/Options.mdx
index 9cc682ce..fe81b85c 100644
--- a/packages/demo/src/pages/Options.mdx
+++ b/packages/demo/src/pages/Options.mdx
@@ -293,6 +293,7 @@ Rounding unit. For example, 0.1 rounds to 0.1 decimal point(6.1234 => 6.1), 5 ro
+
### nested
Whether the event propagates to other instances when the coordinates reach the end of the movable area.
diff --git a/packages/demo/src/pages/demos/axesboard.tsx b/packages/demo/src/pages/demos/axesboard.tsx
index 1b9097de..569a78fa 100644
--- a/packages/demo/src/pages/demos/axesboard.tsx
+++ b/packages/demo/src/pages/demos/axesboard.tsx
@@ -63,6 +63,14 @@ export default function AxesBoard({ axis, demoType, options, panInputOptions, pi
const maxHeight = boardHeight - targetHeight;
const xRange = axis && axis.x && axis.x.range && axis.x.range[1];
const yRange = axis && axis.y && axis.y.range && axis.y.range[1];
+
+ window.addEventListener("resize", () => {
+ const board = document.getElementById("board");
+ const newWidth = board.getBoundingClientRect().width;
+ const newHeight = board.getBoundingClientRect().height;
+ setTo({ x: (newWidth - targetWidth) / 2, y: (newHeight - targetHeight) / 2 });
+ });
+
if (isNested) {
const innerBoardWidth = innerBoard.current.getBoundingClientRect().width;
const innerBoardHeight = innerBoard.current.getBoundingClientRect().height;
@@ -122,7 +130,7 @@ export default function AxesBoard({ axis, demoType, options, panInputOptions, pi
}, []);
return options?.nested ? (
-
+
x: {x} y: {y}
@@ -138,7 +146,7 @@ export default function AxesBoard({ axis, demoType, options, panInputOptions, pi
) : (
-
+
x: {x} y: {y}
{ onClick(); }}>