/**
* Copyright (c) 2015 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import Component from "@egjs/component";
import ImReady from "@egjs/imready";
import Viewport from "./components/Viewport";
import Panel from "./components/Panel";
import { merge, getProgress, parseElement, isString, counter, findIndex } from "./utils";
import { DEFAULT_OPTIONS, EVENTS, DIRECTION, AXES_EVENTS, STATE_TYPE, DEFAULT_MOVE_TYPE_OPTIONS } from "./consts";
import {
FlickingOptions,
FlickingEvent,
Direction,
EventType,
FlickingPanel,
TriggerCallback,
FlickingContext,
FlickingStatus,
Plugin,
ElementLike,
DestroyOption,
BeforeSyncResult,
SyncResult,
ChangeEvent,
SelectEvent,
NeedPanelEvent,
VisibleChangeEvent,
ContentErrorEvent,
MoveTypeStringOption,
ValueOf,
} from "./types";
// import { sendEvent } from "./ga/ga";
import { DiffResult } from "@egjs/list-differ";
/**
* @memberof eg
* @extends eg.Component
* @support {"ie": "10+", "ch" : "latest", "ff" : "latest", "sf" : "latest" , "edge" : "latest", "ios" : "7+", "an" : "4.X+"}
* @requires {@link https://github.com/naver/egjs-component|eg.Component}
* @requires {@link https://github.com/naver/egjs-axes|eg.Axes}
* @see Easing Functions Cheat Sheet {@link http://easings.net/} <ko>이징 함수 Cheat Sheet {@link http://easings.net/}</ko>
*/
class Flicking extends Component<{
holdStart: FlickingEvent;
holdEnd: FlickingEvent;
moveStart: FlickingEvent;
move: FlickingEvent;
moveEnd: FlickingEvent;
change: ChangeEvent;
restore: FlickingEvent;
select: SelectEvent;
needPanel: NeedPanelEvent;
visibleChange: VisibleChangeEvent;
contentError: ContentErrorEvent;
}> {
/**
* Version info string
* @ko 버전정보 문자열
* @example
* eg.Flicking.VERSION; // ex) 3.0.0
* @memberof eg.Flicking
*/
public static VERSION: string = "#__VERSION__#";
/**
* Direction constant - "PREV" or "NEXT"
* @ko 방향 상수 - "PREV" 또는 "NEXT"
* @type {object}
* @property {"PREV"} PREV - Prev direction from current hanger position.<br/>It's `left(←️)` direction when `horizontal: true`.<br/>Or, `up(↑️)` direction when `horizontal: false`.<ko>현재 행어를 기준으로 이전 방향.<br/>`horizontal: true`일 경우 `왼쪽(←️)` 방향.<br/>`horizontal: false`일 경우 `위쪽(↑️)`방향이다.</ko>
* @property {"NEXT"} NEXT - Next direction from current hanger position.<br/>It's `right(→)` direction when `horizontal: true`.<br/>Or, `down(↓️)` direction when `horizontal: false`.<ko>현재 행어를 기준으로 다음 방향.<br/>`horizontal: true`일 경우 `오른쪽(→)` 방향.<br/>`horizontal: false`일 경우 `아래쪽(↓️)`방향이다.</ko>
* @example
* eg.Flicking.DIRECTION.PREV; // "PREV"
* eg.Flicking.DIRECTION.NEXT; // "NEXT"
*/
public static DIRECTION: Direction = DIRECTION;
/**
* Event type object with event name strings.
* @ko 이벤트 이름 문자열들을 담은 객체
* @type {object}
* @property {"holdStart"} HOLD_START - holdStart event<ko>holdStart 이벤트</ko>
* @property {"holdEnd"} HOLD_END - holdEnd event<ko>holdEnd 이벤트</ko>
* @property {"moveStart"} MOVE_START - moveStart event<ko>moveStart 이벤트</ko>
* @property {"move"} MOVE - move event<ko>move 이벤트</ko>
* @property {"moveEnd"} MOVE_END - moveEnd event<ko>moveEnd 이벤트</ko>
* @property {"change"} CHANGE - change event<ko>change 이벤트</ko>
* @property {"restore"} RESTORE - restore event<ko>restore 이벤트</ko>
* @property {"select"} SELECT - select event<ko>select 이벤트</ko>
* @property {"needPanel"} NEED_PANEL - needPanel event<ko>needPanel 이벤트</ko>
* @example
* eg.Flicking.EVENTS.MOVE_START; // "MOVE_START"
*/
public static EVENTS: EventType = EVENTS;
public options: FlickingOptions;
private wrapper: HTMLElement;
private viewport: Viewport;
private contentsReadyChecker: ImReady | null = null;
private eventContext: FlickingContext;
private isPanelChangedAtBeforeSync: boolean = false;
/**
* @param element A base element for the eg.Flicking module. When specifying a value as a `string` type, you must specify a css selector string to select the element.<ko>eg.Flicking 모듈을 사용할 기준 요소. `string`타입으로 값 지정시 요소를 선택하기 위한 css 선택자 문자열을 지정해야 한다.</ko>
* @param options An option object of the eg.Flicking module<ko>eg.Flicking 모듈의 옵션 객체</ko>
* @param {string} [options.classPrefix="eg-flick"] A prefix of class names will be added for the panels, viewport, and camera.<ko>패널들과 뷰포트, 카메라에 추가될 클래스 이름의 접두사.</ko>
* @param {number} [options.deceleration=0.0075] Deceleration value for panel movement animation for animation triggered by manual user input. A higher value means a shorter running time.<ko>사용자의 동작으로 가속도가 적용된 패널 이동 애니메이션의 감속도. 값이 높을수록 애니메이션 실행 시간이 짧아진다.</ko>
* @param {boolean} [options.horizontal=true] The direction of panel movement. (true: horizontal, false: vertical)<ko>패널 이동 방향. (true: 가로방향, false: 세로방향)</ko>
* @param {boolean} [options.circular=false] Enables circular mode, which connects first/last panel for continuous scrolling.<ko>순환 모드를 활성화한다. 순환 모드에서는 양 끝의 패널이 서로 연결되어 끊김없는 스크롤이 가능하다.</ko>
* @param {boolean} [options.infinite=false] Enables infinite mode, which can automatically trigger needPanel until reaching the last panel's index reaches the lastIndex.<ko>무한 모드를 활성화한다. 무한 모드에서는 needPanel 이벤트를 자동으로 트리거한다. 해당 동작은 마지막 패널의 인덱스가 lastIndex와 일치할때까지 일어난다.</ko>
* @param {number} [options.infiniteThreshold=0] A Threshold from viewport edge before triggering `needPanel` event in infinite mode.<ko>무한 모드에서 `needPanel`이벤트가 발생하기 위한 뷰포트 끝으로부터의 최대 거리.</ko>
* @param {number} [options.lastIndex=Infinity] Maximum panel index that Flicking can set. Flicking won't trigger `needPanel` when the event's panel index is greater than it.<br/>Also, if the last panel's index reached a given index, you can't add more panels.<ko>Flicking이 설정 가능한 패널의 최대 인덱스. `needPanel` 이벤트에 지정된 인덱스가 최대 패널의 개수보다 같거나 커야 하는 경우에 이벤트를 트리거하지 않게 한다.<br>또한, 마지막 패널의 인덱스가 주어진 인덱스와 동일할 경우, 새로운 패널을 더 이상 추가할 수 없다.</ko>
* @param {number} [options.threshold=40] Movement threshold to change panel(unit: pixel). It should be dragged above the threshold to change the current panel.<ko>패널 변경을 위한 이동 임계값 (단위: 픽셀). 주어진 값 이상으로 스크롤해야만 패널 변경이 가능하다.</ko>
* @param {number} [options.duration=100] Duration of the panel movement animation. (unit: ms)<ko>패널 이동 애니메이션 진행 시간.(단위: ms)</ko>
* @param {function} [options.panelEffect=x => 1 - Math.pow(1 - x, 3)] An easing function applied to the panel movement animation. Default value is `easeOutCubic`.<ko>패널 이동 애니메이션에 적용할 easing함수. 기본값은 `easeOutCubic`이다.</ko>
* @param {number} [options.defaultIndex=0] Index of the panel to set as default when initializing. A zero-based integer.<ko>초기화시 지정할 디폴트 패널의 인덱스로, 0부터 시작하는 정수.</ko>
* @param {string[]} [options.inputType=["touch,"mouse"]] Types of input devices to enable.({@link https://naver.github.io/egjs-axes/release/latest/doc/global.html#PanInputOption Reference})<ko>활성화할 입력 장치 종류. ({@link https://naver.github.io/egjs-axes/release/latest/doc/global.html#PanInputOption 참고})</ko>
* @param {number} [options.thresholdAngle=45] The threshold angle value(0 ~ 90).<br>If the input angle from click/touched position is above or below this value in horizontal and vertical mode each, scrolling won't happen.<ko>스크롤 동작을 막기 위한 임계각(0 ~ 90).<br>클릭/터치한 지점으로부터 계산된 사용자 입력의 각도가 horizontal/vertical 모드에서 각각 크거나 작으면, 스크롤 동작이 이루어지지 않는다.</ko>
* @param {number|string|number[]|string[]} [options.bounce=[10,10]] The size value of the bounce area. Only can be enabled when `circular=false`.<br>You can set different bounce value for prev/next direction by using array.<br>`number` for px value, and `string` for px, and % value relative to viewport size.(ex - 0, "10px", "20%")<ko>바운스 영역의 크기값. `circular=false`인 경우에만 사용할 수 있다.<br>배열을 통해 prev/next 방향에 대해 서로 다른 바운스 값을 지정 가능하다.<br>`number`를 통해 px값을, `stirng`을 통해 px 혹은 뷰포트 크기 대비 %값을 사용할 수 있다.(ex - 0, "10px", "20%")</ko>
* @param {boolean} [options.autoResize=false] Whether the `resize` method should be called automatically after a window resize event.<ko>window의 `resize` 이벤트 이후 자동으로 resize()메소드를 호출할지의 여부.</ko>
* @param {boolean} [options.adaptive=false] Whether the height(horizontal)/width(vertical) of the viewport element reflects the height/width value of the panel after completing the movement.<ko>목적 패널로 이동한 후 그 패널의 높이(horizontal)/너비(vertical)값을 뷰포트 요소의 높이/너비값에 반영할지 여부.</ko>
* @param {number|""} [options.zIndex=2000] z-index value for viewport element.<ko>뷰포트 엘리먼트의 z-index 값.</ko>
* @param {boolean} [options.bound=false] Prevent the view from going out of the first/last panel. Only can be enabled when `circular=false`.<ko>뷰가 첫번째와 마지막 패널 밖으로 나가는 것을 막아준다. `circular=false`인 경우에만 사용할 수 있다.</ko>
* @param {boolean} [options.overflow=false] Disables CSS property `overflow: hidden` in viewport if `true`.<ko>`true`로 설정시 뷰포트에 `overflow: hidden` 속성을 해제한다.</ko>
* @param {string} [options.hanger="50%"] The reference position of the hanger in the viewport, which hangs panel anchors should be stopped at.<br>It should be provided in px or % value of viewport size.<br>You can combinate those values with plus/minus sign.<br>ex) "50", "100px", "0%", "25% + 100px"<ko>뷰포트 내부의 행어의 위치. 패널의 앵커들이 뷰포트 내에서 멈추는 지점에 해당한다.<br>px값이나, 뷰포트의 크기 대비 %값을 사용할 수 있고, 이를 + 혹은 - 기호로 연계하여 사용할 수도 있다.<br>예) "50", "100px", "0%", "25% + 100px"</ko>
* @param {string} [options.anchor="50%"] The reference position of the anchor in panels, which can be hanged by viewport hanger.<br>It should be provided in px or % value of panel size.<br>You can combinate those values with plus/minus sign.<br>ex) "50", "100px", "0%", "25% + 100px"<ko>패널 내부의 앵커의 위치. 뷰포트의 행어와 연계하여 패널이 화면 내에서 멈추는 지점을 설정할 수 있다.<br>px값이나, 패널의 크기 대비 %값을 사용할 수 있고, 이를 + 혹은 - 기호로 연계하여 사용할 수도 있다.<br>예) "50", "100px", "0%", "25% + 100px"</ko>
* @param {number} [options.gap=0] Space value between panels. Should be given in number.(px)<ko>패널간에 부여할 간격의 크기를 나타내는 숫자.(px)</ko>
* @param {eg.Flicking.MoveTypeOption} [options.moveType="snap"] Movement style by user input. (ex: snap, freeScroll)<ko>사용자 입력에 의한 이동 방식.(ex: snap, freeScroll)</ko>
* @param {boolean} [options.useOffset=false] Whether to use `offsetWidth`/`offsetHeight` instead of `getBoundingClientRect` for panel/viewport size calculation.<br/>You can use this option to calculate the original panel size when CSS transform is applied to viewport or panel.<br/>⚠️ If panel size is not fixed integer value, there can be a 1px gap between panels.<ko>패널과 뷰포트의 크기를 계산할 때 `offsetWidth`/`offsetHeight`를 `getBoundingClientRect` 대신 사용할지 여부.<br/>패널이나 뷰포트에 CSS transform이 설정되어 있을 때 원래 패널 크기를 계산하려면 옵션을 활성화한다.<br/>⚠️ 패널의 크기가 정수로 고정되어있지 않다면 패널 사이에 1px의 공간이 생길 수 있다.</ko>
* @param {boolean} [options.renderOnlyVisible=false] Whether to render visible panels only. This can dramatically increase performance when there're many panels.<ko>보이는 패널만 렌더링할지 여부를 설정한다. 패널이 많을 경우에 퍼포먼스를 크게 향상시킬 수 있다.</ko>
* @param {boolean|string[]} [options.isEqualSize=false] This option indicates whether all panels have the same size(true) of first panel, or it can hold a list of class names that determines panel size.<br/>Enabling this option can increase performance while recalculating panel size.<ko>모든 패널의 크기가 동일한지(true), 혹은 패널 크기를 결정하는 패널 클래스들의 리스트.<br/>이 옵션을 설정하면 패널 크기 재설정시에 성능을 높일 수 있다.</ko>
* @param {boolean} [options.isConstantSize=false] Whether all panels have a constant size that won't be changed after resize. Enabling this option can increase performance while recalculating panel size.<ko>모든 패널의 크기가 불변인지의 여부. 이 옵션을 'true'로 설정하면 패널 크기 재설정시에 성능을 높일 수 있다.</ko>
* @param {boolean} [options.renderExternal=false] Whether to use external rendering. It will delegate DOM manipulation and can synchronize the rendered state by calling `sync()` method. You can use this option to use in frameworks like React, Vue, Angular, which has its states and rendering methods.<ko>외부 렌더링을 사용할 지의 여부. 이 옵션을 사용시 렌더링을 외부에 위임할 수 있고, `sync()`를 호출하여 그 상태를 동기화할 수 있다. 이 옵션을 사용하여, React, Vue, Angular 등 자체적인 상태와 렌더링 방법을 갖는 프레임워크에 대응할 수 있다.</ko>
* @param {boolean} [options.resizeOnContentsReady=false] Whether to resize the Flicking after the image/video elements inside viewport are ready.<br/>Use this property to prevent wrong Flicking layout caused by dynamic image / video sizes.<ko>Flicking 내부의 이미지 / 비디오 엘리먼트들이 전부 로드되었을 때 Flicking의 크기를 재계산하기 위한 옵션.<br/>이미지 / 비디오 크기가 고정 크기가 아닐 경우 사용하여 레이아웃이 잘못되는 것을 방지할 수 있다.</ko>
* @param {boolean} [options.collectStatistics=true] Whether to collect statistics on how you are using `Flicking`. These statistical data do not contain any personal information and are used only as a basis for the development of a user-friendly product.<ko>어떻게 `Flicking`을 사용하고 있는지에 대한 통계 수집 여부를 나타낸다. 이 통계자료는 개인정보를 포함하고 있지 않으며 오직 사용자 친화적인 제품으로 발전시키기 위한 근거자료로서 활용한다.</ko>
*/
constructor(
element: string | HTMLElement,
options: Partial<FlickingOptions> = {},
) {
super();
// Set flicking wrapper user provided
let wrapper: HTMLElement | null;
if (isString(element)) {
wrapper = document.querySelector(element);
if (!wrapper) {
throw new Error("Base element doesn't exist.");
}
} else if (element.nodeName && element.nodeType === 1) {
wrapper = element;
} else {
throw new Error("Element should be provided in string or HTMLElement.");
}
this.wrapper = wrapper;
// Override default options
this.options = merge({}, DEFAULT_OPTIONS, options) as FlickingOptions;
// Override moveType option
const currentOptions = this.options;
const moveType = currentOptions.moveType as MoveTypeStringOption;
if (moveType in DEFAULT_MOVE_TYPE_OPTIONS) {
currentOptions.moveType = DEFAULT_MOVE_TYPE_OPTIONS[moveType as keyof typeof DEFAULT_MOVE_TYPE_OPTIONS];
}
// Make viewport instance with panel container element
this.viewport = new Viewport(this, this.options, this.triggerEvent);
this.listenInput();
this.listenResize();
// if (this.options.collectStatistics) {
// sendEvent(
// "usage",
// "options",
// options,
// );
// }
}
/**
* Move to the previous panel if it exists.
* @ko 이전 패널이 존재시 해당 패널로 이동한다.
* @param [duration=options.duration] Duration of the panel movement animation.(unit: ms)<ko>패널 이동 애니메이션 진행 시간.(단위: ms)</ko>
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public prev(duration?: number): this {
const currentPanel = this.getCurrentPanel();
const currentState = this.viewport.stateMachine.getState();
if (currentPanel && currentState.type === STATE_TYPE.IDLE) {
const prevPanel = currentPanel.prev();
if (prevPanel) {
prevPanel.focus(duration);
}
}
return this;
}
/**
* Move to the next panel if it exists.
* @ko 다음 패널이 존재시 해당 패널로 이동한다.
* @param [duration=options.duration] Duration of the panel movement animation(unit: ms).<ko>패널 이동 애니메이션 진행 시간.(단위: ms)</ko>
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public next(duration?: number): this {
const currentPanel = this.getCurrentPanel();
const currentState = this.viewport.stateMachine.getState();
if (currentPanel && currentState.type === STATE_TYPE.IDLE) {
const nextPanel = currentPanel.next();
if (nextPanel) {
nextPanel.focus(duration);
}
}
return this;
}
/**
* Move to the panel of given index.
* @ko 주어진 인덱스에 해당하는 패널로 이동한다.
* @param index The index number of the panel to move.<ko>이동할 패널의 인덱스 번호.</ko>
* @param duration [duration=options.duration] Duration of the panel movement.(unit: ms)<ko>패널 이동 애니메이션 진행 시간.(단위: ms)</ko>
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public moveTo(index: number, duration?: number): this {
const viewport = this.viewport;
const panel = viewport.panelManager.get(index);
const state = viewport.stateMachine.getState();
if (!panel || state.type !== STATE_TYPE.IDLE) {
return this;
}
const anchorPosition = panel.getAnchorPosition();
const hangerPosition = viewport.getHangerPosition();
let targetPanel = panel;
if (this.options.circular) {
const scrollAreaSize = viewport.getScrollAreaSize();
// Check all three possible locations, find the nearest position among them.
const possiblePositions = [
anchorPosition - scrollAreaSize,
anchorPosition,
anchorPosition + scrollAreaSize,
];
const nearestPosition = possiblePositions.reduce((nearest, current) => {
return (Math.abs(current - hangerPosition) < Math.abs(nearest - hangerPosition))
? current
: nearest;
}, Infinity) - panel.getRelativeAnchorPosition();
const identicals = panel.getIdenticalPanels();
const offset = nearestPosition - anchorPosition;
if (offset > 0) {
// First cloned panel is nearest
targetPanel = identicals[1];
} else if (offset < 0) {
// Last cloned panel is nearest
targetPanel = identicals[identicals.length - 1];
}
targetPanel = targetPanel.clone(targetPanel.getCloneIndex(), true);
targetPanel.setPosition(nearestPosition);
}
const currentIndex = this.getIndex();
if (hangerPosition === targetPanel.getAnchorPosition() && currentIndex === index) {
return this;
}
const eventType = panel.getIndex() === viewport.getCurrentIndex()
? ""
: EVENTS.CHANGE;
viewport.moveTo(
targetPanel,
viewport.findEstimatedPosition(targetPanel),
eventType,
null,
duration,
);
return this;
}
/**
* Return index of the current panel. `-1` if no panel exists.
* @ko 현재 패널의 인덱스 번호를 반환한다. 패널이 하나도 없을 경우 `-1`을 반환한다.
* @return Current panel's index, zero-based integer.<ko>현재 패널의 인덱스 번호. 0부터 시작하는 정수.</ko>
*/
public getIndex(): number {
return this.viewport.getCurrentIndex();
}
/**
* Return the wrapper element user provided in constructor.
* @ko 사용자가 생성자에서 제공한 래퍼 엘리먼트를 반환한다.
* @return Wrapper element user provided.<ko>사용자가 제공한 래퍼 엘리먼트.</ko>
*/
public getElement(): HTMLElement {
return this.wrapper;
}
/**
* Return the viewport element's size.
* @ko 뷰포트 엘리먼트의 크기를 반환한다.
* @return Width if horizontal: true, height if horizontal: false
*/
public getSize(): number {
return this.viewport.getSize();
}
/**
* Return current panel. `null` if no panel exists.
* @ko 현재 패널을 반환한다. 패널이 하나도 없을 경우 `null`을 반환한다.
* @return Current panel.<ko>현재 패널.</ko>
*/
public getCurrentPanel(): FlickingPanel | null {
const viewport = this.viewport;
const panel = viewport.getCurrentPanel();
return panel
? panel
: null;
}
/**
* Return the panel of given index. `null` if it doesn't exists.
* @ko 주어진 인덱스에 해당하는 패널을 반환한다. 해당 패널이 존재하지 않을 시 `null`이다.
* @return Panel of given index.<ko>주어진 인덱스에 해당하는 패널.</ko>
*/
public getPanel(index: number): FlickingPanel | null {
const viewport = this.viewport;
const panel = viewport.panelManager.get(index);
return panel
? panel
: null;
}
/**
* Return all panels.
* @ko 모든 패널들을 반환한다.
* @param - Should include cloned panels or not.<ko>복사된 패널들을 포함할지의 여부.</ko>
* @return All panels.<ko>모든 패널들.</ko>
*/
public getAllPanels(includeClone?: boolean): FlickingPanel[] {
const viewport = this.viewport;
const panelManager = viewport.panelManager;
const panels = includeClone
? panelManager.allPanels()
: panelManager.originalPanels();
return panels
.filter(panel => !!panel);
}
/**
* Return the panels currently shown in viewport area.
* @ko 현재 뷰포트 영역에서 보여지고 있는 패널들을 반환한다.
* @return Panels currently shown in viewport area.<ko>현재 뷰포트 영역에 보여지는 패널들</ko>
*/
public getVisiblePanels(): FlickingPanel[] {
return this.viewport.calcVisiblePanels();
}
/**
* Return length of original panels.
* @ko 원본 패널의 개수를 반환한다.
* @return Length of original panels.<ko>원본 패널의 개수</ko>
*/
public getPanelCount(): number {
return this.viewport.panelManager.getPanelCount();
}
/**
* Return how many groups of clones are created.
* @ko 몇 개의 클론 그룹이 생성되었는지를 반환한다.
* @return Length of cloned panel groups.<ko>클론된 패널 그룹의 개수</ko>
*/
public getCloneCount(): number {
return this.viewport.panelManager.getCloneCount();
}
/**
* Get maximum panel index for `infinite` mode.
* @ko `infinite` 모드에서 적용되는 추가 가능한 패널의 최대 인덱스 값을 반환한다.
* @see {@link eg.Flicking.FlickingOptions}
* @return Maximum index of panel that can be added.<ko>최대 추가 가능한 패널의 인덱스.</ko>
*/
public getLastIndex(): number {
return this.viewport.panelManager.getLastIndex();
}
/**
* Set maximum panel index for `infinite' mode.<br>[needPanel]{@link eg.Flicking#events:needPanel} won't be triggered anymore when last panel's index reaches it.<br>Also, you can't add more panels after it.
* @ko `infinite` 모드에서 적용되는 패널의 최대 인덱스를 설정한다.<br>마지막 패널의 인덱스가 설정한 값에 도달할 경우 더 이상 [needPanel]{@link eg.Flicking#events:needPanel} 이벤트가 발생되지 않는다.<br>또한, 설정한 인덱스 이후로 새로운 패널을 추가할 수 없다.
* @param - Maximum panel index.
* @see {@link eg.Flicking.FlickingOptions}
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public setLastIndex(index: number): this {
this.viewport.setLastIndex(index);
return this;
}
/**
* Return panel movement animation.
* @ko 현재 패널 이동 애니메이션이 진행 중인지를 반환한다.
* @return Is animating or not.<ko>애니메이션 진행 여부.</ko>
*/
public isPlaying(): boolean {
return this.viewport.stateMachine.getState().playing;
}
/**
* Unblock input devices.
* @ko 막았던 입력 장치로부터의 입력을 푼다.
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public enableInput(): this {
this.viewport.enable();
return this;
}
/**
* Block input devices.
* @ko 입력 장치로부터의 입력을 막는다.
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public disableInput(): this {
this.viewport.disable();
return this;
}
/**
* Get current flicking status. You can restore current state by giving returned value to [setStatus()]{@link eg.Flicking#setStatus}.
* @ko 현재 상태 값을 반환한다. 반환받은 값을 [setStatus()]{@link eg.Flicking#setStatus} 메소드의 인자로 지정하면 현재 상태를 복원할 수 있다.
* @return An object with current status value information.<ko>현재 상태값 정보를 가진 객체.</ko>
*/
public getStatus(): FlickingStatus {
const viewport = this.viewport;
const panels = viewport.panelManager.originalPanels()
.filter(panel => !!panel)
.map(panel => {
return {
html: panel.getElement().outerHTML,
index: panel.getIndex(),
position: panel.getPosition(),
};
});
return {
index: viewport.getCurrentIndex(),
panels,
position: viewport.getCameraPosition(),
};
}
/**
* Restore to the state of the `status`.
* @ko `status`의 상태로 복원한다.
* @param status Status value to be restored. You can specify the return value of the [getStatus()]{@link eg.Flicking#getStatus} method.<ko>복원할 상태 값. [getStatus()]{@link eg.Flicking#getStatus}메서드의 반환값을 지정하면 된다.</ko>
*/
public setStatus(status: FlickingStatus): void {
this.viewport.restore(status);
}
/**
* Add plugins that can have different effects on Flicking.
* @ko 플리킹에 다양한 효과를 부여할 수 있는 플러그인을 추가한다.
* @param - The plugin(s) to add.<ko>추가할 플러그인(들).</ko>
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public addPlugins(plugins: Plugin | Plugin[]) {
this.viewport.addPlugins(plugins);
return this;
}
/**
* Remove plugins from Flicking.
* @ko 플리킹으로부터 플러그인들을 제거한다.
* @param - The plugin(s) to remove.<ko>제거 플러그인(들).</ko>
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public removePlugins(plugins: Plugin | Plugin[]) {
this.viewport.removePlugins(plugins);
return this;
}
/**
* Return the reference element and all its children to the state they were in before the instance was created. Remove all attached event handlers. Specify `null` for all attributes of the instance (including inherited attributes).
* @ko 기준 요소와 그 하위 패널들을 인스턴스 생성전의 상태로 되돌린다. 부착된 모든 이벤트 핸들러를 탈거한다. 인스턴스의 모든 속성(상속받은 속성포함)에 `null`을 지정한다.
* @example
* const flick = new eg.Flicking("#flick");
* flick.destroy();
* console.log(flick.moveTo); // null
*/
public destroy(option: Partial<DestroyOption> = {}): void {
this.off();
if (this.options.autoResize) {
window.removeEventListener("resize", this.resize);
}
this.viewport.destroy(option);
this.contentsReadyChecker?.destroy();
// release resources
for (const x in this) {
(this as any)[x] = null;
}
}
/**
* Update panels to current state.
* @ko 패널들을 현재 상태에 맞춰 갱신한다.
* @method
* @return {eg.Flicking} The instance itself.<ko>인스턴스 자기 자신.</ko>
*/
public resize = (): this => {
const viewport = this.viewport;
const options = this.options;
const wrapper = this.getElement();
const allPanels = viewport.panelManager.allPanels();
if (!options.isConstantSize) {
allPanels.forEach(panel => panel.unCacheBbox());
}
const shouldResetElements = options.renderOnlyVisible
&& !options.isConstantSize
&& options.isEqualSize !== true;
// Temporarily set parent's height to prevent scroll (#333)
const parent = wrapper.parentElement!;
const origStyle = parent.style.height;
parent.style.height = `${parent.offsetHeight}px`;
viewport.unCacheBbox();
// This should be done before adding panels, to lower performance issue
viewport.updateBbox();
if (shouldResetElements) {
viewport.appendUncachedPanelElements(allPanels as Panel[]);
}
viewport.resize();
parent.style.height = origStyle;
return this;
}
/**
* Add new panels at the beginning of panels.
* @ko 제일 앞에 새로운 패널을 추가한다.
* @param element - Either HTMLElement, HTML string, or array of them.<br>It can be also HTML string of multiple elements with same depth.<ko>HTMLElement 혹은 HTML 문자열, 혹은 그것들의 배열도 가능하다.<br>또한, 같은 depth의 여러 개의 엘리먼트에 해당하는 HTML 문자열도 가능하다.</ko>
* @return Array of appended panels.<ko>추가된 패널들의 배열</ko>
* @example
* // Suppose there were no panels at initialization
* const flicking = new eg.Flicking("#flick");
* flicking.replace(3, document.createElement("div")); // Add new panel at index 3
* flicking.prepend("\<div\>Panel\</div\>"); // Prepended at index 2
* flicking.prepend(["\<div\>Panel\</div\>", document.createElement("div")]); // Prepended at index 0, 1
* flicking.prepend("\<div\>Panel\</div\>"); // Prepended at index 0, pushing every panels behind it.
*/
public prepend(element: ElementLike | ElementLike[]): FlickingPanel[] {
const viewport = this.viewport;
const parsedElements = parseElement(element);
const insertingIndex = Math.max(viewport.panelManager.getRange().min - parsedElements.length, 0);
const prependedPanels = viewport.insert(insertingIndex, parsedElements);
this.checkContentsReady(prependedPanels);
return prependedPanels;
}
/**
* Add new panels at the end of panels.
* @ko 제일 끝에 새로운 패널을 추가한다.
* @param element - Either HTMLElement, HTML string, or array of them.<br>It can be also HTML string of multiple elements with same depth.<ko>HTMLElement 혹은 HTML 문자열, 혹은 그것들의 배열도 가능하다.<br>또한, 같은 depth의 여러 개의 엘리먼트에 해당하는 HTML 문자열도 가능하다.</ko>
* @return Array of appended panels.<ko>추가된 패널들의 배열</ko>
* @example
* // Suppose there were no panels at initialization
* const flicking = new eg.Flicking("#flick");
* flicking.append(document.createElement("div")); // Appended at index 0
* flicking.append("\<div\>Panel\</div\>"); // Appended at index 1
* flicking.append(["\<div\>Panel\</div\>", document.createElement("div")]); // Appended at index 2, 3
* // Even this is possible
* flicking.append("\<div\>Panel 1\</div\>\<div\>Panel 2\</div\>"); // Appended at index 4, 5
*/
public append(element: ElementLike | ElementLike[]): FlickingPanel[] {
const viewport = this.viewport;
const appendedPanels = viewport.insert(viewport.panelManager.getRange().max + 1, element);
this.checkContentsReady(appendedPanels);
return appendedPanels;
}
/**
* Replace existing panels with new panels from given index. If target index is empty, add new panel at target index.
* @ko 주어진 인덱스로부터의 패널들을 새로운 패널들로 교체한다. 인덱스에 해당하는 자리가 비어있다면, 새로운 패널을 해당 자리에 집어넣는다.
* @param index - Start index to replace new panels.<ko>새로운 패널들로 교체할 시작 인덱스</ko>
* @param element - Either HTMLElement, HTML string, or array of them.<br>It can be also HTML string of multiple elements with same depth.<ko>HTMLElement 혹은 HTML 문자열, 혹은 그것들의 배열도 가능하다.<br>또한, 같은 depth의 여러 개의 엘리먼트에 해당하는 HTML 문자열도 가능하다.</ko>
* @return Array of created panels by replace.<ko>교체되어 새롭게 추가된 패널들의 배열</ko>
* @example
* // Suppose there were no panels at initialization
* const flicking = new eg.Flicking("#flick");
*
* // This will add new panel at index 3,
* // Index 0, 1, 2 is empty at this moment.
* // [empty, empty, empty, PANEL]
* flicking.replace(3, document.createElement("div"));
*
* // As index 2 was empty, this will also add new panel at index 2.
* // [empty, empty, PANEL, PANEL]
* flicking.replace(2, "\<div\>Panel\</div\>");
*
* // Index 3 was not empty, so it will replace previous one.
* // It will also add new panels at index 4 and 5.
* // before - [empty, empty, PANEL, PANEL]
* // after - [empty, empty, PANEL, NEW_PANEL, NEW_PANEL, NEW_PANEL]
* flicking.replace(3, ["\<div\>Panel\</div\>", "\<div\>Panel\</div\>", "\<div\>Panel\</div\>"])
*/
public replace(index: number, element: ElementLike | ElementLike[]): FlickingPanel[] {
const replacedPanels = this.viewport.replace(index, element);
this.checkContentsReady(replacedPanels);
return replacedPanels;
}
/**
* Remove panel at target index. This will decrease index of panels behind it.
* @ko `index`에 해당하는 자리의 패널을 제거한다. 수행시 `index` 이후의 패널들의 인덱스가 감소된다.
* @param index - Index of panel to remove.<ko>제거할 패널의 인덱스</ko>
* @param {number} [deleteCount=1] - Number of panels to remove from index.<ko>`index` 이후로 제거할 패널의 개수.</ko>
* @return Array of removed panels<ko>제거된 패널들의 배열</ko>
*/
public remove(index: number, deleteCount: number = 1): FlickingPanel[] {
return this.viewport.remove(index, deleteCount);
}
/**
* Get indexes to render. Should be used with `renderOnlyVisible` option.
* `beforeSync` should be called before this method for a correct result.
* @private
* @ko 렌더링이 필요한 인덱스들을 반환한다. `renderOnlyVisible` 옵션과 함께 사용해야 한다. 정확한 결과를 위해선 `beforeSync`를 이전에 호출해야만 합니다.
* @param - Info object of how panel infos are changed.<ko>패널 정보들의 변경 정보를 담는 오브젝트.</ko>
* @return Array of indexes to render.<ko>렌더링할 인덱스의 배열</ko>
*/
public getRenderingIndexes(diffResult: DiffResult<any>): number[] {
const viewport = this.viewport;
const visiblePanels = viewport.getVisiblePanels();
const maintained = diffResult.maintained.reduce((values: {[key: number]: number}, [before, after]) => {
values[after] = before;
return values;
}, {});
const panelCount = diffResult.list.length;
const added = diffResult.added;
const getPanelAbsIndex = (panel: Panel) => {
return panel.getIndex() + (panel.getCloneIndex() + 1) * panelCount;
};
const visibleIndexes = visiblePanels.map(panel => getPanelAbsIndex(panel))
.filter(val => maintained[val % panelCount] != null);
const renderingPanels = [...visibleIndexes, ...added];
const allPanels = viewport.panelManager.allPanels();
viewport.setVisiblePanels(renderingPanels.map(index => allPanels[index]));
return renderingPanels;
}
/**
* Synchronize info of panels instance with info given by external rendering.
* @ko 외부 렌더링 방식에 의해 입력받은 패널의 정보와 현재 플리킹이 갖는 패널 정보를 동기화한다.
* @private
* @param - Info object of how panel infos are changed.<ko>패널 정보들의 변경 정보를 담는 오브젝트.</ko>
* @param - Whether called from sync method <ko> sync 메소드로부터 호출됐는지 여부 </ko>
*/
public beforeSync(diffInfo: BeforeSyncResult) {
const { maintained, added, changed, removed } = diffInfo;
const viewport = this.viewport;
const panelManager = viewport.panelManager;
const isCircular = this.options.circular;
const currentPanel = viewport.getCurrentPanel();
const cloneCount = panelManager.getCloneCount();
const prevClonedPanels = panelManager.clonedPanels();
// Update visible panels
const newVisiblePanels = viewport.getVisiblePanels()
.filter(panel => findIndex(removed, index => {
return index === panel.getIndex();
}) < 0);
viewport.setVisiblePanels(newVisiblePanels);
// Did not changed at all
if (
added.length <= 0
&& removed.length <= 0
&& changed.length <= 0
&& cloneCount === prevClonedPanels.length
) {
return this;
}
const prevOriginalPanels = panelManager.originalPanels();
const newPanels: Panel[] = [];
const newClones: Panel[][] = counter(cloneCount).map(() => []);
maintained.forEach(([beforeIdx, afterIdx]) => {
newPanels[afterIdx] = prevOriginalPanels[beforeIdx];
newPanels[afterIdx].setIndex(afterIdx);
});
added.forEach(addIndex => {
newPanels[addIndex] = new Panel(null, addIndex, this.viewport);
});
if (isCircular) {
counter(cloneCount).forEach(groupIndex => {
const prevCloneGroup = prevClonedPanels[groupIndex];
const newCloneGroup = newClones[groupIndex];
maintained.forEach(([beforeIdx, afterIdx]) => {
newCloneGroup[afterIdx] = prevCloneGroup
? prevCloneGroup[beforeIdx]
: newPanels[afterIdx].clone(groupIndex, false);
newCloneGroup[afterIdx].setIndex(afterIdx);
});
added.forEach(addIndex => {
const newPanel = newPanels[addIndex];
newCloneGroup[addIndex] = newPanel.clone(groupIndex, false);
});
});
}
added.forEach(index => { viewport.updateCheckedIndexes({ min: index, max: index }); });
removed.forEach(index => { viewport.updateCheckedIndexes({ min: index - 1, max: index + 1 }); });
const checkedIndexes = viewport.getCheckedIndexes();
checkedIndexes.forEach(([min, max], idx) => {
// Push checked indexes backward
const pushedIndex = added.filter(index => index < min && panelManager.has(index)).length
- removed.filter(index => index < min).length;
checkedIndexes.splice(idx, 1, [min + pushedIndex, max + pushedIndex]);
});
// Only effective only when there are least one panel which have changed its index
if (changed.length > 0) {
// Removed checked index by changed ones after pushing
maintained.forEach(([, next]) => { viewport.updateCheckedIndexes({ min: next, max: next }); });
}
panelManager.replacePanels(newPanels, newClones);
if (!currentPanel && newPanels.length > 0) {
viewport.setCurrentPanel(newPanels[0]);
} else if (newPanels.length <= 0) {
viewport.setCurrentPanel(undefined);
}
this.isPanelChangedAtBeforeSync = true;
}
/**
* Synchronize info of panels with DOM info given by external rendering.
* @ko 외부 렌더링 방식에 의해 입력받은 DOM의 정보와 현재 플리킹이 갖는 패널 정보를 동기화 한다.
* @private
* @param - Info object of how panel elements are changed.<ko>패널의 DOM 요소들의 변경 정보를 담는 오브젝트.</ko>
*/
public sync(diffInfo: SyncResult): this {
const { list, maintained, added, changed, removed } = diffInfo;
// Did not changed at all
if (added.length <= 0 && removed.length <= 0 && changed.length <= 0) {
return this;
}
const viewport = this.viewport;
const { renderOnlyVisible, circular } = this.options;
const panelManager = viewport.panelManager;
if (!renderOnlyVisible) {
const indexRange = panelManager.getRange();
let beforeDiffInfo: BeforeSyncResult = diffInfo;
if (circular) {
const prevOriginalPanelCount = indexRange.max;
const originalPanelCount = (list.length / (panelManager.getCloneCount() + 1)) >> 0;
const originalAdded = added.filter(index => index < originalPanelCount);
const originalRemoved = removed.filter(index => index <= prevOriginalPanelCount);
const originalMaintained = maintained.filter(([beforeIdx]) => beforeIdx <= prevOriginalPanelCount);
const originalChanged = changed.filter(([beforeIdx]) => beforeIdx <= prevOriginalPanelCount);
beforeDiffInfo = {
added: originalAdded,
maintained: originalMaintained,
removed: originalRemoved,
changed: originalChanged,
};
}
this.beforeSync(beforeDiffInfo);
}
const visiblePanels = renderOnlyVisible
? viewport.getVisiblePanels()
: this.getAllPanels(true);
added.forEach(addedIndex => {
const addedElement = list[addedIndex];
const beforePanel = visiblePanels[addedIndex] as Panel;
beforePanel.setElement(addedElement);
// As it can be 0
beforePanel.unCacheBbox();
});
if (this.isPanelChangedAtBeforeSync) {
// Reset visible panels
viewport.setVisiblePanels([]);
this.isPanelChangedAtBeforeSync = false;
}
viewport.resize();
return this;
}
private listenInput(): void {
const flicking = this;
const viewport = flicking.viewport;
const stateMachine = viewport.stateMachine;
// Set event context
flicking.eventContext = {
flicking,
viewport: flicking.viewport,
transitTo: stateMachine.transitTo,
triggerEvent: flicking.triggerEvent,
moveCamera: flicking.moveCamera,
stopCamera: viewport.stopCamera,
};
const handlers = {};
for (const key in AXES_EVENTS) {
const eventType = AXES_EVENTS[key];
handlers[eventType] = (e: any) => stateMachine.fire(eventType, e, flicking.eventContext);
}
// Connect Axes instance with PanInput
flicking.viewport.connectAxesHandler(handlers);
}
private listenResize(): void {
const options = this.options;
if (options.autoResize) {
window.addEventListener("resize", this.resize);
}
if (options.resizeOnContentsReady) {
const contentsReadyChecker = new ImReady();
contentsReadyChecker.on("preReady", () => {
this.resize();
});
contentsReadyChecker.on("readyElement", e => {
if (e.hasLoading && e.isPreReadyOver) {
this.resize();
}
});
contentsReadyChecker.on("error", e => {
this.trigger(EVENTS.CONTENT_ERROR, {
type: EVENTS.CONTENT_ERROR,
element: e.element,
});
});
contentsReadyChecker.check([this.wrapper]);
this.contentsReadyChecker = contentsReadyChecker;
}
}
private triggerEvent = <T extends FlickingEvent>(
eventName: ValueOf<Omit<EventType, "VISIBLE_CHANGE">>, // visibleChange event has no common event definition from other events
axesEvent: any,
isTrusted: boolean,
params: Partial<T> = {},
): TriggerCallback => {
const viewport = this.viewport;
let canceled: boolean = true;
// Ignore events before viewport is initialized
if (viewport) {
const state = viewport.stateMachine.getState();
const { prev, next } = viewport.getScrollArea();
const pos = viewport.getCameraPosition();
let progress = getProgress(pos, [prev, prev, next]);
if (this.options.circular) {
progress %= 1;
}
canceled = !super.trigger(eventName, merge({
type: eventName,
index: this.getIndex(),
panel: this.getCurrentPanel(),
direction: state.direction,
holding: state.holding,
progress,
axesEvent,
isTrusted,
}, params) as FlickingEvent);
}
return {
onSuccess(callback: () => void): TriggerCallback {
if (!canceled) {
callback();
}
return this;
},
onStopped(callback: () => void): TriggerCallback {
if (canceled) {
callback();
}
return this;
},
} as TriggerCallback;
}
// Return result of "move" event triggered
private moveCamera = (axesEvent: any): TriggerCallback => {
const viewport = this.viewport;
const state = viewport.stateMachine.getState();
const options = this.options;
const pos = axesEvent.pos.flick;
const previousPosition = viewport.getCameraPosition();
if (axesEvent.isTrusted && state.holding) {
const inputOffset = options.horizontal
? axesEvent.inputEvent.offsetX
: axesEvent.inputEvent.offsetY;
const isNextDirection = inputOffset < 0;
let cameraChange = pos - previousPosition;
const looped = isNextDirection === (pos < previousPosition);
if (options.circular && looped) {
// Reached at max/min range of axes
const scrollAreaSize = viewport.getScrollAreaSize();
cameraChange = (cameraChange > 0 ? -1 : 1) * (scrollAreaSize - Math.abs(cameraChange));
}
const currentDirection = cameraChange === 0
? state.direction
: cameraChange > 0
? DIRECTION.NEXT
: DIRECTION.PREV;
state.direction = currentDirection;
}
state.delta += axesEvent.delta.flick;
viewport.moveCamera(pos, axesEvent);
return this.triggerEvent(EVENTS.MOVE, axesEvent, axesEvent.isTrusted)
.onStopped(() => {
// Undo camera movement
viewport.moveCamera(previousPosition, axesEvent);
});
}
private checkContentsReady(panels: FlickingPanel[]) {
this.contentsReadyChecker?.check(panels.map(panel => panel.getElement()));
}
}
export default Flicking;