@egjs/flicking
Version:
Everyday 30 million people experience. It's reliable, flexible and extendable carousel.
626 lines (554 loc) • 22.9 kB
text/typescript
/*
* Copyright (c) 2015 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import Flicking from "../../Flicking";
import { getElementSize, getProgress, getStyle, parseAlign, setSize } from "../../utils";
import { ALIGN, DIRECTION } from "../../const/external";
import { LiteralUnion, ValueOf } from "../../type/internal";
import ElementProvider from "./provider/ElementProvider";
export interface PanelOptions {
index: number;
align: LiteralUnion<ValueOf<typeof ALIGN>> | number;
flicking: Flicking;
elementProvider: ElementProvider;
}
/**
* A slide data component that holds information of a single HTMLElement
* @ko 슬라이드 데이터 컴포넌트로, 단일 HTMLElement의 정보를 갖고 있습니다
*/
class Panel {
// Internal States
protected _flicking: Flicking;
protected _elProvider: ElementProvider;
protected _index: number;
protected _pos: number;
protected _size: number;
protected _height: number;
protected _margin: { prev: number; next: number };
protected _alignPos: number; // Actual align pos
protected _rendered: boolean;
protected _removed: boolean;
protected _loading: boolean;
protected _toggleDirection: ValueOf<typeof DIRECTION>;
protected _toggled: boolean;
protected _togglePosition: number;
// Options
protected _align: PanelOptions["align"];
// Internal States Getter
/**
* `HTMLElement` that panel's referencing
* @ko 패널이 참조하고 있는 `HTMLElement`
* @type {HTMLElement}
* @readonly
*/
public get element() { return this._elProvider.element; }
/**
* @internal
* @readonly
*/
public get elementProvider() { return this._elProvider; }
/**
* Index of the panel
* @ko 패널의 인덱스
* @type {number}
* @readonly
*/
public get index() { return this._index; }
/**
* Position of the panel, including {@link Panel#alignPosition alignPosition}
* @ko 패널의 현재 좌표, {@link Panel#alignPosition alignPosition}을 포함하고 있습니다
* @type {number}
* @readonly
*/
public get position() { return this._pos + this._alignPos; }
/**
* Cached size of the panel element
* This is equal to {@link Panel#element element}'s `offsetWidth` if {@link Flicking#horizontal horizontal} is `true`, and `offsetHeight` else
* @ko 패널 엘리먼트의 캐시된 크기
* 이 값은 {@link Flicking#horizontal horizontal}이 `true`일 경우 {@link Panel#element element}의 `offsetWidth`와 동일하고, `false`일 경우 `offsetHeight`와 동일합니다
* @type {number}
* @readonly
*/
public get size() { return this._size; }
/**
* Panel's size including CSS `margin`
* This value includes {@link Panel#element element}'s margin left/right if {@link Flicking#horizontal horizontal} is `true`, and margin top/bottom else
* @ko CSS `margin`을 포함한 패널의 크기
* 이 값은 {@link Flicking#horizontal horizontal}이 `true`일 경우 margin left/right을 포함하고, `false`일 경우 margin top/bottom을 포함합니다
* @type {number}
* @readonly
*/
public get sizeIncludingMargin() { return this._size + this._margin.prev + this._margin.next; }
/**
* Height of the panel element
* @ko 패널 엘리먼트의 높이
* @type {number}
* @readonly
*/
public get height() { return this._height; }
/**
* Cached CSS `margin` value of the panel element
* @ko 패널 엘리먼트의 CSS `margin` 값
* @type {object}
* @property {number} prev CSS `margin-left` when the {@link Flicking#horizontal horizontal} is `true`, and `margin-top` else
* <ko>{@link Flicking#horizontal horizontal}이 `true`일 경우 `margin-left`, `false`일 경우 `margin-top`에 해당하는 값</ko>
* @property {number} next CSS `margin-right` when the {@link Flicking#horizontal horizontal} is `true`, and `margin-bottom` else
* <ko>{@link Flicking#horizontal horizontal}이 `true`일 경우 `margin-right`, `false`일 경우 `margin-bottom`에 해당하는 값</ko>
* @readonly
*/
public get margin() { return this._margin; }
/**
* Align position inside the panel where {@link Camera}'s {@link Camera#alignPosition alignPosition} inside viewport should be located at
* @ko 패널의 정렬 기준 위치. {@link Camera}의 뷰포트 내에서의 {@link Camera#alignPosition alignPosition}이 위치해야 하는 곳입니다
* @type {number}
* @readonly
*/
public get alignPosition() { return this._alignPos; }
/**
* A value indicating whether the panel's {@link Flicking#remove remove}d
* @ko 패널이 {@link Flicking#remove remove}되었는지 여부를 나타내는 값
* @type {boolean}
* @readonly
*/
public get removed() { return this._removed; }
/**
* A value indicating whether the panel's element is being rendered on the screen
* @ko 패널의 엘리먼트가 화면상에 렌더링되고있는지 여부를 나타내는 값
* @type {boolean}
* @readonly
*/
public get rendered() { return this._rendered; }
/**
* A value indicating whether the panel's image/video is not loaded and waiting for resize
* @ko 패널 내부의 이미지/비디오가 아직 로드되지 않아 {@link Panel#resize resize}될 것인지를 나타내는 값
* @type {boolean}
* @readonly
*/
public get loading() { return this._loading; }
/**
* Panel element's range of the bounding box
* @ko 패널 엘리먼트의 Bounding box 범위
* @type {object}
* @property {number} [min] Bounding box's left({@link Flicking#horizontal horizontal}: true) / top({@link Flicking#horizontal horizontal}: false)
* @property {number} [max] Bounding box's right({@link Flicking#horizontal horizontal}: true) / bottom({@link Flicking#horizontal horizontal}: false)
* @readonly
*/
public get range() { return { min: this._pos, max: this._pos + this._size }; }
/**
* A value indicating whether the panel's position is toggled by circular behavior
* @ko 패널의 위치가 circular 동작에 의해 토글되었는지 여부를 나타내는 값
* @type {boolean}
* @readonly
*/
public get toggled() { return this._toggled; }
/**
* A direction where the panel's position is toggled
* @ko 패널의 위치가 circular 동작에 의해 토글되는 방향
* @type {DIRECTION}
* @readonly
*/
public get toggleDirection() { return this._toggleDirection; }
/**
* Actual position offset determined by {@link Panel#order}
* @ko {@link Panel#order}에 의한 실제 위치 변경값
* @type {number}
* @readonly
*/
public get offset() {
const toggleDirection = this._toggleDirection;
const cameraRangeDiff = this._flicking.camera.rangeDiff;
return toggleDirection === DIRECTION.NONE || !this._toggled
? 0
: toggleDirection === DIRECTION.PREV
? -cameraRangeDiff
: cameraRangeDiff;
}
/**
* Progress of movement between previous or next panel relative to current panel
* @ko 이 패널로부터 이전/다음 패널으로의 이동 진행률
* @type {number}
* @readonly
*/
public get progress() {
const flicking = this._flicking;
return this.index - flicking.camera.progress;
}
/**
* Progress of movement between points that panel is completely invisible outside of viewport(prev direction: -1, selected point: 0, next direction: 1)
* @ko 현재 패널이 뷰포트 영역 밖으로 완전히 사라지는 지점을 기준으로 하는 진행도(prev방향: -1, 선택 지점: 0, next방향: 1)
* @type {number}
* @readonly
*/
public get outsetProgress() {
const position = this.position + this.offset;
const alignPosition = this._alignPos;
const camera = this._flicking.camera;
const camPos = camera.position;
if (camPos === position) {
return 0;
}
if (camPos < position) {
const disappearPosNext = position + (camera.size - camera.alignPosition) + alignPosition;
return -getProgress(camPos, position, disappearPosNext);
} else {
const disappearPosPrev = position - (camera.alignPosition + this._size - alignPosition);
return 1 - getProgress(camPos, disappearPosPrev, position);
}
}
/**
* Percentage of area where panel is visible in the viewport
* @ko 뷰포트 안에서 패널이 보이는 영역의 비율
* @type {number}
* @readonly
*/
public get visibleRatio() {
const range = this.range;
const size = this._size;
const offset = this.offset;
const visibleRange = this._flicking.camera.visibleRange;
const checkingRange = {
min: range.min + offset,
max: range.max + offset
};
if (checkingRange.max <= visibleRange.min || checkingRange.min >= visibleRange.max) {
return 0;
}
let visibleSize = size;
if (visibleRange.min > checkingRange.min) {
visibleSize -= visibleRange.min - checkingRange.min;
}
if (visibleRange.max < checkingRange.max) {
visibleSize -= checkingRange.max - visibleRange.max;
}
return visibleSize / size;
}
public set loading(val: boolean) { this._loading = val; }
// Options Getter
/**
* A value indicating where the {@link Panel#alignPosition alignPosition} should be located at inside the panel element
* @ko {@link Panel#alignPosition alignPosition}이 패널 내의 어디에 위치해야 하는지를 나타내는 값
* @type {Constants.ALIGN | string | number}
*/
public get align() { return this._align; }
// Options Setter
public set align(val: PanelOptions["align"]) {
this._align = val;
this._updateAlignPos();
}
/**
* @param {object} options An options object<ko>옵션 오브젝트</ko>
* @param {number} [options.index] An initial index of the panel<ko>패널의 초기 인덱스</ko>
* @param {Constants.ALIGN | string | number} [options.align] An initial {@link Flicking#align align} value of the panel<ko>패널의 초기 {@link Flicking#align align}값</ko>
* @param {Flicking} [options.flicking] A Flicking instance panel's referencing<ko>패널이 참조하는 {@link Flicking} 인스턴스</ko>
* @param {Flicking} [options.elementProvider] A provider instance that redirects elements<ko>실제 엘리먼트를 반환하는 엘리먼트 공급자의 인스턴스</ko>
*/
public constructor({
index,
align,
flicking,
elementProvider
}: PanelOptions) {
this._index = index;
this._flicking = flicking;
this._elProvider = elementProvider;
this._align = align;
this._removed = false;
this._rendered = true;
this._loading = false;
this._resetInternalStates();
}
/**
* Mark panel element to be appended on the camera element
* @internal
*/
public markForShow() {
this._rendered = true;
this._elProvider.show(this._flicking);
}
/**
* Mark panel element to be removed from the camera element
* @internal
*/
public markForHide() {
this._rendered = false;
this._elProvider.hide(this._flicking);
}
/**
* Update size of the panel
* @ko 패널의 크기를 갱신합니다
* @param {object} cached Predefined cached size of the panel<ko>사전에 캐시된 패널의 크기 정보</ko>
* @chainable
* @return {this}
*/
public resize(cached?: {
size: number;
height?: number;
margin: { prev: number; next: number };
}): this {
const el = this.element;
const flicking = this._flicking;
const {
horizontal,
useFractionalSize
} = flicking;
if (cached) {
this._size = cached.size;
this._margin = { ...cached.margin };
this._height = cached.height ?? getElementSize({
el,
horizontal: false,
useFractionalSize,
useOffset: true,
style: getStyle(el)
});
} else {
const elStyle = getStyle(el);
this._size = getElementSize({
el,
horizontal,
useFractionalSize,
useOffset: true,
style: elStyle
});
this._margin = horizontal
? {
prev: parseFloat(elStyle.marginLeft || "0"),
next: parseFloat(elStyle.marginRight || "0")
} : {
prev: parseFloat(elStyle.marginTop || "0"),
next: parseFloat(elStyle.marginBottom || "0")
};
this._height = horizontal
? getElementSize({
el,
horizontal: false,
useFractionalSize,
useOffset: true,
style: elStyle
})
: this._size;
}
this.updatePosition();
this._updateAlignPos();
return this;
}
/**
* Change panel's size. This will change the actual size of the panel element by changing its CSS width/height property
* @ko 패널 크기를 변경합니다. 패널 엘리먼트에 해당 크기의 CSS width/height를 적용합니다
* @param {object} [size] New panel size<ko>새 패널 크기</ko>
* @param {number|string} [size.width] CSS string or number(in px)<ko>CSS 문자열 또는 숫자(px)</ko>
* @param {number|string} [size.height] CSS string or number(in px)<ko>CSS 문자열 또는 숫자(px)</ko>
* @chainable
* @return {this}
*/
public setSize(size: Partial<{
width: number | string;
height: number | string;
}>): this {
setSize(this.element, size);
return this;
}
/**
* Check whether the given element is inside of this panel's {@link Panel#element element}
* @ko 해당 엘리먼트가 이 패널의 {@link Panel#element element} 내에 포함되어 있는지를 반환합니다
* @param {HTMLElement} element The HTMLElement to check<ko>확인하고자 하는 HTMLElement</ko>
* @return {boolean} A Boolean value indicating the element is inside of this panel {@link Panel#element element}<ko>패널의 {@link Panel#element element}내에 해당 엘리먼트 포함 여부</ko>
*/
public contains(element: HTMLElement): boolean {
return !!this.element?.contains(element);
}
/**
* Reset internal state and set {@link Panel#removed removed} to `true`
* @ko 내부 상태를 초기화하고 {@link Panel#removed removed}를 `true`로 설정합니다.
* @return {void}
*/
public destroy(): void {
this._resetInternalStates();
this._removed = true;
}
/**
* Check whether the given position is inside of this panel's {@link Panel#range range}
* @ko 주어진 좌표가 현재 패널의 {@link Panel#range range}내에 속해있는지를 반환합니다.
* @param {number} pos A position to check<ko>확인하고자 하는 좌표</ko>
* @param {boolean} [includeMargin=false] Include {@link Panel#margin margin} to the range<ko>패널 영역에 {@link Panel#margin margin}값을 포함시킵니다</ko>
* @return {boolean} A Boolean value indicating whether the given position is included in the panel range<ko>해당 좌표가 패널 영역 내에 속해있는지 여부</ko>
*/
public includePosition(pos: number, includeMargin: boolean = false): boolean {
return this.includeRange(pos, pos, includeMargin);
}
/**
* Check whether the given range is fully included in this panel's area (inclusive)
* @ko 주어진 범위가 이 패널 내부에 완전히 포함되는지를 반환합니다
* @param {number} min Minimum value of the range to check<ko>확인하고자 하는 최소 범위</ko>
* @param {number} max Maximum value of the range to check<ko>확인하고자 하는 최대 범위</ko>
* @param {boolean} [includeMargin=false] Include {@link Panel#margin margin} to the range<ko>패널 영역에 {@link Panel#margin margin}값을 포함시킵니다</ko>
* @returns {boolean} A Boolean value indicating whether the given range is fully included in the panel range<ko>해당 범위가 패널 영역 내에 완전히 속해있는지 여부</ko>
*/
public includeRange(min: number, max: number, includeMargin: boolean = false): boolean {
const margin = this._margin;
const panelRange = this.range;
if (includeMargin) {
panelRange.min -= margin.prev;
panelRange.max += margin.next;
}
return max >= panelRange.min && min <= panelRange.max;
}
/**
* Check whether the panel is visble in the given range (exclusive)
* @ko 주어진 범위 내에서 이 패널의 일부가 보여지는지를 반환합니다
* @param {number} min Minimum value of the range to check<ko>확인하고자 하는 최소 범위</ko>
* @param {number} max Maximum value of the range to check<ko>확인하고자 하는 최대 범위</ko>
* @returns {boolean} A Boolean value indicating whether the panel is visible<ko>해당 범위 내에서 패널을 볼 수 있는지 여부</ko>
*/
public isVisibleOnRange(min: number, max: number): boolean {
const panelRange = this.range;
return max > panelRange.min && min < panelRange.max;
}
/**
* Move {@link Camera} to this panel
* @ko {@link Camera}를 이 패널로 이동합니다
* @param {number} [duration] Duration of the animation (unit: ms)<ko>애니메이션 진행 시간 (단위: ms)</ko>
* @returns {Promise<void>} A Promise which will be resolved after reaching the panel<ko>패널 도달시에 resolve되는 Promise</ko>
*/
public focus(duration?: number) {
return this._flicking.moveTo(this._index, duration);
}
/**
* Get previous(`index - 1`) panel. When the previous panel does not exist, this will return `null` instead
* If the {@link Flicking#circularEnabled circular} is enabled, this will return the last panel if called from the first panel
* @ko 이전(`index - 1`) 패널을 반환합니다. 이전 패널이 없을 경우 `null`을 반환합니다
* {@link Flicking#circularEnabled circular} 모드가 활성화되었을 때 첫번째 패널에서 이 메소드를 호출할 경우 마지막 패널을 반환합니다
* @returns {Panel | null} The previous panel<ko>이전 패널</ko>
*/
public prev(): Panel | null {
const index = this._index;
const flicking = this._flicking;
const renderer = flicking.renderer;
const panelCount = renderer.panelCount;
if (panelCount === 1) return null;
return flicking.circularEnabled
? renderer.getPanel(index === 0 ? panelCount - 1 : index - 1)
: renderer.getPanel(index - 1);
}
/**
* Get next(`index + 1`) panel. When the next panel does not exist, this will return `null` instead
* If the {@link Flicking#circularEnabled circular} is enabled, this will return the first panel if called from the last panel
* @ko 다음(`index + 1`) 패널을 반환합니다. 다음 패널이 없을 경우 `null`을 반환합니다
* {@link Flicking#circularEnabled circular} 모드가 활성화되었을 때 마지막 패널에서 이 메소드를 호출할 경우 첫번째 패널을 반환합니다
* @returns {Panel | null} The previous panel<ko>다음 패널</ko>
*/
public next(): Panel | null {
const index = this._index;
const flicking = this._flicking;
const renderer = flicking.renderer;
const panelCount = renderer.panelCount;
if (panelCount === 1) return null;
return flicking.circularEnabled
? renderer.getPanel(index === panelCount - 1 ? 0 : index + 1)
: renderer.getPanel(index + 1);
}
/**
* Increase panel's index by the given value
* @ko 패널의 인덱스를 주어진 값만큼 증가시킵니다
* @internal
* @chainable
* @param val An integer greater than or equal to 0<ko>0보다 같거나 큰 정수</ko>
* @returns {this}
*/
public increaseIndex(val: number): this {
this._index += Math.max(val, 0);
return this;
}
/**
* Decrease panel's index by the given value
* @ko 패널의 인덱스를 주어진 값만큼 감소시킵니다
* @internal
* @chainable
* @param val An integer greater than or equal to 0<ko>0보다 같거나 큰 정수</ko>
* @returns {this}
*/
public decreaseIndex(val: number): this {
this._index -= Math.max(val, 0);
return this;
}
/**
* @internal
*/
public updatePosition(): this {
const prevPanel = this._flicking.renderer.panels[this._index - 1];
this._pos = prevPanel
? prevPanel.range.max + prevPanel.margin.next + this._margin.prev
: this._margin.prev;
return this;
}
/**
* @internal
* @return {boolean} toggled
*/
public toggle(prevPos: number, newPos: number): boolean {
const toggleDirection = this._toggleDirection;
const togglePosition = this._togglePosition;
if (toggleDirection === DIRECTION.NONE || newPos === prevPos) return false;
const prevToggled = this._toggled;
if (newPos > prevPos) {
if (togglePosition >= prevPos && togglePosition <= newPos) {
this._toggled = toggleDirection === DIRECTION.NEXT;
}
} else {
if (togglePosition <= prevPos && togglePosition >= newPos) {
this._toggled = toggleDirection !== DIRECTION.NEXT;
}
}
return prevToggled !== this._toggled;
}
/**
* @internal
*/
public updateCircularToggleDirection(): this {
const flicking = this._flicking;
if (!flicking.circularEnabled) {
this._toggleDirection = DIRECTION.NONE;
this._togglePosition = 0;
this._toggled = false;
return this;
}
const camera = flicking.camera;
const camRange = camera.range;
const camAlignPosition = camera.alignPosition;
const camVisibleRange = camera.visibleRange;
const camVisibleSize = camVisibleRange.max - camVisibleRange.min;
const minimumVisible = camRange.min - camAlignPosition;
const maximumVisible = camRange.max - camAlignPosition + camVisibleSize;
const shouldBeVisibleAtMin = this.includeRange(maximumVisible - camVisibleSize, maximumVisible, false);
const shouldBeVisibleAtMax = this.includeRange(minimumVisible, minimumVisible + camVisibleSize, false);
this._toggled = false;
if (shouldBeVisibleAtMin) {
this._toggleDirection = DIRECTION.PREV;
this._togglePosition = this.range.max + camRange.min - camRange.max + camAlignPosition;
this.toggle(Infinity, camera.position);
} else if (shouldBeVisibleAtMax) {
this._toggleDirection = DIRECTION.NEXT;
this._togglePosition = this.range.min + camRange.max - camVisibleSize + camAlignPosition;
this.toggle(-Infinity, camera.position);
} else {
this._toggleDirection = DIRECTION.NONE;
this._togglePosition = 0;
}
return this;
}
private _updateAlignPos() {
this._alignPos = parseAlign(this._align, this._size);
}
private _resetInternalStates() {
this._size = 0;
this._pos = 0;
this._margin = { prev: 0, next: 0 };
this._height = 0;
this._alignPos = 0;
this._toggled = false;
this._togglePosition = 0;
this._toggleDirection = DIRECTION.NONE;
}
}
export default Panel;