/*
* Copyright (c) 2015 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import Axes, { PanInput, AxesEvents, OnRelease } from "@egjs/axes";
import Flicking from "../Flicking";
import FlickingError from "../core/FlickingError";
import * as AXES from "../const/axes";
import * as ERROR from "../const/error";
import { circulatePosition, getFlickingAttached, parseBounce } from "../utils";
import { ControlParams } from "../type/external";
import StateMachine from "./StateMachine";
/**
* A controller that handles the {@link https://naver.github.io/egjs-axes/ @egjs/axes} events
* @ko {@link https://naver.github.io/egjs-axes/ @egjs/axes}의 이벤트를 처리하는 컨트롤러 컴포넌트
* @internal
*/
class AxesController {
private _flicking: Flicking | null;
private _axes: Axes | null;
private _panInput: PanInput | null;
private _stateMachine: StateMachine;
private _animatingContext: { start: number; end: number; offset: number };
private _dragged: boolean;
/**
* An {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html Axes} instance
* @ko {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html Axes}의 인스턴스
* @type {Axes}
* @see https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html
* @readonly
*/
public get axes() { return this._axes; }
/**
* @internal
*/
public get stateMachine() { return this._stateMachine; }
/**
* A activated {@link State} that shows the current status of the user input or the animation
* @ko 현재 활성화된 {@link State} 인스턴스로 사용자 입력 또는 애니메이션 상태를 나타냅니다
* @type {State}
*/
public get state() { return this._stateMachine.state; }
/**
* A context of the current animation playing
* @ko 현재 재생중인 애니메이션 정보
* @type {object}
* @property {number} start A start position of the animation애니메이션 시작 지점
* @property {number} end A end position of the animation애니메이션 끝 지점
* @property {number} offset camera offset카메라 오프셋
* @readonly
*/
public get animatingContext() { return this._animatingContext; }
/**
* A current control parameters of the Axes instance
* @ko 활성화된 현재 Axes 패러미터들
* @type {ControlParams}
*/
public get controlParams(): ControlParams {
const axes = this._axes;
if (!axes) {
return {
range: { min: 0, max: 0 },
position: 0,
circular: false
};
}
const axis = axes.axis[AXES.POSITION_KEY];
return {
range: { min: axis.range![0], max: axis.range![1] },
circular: (axis.circular as boolean[])[0],
position: this.position
};
}
/**
* A Boolean indicating whether the user input is enabled
* @ko 현재 사용자 입력이 활성화되었는지를 나타내는 값
* @type {boolean}
* @readonly
*/
public get enabled() { return this._panInput?.isEnabled() ?? false; }
/**
* Current position value in {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html Axes} instance
* @ko {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html Axes} 인스턴스 내부의 현재 좌표 값
* @type {number}
* @readonly
*/
public get position() { return this._axes?.get([AXES.POSITION_KEY])[AXES.POSITION_KEY] ?? 0; }
/**
* Current range value in {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html Axes} instance
* @ko {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html Axes} 인스턴스 내부의 현재 이동 범위 값
* @type {number[]}
* @readonly
*/
public get range() { return this._axes?.axis[AXES.POSITION_KEY].range ?? [0, 0]; }
/**
* Actual bounce size(px)
* @ko 적용된 bounce 크기(px 단위)
* @type {number[]}
* @readonly
*/
public get bounce() { return this._axes?.axis[AXES.POSITION_KEY].bounce as number[] | undefined; }
/** */
public constructor() {
this._resetInternalValues();
this._stateMachine = new StateMachine();
}
/**
* Initialize AxesController
* @ko AxesController를 초기화합니다
* @param {Flicking} flicking An instance of Flicking
* @chainable
* @return {this}
*/
public init(flicking: Flicking): this {
this._flicking = flicking;
this._axes = new Axes({
[AXES.POSITION_KEY]: {
range: [0, 0],
circular: false,
bounce: [0, 0]
}
}, {
deceleration: flicking.deceleration,
interruptable: flicking.interruptable,
nested: flicking.nested,
easing: flicking.easing
});
this._panInput = new PanInput(flicking.viewport.element, {
inputType: flicking.inputType,
iOSEdgeSwipeThreshold: flicking.iOSEdgeSwipeThreshold,
scale: flicking.horizontal ? [-1, 0] : [0, -1],
releaseOnScroll: true
});
const axes = this._axes;
axes.connect(flicking.horizontal ? [AXES.POSITION_KEY, ""] : ["", AXES.POSITION_KEY], this._panInput);
for (const key in AXES.EVENT) {
const eventType = AXES.EVENT[key] as keyof AxesEvents;
axes.on(eventType, (e: AxesEvents[typeof eventType]) => {
this._stateMachine.fire(eventType, {
flicking,
axesEvent: e
});
});
}
return this;
}
/**
* Destroy AxesController and return to initial state
* @ko AxesController를 초기 상태로 되돌립니다
* @return {void}
*/
public destroy(): void {
if (this._axes) {
this.removePreventClickHandler();
this._axes.destroy();
}
this._panInput?.destroy();
this._resetInternalValues();
}
/**
* Enable input from the user (mouse/touch)
* @ko 사용자의 입력(마우스/터치)를 활성화합니다
* @chainable
* @return {this}
*/
public enable(): this {
this._panInput?.enable();
return this;
}
/**
* Disable input from the user (mouse/touch)
* @ko 사용자의 입력(마우스/터치)를 막습니다
* @chainable
* @return {this}
*/
public disable(): this {
this._panInput?.disable();
return this;
}
/**
* Releases ongoing user input (mouse/touch)
* @ko 사용자의 현재 입력(마우스/터치)를 중단시킵니다
* @chainable
* @return {this}
*/
public release(): this {
this._panInput?.release();
return this;
}
/**
* Update {@link https://naver.github.io/egjs-axes/ @egjs/axes}'s state
* @ko {@link https://naver.github.io/egjs-axes/ @egjs/axes}의 상태를 갱신합니다
* @chainable
* @throws {FlickingError}
* {@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} When {@link AxesController#init init} is not called before
* {@link AxesController#init init}이 이전에 호출되지 않은 경우
* @return {this}
*/
public update(controlParams: ControlParams): this {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const axes = this._axes!;
const axis = axes.axis[AXES.POSITION_KEY];
axis.circular = [controlParams.circular, controlParams.circular];
axis.range = [controlParams.range.min, controlParams.range.max];
axis.bounce = parseBounce(flicking.bounce, camera.size);
axes.axisManager.set({ [AXES.POSITION_KEY]: controlParams.position });
return this;
}
/**
* Attach a handler to the camera element to prevent click events during animation
* @ko 카메라 엘리먼트에 애니메이션 도중에 클릭 이벤트를 방지하는 핸들러를 부착합니다
* @return {this}
*/
public addPreventClickHandler(): this {
const flicking = getFlickingAttached(this._flicking);
const axes = this._axes!;
const cameraEl = flicking.camera.element;
axes.on(AXES.EVENT.HOLD, this._onAxesHold);
axes.on(AXES.EVENT.CHANGE, this._onAxesChange);
cameraEl.addEventListener("click", this._preventClickWhenDragged, true);
return this;
}
/**
* Detach a handler to the camera element to prevent click events during animation
* @ko 카메라 엘리먼트에 애니메이션 도중에 클릭 이벤트를 방지하는 핸들러를 탈착합니다
* @return {this}
*/
public removePreventClickHandler(): this {
const flicking = getFlickingAttached(this._flicking);
const axes = this._axes!;
const cameraEl = flicking.camera.element;
axes.off(AXES.EVENT.HOLD, this._onAxesHold);
axes.off(AXES.EVENT.CHANGE, this._onAxesChange);
cameraEl.removeEventListener("click", this._preventClickWhenDragged, true);
return this;
}
/**
* Run Axes's {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html#setTo setTo} using the given position
* @ko Axes의 {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html#setTo setTo} 메소드를 주어진 좌표를 이용하여 수행합니다
* @param {number} position A position to move이동할 좌표
* @param {number} duration Duration of the animation (unit: ms)애니메이션 진행 시간 (단위: ms)
* @param {number} [axesEvent] If provided, it'll use its {@link https://naver#github#io/egjs-axes/release/latest/doc/eg#Axes#html#setTo setTo} method instead이 값이 주어졌을 경우, 해당 이벤트의 {@link https://naver#github#io/egjs-axes/release/latest/doc/eg#Axes#html#setTo setTo} 메소드를 대신해서 사용합니다.
* @throws {FlickingError}
* |code|condition|
* |---|---|
* |{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING}|When {@link Control#init init} is not called before|
* |{@link ERROR_CODE ANIMATION_INTERRUPTED}|When the animation is interrupted by user input|
*
*
* |code|condition|
* |---|---|
* |{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING}|{@link Control#init init}이 이전에 호출되지 않은 경우|
* |{@link ERROR_CODE ANIMATION_INTERRUPTED}|사용자 입력에 의해 애니메이션이 중단된 경우|
*
*
* @return {Promise} A Promise which will be resolved after reaching the target position해당 좌표 도달시에 resolve되는 Promise
*/
public animateTo(position: number, duration: number, axesEvent?: OnRelease): Promise {
const axes = this._axes;
const state = this._stateMachine.state;
if (!axes) {
return Promise.reject(new FlickingError(ERROR.MESSAGE.NOT_ATTACHED_TO_FLICKING, ERROR.CODE.NOT_ATTACHED_TO_FLICKING));
}
const startPos = axes.get([AXES.POSITION_KEY])[AXES.POSITION_KEY];
if (startPos === position) {
const flicking = getFlickingAttached(this._flicking);
flicking.camera.lookAt(position);
if (state.targetPanel) {
flicking.control.setActive(state.targetPanel, flicking.control.activePanel, axesEvent?.isTrusted ?? false);
}
return Promise.resolve();
}
this._animatingContext = {
start: startPos,
end: position,
offset: 0
};
const animate = () => {
const resetContext = () => {
this._animatingContext = { start: 0, end: 0, offset: 0 };
};
axes.once(AXES.EVENT.FINISH, resetContext);
if (axesEvent) {
axesEvent.setTo({ [AXES.POSITION_KEY]: position }, duration);
} else {
axes.setTo({ [AXES.POSITION_KEY]: position }, duration);
}
};
if (duration === 0) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
animate();
const newPos = flicking.circularEnabled
? circulatePosition(position, camera.range.min, camera.range.max)
: position;
axes.axisManager.set({ [AXES.POSITION_KEY]: newPos });
return Promise.resolve();
} else {
return new Promise((resolve, reject) => {
const animationFinishHandler = () => {
axes.off(AXES.EVENT.HOLD, interruptionHandler);
resolve();
};
const interruptionHandler = () => {
axes.off(AXES.EVENT.FINISH, animationFinishHandler);
reject(new FlickingError(ERROR.MESSAGE.ANIMATION_INTERRUPTED, ERROR.CODE.ANIMATION_INTERRUPTED));
};
axes.once(AXES.EVENT.FINISH, animationFinishHandler);
axes.once(AXES.EVENT.HOLD, interruptionHandler);
animate();
});
}
}
private _resetInternalValues() {
this._flicking = null;
this._axes = null;
this._panInput = null;
this._animatingContext = { start: 0, end: 0, offset: 0 };
this._dragged = false;
}
private _onAxesHold = () => {
this._dragged = false;
};
private _onAxesChange = () => {
this._dragged = !!this._panInput?.isEnabled();
};
private _preventClickWhenDragged = (e: MouseEvent) => {
if (this._dragged) {
e.preventDefault();
e.stopPropagation();
}
this._dragged = false;
};
}
export default AxesController;