/*
* Copyright (c) 2015 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import { OnRelease } from "@egjs/axes";
import Panel from "../core/panel/Panel";
import FlickingError from "../core/FlickingError";
import { clamp, getFlickingAttached, getMinusCompensatedIndex, isBetween } from "../utils";
import * as ERROR from "../const/error";
import Control from "./Control";
/**
* An options for the {@link StrictControl}
* @ko {@link StrictControl} 생성시 사용되는 옵션
* @interface
* @property {number} count Maximum number of panels that can be moved at a time최대로 움직일 수 있는 패널의 개수
*/
export interface StrictControlOptions {
count: number;
}
/**
* A {@link Control} that allow you to select the maximum number of panels to move at a time
* @ko 한번에 최대로 이동할 패널의 개수를 선택 가능한 {@link Control}
*/
class StrictControl extends Control {
private _count: number;
private _indexRange: { min: number; max: number };
/**
* Maximum number of panels that can be moved at a time
* @ko 최대로 움직일 수 있는 패널의 개수
* @type {number}
* @default 1
*/
public get count() { return this._count; }
public set count(val: StrictControlOptions["count"]) { this._count = val; }
/** */
public constructor({
count = 1
}: Partial = {}) {
super();
this._count = count;
this._resetIndexRange();
}
/**
* Destroy Control and return to initial state
* @ko Control을 초기 상태로 되돌립니다
* @return {void}
*/
public destroy() {
super.destroy();
this._resetIndexRange();
}
/**
* Update {@link Control#controller controller}'s state
* @ko {@link Control#controller controller}의 내부 상태를 갱신합니다
* @chainable
* @return {this}
*/
public updateInput(): this {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const renderer = flicking.renderer;
const controller = this._controller;
const controlParams = camera.controlParams;
const count = this._count;
const activePanel = controller.state.animating
? camera.findNearestAnchor(camera.position)?.panel
: this._activePanel;
if (!activePanel) {
controller.update(controlParams);
this._resetIndexRange();
return this;
}
const cameraRange = controlParams.range;
const currentPos = activePanel.position;
const currentIndex = activePanel.index;
const panelCount = renderer.panelCount;
let prevPanelIndex = currentIndex - count;
let nextPanelIndex = currentIndex + count;
if (prevPanelIndex < 0) {
prevPanelIndex = flicking.circularEnabled
? getMinusCompensatedIndex((prevPanelIndex + 1) % panelCount - 1, panelCount)
: clamp(prevPanelIndex, 0, panelCount - 1);
}
if (nextPanelIndex >= panelCount) {
nextPanelIndex = flicking.circularEnabled
? nextPanelIndex % panelCount
: clamp(nextPanelIndex, 0, panelCount - 1);
}
const prevPanel = renderer.panels[prevPanelIndex];
const nextPanel = renderer.panels[nextPanelIndex];
let prevPos = Math.max(prevPanel.position, cameraRange.min);
let nextPos = Math.min(nextPanel.position, cameraRange.max);
if (prevPos > currentPos) {
prevPos -= camera.rangeDiff;
}
if (nextPos < currentPos) {
nextPos += camera.rangeDiff;
}
controlParams.range = {
min: prevPos,
max: nextPos
};
if (controlParams.circular) {
if (controlParams.position < prevPos) {
controlParams.position += camera.rangeDiff;
}
if (controlParams.position > nextPos) {
controlParams.position -= camera.rangeDiff;
}
}
controlParams.circular = false;
controller.update(controlParams);
this._indexRange = {
min: prevPanel.index,
max: nextPanel.index
};
return this;
}
public async moveToPanel(panel: Panel, options: Parameters[1]): Promise {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const controller = this._controller;
controller.update(camera.controlParams);
return super.moveToPanel(panel, options);
}
/**
* Move {@link Camera} to the given position
* @ko {@link Camera}를 주어진 좌표로 이동합니다
* @param {number} position The target position to move이동할 좌표
* @param {number} duration Duration of the panel movement animation (unit: ms).패널 이동 애니메이션 진행 시간 (단위: ms)
* @param {object} [axesEvent] {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html#event:release release} event of {@link https://naver.github.io/egjs-axes/ Axes}
* {@link https://naver.github.io/egjs-axes/ Axes}의 {@link https://naver.github.io/egjs-axes/release/latest/doc/eg.Axes.html#event:release release} 이벤트
* @fires Flicking#moveStart
* @fires Flicking#move
* @fires Flicking#moveEnd
* @fires Flicking#willChange
* @fires Flicking#changed
* @fires Flicking#willRestore
* @fires Flicking#restored
* @fires Flicking#needPanel
* @fires Flicking#visibleChange
* @fires Flicking#reachEdge
* @throws {FlickingError}
* |code|condition|
* |---|---|
* |{@link ERROR_CODE POSITION_NOT_REACHABLE}|When the given panel is already removed or not in the Camera's {@link Camera#range range}|
* |{@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|
* |{@link ERROR_CODE STOP_CALLED_BY_USER}|When the animation is interrupted by user input|
*
*
* |code|condition|
* |---|---|
* |{@link ERROR_CODE POSITION_NOT_REACHABLE}|주어진 패널이 제거되었거나, Camera의 {@link Camera#range range} 밖에 있을 경우|
* |{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING}|{@link Control#init init}이 이전에 호출되지 않은 경우|
* |{@link ERROR_CODE ANIMATION_INTERRUPTED}|사용자 입력에 의해 애니메이션이 중단된 경우|
* |{@link ERROR_CODE STOP_CALLED_BY_USER}|발생된 이벤트들 중 하나라도 `stop()`이 호출된 경우|
*
*
* @return {Promise} A Promise which will be resolved after reaching the target position해당 좌표 도달시에 resolve되는 Promise
*/
public moveToPosition(position: number, duration: number, axesEvent?: OnRelease) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const activePanel = this._activePanel;
const axesRange = this._controller.range;
const indexRange = this._indexRange;
const cameraRange = camera.range;
const state = this._controller.state;
const clampedPosition = clamp(camera.clampToReachablePosition(position), axesRange[0], axesRange[1]);
const anchorAtPosition = camera.findAnchorIncludePosition(clampedPosition);
if (!anchorAtPosition || !activePanel) {
return Promise.reject(new FlickingError(ERROR.MESSAGE.POSITION_NOT_REACHABLE(position), ERROR.CODE.POSITION_NOT_REACHABLE));
}
const prevPos = activePanel.position;
const posDelta = flicking.animating
? state.delta
: position - camera.position;
const isOverThreshold = Math.abs(posDelta) >= flicking.threshold;
const adjacentAnchor = (position > prevPos)
? camera.getNextAnchor(anchorAtPosition)
: camera.getPrevAnchor(anchorAtPosition);
let targetPos: number;
let targetPanel: Panel;
const anchors = camera.anchorPoints;
const firstAnchor = anchors[0];
const lastAnchor = anchors[anchors.length - 1];
const shouldBounceToFirst = position <= cameraRange.min && isBetween(firstAnchor.panel.index, indexRange.min, indexRange.max);
const shouldBounceToLast = position >= cameraRange.max && isBetween(lastAnchor.panel.index, indexRange.min, indexRange.max);
const isAdjacent = adjacentAnchor && (indexRange.min <= indexRange.max
? isBetween(adjacentAnchor.index, indexRange.min, indexRange.max)
: adjacentAnchor.index >= indexRange.min || adjacentAnchor.index <= indexRange.max);
if (shouldBounceToFirst || shouldBounceToLast) {
// In bounce area
const targetAnchor = position < cameraRange.min ? firstAnchor : lastAnchor;
targetPanel = targetAnchor.panel;
targetPos = targetAnchor.position;
} else if (isOverThreshold && anchorAtPosition.position !== activePanel.position) {
// Move to anchor at position
targetPanel = anchorAtPosition.panel;
targetPos = anchorAtPosition.position;
} else if (isOverThreshold && isAdjacent) {
// Move to adjacent anchor
targetPanel = adjacentAnchor!.panel;
targetPos = adjacentAnchor!.position;
} else {
// Restore to active panel
targetPos = camera.clampToReachablePosition(activePanel.position);
targetPanel = activePanel;
}
this._triggerIndexChangeEvent(targetPanel, position, axesEvent);
return this._animateToPosition({
position: targetPos,
duration,
newActivePanel: targetPanel,
axesEvent
});
}
public setActive = (newActivePanel: Panel, prevActivePanel: Panel | null, isTrusted: boolean) => {
super.setActive(newActivePanel, prevActivePanel, isTrusted);
this.updateInput();
};
private _resetIndexRange() {
this._indexRange = { min: 0, max: 0 };
}
}
export default StrictControl;