@egjs/flicking
Version:
Everyday 30 million people experience. It's reliable, flexible and extendable carousel.
7,047 lines • 228 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __reflectGet = Reflect.get;
var __pow = Math.pow;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj);
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
import Component, { ComponentEvent } from "@egjs/component";
import Axes, { PanInput } from "@egjs/axes";
import ImReady from "@egjs/imready";
const ALIGN = {
/** left/top align */
PREV: "prev",
/** center align */
CENTER: "center",
/** right/bottom align */
NEXT: "next"
};
const DIRECTION = {
/** "left" when {@link Flicking.horizontal | horizontal} is true, and "top" when {@link Flicking.horizontal | horizontal} is false */
PREV: "PREV",
/** "right" when {@link Flicking.horizontal | horizontal} is true, and "bottom" when {@link Flicking.horizontal | horizontal} is false */
NEXT: "NEXT",
/** This value usually means it's the same position */
NONE: null
};
const MOVE_TYPE = {
/** Flicking's {@link Flicking.moveType | moveType} that enables {@link SnapControl} as a Flicking's {@link Flicking.control | control} */
SNAP: "snap",
/** Flicking's {@link Flicking.moveType | moveType} that enables {@link FreeControl} as a Flicking's {@link Flicking.control | control} */
FREE_SCROLL: "freeScroll",
/** Flicking's {@link Flicking.moveType | moveType} that enables {@link StrictControl} as a Flicking's {@link Flicking.control | control} */
STRICT: "strict"
};
const CLASS = {
VIEWPORT: "flicking-viewport",
CAMERA: "flicking-camera",
VERTICAL: "vertical",
HIDDEN: "flicking-hidden",
DEFAULT_VIRTUAL: "flicking-panel"
};
const CIRCULAR_FALLBACK = {
/** "linear" */
LINEAR: "linear",
/** "bound" */
BOUND: "bound"
};
const ORDER = {
/** "ltr" */
LTR: "ltr",
/** "rtl" */
RTL: "rtl"
};
const MOVE_DIRECTION = {
/** horizontal */
HORIZONTAL: "horizontal",
/** vertical */
VERTICAL: "vertical"
};
class AnchorPoint {
/**
* Index of AnchorPoint
* @readonly
*/
get index() {
return this._index;
}
/**
* Position of AnchorPoint
* @readonly
*/
get position() {
return this._pos;
}
/**
* A {@link Panel} instance AnchorPoint is referencing to
* @readonly
*/
get panel() {
return this._panel;
}
/**
* @param options - {@link AnchorPointOptions}
*/
constructor(options) {
const { index, position, panel } = options;
this._index = index;
this._pos = position;
this._panel = panel;
}
}
const errors = {
WRONG_TYPE: {
code: 0,
message: (wrongVal, correctTypes) => `${wrongVal}(${typeof wrongVal}) is not a ${correctTypes.map((type) => `"${type}"`).join(" or ")}.`
},
ELEMENT_NOT_FOUND: {
code: 1,
message: (selector) => `Element with selector "${selector}" not found.`
},
VAL_MUST_NOT_NULL: {
code: 2,
message: (val, name) => `${name} should be provided. Given: ${val}`
},
NOT_ATTACHED_TO_FLICKING: {
code: 3,
message: 'This module is not attached to the Flicking instance. "init()" should be called first.'
},
WRONG_OPTION: {
code: 4,
message: (optionName, val) => `Option "${optionName}" is not in correct format, given: ${val}`
},
INDEX_OUT_OF_RANGE: {
code: 5,
message: (val, min, max) => `Index "${val}" is out of range: should be between ${min} and ${max}.`
},
POSITION_NOT_REACHABLE: {
code: 6,
message: (position) => `Position "${position}" is not reachable.`
},
TRANSFORM_NOT_SUPPORTED: {
code: 7,
message: "Browser does not support CSS transform."
},
STOP_CALLED_BY_USER: {
code: 8,
message: "Event stop() is called by user."
},
ANIMATION_INTERRUPTED: {
code: 9,
message: "Animation is interrupted by user input."
},
ANIMATION_ALREADY_PLAYING: {
code: 10,
message: "Animation is already playing."
},
NOT_ALLOWED_IN_FRAMEWORK: {
code: 11,
message: "This behavior is not allowed in the frameworks like React, Vue, or Angular."
},
NOT_INITIALIZED: {
code: 12,
message: "Flicking is not initialized yet, call init() first."
},
NO_ACTIVE: {
code: 13,
message: "There's no active panel that Flicking has selected. This may be due to the absence of any panels."
},
NOT_ALLOWED_IN_VIRTUAL: {
code: 14,
message: "This behavior is not allowed when the virtual option is enabled"
}
};
const CODE = Object.keys(errors).reduce(
(acc, key) => {
acc[key] = errors[key].code;
return acc;
},
{}
);
const MESSAGE = Object.keys(errors).reduce(
(acc, key) => {
acc[key] = errors[key].message;
return acc;
},
{}
);
const merge = (target, ...sources) => {
sources.forEach((source) => {
Object.keys(source).forEach((key) => {
target[key] = source[key];
});
});
return target;
};
const getElement = (el, parent) => {
let targetEl = null;
if (isString(el)) {
const parentEl = parent ? parent : document;
const queryResult = parentEl.querySelector(el);
if (!queryResult) {
throw new FlickingError(MESSAGE.ELEMENT_NOT_FOUND(el), CODE.ELEMENT_NOT_FOUND);
}
targetEl = queryResult;
} else if (el && el.nodeType === Node.ELEMENT_NODE) {
targetEl = el;
}
if (!targetEl) {
throw new FlickingError(MESSAGE.WRONG_TYPE(el, ["HTMLElement", "string"]), CODE.WRONG_TYPE);
}
return targetEl;
};
const checkExistence = (value, nameOnErrMsg) => {
if (value == null) {
throw new FlickingError(MESSAGE.VAL_MUST_NOT_NULL(value, nameOnErrMsg), CODE.VAL_MUST_NOT_NULL);
}
};
const clamp = (x, min, max) => Math.max(Math.min(x, max), min);
const getFlickingAttached = (val) => {
if (!val) {
throw new FlickingError(MESSAGE.NOT_ATTACHED_TO_FLICKING, CODE.NOT_ATTACHED_TO_FLICKING);
}
return val;
};
const toArray = (iterable) => [].slice.call(iterable);
const parseAlign$1 = (align, size) => {
let alignPoint;
if (isString(align)) {
switch (align) {
case ALIGN.PREV:
alignPoint = 0;
break;
case ALIGN.CENTER:
alignPoint = 0.5 * size;
break;
case ALIGN.NEXT:
alignPoint = size;
break;
default:
alignPoint = parseArithmeticSize(align, size);
if (alignPoint == null) {
throw new FlickingError(MESSAGE.WRONG_OPTION("align", align), CODE.WRONG_OPTION);
}
}
} else {
alignPoint = align;
}
return alignPoint;
};
const parseBounce = (bounce, size) => {
let parsedBounce;
if (Array.isArray(bounce)) {
parsedBounce = bounce.map((val) => parseArithmeticSize(val, size));
} else {
const parsedVal = parseArithmeticSize(bounce, size);
parsedBounce = [parsedVal, parsedVal];
}
return parsedBounce.map((val) => {
if (val == null) {
throw new FlickingError(MESSAGE.WRONG_OPTION("bounce", bounce), CODE.WRONG_OPTION);
}
return val;
});
};
const parseArithmeticSize = (cssValue, base) => {
const parsed = parseArithmeticExpression(cssValue);
if (parsed == null) return null;
return parsed.percentage * base + parsed.absolute;
};
const parseArithmeticExpression = (cssValue) => {
const cssRegex = /(?:(\+|-)\s*)?(\d+(?:\.\d+)?(%|px)?)/g;
if (typeof cssValue === "number") {
return { percentage: 0, absolute: cssValue };
}
const parsed = {
percentage: 0,
absolute: 0
};
let idx = 0;
let matchResult = cssRegex.exec(cssValue);
while (matchResult != null) {
let sign = matchResult[1];
const value = matchResult[2];
const unit = matchResult[3];
const parsedValue = parseFloat(value);
if (idx <= 0) {
sign = sign || "+";
}
if (!sign) {
return null;
}
const signMultiplier = sign === "+" ? 1 : -1;
if (unit === "%") {
parsed.percentage += signMultiplier * (parsedValue / 100);
} else {
parsed.absolute += signMultiplier * parsedValue;
}
++idx;
matchResult = cssRegex.exec(cssValue);
}
if (idx === 0) {
return null;
}
return parsed;
};
const parseCSSSizeValue = (val) => isString(val) ? val : `${val}px`;
const parsePanelAlign = (align) => typeof align === "object" ? align.panel : align;
const getDirection = (start, end) => {
if (start === end) return DIRECTION.NONE;
return start < end ? DIRECTION.NEXT : DIRECTION.PREV;
};
const parseElement = (element) => {
if (!Array.isArray(element)) {
element = [element];
}
const elements = [];
element.forEach((el) => {
if (isString(el)) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = el;
elements.push(...toArray(tempDiv.children));
while (tempDiv.firstChild) {
tempDiv.removeChild(tempDiv.firstChild);
}
} else if (el && el.nodeType === Node.ELEMENT_NODE) {
elements.push(el);
} else {
throw new FlickingError(MESSAGE.WRONG_TYPE(el, ["HTMLElement", "string"]), CODE.WRONG_TYPE);
}
});
return elements;
};
const getMinusCompensatedIndex = (idx, max) => idx < 0 ? clamp(idx + max, 0, max) : clamp(idx, 0, max);
const includes = (array, target) => {
for (const val of array) {
if (val === target) return true;
}
return false;
};
const isString = (val) => typeof val === "string";
const circulatePosition = (pos, min, max) => {
const size = max - min;
if (pos < min) {
const offset = (min - pos) % size;
pos = max - offset;
} else if (pos > max) {
const offset = (pos - max) % size;
pos = min + offset;
}
return pos;
};
const find = (array, checker) => {
for (const val of array) {
if (checker(val)) {
return val;
}
}
return null;
};
const findRight = (array, checker) => {
for (let idx = array.length - 1; idx >= 0; idx--) {
const val = array[idx];
if (checker(val)) {
return val;
}
}
return null;
};
const findIndex = (array, checker) => {
for (let idx = 0; idx < array.length; idx++) {
if (checker(array[idx])) {
return idx;
}
}
return -1;
};
const getProgress$1 = (pos, prev, next) => (pos - prev) / (next - prev);
const getStyle = (el) => {
if (!el) {
return {};
}
return window.getComputedStyle(el) || el.currentStyle;
};
const setSize = (el, {
width,
height
}) => {
if (!el) {
return;
}
if (width != null) {
if (isString(width)) {
el.style.width = width;
} else {
el.style.width = `${width}px`;
}
}
if (height != null) {
if (isString(height)) {
el.style.height = height;
} else {
el.style.height = `${height}px`;
}
}
};
const isBetween = (val, min, max) => val >= min && val <= max;
const circulateIndex = (index, max) => {
if (index >= max) {
return index % max;
} else if (index < 0) {
return getMinusCompensatedIndex((index + 1) % max - 1, max);
} else {
return index;
}
};
const range = (end) => {
const arr = new Array(end);
for (let i = 0; i < end; i++) {
arr[i] = i;
}
return arr;
};
const getElementSize = ({
el,
horizontal,
useFractionalSize,
useOffset,
style
}) => {
let size = 0;
if (useFractionalSize) {
const baseSize = parseFloat(horizontal ? style.width : style.height) || 0;
const isBorderBoxSizing = style.boxSizing === "border-box";
const border = horizontal ? parseFloat(style.borderLeftWidth || "0") + parseFloat(style.borderRightWidth || "0") : parseFloat(style.borderTopWidth || "0") + parseFloat(style.borderBottomWidth || "0");
if (isBorderBoxSizing) {
size = useOffset ? baseSize : baseSize - border;
} else {
const padding = horizontal ? parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0") : parseFloat(style.paddingTop || "0") + parseFloat(style.paddingBottom || "0");
size = useOffset ? baseSize + padding + border : baseSize + padding;
}
} else {
const sizeStr = horizontal ? "Width" : "Height";
size = useOffset ? el[`offset${sizeStr}`] : el[`client${sizeStr}`];
}
return Math.max(size, 0);
};
const setPrototypeOf = Object.setPrototypeOf || ((obj, proto) => {
obj.__proto__ = proto;
return obj;
});
const camelize = (str) => {
return str.replace(/[\s-_]([a-z])/g, (all, letter) => letter.toUpperCase());
};
const getDataAttributes = (element, attributePrefix) => {
const dataAttributes = {};
const attributes = element.attributes;
const length = attributes.length;
for (let i = 0; i < length; ++i) {
const attribute = attributes[i];
const { name, value } = attribute;
if (name.indexOf(attributePrefix) === -1) {
continue;
}
dataAttributes[camelize(name.replace(attributePrefix, ""))] = value;
}
return dataAttributes;
};
class FlickingError extends Error {
/**
* @param message - Error message
* @param code - Error code
*/
constructor(message, code) {
super(message);
setPrototypeOf(this, FlickingError.prototype);
this.name = "FlickingError";
this.code = code;
}
}
const EVENTS = {
/** ready event */
READY: "ready",
/** beforeResize event */
BEFORE_RESIZE: "beforeResize",
/** afterResize event */
AFTER_RESIZE: "afterResize",
/** holdStart event */
HOLD_START: "holdStart",
/** holdEnd event */
HOLD_END: "holdEnd",
/** moveStart event */
MOVE_START: "moveStart",
/** move event */
MOVE: "move",
/** moveEnd event */
MOVE_END: "moveEnd",
/** willChange event */
WILL_CHANGE: "willChange",
/** changed event */
CHANGED: "changed",
/** willRestore event */
WILL_RESTORE: "willRestore",
/** restored event */
RESTORED: "restored",
/** select event */
SELECT: "select",
/** needPanel event */
NEED_PANEL: "needPanel",
/** visibleChange event */
VISIBLE_CHANGE: "visibleChange",
/** reachEdge event */
REACH_EDGE: "reachEdge",
/**
* panelChange event
* @since 4.1.0
*/
PANEL_CHANGE: "panelChange"
};
class CameraMode {
constructor(flicking) {
this._flicking = flicking;
}
getAnchors() {
const panels = this._flicking.renderer.panels;
return panels.map(
(panel, index) => new AnchorPoint({
index,
position: panel.position,
panel
})
);
}
findAnchorIncludePosition(position) {
const anchors = this._flicking.camera.anchorPoints;
const anchorsIncludingPosition = anchors.filter((anchor) => anchor.panel.includePosition(position, true));
return anchorsIncludingPosition.reduce((nearest, anchor) => {
if (!nearest) return anchor;
return Math.abs(nearest.position - position) < Math.abs(anchor.position - position) ? nearest : anchor;
}, null);
}
findNearestAnchor(position) {
const anchors = this._flicking.camera.anchorPoints;
if (anchors.length <= 0) return null;
let prevDist = Infinity;
for (let anchorIdx = 0; anchorIdx < anchors.length; anchorIdx++) {
const anchor = anchors[anchorIdx];
const dist = Math.abs(anchor.position - position);
if (dist > prevDist) {
return anchors[anchorIdx - 1];
}
prevDist = dist;
}
return anchors[anchors.length - 1];
}
clampToReachablePosition(position) {
const camera = this._flicking.camera;
const range2 = camera.range;
return clamp(position, range2.min, range2.max);
}
getCircularOffset() {
return 0;
}
canReach(panel) {
const camera = this._flicking.camera;
const range2 = camera.range;
if (panel.removed) return false;
const panelPos = panel.position;
return panelPos >= range2.min && panelPos <= range2.max;
}
canSee(panel) {
const camera = this._flicking.camera;
const visibleRange = camera.visibleRange;
return panel.isVisibleOnRange(visibleRange.min, visibleRange.max);
}
}
class BoundCameraMode extends CameraMode {
checkAvailability() {
const flicking = this._flicking;
const renderer = flicking.renderer;
const firstPanel = renderer.getPanel(0);
const lastPanel = renderer.getPanel(renderer.panelCount - 1);
if (!firstPanel || !lastPanel) {
return false;
}
const viewportSize = flicking.camera.size;
const firstPanelPrev = firstPanel.range.min;
const lastPanelNext = lastPanel.range.max;
const panelAreaSize = lastPanelNext - firstPanelPrev;
const isBiggerThanViewport = viewportSize < panelAreaSize;
return isBiggerThanViewport;
}
getRange() {
const flicking = this._flicking;
const renderer = flicking.renderer;
const alignPos = flicking.camera.alignPosition;
const firstPanel = renderer.getPanel(0);
const lastPanel = renderer.getPanel(renderer.panelCount - 1);
if (!firstPanel || !lastPanel) {
return { min: 0, max: 0 };
}
const viewportSize = flicking.camera.size;
const firstPanelPrev = firstPanel.range.min;
const lastPanelNext = lastPanel.range.max;
const panelAreaSize = lastPanelNext - firstPanelPrev;
const isBiggerThanViewport = viewportSize < panelAreaSize;
const firstPos = firstPanelPrev + alignPos;
const lastPos = lastPanelNext - viewportSize + alignPos;
if (isBiggerThanViewport) {
return { min: firstPos, max: lastPos };
} else {
const align = flicking.camera.align;
const alignVal = typeof align === "object" ? align.camera : align;
const pos = firstPos + parseAlign$1(alignVal, lastPos - firstPos);
return { min: pos, max: pos };
}
}
getAnchors() {
const flicking = this._flicking;
const camera = flicking.camera;
const panels = flicking.renderer.panels;
if (panels.length <= 0) {
return [];
}
const range2 = flicking.camera.range;
const reachablePanels = panels.filter((panel) => camera.canReach(panel));
if (reachablePanels.length > 0) {
const shouldPrependBoundAnchor = reachablePanels[0].position !== range2.min;
const shouldAppendBoundAnchor = reachablePanels[reachablePanels.length - 1].position !== range2.max;
const indexOffset = shouldPrependBoundAnchor ? 1 : 0;
const newAnchors = reachablePanels.map(
(panel, idx) => new AnchorPoint({
index: idx + indexOffset,
position: panel.position,
panel
})
);
if (shouldPrependBoundAnchor) {
newAnchors.splice(
0,
0,
new AnchorPoint({
index: 0,
position: range2.min,
panel: panels[reachablePanels[0].index - 1]
})
);
}
if (shouldAppendBoundAnchor) {
newAnchors.push(
new AnchorPoint({
index: newAnchors.length,
position: range2.max,
panel: panels[reachablePanels[reachablePanels.length - 1].index + 1]
})
);
}
return newAnchors;
} else if (range2.min !== range2.max) {
const nearestPanelAtMin = this._findNearestPanel(range2.min, panels);
const panelAtMin = nearestPanelAtMin.index === panels.length - 1 ? nearestPanelAtMin.prev() : nearestPanelAtMin;
const panelAtMax = panelAtMin.next();
return [
new AnchorPoint({
index: 0,
position: range2.min,
panel: panelAtMin
}),
new AnchorPoint({
index: 1,
position: range2.max,
panel: panelAtMax
})
];
} else {
return [
new AnchorPoint({
index: 0,
position: range2.min,
panel: this._findNearestPanel(range2.min, panels)
})
];
}
}
findAnchorIncludePosition(position) {
const camera = this._flicking.camera;
const range2 = camera.range;
const anchors = camera.anchorPoints;
if (anchors.length <= 0) return null;
if (position <= range2.min) {
return anchors[0];
} else if (position >= range2.max) {
return anchors[anchors.length - 1];
} else {
return super.findAnchorIncludePosition(position);
}
}
/**
* @internal
*/
_findNearestPanel(pos, panels) {
let prevDist = Infinity;
for (let panelIdx = 0; panelIdx < panels.length; panelIdx++) {
const panel = panels[panelIdx];
const dist = Math.abs(panel.position - pos);
if (dist > prevDist) {
return panels[panelIdx - 1];
}
prevDist = dist;
}
return panels[panels.length - 1];
}
}
class CircularCameraMode extends CameraMode {
checkAvailability() {
const flicking = this._flicking;
const renderer = flicking.renderer;
const panels = renderer.panels;
if (panels.length <= 0) {
return false;
}
const firstPanel = panels[0];
const lastPanel = panels[panels.length - 1];
const firstPanelPrev = firstPanel.range.min - firstPanel.margin.prev;
const lastPanelNext = lastPanel.range.max + lastPanel.margin.next;
const visibleSize = flicking.camera.size;
const panelSizeSum = lastPanelNext - firstPanelPrev;
const canSetCircularMode = panels.every((panel) => panelSizeSum - panel.size >= visibleSize);
return canSetCircularMode;
}
getRange() {
const flicking = this._flicking;
const panels = flicking.renderer.panels;
if (panels.length <= 0) {
return { min: 0, max: 0 };
}
const firstPanel = panels[0];
const lastPanel = panels[panels.length - 1];
const firstPanelPrev = firstPanel.range.min - firstPanel.margin.prev;
const lastPanelNext = lastPanel.range.max + lastPanel.margin.next;
return { min: firstPanelPrev, max: lastPanelNext };
}
getAnchors() {
const flicking = this._flicking;
const panels = flicking.renderer.panels;
return panels.map(
(panel, index) => new AnchorPoint({
index,
position: panel.position,
panel
})
);
}
findNearestAnchor(position) {
const camera = this._flicking.camera;
const anchors = camera.anchorPoints;
if (anchors.length <= 0) return null;
const camRange = camera.range;
let minDist = Infinity;
let minDistIndex = -1;
for (let anchorIdx = 0; anchorIdx < anchors.length; anchorIdx++) {
const anchor = anchors[anchorIdx];
const dist = Math.min(
Math.abs(anchor.position - position),
Math.abs(anchor.position - camRange.min + camRange.max - position),
Math.abs(position - camRange.min + camRange.max - anchor.position)
);
if (dist < minDist) {
minDist = dist;
minDistIndex = anchorIdx;
}
}
return anchors[minDistIndex];
}
findAnchorIncludePosition(position) {
const camera = this._flicking.camera;
const range2 = camera.range;
const anchors = camera.anchorPoints;
const rangeDiff = camera.rangeDiff;
const anchorCount = anchors.length;
const positionInRange = circulatePosition(position, range2.min, range2.max);
let anchorInRange = super.findAnchorIncludePosition(positionInRange);
if (anchorCount > 0 && (position === range2.min || position === range2.max)) {
const possibleAnchors = [
anchorInRange,
new AnchorPoint({
index: 0,
position: anchors[0].position + rangeDiff,
panel: anchors[0].panel
}),
new AnchorPoint({
index: anchorCount - 1,
position: anchors[anchorCount - 1].position - rangeDiff,
panel: anchors[anchorCount - 1].panel
})
].filter((anchor) => !!anchor);
anchorInRange = possibleAnchors.reduce((nearest, anchor) => {
if (!nearest) return anchor;
return Math.abs(nearest.position - position) < Math.abs(anchor.position - position) ? nearest : anchor;
}, null);
}
if (!anchorInRange) return null;
if (position < range2.min) {
const loopCount = -Math.floor((range2.min - position) / rangeDiff) - 1;
return new AnchorPoint({
index: anchorInRange.index,
position: anchorInRange.position + rangeDiff * loopCount,
panel: anchorInRange.panel
});
} else if (position > range2.max) {
const loopCount = Math.floor((position - range2.max) / rangeDiff) + 1;
return new AnchorPoint({
index: anchorInRange.index,
position: anchorInRange.position + rangeDiff * loopCount,
panel: anchorInRange.panel
});
}
return anchorInRange;
}
getCircularOffset() {
const flicking = this._flicking;
const camera = flicking.camera;
if (!camera.circularEnabled) return 0;
const toggled = flicking.panels.filter((panel) => panel.toggled);
const toggledPrev = toggled.filter((panel) => panel.toggleDirection === DIRECTION.PREV);
const toggledNext = toggled.filter((panel) => panel.toggleDirection === DIRECTION.NEXT);
return this._calcPanelAreaSum(toggledPrev) - this._calcPanelAreaSum(toggledNext);
}
clampToReachablePosition(position) {
return position;
}
canReach(panel) {
if (panel.removed) return false;
return true;
}
canSee(panel) {
const camera = this._flicking.camera;
const range2 = camera.range;
const rangeDiff = camera.rangeDiff;
const visibleRange = camera.visibleRange;
const visibleInCurrentRange = super.canSee(panel);
if (visibleRange.min < range2.min) {
return visibleInCurrentRange || panel.isVisibleOnRange(visibleRange.min + rangeDiff, visibleRange.max + rangeDiff);
} else if (visibleRange.max > range2.max) {
return visibleInCurrentRange || panel.isVisibleOnRange(visibleRange.min - rangeDiff, visibleRange.max - rangeDiff);
}
return visibleInCurrentRange;
}
/**
* @internal
*/
_calcPanelAreaSum(panels) {
return panels.reduce((sum, panel) => sum + panel.sizeIncludingMargin, 0);
}
}
class LinearCameraMode extends CameraMode {
checkAvailability() {
return true;
}
getRange() {
var _a, _b;
const renderer = this._flicking.renderer;
const firstPanel = renderer.getPanel(0);
const lastPanel = renderer.getPanel(renderer.panelCount - 1);
return { min: (_a = firstPanel == null ? void 0 : firstPanel.position) != null ? _a : 0, max: (_b = lastPanel == null ? void 0 : lastPanel.position) != null ? _b : 0 };
}
}
class Camera {
/**
* Creates a new Camera instance
* @param flicking - An instance of {@link Flicking}
* @param options - Options for the Camera
*/
constructor(flicking, { align = ALIGN.CENTER } = {}) {
this._lookedOffset = 0;
this._checkTranslateSupport = () => {
const transforms = ["webkitTransform", "msTransform", "MozTransform", "OTransform", "transform"];
const supportedStyle = document.documentElement.style;
let transformName = "";
for (const prefixedTransform of transforms) {
if (prefixedTransform in supportedStyle) {
transformName = prefixedTransform;
}
}
if (!transformName) {
throw new FlickingError(MESSAGE.TRANSFORM_NOT_SUPPORTED, CODE.TRANSFORM_NOT_SUPPORTED);
}
this._transform = transformName;
};
this._flicking = flicking;
this._resetInternalValues();
this._align = align;
}
// Internal states getter
/**
* The camera element(`.flicking-camera`)
* @readonly
*/
get element() {
return this._el;
}
/**
* An array of the child elements of the camera element(`.flicking-camera`)
* @readonly
*/
get children() {
return toArray(this._el.children);
}
/**
* Current position of the camera
* @readonly
*/
get position() {
return this._position;
}
/**
* Align position inside the viewport where {@link Panel}'s {@link Panel.alignPosition | alignPosition} should be located at
* @readonly
*/
get alignPosition() {
return this._alignPos;
}
/**
* Position offset, used for the {@link Flicking.renderOnlyVisible | renderOnlyVisible} option
* @defaultValue 0
* @readonly
*/
get offset() {
return this._offset - this._circularOffset;
}
/**
* Whether the `circular` option is enabled.
* @remarks
* The {@link Flicking.circular | circular} option can't be enabled when sum of the panel sizes are too small.
* @defaultValue false
* @readonly
*/
get circularEnabled() {
return this._circularEnabled;
}
/**
* A current camera mode
* @readonly
*/
get mode() {
return this._mode;
}
/**
* A range that Camera's {@link Camera.position | position} can reach
* @readonly
*/
get range() {
return this._range;
}
/**
* A difference between Camera's minimum and maximum position that can reach
* @readonly
*/
get rangeDiff() {
return this._range.max - this._range.min;
}
/**
* An array of visible panels from the current position
* @readonly
*/
get visiblePanels() {
return this._visiblePanels;
}
/**
* A range of the visible area from the current position
* @readonly
*/
get visibleRange() {
return { min: this._position - this._alignPos, max: this._position - this._alignPos + this.size };
}
/**
* An array of {@link AnchorPoint}s that Camera can be stopped at
* @readonly
*/
get anchorPoints() {
return this._anchors;
}
/**
* A current parameters of the Camera for updating {@link AxesController}
* @readonly
*/
get controlParams() {
return { range: this._range, position: this._position, circular: this._circularEnabled };
}
/**
* A Boolean value indicating whether Camera's over the minimum or maximum position reachable
* @readonly
*/
get atEdge() {
return this._position <= this._range.min || this._position >= this._range.max;
}
/**
* Return the size of the viewport
* @readonly
*/
get size() {
const flicking = this._flicking;
return flicking ? flicking.horizontal ? flicking.viewport.width : flicking.viewport.height : 0;
}
/**
* Return the camera's position progress from the first panel to last panel
* @remarks
* Range is from 0 to last panel's index
* @readonly
*/
get progress() {
const flicking = this._flicking;
const position = this._position + this._offset;
const nearestAnchor = this.findNearestAnchor(this._position);
if (!flicking || !nearestAnchor) {
return NaN;
}
const nearestPanel = nearestAnchor.panel;
const panelPos = nearestPanel.position + nearestPanel.offset;
const bounceSize = flicking.control.controller.bounce;
const { min: prevRange, max: nextRange } = this.range;
const rangeDiff = this.rangeDiff;
if (position === panelPos) {
return nearestPanel.index;
}
if (position < panelPos) {
const prevPanel = nearestPanel.prev();
let prevPosition = prevPanel ? prevPanel.position + prevPanel.offset : prevRange - bounceSize[0];
if (prevPosition > panelPos) {
prevPosition -= rangeDiff;
}
return nearestPanel.index - 1 + getProgress$1(position, prevPosition, panelPos);
} else {
const nextPanel = nearestPanel.next();
let nextPosition = nextPanel ? nextPanel.position + nextPanel.offset : nextRange + bounceSize[1];
if (nextPosition < panelPos) {
nextPosition += rangeDiff;
}
return nearestPanel.index + getProgress$1(position, panelPos, nextPosition);
}
}
/**
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/direction | direction} CSS property applied to the camera element(`.flicking-camera`)
* @readonly
*/
get panelOrder() {
return this._panelOrder;
}
// Options Getter
/**
* A value indicating where the {@link Camera.alignPosition | alignPosition} should be located at inside the viewport element
*/
get align() {
return this._align;
}
// Options Setter
set align(val) {
this._align = val;
}
/**
* Initialize Camera
* @remarks
* This method is called automatically during {@link Flicking.init}. It finds the camera element inside the viewport.
* @throws {@link InitializationErrors}
* @returns The current instance for method chaining
*/
init() {
const viewportEl = this._flicking.viewport.element;
checkExistence(viewportEl.firstElementChild, "First element child of the viewport element");
this._el = viewportEl.firstElementChild;
this._checkTranslateSupport();
this._updateMode();
this.updatePanelOrder();
return this;
}
/**
* Destroy Camera and return to initial state
* @remarks
* This method resets all internal values to their initial state.
* @returns The current instance for method chaining
*/
destroy() {
this._resetInternalValues();
return this;
}
/**
* Move to the given position and apply CSS transform
* @remarks
* This method updates the camera position, toggles panels for circular mode, and refreshes visible panels.
* @param pos - A new position
* @throws {@link InitializationErrors}
*/
lookAt(pos) {
const prevOffset = this._offset;
const isChangedOffset = this._lookedOffset !== prevOffset;
const flicking = getFlickingAttached(this._flicking);
const prevPos = this._position;
this._position = pos;
const toggled = this._togglePanels(prevPos, pos);
this._refreshVisiblePanels();
this._checkNeedPanel();
this._checkReachEnd(prevPos, pos);
if (toggled) {
void flicking.renderer.render().then(() => {
this.updateOffset();
this._lookedOffset = this._offset;
});
} else if (isChangedOffset) {
this.updateOffset();
this._lookedOffset = this._offset;
} else {
this.applyTransform();
}
}
/**
* Return a previous {@link AnchorPoint} of given {@link AnchorPoint}
* @remarks
* If it does not exist, return `null` instead
* @param anchor - A reference {@link AnchorPoint}
* @returns The previous {@link AnchorPoint}
*/
getPrevAnchor(anchor) {
if (!this._circularEnabled || anchor.index !== 0) {
return this._anchors[anchor.index - 1] || null;
} else {
const anchors = this._anchors;
const rangeDiff = this.rangeDiff;
const lastAnchor = anchors[anchors.length - 1];
return new AnchorPoint({
index: lastAnchor.index,
position: lastAnchor.position - rangeDiff,
panel: lastAnchor.panel
});
}
}
/**
* Return a next {@link AnchorPoint} of given {@link AnchorPoint}
* @remarks
* If it does not exist, return `null` instead
* @param anchor - A reference {@link AnchorPoint}
* @returns The next {@link AnchorPoint}
*/
getNextAnchor(anchor) {
const anchors = this._anchors;
if (!this._circularEnabled || anchor.index !== anchors.length - 1) {
return anchors[anchor.index + 1] || null;
} else {
const rangeDiff = this.rangeDiff;
const firstAnchor = anchors[0];
return new AnchorPoint({
index: firstAnchor.index,
position: firstAnchor.position + rangeDiff,
panel: firstAnchor.panel
});
}
}
/**
* Return the camera's position progress in the panel below
* @remarks
* Value is from 0 to 1 when the camera's inside panel.
* Value can be lower than 0 or bigger than 1 when it's in the margin area
* @param panel - A panel to check
* @returns Progress value from 0 to 1 (or outside this range when in margin area)
*/
getProgressInPanel(panel) {
const panelRange = panel.range;
return (this._position - panelRange.min) / (panelRange.max - panelRange.min);
}
/**
* Return {@link AnchorPoint} that includes given position
* @remarks
* If there's no {@link AnchorPoint} that includes the given position, return `null` instead
* @param position - A position to check
* @returns The {@link AnchorPoint} that includes the given position
*/
findAnchorIncludePosition(position) {
return this._mode.findAnchorIncludePosition(position);
}
/**
* Return {@link AnchorPoint} nearest to given position
* @remarks
* If there're no {@link AnchorPoint}s, return `null` instead
* @param position - A position to check
* @returns The {@link AnchorPoint} nearest to the given position
*/
findNearestAnchor(position) {
return this._mode.findNearestAnchor(position);
}
/**
* Return {@link AnchorPoint} that matches {@link Flicking.currentPanel}
* @returns The {@link AnchorPoint} that matches current panel
*/
findActiveAnchor() {
var _a;
const flicking = getFlickingAttached(this._flicking);
const activePanel = flicking.control.activePanel;
if (!activePanel) return null;
return (_a = find(this._anchors, (anchor) => anchor.panel.index === activePanel.index)) != null ? _a : this.findNearestAnchor(activePanel.position);
}
/**
* Clamp the given position between camera's range
* @param position - A position to clamp
* @returns A clamped position
*/
clampToReachablePosition(position) {
return this._mode.clampToReachablePosition(position);
}
/**
* Check whether the given panel is inside of the Camera's range
* @param panel - An instance of {@link Panel} to check
* @returns Whether the panel's inside Camera's range
*/
canReach(panel) {
return this._mode.canReach(panel);
}
/**
* Check whether the given panel element is visible at the current position
* @param panel - An instance of {@link Panel} to check
* @returns Whether the panel element is visible at the current position
*/
canSee(panel) {
return this._mode.canSee(panel);
}
/**
* Update {@link Camera.range | range} of Camera
* @remarks
* This method recalculates the camera range based on the current panel positions and circular mode settings.
* @throws {@link InitializationErrors}
* @returns The current instance for method chaining
*/
updateRange() {
const flicking = getFlickingAttached(this._flicking);
const renderer = flicking.renderer;
const panels = renderer.panels;
this._updateMode();
this._range = this._mode.getRange();
panels.forEach((panel) => panel.updateCircularToggleDirection());
return this;
}
/**
* Update Camera's {@link Camera.alignPosition | alignPosition}
* @returns The current instance for method chaining
*/
updateAlignPos() {
const align = this._align;
const alignVal = typeof align === "object" ? align.camera : align;
this._alignPos = parseAlign$1(alignVal, this.size);
return this;
}
/**
* Update Camera's {@link Camera.anchorPoints | anchorPoints}
* @remarks
* Anchor points are positions where the camera can stop. This method recalculates them based on the current mode.
* @throws {@link InitializationErrors}
* @returns The current instance for method chaining
*/
updateAnchors() {
this._anchors = this._mode.getAnchors();
return this;
}
/**
* Update Viewport's height to visible panel's max height
* @remarks
* This method only takes effect when {@link FlickingOptions.horizontal | horizontal} is `true` and {@link FlickingOptions.adaptive | adaptive} is enabled.
* @throws {@link InitializationErrors}
*/
updateAdaptiveHeight() {
const flicking = getFlickingAttached(this._flicking);
const activePanel = flicking.control.activePanel;
const visiblePanels = flicking.visiblePanels;
const selectedPanels = [...visiblePanels];
if (activePanel) {
selectedPanels.push(activePanel);
}
if (!flicking.horizontal || !flicking.adaptive || !selectedPanels.length) return;
const maxHeight = Math.max(...selectedPanels.map((panel) => panel.height));
flicking.viewport.setSize({
height: maxHeight
});
}
/**
* Update current offset of the camera
* @returns The current instance for method chaining
*/
updateOffset() {
const flicking = getFlickingAttached(this._flicking);
const position = this._position;
const unRenderedPanels = flicking.panels.filter((panel) => !panel.rendered);
this._offset = unRenderedPanels.filter((panel) => panel.position + panel.offset < position).reduce((offset, panel) => offset + panel.sizeIncludingMargin, 0);
this._circularOffset = this._mode.getCircularOffset();
this.applyTransform();
return this;
}
/**
* Update direction to match the {@link https://developer.mozilla.org/en-US/docs/Web/CSS/direction | direction} CSS property applied to the camera element
* @returns The current instance for method chaining
*/
updatePanelOrder() {
const flicking = getFlickingAttached(this._flicking);
if (!flicking.horizontal) return this;
const el = this._el;
const direction = getStyle(el).direction;
if (direction !== this._panelOrder) {
this._panelOrder = direction === ORDER.RTL ? ORDER.RTL : ORDER.LTR;
if (flicking.initialized) {
flicking.control.controller.updateDirection();
}
}
return this;
}
/**
* Reset the history of {@link Flicking.event:needPanel | needPanel} events so it can be triggered again
* @returns The current instance for method chaining
*/
resetNeedPanelHistory() {
this._needPanelTriggered = { prev: false, next: false };
return this;
}
/**
* Apply "transform" style with the current position to camera element
* @returns The current instance for method chaining
*/
applyTransform() {
const el = this._el;
const flicking = getFlickingAttached(this._flicking);
const renderer = flicking.renderer;
if (renderer.rendering || !flicking.initialized) return this;
const actualPosition = this._position - this._alignPos - this._offset + this._circularOffset;
const sign = !flicking.horizontal || this._panelOrder !== ORDER.RTL ? -1 : 1;
const posText = flicking.usePercentagePos ? `${sign * actualPosition / this.size * 100}%` : `${sign * actualPosition}px`;
el.style[this._transform] = flicking.horizontal ? `translate(${posText})` : `translate(0, ${posText})`;
return this;
}
/**
* @internal
* @privateRemarks
* Resets all internal state values to their defaults. Called during construction and destruction.
*/
_resetInternalValues() {
this._position = 0;
this._lookedOffset = 0;
this._alignPos = 0;
this._offset = 0;
this._circularOffset = 0;
this._circularEnabled = false;
this._range = { min: 0, max: 0 };
this._visiblePanels = [];
this._anchors = [];
this._needPanelTriggered = { prev: false, next: false };
}
/**
* @internal
* @privateRemarks
* Updates the list of visible panels and triggers {@link VisibleChangeEvent} if panels were added or removed.
*/
_refreshVisiblePanels() {
const flicking = getFlickingAttached(this._flicking);
const panels = flicking.renderer.panels;
const newVisiblePanels = panels.filter((panel) => this.canSee(panel));
const prevVisiblePanels = this._visiblePanels;
this._visiblePanels = newVisiblePanels;
const added = newVisiblePanels.filter((panel) => !includes(prevVisiblePanels, panel));
const removed = prevVisiblePanels.filter((panel) => !includes(newVisiblePanels, panel));
if (added.length > 0 || removed.length > 0) {
void flicking.renderer.render().then(() => {
flicking.trigger(
new ComponentEvent(EVENTS.VISIBLE_CHANGE, {
added,
removed,
visiblePanels: newVisiblePanels
})
);
});
}
}
/**
* @internal
* @privateRemarks
* Checks if the camera is near the edges and triggers {@link NeedPanelEvent} for infinite scrolling implementations.
*/
_checkNeedPanel() {
const needPanelTriggered = this._needPanelTriggered;
if (needPanelTriggered.prev && needPanelTriggered.next) return;
const flicking = getFlickingAttached(this._flicking);
const panels = flicking.renderer.panels;
if (panels.length <= 0) {
if (!needPanelTriggered.prev) {
flicking.trigger(new ComponentEvent(EVENTS.NEED_PANEL, { direction: DIRECTION.PREV }));
needPanelTriggered.prev = true;
}
if (!needPanelTriggered.next) {
flicking.trigger(new ComponentEvent(EVENTS.NEED_PANEL, { direction: DIRECTION.NEXT }));
needPanelTriggered.next = true;
}
return;
}
const cameraPosition = this._position;
const cameraSize = this.size;
const cameraRange = this._range;
const needPanelThreshold = flicking.needPanelThreshold;
const cameraPrev = cameraPosition - this._alignPos;
const cameraNext = cameraPrev + cameraSize;
const firstPanel = panels[0];
const lastPanel = panels[panels.length - 1];
if (!needPanelTriggered.prev) {
const firstPanelPrev = firstPanel.range.min;
if (cameraPrev <= firstPanelPrev + needPanelThreshold || cameraPosition <= cameraRange.min + needPanelThreshold) {
flicking.trigger(new ComponentEvent(EVENTS.NEED_PANEL, { direction: DIRECTION.PREV }));
needPanelTriggered.prev = true;
}
}
if (!needPanelTriggered.next) {
const lastPanelNext = lastPanel.range.max;
if (cameraNext >= lastPanelNext - needPanelThreshold || cameraPosition >= cameraRange.max - needPanelThreshold) {
flicking.trigger(new ComponentEvent(EVENTS.NEED_PANEL, { direction: DIRECTION.NEXT }));
needPanelTriggered.next = true;
}
}
}
/**
* @internal
* @privateRemarks
* Checks if the camera has reached the edge of the range and triggers {@link ReachEdgeEvent}.
*/
_checkReachEnd(prevPos, newPos) {
const flicking = getFlickingAttached(this._flicking);
const range2 = this._range;
const wasBetweenRange = prevPos > range2.min && prevPos < range2.max;
const isBetweenRange = newPos > range2.min && newPos < range2.max;
if (!wasBetweenRange || isBetweenRange) return;
const direction = newPos <= range2.min ? DIRECTION.PREV : DIRECTION.NEXT;
flicking.trigger(
new ComponentEvent(EVENTS.REACH_EDGE, {
direction
})
);
}
/**
* @internal
* @privateRemarks
* Updates the camera mode based on {@link FlickingOptions.circular | circular} and {@link FlickingOptions.bound | bound} options.
*/
_updateMode() {
const flicking = getFlickingAttached(this._flicking);
if (flicking.circular) {
const circularMode = new CircularCameraMode(flicking);
const canSetCircularMode = circularMode.checkAvailability();
if (canSetCircularMode) {
this._mode = circularMode;
} else {
const fallbackMode = flicking.circularFallback;
this._mode = fallbackMode === CIRCULAR_FALLBACK.BOUND ? new BoundCameraMode(flicking) : new LinearCameraMode(flicking);
}
this._circularEnabled = canSetCircularMode;
} else {
this._mode = flicking.bound ? new BoundCameraMode(flicking) : new LinearCameraMode(flicking);
this._circularEnabled = false;
}
}
/**
* @internal
* @privateRemarks
* Toggles panel positions for circular mode. Returns true if any panel was toggled.
*/
_togglePanels(prevPos, pos) {
if (pos === prevPos) return false;
const flicking = getFlickingAttached(this._flicking);
const panels = flicking.renderer.panels;
const toggled = panels.map((panel) => panel.toggle(prevPos, pos));
return toggled.some((isToggled) => isToggled);
}
}
const EVENT = {
HOLD: "hold",
CHANGE: "change",
RELEASE: "release",
ANIMATION_END: "animationEnd",
FINISH: "finish"
};
const POSITION_KEY = "flick";
var STATE_TYPE = /* @__PURE__ */ ((STATE_TYPE2) => {
STATE_TYPE2[STATE_TYPE2["IDLE"] = 0] = "IDLE";
STATE_TYPE2[STATE_TYPE2["HOLDING"] = 1] = "HOLDING";
STATE_TYPE2[STATE_TYPE2["DRAGGING"] = 2] = "DRAGGING";
STATE_TYPE2[STATE_TYPE2["ANIMATING"] = 3] = "ANIMATING";
STATE_TYPE2[STATE_TYPE2["DISABLED"] = 4] = "DISABLED";
return STATE_TYPE2;
})(STATE_TYPE || {});
class State {
constructor() {
this._delta = 0;
this._targetPanel = null;
}
/**
* A sum of delta values of change events from the last hold event of Axes
* @readonly
*/
get delta() {
return this._delta;
}
/**
* A panel to set as {@link Control.activePanel} after the animation is finished
* @readonly
*/
get targetPanel() {
return this._targetPanel;
}
set targetPanel(val) {
this._targetPanel = val;
}
/**
* An callback which is called when state has changed to this state
* @param prevState - An previous state
*/
onEnter(prevState) {
this._delta = prevState._delta;
this._targetPanel = prevState._targetPanel;
}
/**
* An event handler for Axes's {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-hold | hold} event
* @param ctx - {@link StateContext}
*/
onHold(ctx) {
}
/**
* An event handler for Axes's {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-change | change} event
* @param ctx - {@link StateContext}
*/
onChange(ctx) {
}
/**
* An event handler for Axes's {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-release | release} event
* @param ctx - {@link StateContext}
*/
onRelease(ctx) {
}
/**
* An event handler for Axes's {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-animationEnd | animationEnd} event
* @param ctx - {@link StateContext}
*/
onAnimationEnd(ctx) {
}
/**
* An event handler for Axes's {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-finish | finish} event
* @param ctx - {@link StateContext}
*/
onFinish(ctx) {
}
/**
* @internal
*/
_moveToChangedPosition(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
const delta = axesEvent.delta[POSITION_KEY];
if (!delta) {
return;
}
this._delta += delta;
const camera = flicking.camera;
const prevPosition = camera.position;
const position = axesEvent.pos[POSITION_KEY];
const newPosition = flicking.circularEnabled ? circulatePosition(position, camera.range.min, camera.range.max) : position;
camera.lookAt(newPosition);
const moveEvent = new ComponentEvent(EVENTS.MOVE, {
isTrusted: axesEvent.isTrusted,
holding: this.holding,
direction: getDirection(0, axesEvent.delta[POSITION_KEY]),
axesEvent
});
flicking.trigger(moveEvent);
if (moveEvent.isCanceled()) {
camera.lookAt(prevPosition);
transitTo(
4
/* DISABLED */
);
}
}
}
class AnimatingState extends State {
constructor() {
super(...arguments);
this.holding = false;
this.animating = true;
}
onHold(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
const targetPanel = this._targetPanel;
const control = flicking.control;
this._delta = 0;
flicking.control.updateInput();
if (flicking.changeOnHold && targetPanel) {
control.setActive(targetPanel, control.activePanel, axesEvent.isTrusted);
}
const holdStartEvent = new ComponentEvent(EVENTS.HOLD_START, { axesEvent });
flicking.trigger(holdStartEvent);
if (holdStartEvent.isCanceled()) {
transitTo(STATE_TYPE.DISABLED);
} else {
transitTo(STATE_TYPE.DRAGGING);
}
}
onChange(ctx) {
this._moveToChangedPosition(ctx);
}
onFinish(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
const control = flicking.control;
const controller = control.controller;
const animatingContext = controller.animatingContext;
transitTo(STATE_TYPE.IDLE);
flicking.trigger(
new ComponentEvent(EVENTS.MOVE_END, {
isTrusted: axesEvent.isTrusted,
direction: getDirection(animatingContext.start, animatingContext.end),
axesEvent
})
);
const targetPanel = this._targetPanel;
if (targetPanel) {
control.setActive(targetPanel, control.activePanel, axesEvent.isTrusted);
}
}
}
class DisabledState extends State {
constructor() {
super(...arguments);
this.holding = false;
this.animating = true;
}
onAnimationEnd(ctx) {
const { transitTo } = ctx;
transitTo(STATE_TYPE.IDLE);
}
onChange(ctx) {
const { axesEvent, transitTo } = ctx;
axesEvent.stop();
transitTo(STATE_TYPE.IDLE);
}
onRelease(ctx) {
const { axesEvent, transitTo } = ctx;
if (axesEvent.delta.flick === 0) {
transitTo(STATE_TYPE.IDLE);
}
}
}
class DraggingState extends State {
constructor() {
super(...arguments);
this.holding = true;
this.animating = true;
}
onChange(ctx) {
this._moveToChangedPosition(ctx);
}
onRelease(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
flicking.trigger(
new ComponentEvent(EVENTS.HOLD_END, {
axesEvent
})
);
if (flicking.renderer.panelCount <= 0) {
transitTo(STATE_TYPE.IDLE);
return;
}
transitTo(STATE_TYPE.ANIMATING);
const control = flicking.control;
const position = axesEvent.destPos[POSITION_KEY];
const duration = Math.max(axesEvent.duration, flicking.duration);
try {
void control.moveToPosition(position, duration, axesEvent);
} catch (_err) {
transitTo(STATE_TYPE.IDLE);
axesEvent.setTo({ [POSITION_KEY]: flicking.camera.position }, 0);
}
}
}
class HoldingState extends State {
constructor() {
super(...arguments);
this.holding = true;
this.animating = false;
this._releaseEvent = null;
}
onChange(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
const inputEvent = axesEvent.inputEvent;
if (!inputEvent) {
return;
}
const offset = flicking.horizontal ? inputEvent.offsetX : inputEvent.offsetY;
const moveStartEvent = new ComponentEvent(EVENTS.MOVE_START, {
isTrusted: axesEvent.isTrusted,
holding: this.holding,
direction: getDirection(0, -offset),
axesEvent
});
flicking.trigger(moveStartEvent);
if (moveStartEvent.isCanceled()) {
transitTo(STATE_TYPE.DISABLED);
} else {
transitTo(STATE_TYPE.DRAGGING).onChange(ctx);
}
}
onRelease(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
flicking.trigger(new ComponentEvent(EVENTS.HOLD_END, { axesEvent }));
if (axesEvent.delta.flick !== 0) {
axesEvent.setTo({ flick: flicking.camera.position }, 0);
transitTo(STATE_TYPE.IDLE);
return;
}
this._releaseEvent = axesEvent;
}
onFinish(ctx) {
const { flicking, transitTo } = ctx;
transitTo(STATE_TYPE.IDLE);
if (!this._releaseEvent) {
return;
}
const releaseEvent = this._releaseEvent;
const srcEvent = releaseEvent.inputEvent.srcEvent;
let clickedElement;
if (srcEvent.type === "touchend") {
const touchEvent = srcEvent;
const touch = touchEvent.changedTouches[0];
clickedElement = document.elementFromPoint(touch.clientX, touch.clientY);
} else {
clickedElement = srcEvent.target;
}
const panels = flicking.renderer.panels;
let clickedPanel = null;
for (const panel of panels) {
if (panel.contains(clickedElement)) {
clickedPanel = panel;
break;
}
}
if (clickedPanel) {
const cameraPosition = flicking.camera.position;
const clickedPanelPosition = clickedPanel.position;
flicking.trigger(
new ComponentEvent(EVENTS.SELECT, {
index: clickedPanel.index,
panel: clickedPanel,
// Direction to the clicked panel
direction: getDirection(cameraPosition, clickedPanelPosition)
})
);
}
}
}
class IdleState extends State {
constructor() {
super(...arguments);
this.holding = false;
this.animating = false;
}
onEnter() {
this._delta = 0;
this._targetPanel = null;
}
onHold(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
if (flicking.renderer.panelCount <= 0) {
transitTo(STATE_TYPE.DISABLED);
return;
}
const holdStartEvent = new ComponentEvent(EVENTS.HOLD_START, {
axesEvent
});
flicking.trigger(holdStartEvent);
if (holdStartEvent.isCanceled()) {
transitTo(STATE_TYPE.DISABLED);
} else {
transitTo(STATE_TYPE.HOLDING);
}
}
// By methods call
onChange(ctx) {
const { flicking, axesEvent, transitTo } = ctx;
const controller = flicking.control.controller;
const animatingContext = controller.animatingContext;
const moveStartEvent = new ComponentEvent(EVENTS.MOVE_START, {
isTrusted: axesEvent.isTrusted,
holding: this.holding,
direction: getDirection(animatingContext.start, animatingContext.end),
axesEvent
});
flicking.trigger(moveStartEvent);
if (moveStartEvent.isCanceled()) {
transitTo(STATE_TYPE.DISABLED);
} else {
transitTo(STATE_TYPE.ANIMATING).onChange(ctx);
}
}
}
class StateMachine {
constructor() {
this.transitTo = (nextStateType) => {
let nextState;
switch (nextStateType) {
case STATE_TYPE.IDLE:
nextState = new IdleState();
break;
case STATE_TYPE.HOLDING:
nextState = new HoldingState();
break;
case STATE_TYPE.DRAGGING:
nextState = new DraggingState();
break;
case STATE_TYPE.ANIMATING:
nextState = new AnimatingState();
break;
case STATE_TYPE.DISABLED:
nextState = new DisabledState();
break;
}
nextState.onEnter(this._state);
this._state = nextState;
return this._state;
};
this._state = new IdleState();
}
get state() {
return this._state;
}
fire(eventType, externalCtx) {
const currentState = this._state;
const ctx = __spreadProps(__spreadValues({}, externalCtx), { transitTo: this.transitTo });
switch (eventType) {
case EVENT.HOLD:
currentState.onHold(ctx);
break;
case EVENT.CHANGE:
currentState.onChange(ctx);
break;
case EVENT.RELEASE:
currentState.onRelease(ctx);
break;
case EVENT.ANIMATION_END:
currentState.onAnimationEnd(ctx);
break;
case EVENT.FINISH:
currentState.onFinish(ctx);
break;
}
}
}
class AxesController {
constructor() {
this._onAxesHold = () => {
this._dragged = false;
};
this._onAxesChange = () => {
var _a;
this._dragged = !!((_a = this._panInput) == null ? void 0 : _a.isEnabled());
};
this._preventClickWhenDragged = (e) => {
if (this._dragged) {
e.preventDefault();
e.stopPropagation();
}
this._dragged = false;
};
this._resetInternalValues();
this._stateMachine = new StateMachine();
}
/**
* An {@link https://naver.github.io/egjs-axes/docs/api/Axes | Axes} instance
* @see https://naver.github.io/egjs-axes/docs/api/Axes
* @readonly
*/
get axes() {
return this._axes;
}
/**
* An {@link https://naver.github.io/egjs-axes/docs/api/PanInput | PanInput} instance
* @see https://naver.github.io/egjs-axes/docs/api/PanInput
* @readonly
*/
get panInput() {
return this._panInput;
}
/**
* @internal
*/
get stateMachine() {
return this._stateMachine;
}
/**
* A activated {@link State} that shows the current status of the user input or the animation
*/
get state() {
return this._stateMachine.state;
}
/**
* A context of the current animation playing
* @readonly
*/
get animatingContext() {
return this._animatingContext;
}
/**
* A current control parameters of the Axes instance
*/
get controlParams() {
const axes = this._axes;
if (!axes) {
return {
range: { min: 0, max: 0 },
position: 0,
circular: false
};
}
const axis = axes.axis[POSITION_KEY];
return {
range: { min: axis.range[0], max: axis.range[1] },
circular: axis.circular[0],
position: this.position
};
}
/**
* A Boolean indicating whether the user input is enabled
* @readonly
*/
get enabled() {
var _a, _b;
return (_b = (_a = this._panInput) == null ? void 0 : _a.isEnabled()) != null ? _b : false;
}
/**
* Current position value in {@link https://naver.github.io/egjs-axes/docs/api/Axes | Axes} instance
* @readonly
*/
get position() {
var _a, _b;
return (_b = (_a = this._axes) == null ? void 0 : _a.get([POSITION_KEY])[POSITION_KEY]) != null ? _b : 0;
}
/**
* Current range value in {@link https://naver.github.io/egjs-axes/docs/api/Axes | Axes} instance
* @readonly
*/
get range() {
var _a, _b;
return (_b = (_a = this._axes) == null ? void 0 : _a.axis[POSITION_KEY].range) != null ? _b : [0, 0];
}
/**
* Actual bounce size(px)
* @readonly
*/
get bounce() {
var _a;
return (_a = this._axes) == null ? void 0 : _a.axis[POSITION_KEY].bounce;
}
/**
* Initialize AxesController
* @remarks
* This method creates and configures the Axes and PanInput instances.
* @param flicking - An instance of {@link Flicking}
* @returns The current instance for method chaining
*/
init(flicking) {
this._flicking = flicking;
this._axes = new 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,
threshold: flicking.dragThreshold,
iOSEdgeSwipeThreshold: flicking.iOSEdgeSwipeThreshold,
preventDefaultOnDrag: flicking.preventDefaultOnDrag,
scale: flicking.horizontal ? [flicking.camera.panelOrder === ORDER.RTL ? 1 : -1, 0] : [0, -1],
releaseOnScroll: true
});
const axes = this._axes;
axes.connect(flicking.horizontal ? [POSITION_KEY, ""] : ["", POSITION_KEY], this._panInput);
for (const key in EVENT) {
const eventType = EVENT[key];
axes.on(eventType, (e) => {
this._stateMachine.fire(eventType, {
flicking,
axesEvent: e
});
});
}
return this;
}
/**
* Destroy AxesController and return to initial state
* @remarks
* This method destroys the Axes and PanInput instances and removes all event handlers.
*/
destroy() {
var _a;
if (this._axes) {
this.removePreventClickHandler();
this._axes.destroy();
}
(_a = this._panInput) == null ? void 0 : _a.destroy();
this._resetInternalValues();
}
/**
* Enable input from the user (mouse/touch)
* @remarks
* Enables the PanInput to receive user interactions.
* @returns The current instance for method chaining
*/
enable() {
var _a;
(_a = this._panInput) == null ? void 0 : _a.enable();
return this;
}
/**
* Disable input from the user (mouse/touch)
* @remarks
* Disables the PanInput to ignore user interactions.
* @returns The current instance for method chaining
*/
disable() {
var _a;
(_a = this._panInput) == null ? void 0 : _a.disable();
return this;
}
/**
* Releases ongoing user input (mouse/touch)
* @remarks
* Immediately releases the PanInput, simulating the user lifting their finger.
* @returns The current instance for method chaining
*/
release() {
var _a;
(_a = this._panInput) == null ? void 0 : _a.release();
return this;
}
/**
* Change the destination and duration of the animation currently playing
* @remarks
* This method updates the Axes animation target position and optionally the duration.
* @param position - A position to move
* @param duration - Duration of the animation (unit: ms)
* @returns The current instance for method chaining
*/
updateAnimation(position, duration) {
var _a;
this._animatingContext = __spreadProps(__spreadValues({}, this._animatingContext), {
end: position
});
(_a = this._axes) == null ? void 0 : _a.updateAnimation({
destPos: { [POSITION_KEY]: position },
duration
});
return this;
}
/**
* Stops the animation currently playing
* @remarks
* This method immediately stops the Axes animation at the current position.
* @returns The current instance for method chaining
*/
stopAnimation() {
var _a;
(_a = this._axes) == null ? void 0 : _a.stopAnimation();
return this;
}
/**
* Update {@link https://naver.github.io/egjs-axes/ | @egjs/axes}'s state
* @remarks
* This method synchronizes the Axes state with the given control parameters.
* @param controlParams - Control parameters
* @throws {@link InitializationErrors}
* @returns The current instance for method chaining
*/
update(controlParams) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const axes = this._axes;
const axis = axes.axis[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({ [POSITION_KEY]: controlParams.position });
return this;
}
/**
* Attach a handler to the camera element to prevent click events during animation
* @remarks
* This is used when {@link FlickingOptions.preventClickOnDrag | preventClickOnDrag} is enabled.
* @returns The current instance for method chaining
*/
addPreventClickHandler() {
const flicking = getFlickingAttached(this._flicking);
const axes = this._axes;
const cameraEl = flicking.camera.element;
axes.on(EVENT.HOLD, this._onAxesHold);
axes.on(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
* @remarks
* This is used when {@link FlickingOptions.preventClickOnDrag | preventClickOnDrag} is disabled.
* @returns The current instance for method chaining
*/
removePreventClickHandler() {
const flicking = getFlickingAttached(this._flicking);
const axes = this._axes;
const cameraEl = flicking.camera.element;
axes.off(EVENT.HOLD, this._onAxesHold);
axes.off(EVENT.CHANGE, this._onAxesChange);
cameraEl.removeEventListener("click", this._preventClickWhenDragged, true);
return this;
}
/**
* Run Axes's {@link https://naver.github.io/egjs-axes/docs/api/Axes#setTo | setTo} using the given position
* @remarks
* If the target position equals the current position, the promise resolves immediately without animation.
* @param position - A position to move
* @param duration - Duration of the animation (unit: ms)
* @param axesEvent - If provided, it'll use its {@link https://naver.github.io/egjs-axes/docs/api/Axes#setTo | setTo} method instead
* @throws {@link MovementErrors}
* @returns A Promise which will be resolved after reaching the target position
*/
animateTo(position, duration, axesEvent) {
var _a;
const axes = this._axes;
const state = this._stateMachine.state;
if (!axes) {
return Promise.reject(
new FlickingError(MESSAGE.NOT_ATTACHED_TO_FLICKING, CODE.NOT_ATTACHED_TO_FLICKING)
);
}
const startPos = this.getCurrentPosition();
if (startPos === position) {
const flicking = getFlickingAttached(this._flicking);
flicking.camera.lookAt(position);
if (state.targetPanel) {
flicking.control.setActive(state.targetPanel, flicking.control.activePanel, (_a = axesEvent == null ? void 0 : axesEvent.isTrusted) != null ? _a : 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(EVENT.FINISH, resetContext);
if (axesEvent) {
axesEvent.setTo({ [POSITION_KEY]: position }, duration);
} else {
axes.setTo({ [POSITION_KEY]: position }, duration);
}
};
return new Promise((resolve, reject) => {
const animationFinishHandler = () => {
axes.off(EVENT.HOLD, interruptionHandler);
resolve();
};
const interruptionHandler = () => {
axes.off(EVENT.FINISH, animationFinishHandler);
reject(new FlickingError(MESSAGE.ANIMATION_INTERRUPTED, CODE.ANIMATION_INTERRUPTED));
};
axes.once(EVENT.FINISH, animationFinishHandler);
axes.once(EVENT.HOLD, interruptionHandler);
animate();
});
}
/**
* Returns the current axes position
*/
getCurrentPosition() {
var _a, _b;
return (_b = (_a = this._axes) == null ? void 0 : _a.get([POSITION_KEY])[POSITION_KEY]) != null ? _b : 0;
}
updateDirection() {
const flicking = getFlickingAttached(this._flicking);
const axes = this._axes;
const panInput = this._panInput;
axes.disconnect(panInput);
axes.connect(flicking.horizontal ? [POSITION_KEY, ""] : ["", POSITION_KEY], panInput);
panInput.options.scale = flicking.horizontal ? [flicking.camera.panelOrder === ORDER.RTL ? 1 : -1, 0] : [0, -1];
}
/**
* @internal
* @privateRemarks
* Resets all internal values to their defaults. Called during construction and destruction.
*/
_resetInternalValues() {
this._flicking = null;
this._axes = null;
this._panInput = null;
this._animatingContext = { start: 0, end: 0, offset: 0 };
this._dragged = false;
}
}
class Control {
/**
* A controller that handles the {@link https://naver.github.io/egjs-axes/ | @egjs/axes} events
* @readonly
*/
get controller() {
return this._controller;
}
/**
* Index number of the {@link Flicking.currentPanel | currentPanel}
* @defaultValue 0
* @readonly
*/
get activeIndex() {
var _a, _b;
return (_b = (_a = this._activePanel) == null ? void 0 : _a.index) != null ? _b : -1;
}
/**
* An active panel
* @readonly
*/
get activePanel() {
return this._activePanel;
}
/**
* Whether Flicking's animating
* @readonly
*/
get animating() {
return this._controller.state.animating;
}
/**
* Whether user is clicking or touching
* @readonly
*/
get holding() {
return this._controller.state.holding;
}
constructor() {
this._flicking = null;
this._controller = new AxesController();
this._activePanel = null;
}
/**
* Initialize Control
* @remarks
* This method is called automatically during {@link Flicking.init}. It initializes the internal controller.
* @param flicking - An instance of {@link Flicking}
* @returns The current instance for method chaining
*/
init(flicking) {
this._flicking = flicking;
this._controller.init(flicking);
return this;
}
/**
* Destroy Control and return to initial state
* @remarks
* This method destroys the internal controller and resets all internal values.
*/
destroy() {
this._controller.destroy();
this._flicking = null;
this._activePanel = null;
}
/**
* Enable input from the user (mouse/touch)
* @remarks
* This is a shorthand for `Flicking.enableInput`.
* @returns The current instance for method chaining
*/
enable() {
this._controller.enable();
return this;
}
/**
* Disable input from the user (mouse/touch)
* @remarks
* This is a shorthand for `Flicking.disableInput`.
* @returns The current instance for method chaining
*/
disable() {
this._controller.disable();
return this;
}
/**
* Releases ongoing user input (mouse/touch)
* @remarks
* This method immediately releases the user's input, similar to the user lifting their finger.
* @returns The current instance for method chaining
*/
release() {
this._controller.release();
return this;
}
/**
* Change the destination and duration of the animation currently playing
* @remarks
* This method does nothing if no animation is currently playing.
* @param panel - The target panel to move
* @param duration - Duration of the animation (unit: ms)
* @param direction - Direction to move, only available in the {@link Flicking.circular | circular} mode
* @throws {@link AnimationUpdateErrors}
* @returns The current instance for method chaining
*/
updateAnimation(panel, duration, direction) {
const state = this._controller.state;
const position = this._getPosition(panel, direction != null ? direction : DIRECTION.NONE);
state.targetPanel = panel;
this._controller.updateAnimation(position, duration);
return this;
}
/**
* Stops the animation currently playing
* @remarks
* This method does nothing if no animation is currently playing.
* @returns The current instance for method chaining
*/
stopAnimation() {
const state = this._controller.state;
state.targetPanel = null;
this._controller.stopAnimation();
return this;
}
/**
* Update position after resizing
* @remarks
* This method moves the camera to the active panel's position after a resize operation.
* @param progressInPanel - Previous camera's progress in active panel before resize
* @throws {@link InitializationErrors}
*/
updatePosition(progressInPanel) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const activePanel = this._activePanel;
if (activePanel) {
camera.lookAt(camera.clampToReachablePosition(activePanel.position));
}
}
/**
* Update {@link Control.controller | controller}'s state
* @remarks
* This method synchronizes the controller state with the current camera parameters.
* @returns The current instance for method chaining
*/
updateInput() {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
this._controller.update(camera.controlParams);
return this;
}
/**
* Reset {@link Control.activePanel | activePanel} to `null`
* @remarks
* This method is called when the active panel is removed from the renderer.
* @returns The current instance for method chaining
*/
resetActive() {
this._activePanel = null;
return this;
}
/**
* Move {@link Camera} to the given panel
* @param panel - The target panel to move
* @param options - {@link MoveToPanelParams}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns A Promise which will be resolved after reaching the target panel
*/
moveToPanel(_0, _1) {
return __async(this, arguments, function* (panel, { duration, direction = DIRECTION.NONE, axesEvent }) {
const position = this._getPosition(panel, direction);
this._triggerIndexChangeEvent(panel, panel.position, axesEvent, direction);
return this._animateToPosition({ position, duration, newActivePanel: panel, axesEvent });
});
}
/**
* @internal
* @privateRemarks
* Sets the active panel and triggers {@link ChangedEvent} or {@link RestoredEvent} based on whether the panel changed.
*/
setActive(newActivePanel, prevActivePanel, isTrusted) {
var _a;
const flicking = getFlickingAttached(this._flicking);
this._activePanel = newActivePanel;
this._nextPanel = null;
flicking.camera.updateAdaptiveHeight();
if (newActivePanel !== prevActivePanel) {
flicking.trigger(
new ComponentEvent(EVENTS.CHANGED, {
index: newActivePanel.index,
panel: newActivePanel,
prevIndex: (_a = prevActivePanel == null ? void 0 : prevActivePanel.index) != null ? _a : -1,
prevPanel: prevActivePanel,
isTrusted,
direction: prevActivePanel ? getDirection(prevActivePanel.position, newActivePanel.position) : DIRECTION.NONE
})
);
} else {
flicking.trigger(
new ComponentEvent(EVENTS.RESTORED, {
isTrusted
})
);
}
}
/**
* @internal
* @privateRemarks
* Copies internal state from another Control instance. Used when changing moveType option.
*/
copy(control) {
this._flicking = control._flicking;
this._activePanel = control._activePanel;
this._controller = control._controller;
}
/**
* @internal
* @privateRemarks
* Triggers {@link WillChangeEvent} or {@link WillRestoreEvent} based on whether the target panel differs from the active panel.
*/
_triggerIndexChangeEvent(panel, position, axesEvent, direction) {
var _a;
const flicking = getFlickingAttached(this._flicking);
const triggeringEvent = panel !== this._activePanel ? EVENTS.WILL_CHANGE : EVENTS.WILL_RESTORE;
const camera = flicking.camera;
const activePanel = this._activePanel;
const event = new ComponentEvent(triggeringEvent, {
index: panel.index,
panel,
isTrusted: (axesEvent == null ? void 0 : axesEvent.isTrusted) || false,
direction: direction != null ? direction : getDirection((_a = activePanel == null ? void 0 : activePanel.position) != null ? _a : camera.position, position)
});
this._nextPanel = panel;
flicking.trigger(event);
if (event.isCanceled()) {
throw new FlickingError(MESSAGE.STOP_CALLED_BY_USER, CODE.STOP_CALLED_BY_USER);
}
}
/**
* @internal
* @privateRemarks
* Animates the camera to the target position and handles animation completion or interruption.
*/
_animateToPosition(_0) {
return __async(this, arguments, function* ({
position,
duration,
newActivePanel,
axesEvent
}) {
const flicking = getFlickingAttached(this._flicking);
let nextDuration = duration;
if (Math.abs(nextDuration - position) < flicking.animationThreshold) {
nextDuration = 0;
}
const animate = () => this._controller.animateTo(position, nextDuration, axesEvent);
const state = this._controller.state;
state.targetPanel = newActivePanel;
if (nextDuration <= 0) {
return animate();
} else {
return animate().then(() => __async(this, null, function* () {
if (flicking.initialized) {
yield flicking.renderer.render();
}
})).catch((err) => {
if (axesEvent && err instanceof FlickingError && err.code === CODE.ANIMATION_INTERRUPTED) return;
throw err;
});
}
});
}
/**
* @internal
* @privateRemarks
* Calculates the target position for a panel, considering circular mode and direction constraints.
*/
_getPosition(panel, direction = DIRECTION.NONE) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
let position = panel.position;
const nearestAnchor = camera.findNearestAnchor(position);
if (panel.removed || !nearestAnchor) {
throw new FlickingError(MESSAGE.POSITION_NOT_REACHABLE(panel.position), CODE.POSITION_NOT_REACHABLE);
}
if (!camera.canReach(panel)) {
position = nearestAnchor.position;
panel = nearestAnchor.panel;
} else if (flicking.circularEnabled) {
const camPos = this._controller.position;
const camRangeDiff = camera.rangeDiff;
const possiblePositions = [position, position + camRangeDiff, position - camRangeDiff].filter((pos) => {
if (direction === DIRECTION.NONE) return true;
return direction === DIRECTION.PREV ? pos <= camPos : pos >= camPos;
});
position = possiblePositions.reduce((nearestPosition, pos) => {
if (Math.abs(camPos - pos) < Math.abs(camPos - nearestPosition)) {
return pos;
} else {
return nearestPosition;
}
}, Infinity);
}
return position;
}
}
class FreeControl extends Control {
/**
* Make scroll animation to stop at the start/end of the scroll area, not going out the bounce area
* @defaultValue true
*/
get stopAtEdge() {
return this._stopAtEdge;
}
set stopAtEdge(val) {
this._stopAtEdge = val;
}
constructor(options = {}) {
super();
const { stopAtEdge = true } = options;
this._stopAtEdge = stopAtEdge;
}
/**
* Update position after resizing
* @remarks
* Unlike the base Control, FreeControl preserves the progress within the panel instead of snapping to the panel position.
* @param progressInPanel - Previous camera's progress in active panel before resize
* @throws {@link InitializationErrors}
*/
updatePosition(progressInPanel) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const activePanel = this._activePanel;
if (activePanel) {
const panelRange = activePanel.range;
const newPosition = panelRange.min + (panelRange.max - panelRange.min) * progressInPanel;
camera.lookAt(camera.clampToReachablePosition(newPosition));
}
}
/**
* Move {@link Camera} to the given position
* @remarks
* Unlike SnapControl, FreeControl moves to the exact position without snapping to panel boundaries.
* @param position - The target position to move
* @param duration - Duration of the panel movement animation (unit: ms)
* @param axesEvent - {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-release | release} event of {@link https://naver.github.io/egjs-axes/ | Axes}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns A Promise which will be resolved after reaching the target position
*/
moveToPosition(position, duration, axesEvent) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const targetPos = camera.clampToReachablePosition(position);
const anchorAtPosition = camera.findAnchorIncludePosition(targetPos);
if (!anchorAtPosition) {
return Promise.reject(
new FlickingError(MESSAGE.POSITION_NOT_REACHABLE(position), CODE.POSITION_NOT_REACHABLE)
);
}
const targetPanel = anchorAtPosition.panel;
if (targetPanel !== this._activePanel) {
this._triggerIndexChangeEvent(targetPanel, position, axesEvent);
}
return this._animateToPosition({
position: this._stopAtEdge ? targetPos : position,
duration,
newActivePanel: targetPanel,
axesEvent
});
}
}
class SnapControl extends Control {
/**
* Maximum number of panels can go after release
* @defaultValue Infinity
*/
get count() {
return this._count;
}
set count(val) {
this._count = val;
}
constructor(options = {}) {
super();
const { count = Infinity } = options;
this._count = count;
}
/**
* Move {@link Camera} to the given position
* @remarks
* This method calculates the snap target based on the release momentum and threshold settings.
* @param position - The target position to move
* @param duration - Duration of the panel movement animation (unit: ms)
* @param axesEvent - {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-release | release} event of {@link https://naver.github.io/egjs-axes/ | Axes}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns A Promise which will be resolved after reaching the target position
*/
moveToPosition(position, duration, axesEvent) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const activeAnchor = camera.findActiveAnchor();
const anchorAtCamera = camera.findNearestAnchor(camera.position);
const state = this._controller.state;
if (!activeAnchor || !anchorAtCamera) {
return Promise.reject(
new FlickingError(MESSAGE.POSITION_NOT_REACHABLE(position), CODE.POSITION_NOT_REACHABLE)
);
}
const snapThreshold = this._calcSnapThreshold(flicking.threshold, position, activeAnchor);
const posDelta = flicking.animating ? state.delta : position - camera.position;
const absPosDelta = Math.abs(posDelta);
const snapDelta = axesEvent && axesEvent.delta[POSITION_KEY] !== 0 ? Math.abs(axesEvent.delta[POSITION_KEY]) : absPosDelta;
let targetAnchor;
if (snapDelta >= snapThreshold && snapDelta > 0) {
targetAnchor = this._findSnappedAnchor(position, anchorAtCamera);
} else if (absPosDelta >= flicking.threshold && absPosDelta > 0) {
targetAnchor = this._findAdjacentAnchor(position, posDelta, anchorAtCamera);
} else {
return this.moveToPanel(anchorAtCamera.panel, {
duration,
axesEvent
});
}
this._triggerIndexChangeEvent(targetAnchor.panel, position, axesEvent);
return this._animateToPosition({
position: camera.clampToReachablePosition(targetAnchor.position),
duration,
newActivePanel: targetAnchor.panel,
axesEvent
});
}
/**
* @internal
* @privateRemarks
* Finds the anchor point to snap to based on the target position and count option.
*/
_findSnappedAnchor(position, anchorAtCamera) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const count = this._count;
const currentPos = camera.position;
const clampedPosition = camera.clampToReachablePosition(position);
const anchorAtPosition = camera.findAnchorIncludePosition(clampedPosition);
if (!anchorAtCamera || !anchorAtPosition) {
throw new FlickingError(MESSAGE.POSITION_NOT_REACHABLE(position), CODE.POSITION_NOT_REACHABLE);
}
if (!Number.isFinite(count)) {
return anchorAtPosition;
}
const panelCount = flicking.panelCount;
const anchors = camera.anchorPoints;
let loopCount = Math.sign(position - currentPos) * Math.floor(Math.abs(position - currentPos) / camera.rangeDiff);
if (position > currentPos && anchorAtPosition.index < anchorAtCamera.index || anchorAtPosition.position > anchorAtCamera.position && anchorAtPosition.index === anchorAtCamera.index) {
loopCount += 1;
} else if (position < currentPos && anchorAtPosition.index > anchorAtCamera.index || anchorAtPosition.position < anchorAtCamera.position && anchorAtPosition.index === anchorAtCamera.index) {
loopCount -= 1;
}
const circularIndexOffset = loopCount * panelCount;
const anchorAtPositionIndex = anchorAtPosition.index + circularIndexOffset;
if (Math.abs(anchorAtPositionIndex - anchorAtCamera.index) <= count) {
const anchor = anchors[anchorAtPosition.index];
return new AnchorPoint({
index: anchor.index,
position: anchor.position + loopCount * camera.rangeDiff,
panel: anchor.panel
});
}
if (flicking.circularEnabled) {
const targetAnchor = anchors[circulateIndex(anchorAtCamera.index + Math.sign(position - currentPos) * count, panelCount)];
let loop = Math.floor(count / panelCount);
if (position > currentPos && targetAnchor.index < anchorAtCamera.index) {
loop += 1;
} else if (position < currentPos && targetAnchor.index > anchorAtCamera.index) {
loop -= 1;
}
return new AnchorPoint({
index: targetAnchor.index,
position: targetAnchor.position + loop * camera.rangeDiff,
panel: targetAnchor.panel
});
} else {
return anchors[clamp(anchorAtCamera.index + Math.sign(position - currentPos) * count, 0, anchors.length - 1)];
}
}
/**
* @internal
* @privateRemarks
* Finds the adjacent anchor point based on the movement direction.
*/
_findAdjacentAnchor(position, posDelta, anchorAtCamera) {
var _a;
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
if (camera.circularEnabled) {
const anchorIncludePosition = camera.findAnchorIncludePosition(position);
if (anchorIncludePosition && anchorIncludePosition.position !== anchorAtCamera.position) {
return anchorIncludePosition;
}
}
const adjacentAnchor = (_a = posDelta > 0 ? camera.getNextAnchor(anchorAtCamera) : camera.getPrevAnchor(anchorAtCamera)) != null ? _a : anchorAtCamera;
return adjacentAnchor;
}
/**
* @internal
* @privateRemarks
* Calculates the snap threshold based on the panel size and alignment.
*/
_calcSnapThreshold(threshold, position, activeAnchor) {
const isNextDirection = position > activeAnchor.position;
const panel = activeAnchor.panel;
const panelSize = panel.size;
const alignPos = panel.alignPosition;
return Math.max(
threshold,
isNextDirection ? panelSize - alignPos + panel.margin.next : alignPos + panel.margin.prev
);
}
}
class StrictControl extends Control {
constructor(options = {}) {
super();
this.setActive = (newActivePanel, prevActivePanel, isTrusted) => {
super.setActive(newActivePanel, prevActivePanel, isTrusted);
this.updateInput();
};
const { count = 1 } = options;
this._count = count;
this._resetIndexRange();
}
/**
* Maximum number of panels that can be moved at a time
* @defaultValue 1
*/
get count() {
return this._count;
}
set count(val) {
this._count = val;
}
/**
* Destroy Control and return to initial state
* @remarks
* This method also resets the index range used for movement constraints.
*/
destroy() {
super.destroy();
this._resetIndexRange();
}
/**
* Update {@link Control.controller | controller}'s state
* @remarks
* StrictControl limits the movement range based on the {@link StrictControlOptions.count | count} option.
* @returns The current instance for method chaining
*/
updateInput() {
var _a;
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 ? (_a = camera.findNearestAnchor(camera.position)) == null ? void 0 : _a.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;
}
moveToPanel(panel, options) {
return __async(this, null, function* () {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const controller = this._controller;
controller.update(camera.controlParams);
return __superGet(StrictControl.prototype, this, "moveToPanel").call(this, panel, options);
});
}
/**
* Move {@link Camera} to the given position
* @remarks
* StrictControl restricts movement to panels within the allowed index range based on the count option.
* @param position - The target position to move
* @param duration - Duration of the panel movement animation (unit: ms)
* @param axesEvent - {@link https://naver.github.io/egjs-axes/docs/api/Axes#event-release | release} event of {@link https://naver.github.io/egjs-axes/ | Axes}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns A Promise which will be resolved after reaching the target position
*/
moveToPosition(position, duration, axesEvent) {
var _a;
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const currentPanel = (_a = this._nextPanel) != null ? _a : 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 || !currentPanel) {
return Promise.reject(
new FlickingError(MESSAGE.POSITION_NOT_REACHABLE(position), CODE.POSITION_NOT_REACHABLE)
);
}
const prevPos = currentPanel.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;
let targetPanel;
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) {
const targetAnchor = position < cameraRange.min ? firstAnchor : lastAnchor;
targetPanel = targetAnchor.panel;
targetPos = targetAnchor.position;
} else if (isOverThreshold && anchorAtPosition.position !== currentPanel.position) {
targetPanel = anchorAtPosition.panel;
targetPos = anchorAtPosition.position;
} else if (isOverThreshold && isAdjacent) {
targetPanel = adjacentAnchor.panel;
targetPos = adjacentAnchor.position;
} else {
const anchorAtCamera = camera.findNearestAnchor(camera.position);
if (!anchorAtCamera) {
return Promise.reject(
new FlickingError(MESSAGE.POSITION_NOT_REACHABLE(position), CODE.POSITION_NOT_REACHABLE)
);
}
return this.moveToPanel(anchorAtCamera.panel, {
duration,
axesEvent
});
}
this._triggerIndexChangeEvent(targetPanel, position, axesEvent);
return this._animateToPosition({
position: targetPos,
duration,
newActivePanel: targetPanel,
axesEvent
});
}
/**
* @internal
* @privateRemarks
* Resets the index range to default values.
*/
_resetIndexRange() {
this._indexRange = { min: 0, max: 0 };
}
}
class AutoResizer {
/**
* @param flicking
*/
constructor(flicking) {
this._onResizeWrapper = () => {
this._onResize([]);
};
this._onResize = (entries) => {
const flicking2 = this._flicking;
const resizeDebounce = flicking2.resizeDebounce;
const maxResizeDebounce = flicking2.maxResizeDebounce;
const resizedViewportElement = flicking2.element;
const isResizedViewportOnly = entries.find((e) => e.target === flicking2.element) && entries.length === 1;
if (isResizedViewportOnly) {
const beforeSize = {
width: flicking2.viewport.width,
height: flicking2.viewport.height
};
const afterSize = {
width: getElementSize({
el: resizedViewportElement,
horizontal: true,
useFractionalSize: this._flicking.useFractionalSize,
useOffset: false,
style: getStyle(resizedViewportElement)
}),
height: getElementSize({
el: resizedViewportElement,
horizontal: false,
useFractionalSize: this._flicking.useFractionalSize,
useOffset: false,
style: getStyle(resizedViewportElement)
})
};
if (beforeSize.height === afterSize.height && beforeSize.width === afterSize.width) {
return;
}
}
if (resizeDebounce <= 0) {
void flicking2.resize();
} else {
if (this._maxResizeDebounceTimer <= 0) {
if (maxResizeDebounce > 0 && maxResizeDebounce >= resizeDebounce) {
this._maxResizeDebounceTimer = window.setTimeout(this._doScheduledResize, maxResizeDebounce);
}
}
if (this._resizeTimer > 0) {
clearTimeout(this._resizeTimer);
this._resizeTimer = 0;
}
this._resizeTimer = window.setTimeout(this._doScheduledResize, resizeDebounce);
}
};
this._doScheduledResize = () => {
clearTimeout(this._resizeTimer);
clearTimeout(this._maxResizeDebounceTimer);
this._maxResizeDebounceTimer = -1;
this._resizeTimer = -1;
void this._flicking.resize();
};
this._skipFirstResize = /* @__PURE__ */ (() => {
let isFirstResize = true;
return (entries) => {
if (isFirstResize) {
isFirstResize = false;
return;
}
this._onResize(entries);
};
})();
this._flicking = flicking;
this._enabled = false;
this._resizeObserver = null;
this._resizeTimer = -1;
this._maxResizeDebounceTimer = -1;
}
get enabled() {
return this._enabled;
}
enable() {
const flicking = this._flicking;
const viewport = flicking.viewport;
if (this._enabled) {
this.disable();
}
if (flicking.useResizeObserver && !!window.ResizeObserver) {
const viewportSizeNot0 = viewport.width !== 0 || viewport.height !== 0;
const resizeObserver = viewportSizeNot0 ? new ResizeObserver(this._skipFirstResize) : new ResizeObserver(this._onResize);
this._resizeObserver = resizeObserver;
this.observe(flicking.viewport.element);
if (flicking.observePanelResize) {
this.observePanels();
}
} else {
window.addEventListener("resize", this._onResizeWrapper);
}
this._enabled = true;
return this;
}
observePanels() {
this._flicking.panels.forEach((panel) => {
this.observe(panel.element);
});
return this;
}
unobservePanels() {
this._flicking.panels.forEach((panel) => {
this.unobserve(panel.element);
});
return this;
}
observe(element) {
const resizeObserver = this._resizeObserver;
if (!resizeObserver) return this;
resizeObserver.observe(element);
return this;
}
unobserve(element) {
const resizeObserver = this._resizeObserver;
if (!resizeObserver) return this;
resizeObserver.unobserve(element);
if (this._flicking.observePanelResize) {
this.unobservePanels();
}
return this;
}
disable() {
if (!this._enabled) return this;
const resizeObserver = this._resizeObserver;
if (resizeObserver) {
resizeObserver.disconnect();
this._resizeObserver = null;
} else {
window.removeEventListener("resize", this._onResizeWrapper);
}
this._enabled = false;
return this;
}
}
class VanillaElementProvider {
get element() {
return this._element;
}
get rendered() {
return this._rendered;
}
constructor(element) {
this._element = element;
this._rendered = true;
}
show(flicking) {
const el = this.element;
const cameraEl = flicking.camera.element;
if (el.parentElement !== cameraEl) {
cameraEl.appendChild(el);
this._rendered = true;
}
}
hide(flicking) {
const el = this.element;
const cameraEl = flicking.camera.element;
if (el.parentElement === cameraEl) {
cameraEl.removeChild(el);
this._rendered = false;
}
}
}
class VirtualElementProvider {
get element() {
return this._virtualElement.nativeElement;
}
get rendered() {
return this._virtualElement.visible;
}
get _virtualElement() {
const flicking = this._flicking;
const elIndex = this._panel.elementIndex;
const virtualElements = flicking.virtual.elements;
return virtualElements[elIndex];
}
constructor(flicking) {
this._flicking = flicking;
}
init(panel) {
this._panel = panel;
}
show() {
}
hide() {
}
}
class Viewport {
/**
* A viewport(root) element
* @readonly
*/
get element() {
return this._el;
}
/**
* Viewport width, without paddings
* @readonly
*/
get width() {
return this._width - this._padding.left - this._padding.right;
}
/**
* Viewport height, without paddings
* @readonly
*/
get height() {
return this._height - this._padding.top - this._padding.bottom;
}
/**
* Viewport paddings
* @readonly
*/
get padding() {
return this._padding;
}
/**
* @param flicking - Flicking instance
* @param el - A viewport element
*/
constructor(flicking, el) {
this._flicking = flicking;
this._el = el;
this._width = 0;
this._height = 0;
this._padding = {
left: 0,
right: 0,
top: 0,
bottom: 0
};
this._isBorderBoxSizing = false;
}
/**
* Change viewport's size.
* @remarks
* This will change the actual size of `.flicking-viewport` element by changing its CSS width/height property
* @param size - {@link SetSizeParams}
*/
setSize(size) {
const { width, height } = size;
const el = this._el;
const padding = this._padding;
const isBorderBoxSizing = this._isBorderBoxSizing;
if (width != null) {
if (isString(width)) {
el.style.width = width;
} else {
const newWidth = isBorderBoxSizing ? width + padding.left + padding.right : width;
el.style.width = `${newWidth}px`;
}
}
if (height != null) {
if (isString(height)) {
el.style.height = height;
} else {
const newHeight = isBorderBoxSizing ? height + padding.top + padding.bottom : height;
el.style.height = `${newHeight}px`;
}
}
this.resize();
}
/**
* Measure the current size of the viewport element without updating {@link Viewport.width | width}/{@link Viewport.height | height}
* @internal
* @privateRemarks
* Used inside {@link Flicking.resize} to determine whether the panels should be re-rendered (optimizeSizeUpdate)
* while keeping the previous viewport size until the re-rendering finishes.
* The new size is committed afterwards by calling {@link Viewport.resize | resize()}.
* @returns Current width/height of the viewport element, without paddings
*/
measureSize() {
const { width, height, padding } = this._measure();
return {
width: width - padding.left - padding.right,
height: height - padding.top - padding.bottom
};
}
/**
* Update width/height to the current viewport element's size
*/
resize() {
const { width, height, padding, isBorderBoxSizing } = this._measure();
this._width = width;
this._height = height;
this._padding = padding;
this._isBorderBoxSizing = isBorderBoxSizing;
}
_measure() {
const el = this._el;
const elStyle = getStyle(el);
const { useFractionalSize } = this._flicking;
return {
width: getElementSize({
el,
horizontal: true,
useFractionalSize,
useOffset: false,
style: elStyle
}),
height: getElementSize({
el,
horizontal: false,
useFractionalSize,
useOffset: false,
style: elStyle
}),
padding: {
left: elStyle.paddingLeft ? parseFloat(elStyle.paddingLeft) : 0,
right: elStyle.paddingRight ? parseFloat(elStyle.paddingRight) : 0,
top: elStyle.paddingTop ? parseFloat(elStyle.paddingTop) : 0,
bottom: elStyle.paddingBottom ? parseFloat(elStyle.paddingBottom) : 0
},
isBorderBoxSizing: elStyle.boxSizing === "border-box"
};
}
}
class VirtualManager {
get elements() {
return this._elements;
}
// Options
/**
* A rendering function for the panel element's innerHTML
*/
get renderPanel() {
return this._renderPanel;
}
/**
* Initial panel count to render
* @readonly
* @defaultValue -1
*/
get initialPanelCount() {
return this._initialPanelCount;
}
/**
* Whether to cache rendered panel's innerHTML
* @defaultValue false
*/
get cache() {
return this._cache;
}
/**
* The class name that will be applied to rendered panel elements
* @defaultValue "flicking-panel"
*/
get panelClass() {
return this._panelClass;
}
set renderPanel(val) {
this._renderPanel = val;
this._flicking.renderer.panels.forEach((panel) => panel.uncacheRenderResult());
}
set cache(val) {
this._cache = val;
}
set panelClass(val) {
this._panelClass = val;
}
constructor(flicking, options) {
var _a, _b, _c, _d;
this._flicking = flicking;
this._renderPanel = (_a = options == null ? void 0 : options.renderPanel) != null ? _a : (() => "");
this._initialPanelCount = (_b = options == null ? void 0 : options.initialPanelCount) != null ? _b : -1;
this._cache = (_c = options == null ? void 0 : options.cache) != null ? _c : false;
this._panelClass = (_d = options == null ? void 0 : options.panelClass) != null ? _d : CLASS.DEFAULT_VIRTUAL;
this._elements = [];
}
init() {
const flicking = this._flicking;
if (!flicking.virtualEnabled) return;
if (!flicking.externalRenderer && !flicking.renderExternal) {
this._initVirtualElements();
}
const virtualElements = flicking.camera.children;
this._elements = virtualElements.map((el) => ({ nativeElement: el, visible: true }));
}
show(index) {
const el = this._elements[index];
const nativeEl = el.nativeElement;
el.visible = true;
if (nativeEl.style.display) {
nativeEl.style.display = "";
}
}
hide(index) {
const el = this._elements[index];
const nativeEl = el.nativeElement;
el.visible = false;
nativeEl.style.display = "none";
}
/**
* Add new virtual panels at the end of the list
* @param count - The number of panels to add
* @returns The new panels added
*/
append(count = 1) {
const flicking = this._flicking;
return this.insert(flicking.panels.length, count);
}
/**
* Add new virtual panels at the start of the list
* @param count - The number of panels to add
* @returns The new panels added
*/
prepend(count = 1) {
return this.insert(0, count);
}
/**
* Add new virtual panels at the given index
* @param count - The number of panels to add
* @returns The new panels added
*/
insert(index, count = 1) {
if (count <= 0) return [];
const flicking = this._flicking;
return flicking.renderer.batchInsert({ index, elements: range(count), hasDOMInElements: false });
}
/**
* Remove panels at the given index
* @param count - The number of panels to remove
* @returns The panels removed
*/
remove(index, count) {
if (count <= 0) return [];
const flicking = this._flicking;
return flicking.renderer.batchRemove({ index, deleteCount: count, hasDOMInElements: false });
}
/**
* @internal
*/
_initVirtualElements() {
const flicking = this._flicking;
const cameraElement = flicking.camera.element;
const panelsPerView = flicking.panelsPerView;
const fragment = document.createDocumentFragment();
const newElements = range(panelsPerView + 1).map((idx) => {
const panelEl = document.createElement("div");
panelEl.className = this._panelClass;
panelEl.dataset.elementIndex = idx.toString();
return panelEl;
});
newElements.forEach((el) => {
fragment.appendChild(el);
});
cameraElement.appendChild(fragment);
}
}
class Renderer {
// Internal states Getter
/**
* Array of panels
* @readonly
* @see Panel
*/
get panels() {
return this._panels;
}
/**
* A boolean value indicating whether rendering is in progress
* @readonly
* @internal
*/
get rendering() {
return this._rendering;
}
/**
* Count of panels
* @readonly
*/
get panelCount() {
return this._panels.length;
}
/**
* @internal
*/
get strategy() {
return this._strategy;
}
// Options Getter
/**
* A {@link Panel}'s {@link Panel.align | align} value that applied to all panels
*/
get align() {
return this._align;
}
// Options Setter
set align(val) {
this._align = val;
const panelAlign = parsePanelAlign(val);
this._panels.forEach((panel) => {
panel.align = panelAlign;
});
}
/**
* @param options - {@link RendererOptions}
*/
constructor(options) {
const { align = ALIGN.CENTER, strategy } = options;
this._flicking = null;
this._panels = [];
this._rendering = false;
this._align = align;
this._strategy = strategy;
}
/**
* Initialize Renderer
* @remarks
* This method is called automatically during {@link Flicking.init}. It collects existing panel elements.
* @param flicking - An instance of {@link Flicking}
* @returns The current instance for method chaining
*/
init(flicking) {
this._flicking = flicking;
this._collectPanels();
return this;
}
/**
* Destroy Renderer and return to initial state
* @remarks
* This method clears all panel references and resets the internal state.
*/
destroy() {
this._flicking = null;
this._panels = [];
}
/**
* Return the {@link Panel} at the given index. `null` if it doesn't exist.
* @remarks
* This is equivalent to accessing `Flicking.panels[index]`.
* @param index - Index of the panel to get
* @returns Panel at the given index
* @see Panel
*/
getPanel(index) {
return this._panels[index] || null;
}
forceRenderAllPanels() {
this._panels.forEach((panel) => panel.markForShow());
return Promise.resolve();
}
/**
* Return Rendered Panels
* @returns Rendered Panels
*/
getRenderedPanels() {
const flicking = getFlickingAttached(this._flicking);
return flicking.renderer.panels.filter((panel) => panel.rendered);
}
/**
* Update all panel sizes
* @returns The current instance for method chaining
*/
updatePanelSize() {
const flicking = getFlickingAttached(this._flicking);
const panels = this._panels;
if (panels.length <= 0) return this;
if (flicking.panelsPerView > 0) {
const firstPanel = panels[0];
firstPanel.resize();
this._updatePanelSizeByGrid(firstPanel, panels);
} else {
flicking.panels.forEach((panel) => panel.resize());
}
return this;
}
/**
* Insert new panels at given index
* @remarks
* This will increase index of panels after by the number of panels added.
* @param items - An array of {@link BatchInsertParams}
* @throws {@link DOMManipulationErrors}
* @returns An array of inserted panels
*/
batchInsert(...items) {
const allPanelsInserted = this.batchInsertDefer(...items);
if (allPanelsInserted.length <= 0) return [];
this.updateAfterPanelChange(allPanelsInserted, []);
return allPanelsInserted;
}
/**
* @internal
* @privateRemarks
* Defers update. Camera position & others will be updated after calling updateAfterPanelChange.
*/
batchInsertDefer(...items) {
const panels = this._panels;
const flicking = getFlickingAttached(this._flicking);
const prevFirstPanel = panels[0];
const align = parsePanelAlign(this._align);
const allPanelsInserted = items.reduce((addedPanels, item) => {
var _a;
const insertingIdx = getMinusCompensatedIndex(item.index, panels.length);
const panelsPushed = panels.slice(insertingIdx);
const panelsInserted = item.elements.map(
(el, idx) => this._createPanel(el, { index: insertingIdx + idx, align, flicking })
);
panels.splice(insertingIdx, 0, ...panelsInserted);
if (item.hasDOMInElements) {
this._insertPanelElements(panelsInserted, (_a = panelsPushed[0]) != null ? _a : null);
}
if (flicking.panelsPerView > 0) {
const firstPanel = prevFirstPanel || panelsInserted[0].resize();
this._updatePanelSizeByGrid(firstPanel, panelsInserted);
} else {
panelsInserted.forEach((panel) => panel.resize());
}
panelsPushed.forEach((panel) => {
panel.increaseIndex(panelsInserted.length);
panel.updatePosition();
});
return [...addedPanels, ...panelsInserted];
}, []);
return allPanelsInserted;
}
/**
* Remove the panel at the given index
* @remarks
* This will decrease index of panels after by the number of panels removed.
* @param items - An array of {@link BatchRemoveParams}
* @throws {@link DOMManipulationErrors}
* @returns An array of removed panels
*/
batchRemove(...items) {
const allPanelsRemoved = this.batchRemoveDefer(...items);
if (allPanelsRemoved.length <= 0) return [];
this.updateAfterPanelChange([], allPanelsRemoved);
return allPanelsRemoved;
}
/**
* @internal
* @privateRemarks
* Defers update. Camera position & others will be updated after calling updateAfterPanelChange.
*/
batchRemoveDefer(...items) {
const panels = this._panels;
const flicking = getFlickingAttached(this._flicking);
const { control } = flicking;
const activePanel = control.activePanel;
const allPanelsRemoved = items.reduce((removed, item) => {
const { index, deleteCount } = item;
const removingIdx = getMinusCompensatedIndex(index, panels.length);
const panelsPulled = panels.slice(removingIdx + deleteCount);
const panelsRemoved = panels.splice(removingIdx, deleteCount);
if (panelsRemoved.length <= 0) return [];
panelsPulled.forEach((panel) => {
panel.decreaseIndex(panelsRemoved.length);
panel.updatePosition();
});
if (item.hasDOMInElements) {
this._removePanelElements(panelsRemoved);
}
panelsRemoved.forEach((panel) => panel.destroy());
if (includes(panelsRemoved, activePanel)) {
control.resetActive();
}
return [...removed, ...panelsRemoved];
}, []);
return allPanelsRemoved;
}
/**
* @internal
* @privateRemarks
* Updates camera, control, and triggers {@link PanelChangeEvent} after panels are added or removed.
*/
updateAfterPanelChange(panelsAdded, panelsRemoved) {
var _a;
const flicking = getFlickingAttached(this._flicking);
const { camera, control } = flicking;
const panels = this._panels;
const activePanel = control.activePanel;
this._updateCameraAndControl();
if (flicking.autoResize && flicking.useResizeObserver) {
panelsAdded.forEach((panel) => {
if (panel.element) {
flicking.autoResizer.observe(panel.element);
}
});
panelsRemoved.forEach((panel) => {
if (panel.element) {
flicking.autoResizer.unobserve(panel.element);
}
});
}
void this.render();
if (!flicking.animating) {
if (!activePanel || activePanel.removed) {
if (panels.length <= 0) {
camera.lookAt(0);
} else {
let targetIndex = (_a = activePanel == null ? void 0 : activePanel.index) != null ? _a : 0;
if (targetIndex > panels.length - 1) {
targetIndex = panels.length - 1;
}
void control.moveToPanel(panels[targetIndex], {
duration: 0
}).catch(() => void 0);
}
} else {
void control.moveToPanel(activePanel, {
duration: 0
}).catch(() => void 0);
}
}
flicking.camera.updateOffset();
if (panelsAdded.length > 0 || panelsRemoved.length > 0) {
flicking.trigger(
new ComponentEvent(EVENTS.PANEL_CHANGE, {
added: panelsAdded,
removed: panelsRemoved
})
);
this.checkPanelContentsReady([...panelsAdded, ...panelsRemoved]);
}
}
/**
* @internal
* @privateRemarks
* Checks if panel contents (images/videos) are ready and triggers resize when loaded.
*/
checkPanelContentsReady(checkingPanels) {
const flicking = getFlickingAttached(this._flicking);
const resizeOnContentsReady = flicking.resizeOnContentsReady;
const panels = this._panels;
if (!resizeOnContentsReady || flicking.virtualEnabled) return;
const hasContents = (panel) => panel.element && !!panel.element.querySelector("img, video");
checkingPanels = checkingPanels.filter((panel) => hasContents(panel));
if (checkingPanels.length <= 0) return;
const contentsReadyChecker = new ImReady();
checkingPanels.forEach((panel) => {
panel.loading = true;
});
contentsReadyChecker.on("readyElement", (e) => {
if (!this._flicking) {
contentsReadyChecker.destroy();
return;
}
const panel = checkingPanels[e.index];
const camera = flicking.camera;
const control = flicking.control;
const prevProgressInPanel = control.activePanel ? camera.getProgressInPanel(control.activePanel) : 0;
panel.loading = false;
panel.resize();
panels.slice(panel.index + 1).forEach((panelBehind) => panelBehind.updatePosition());
if (!flicking.initialized) return;
camera.updateRange();
camera.updateOffset();
camera.updateAnchors();
if (control.animating) ;
else {
control.updatePosition(prevProgressInPanel);
control.updateInput();
}
});
contentsReadyChecker.on("preReady", (e) => {
if (this._flicking) {
void this.render();
}
if (e.readyCount === e.totalCount) {
contentsReadyChecker.destroy();
}
});
contentsReadyChecker.on("ready", () => {
if (this._flicking) {
void this.render();
}
contentsReadyChecker.destroy();
});
contentsReadyChecker.check(checkingPanels.map((panel) => panel.element));
}
/**
* @internal
* @privateRemarks
* Updates camera range, anchors, and control input after panel changes.
*/
_updateCameraAndControl() {
const flicking = getFlickingAttached(this._flicking);
const { camera, control } = flicking;
camera.updateRange();
camera.updateOffset();
camera.updateAnchors();
camera.resetNeedPanelHistory();
control.updateInput();
}
/**
* @internal
* @privateRemarks
* Marks only visible panels for rendering when {@link FlickingOptions.renderOnlyVisible | renderOnlyVisible} is enabled.
*/
_showOnlyVisiblePanels(flicking) {
const panels = flicking.renderer.panels;
const camera = flicking.camera;
const visibleIndexes = camera.visiblePanels.reduce((visibles, panel) => {
visibles[panel.index] = true;
return visibles;
}, {});
panels.forEach((panel) => {
if (panel.index in visibleIndexes || panel.loading) {
panel.markForShow();
} else if (!flicking.holding) {
panel.markForHide();
}
});
}
/**
* @internal
* @privateRemarks
* Calculates and applies panel sizes when {@link FlickingOptions.panelsPerView | panelsPerView} is enabled.
*/
_updatePanelSizeByGrid(referencePanel, panels) {
const flicking = getFlickingAttached(this._flicking);
const panelsPerView = flicking.panelsPerView;
if (panelsPerView <= 0) {
throw new FlickingError(MESSAGE.WRONG_OPTION("panelsPerView", panelsPerView), CODE.WRONG_OPTION);
}
if (panels.length <= 0) return;
const viewportSize = flicking.camera.size;
const gap = referencePanel.margin.prev + referencePanel.margin.next;
const panelSize = (viewportSize - gap * (panelsPerView - 1)) / panelsPerView;
const panelSizeObj = flicking.horizontal ? { width: panelSize } : { height: panelSize };
const firstPanelSizeObj = __spreadValues({
size: panelSize,
margin: referencePanel.margin
}, !flicking.horizontal && { height: referencePanel.height });
if (!flicking.noPanelStyleOverride) {
this._strategy.updatePanelSizes(flicking, panelSizeObj);
}
flicking.panels.forEach((panel) => panel.resize(firstPanelSizeObj));
}
/**
* @internal
* @privateRemarks
* Removes all child elements from the camera element.
*/
_removeAllChildsFromCamera() {
const flicking = getFlickingAttached(this._flicking);
const cameraElement = flicking.camera.element;
while (cameraElement.firstChild) {
cameraElement.removeChild(cameraElement.firstChild);
}
}
/**
* @internal
* @privateRemarks
* Inserts panel elements into the camera element at the specified position.
*/
_insertPanelElements(panels, nextSibling = null) {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
const cameraElement = camera.element;
const nextSiblingElement = (nextSibling == null ? void 0 : nextSibling.element) || null;
const fragment = document.createDocumentFragment();
panels.forEach((panel) => fragment.appendChild(panel.element));
const referenceElement = nextSiblingElement && nextSiblingElement.parentNode === cameraElement ? nextSiblingElement : null;
cameraElement.insertBefore(fragment, referenceElement);
}
/**
* @internal
* @privateRemarks
* Removes panel elements from the camera element.
*/
_removePanelElements(panels) {
const flicking = getFlickingAttached(this._flicking);
const cameraElement = flicking.camera.element;
panels.forEach((panel) => {
if (panel.element.parentNode === cameraElement) {
cameraElement.removeChild(panel.element);
}
});
}
/**
* @internal
* @privateRemarks
* Called after rendering to apply the camera transform.
*/
_afterRender() {
const flicking = getFlickingAttached(this._flicking);
flicking.camera.applyTransform();
if (flicking.useCSSOrder) {
const panels = flicking.panels;
this._strategy.getRenderingIndexesByOrder(flicking).forEach((domIndex, index) => {
var _a;
if ((_a = panels[domIndex]) == null ? void 0 : _a.element) {
panels[domIndex].element.style.order = `${index}`;
}
});
}
}
}
class ExternalRenderer extends Renderer {
/* eslint-disable @typescript-eslint/no-unused-vars */
_removePanelElements(panels) {
}
_removeAllChildsFromCamera() {
}
/* eslint-enable @typescript-eslint/no-unused-vars */
}
class VanillaRenderer extends Renderer {
// eslint-disable-next-line @typescript-eslint/require-await
render() {
return __async(this, null, function* () {
const flicking = getFlickingAttached(this._flicking);
const strategy = this._strategy;
strategy.updateRenderingPanels(flicking);
strategy.renderPanels(flicking);
this._resetPanelElementOrder();
this._afterRender();
});
}
_collectPanels() {
const flicking = getFlickingAttached(this._flicking);
const camera = flicking.camera;
this._removeAllTextNodes();
this._panels = this._strategy.collectPanels(flicking, camera.children);
}
_createPanel(el, options) {
return this._strategy.createPanel(el, options);
}
/**
* @internal
*/
_resetPanelElementOrder() {
const flicking = getFlickingAttached(this._flicking);
const cameraEl = flicking.camera.element;
let reversedElements = [];
if (flicking.useCSSOrder) {
reversedElements = this.getRenderedPanels().map((panel) => panel.element).reverse();
} else {
reversedElements = this._strategy.getRenderingElementsByOrder(flicking).reverse();
}
reversedElements.forEach((el, idx) => {
const nextEl = reversedElements[idx - 1] ? reversedElements[idx - 1] : null;
if (el.nextElementSibling !== nextEl) {
cameraEl.insertBefore(el, nextEl);
}
});
}
/**
* @internal
*/
_removeAllTextNodes() {
const flicking = getFlickingAttached(this._flicking);
const cameraElement = flicking.camera.element;
toArray(cameraElement.childNodes).forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
cameraElement.removeChild(node);
}
});
}
}
class Panel {
// Internal States Getter
/**
* `HTMLElement` that panel's referencing
* @readonly
*/
get element() {
return this._elProvider.element;
}
/**
* @internal
* @readonly
*/
get elementProvider() {
return this._elProvider;
}
/**
* Index of the panel
* @readonly
*/
get index() {
return this._index;
}
/**
* Position of the panel, including {@link Panel.alignPosition | alignPosition}
* @readonly
*/
get position() {
return this._pos + this._alignPos;
}
/**
* Cached size of the panel element
* @remarks
* This is equal to {@link Panel.element | element}'s `offsetWidth` if {@link Flicking.horizontal | horizontal} is `true`, and `offsetHeight` else
* @readonly
*/
get size() {
return this._size;
}
/**
* Panel's size including CSS `margin`
* @remarks
* This value includes {@link Panel.element | element}'s margin left/right if {@link Flicking.horizontal | horizontal} is `true`, and margin top/bottom else
* @readonly
*/
get sizeIncludingMargin() {
return this._size + this._margin.prev + this._margin.next;
}
/**
* Height of the panel element
* @readonly
*/
get height() {
return this._height;
}
/**
* Cached CSS `margin` value of the panel element
* @readonly
*/
get margin() {
return this._margin;
}
/**
* Align position inside the panel where {@link Camera}'s {@link Camera.alignPosition | alignPosition} inside viewport should be located at
* @readonly
*/
get alignPosition() {
return this._alignPos;
}
/**
* A value indicating whether the panel's {@link Flicking.remove | remove}d
* @readonly
*/
get removed() {
return this._removed;
}
/**
* A value indicating whether the panel's element is being rendered on the screen
* @readonly
*/
get rendered() {
return this._rendered;
}
/**
* A value indicating whether the panel's image/video is not loaded and waiting for resize
* @readonly
*/
get loading() {
return this._loading;
}
/**
* Panel element's range of the bounding box
* @readonly
*/
get range() {
return { min: this._pos, max: this._pos + this._size };
}
/**
* A value indicating whether the panel's position is toggled by circular behavior
* @readonly
*/
get toggled() {
return this._toggled;
}
/**
* A direction where the panel's position is toggled
* @readonly
*/
get toggleDirection() {
return this._toggleDirection;
}
/**
* Actual position offset determined by {@link Panel.order}
* @readonly
*/
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
* @readonly
*/
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)
* @readonly
*/
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$1(camPos, position, disappearPosNext);
} else {
const disappearPosPrev = position - (camera.alignPosition + this._size - alignPosition);
return 1 - getProgress$1(camPos, disappearPosPrev, position);
}
}
/**
* Percentage of area where panel is visible in the viewport
* @readonly
*/
get visibleRatio() {
const range2 = this.range;
const size = this._size;
const offset = this.offset;
const visibleRange = this._flicking.camera.visibleRange;
const checkingRange = {
min: range2.min + offset,
max: range2.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;
}
set loading(val) {
this._loading = val;
}
// Options Getter
/**
* A value indicating where the {@link Panel.alignPosition | alignPosition} should be located at inside the panel element
*/
get align() {
return this._align;
}
// Options Setter
set align(val) {
this._align = val;
this._updateAlignPos();
}
/**
* Creates a new Panel instance
* @param panelOptions - Options for creating the panel
*/
constructor(panelOptions) {
const { 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();
}
/**
* @internal
* @privateRemarks
* Marks this panel to be rendered on the camera element.
*/
markForShow() {
this._rendered = true;
this._elProvider.show(this._flicking);
}
/**
* @internal
* @privateRemarks
* Marks this panel to be hidden from the camera element.
*/
markForHide() {
this._rendered = false;
this._elProvider.hide(this._flicking);
}
/**
* Update size of the panel
* @remarks
* This method recalculates the panel's size, margin, and position based on the current DOM state.
* @param cached - Predefined cached size of the panel
* @returns The current instance for method chaining
*/
resize(cached) {
var _a;
const el = this.element;
const flicking = this._flicking;
const { horizontal, useFractionalSize } = flicking;
if (!el) {
return this;
}
if (cached) {
this._size = cached.size;
this._margin = __spreadValues({}, cached.margin);
this._height = (_a = cached.height) != null ? _a : 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
* @remarks
* This will change the actual size of the panel element by changing its CSS width/height property.
* @param size - {@link SetSizeParams}
* @returns The current instance for method chaining
*/
setSize(size) {
setSize(this.element, size);
return this;
}
/**
* Check whether the given element is inside of this panel's {@link Panel.element | element}
* @remarks
* This is useful for determining which panel contains a clicked element.
* @param element - The HTMLElement to check
* @returns A Boolean value indicating the element is inside of this panel {@link Panel.element | element}
*/
contains(element) {
var _a;
return !!((_a = this.element) == null ? void 0 : _a.contains(element));
}
/**
* Reset internal state and set {@link Panel.removed | removed} to `true`
* @remarks
* After calling this method, the panel should no longer be used.
*/
destroy() {
this._resetInternalStates();
this._removed = true;
}
/**
* Check whether the given position is inside of this panel's {@link Panel.range | range}
* @param pos - A position to check
* @param includeMargin - Include {@link Panel.margin | margin} to the range
* @returns A Boolean value indicating whether the given position is included in the panel range
*/
includePosition(pos, includeMargin = false) {
return this.includeRange(pos, pos, includeMargin);
}
/**
* Check whether the given range is fully included in this panel's area (inclusive)
* @param min - Minimum value of the range to check
* @param max - Maximum value of the range to check
* @param includeMargin - Include {@link Panel.margin | margin} to the range
* @returns A Boolean value indicating whether the given range is fully included in the panel range
*/
includeRange(min, max, includeMargin = false) {
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)
* @param min - Minimum value of the range to check
* @param max - Maximum value of the range to check
* @returns A Boolean value indicating whether the panel is visible
*/
isVisibleOnRange(min, max) {
const panelRange = this.range;
return max > panelRange.min && min < panelRange.max;
}
/**
* Move {@link Camera} to this panel
* @remarks
* This is equivalent to calling `Flicking.moveTo(panel.index, duration)`.
* @param duration - Duration of the animation (unit: ms). Defaults to {@link FlickingOptions.duration}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns A Promise which will be resolved after reaching the panel
*/
focus(duration) {
return this._flicking.moveTo(this._index, duration);
}
/**
* Get previous(`index - 1`) panel.
* @remarks
* 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
* @returns The previous panel
*/
prev() {
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.
* @remarks
* 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
* @returns The next panel
*/
next() {
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);
}
/**
* @internal
* @privateRemarks
* Increases the panel's index by the given value. Called when panels are inserted before this panel.
*/
increaseIndex(val) {
this._index += Math.max(val, 0);
return this;
}
/**
* @internal
* @privateRemarks
* Decreases the panel's index by the given value. Called when panels are removed before this panel.
*/
decreaseIndex(val) {
this._index -= Math.max(val, 0);
return this;
}
/**
* @internal
* @privateRemarks
* Recalculates the panel's position based on the previous panel's position and margins.
*/
updatePosition() {
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
* @privateRemarks
* Toggles the panel's position for circular mode. Returns true if the panel was toggled.
*/
toggle(prevPos, newPos) {
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
* @privateRemarks
* Updates the toggle direction for circular mode based on the panel's visibility at the camera range edges.
*/
updateCircularToggleDirection() {
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;
}
/**
* @internal
* @privateRemarks
* Recalculates the align position based on the align option and panel size.
*/
_updateAlignPos() {
this._alignPos = parseAlign$1(this._align, this._size);
}
/**
* @internal
* @privateRemarks
* Resets all internal state values to their defaults.
*/
_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;
}
}
class NormalRenderingStrategy {
constructor(options) {
const { providerCtor } = options;
this._providerCtor = providerCtor;
}
renderPanels() {
}
getRenderingIndexesByOrder(flicking) {
const renderedPanels = flicking.renderer.panels.filter((panel) => panel.rendered);
const toggledPrev = renderedPanels.filter((panel) => panel.toggled && panel.toggleDirection === DIRECTION.PREV);
const toggledNext = renderedPanels.filter((panel) => panel.toggled && panel.toggleDirection === DIRECTION.NEXT);
const notToggled = renderedPanels.filter((panel) => !panel.toggled);
return [...toggledPrev, ...notToggled, ...toggledNext].map((panel) => panel.index);
}
getRenderingElementsByOrder(flicking) {
const panels = flicking.panels;
return this.getRenderingIndexesByOrder(flicking).map((index) => panels[index].element);
}
updateRenderingPanels(flicking) {
if (flicking.renderOnlyVisible) {
this._showOnlyVisiblePanels(flicking);
} else {
flicking.panels.forEach((panel) => panel.markForShow());
}
}
collectPanels(flicking, elements) {
const align = parsePanelAlign(flicking.renderer.align);
return elements.map(
(el, index) => new Panel({
index,
elementProvider: new this._providerCtor(el),
align,
flicking
})
);
}
createPanel(element, options) {
return new Panel(__spreadProps(__spreadValues({}, options), {
elementProvider: new this._providerCtor(element)
}));
}
updatePanelSizes(flicking, size) {
flicking.panels.forEach((panel) => panel.setSize(size));
}
/**
* @internal
*/
_showOnlyVisiblePanels(flicking) {
const panels = flicking.renderer.panels;
const camera = flicking.camera;
const visibleIndexes = camera.visiblePanels.reduce((visibles, panel) => {
visibles[panel.index] = true;
return visibles;
}, {});
panels.forEach((panel) => {
if (panel.index in visibleIndexes || panel.loading) {
panel.markForShow();
} else if (!flicking.holding) {
panel.markForHide();
}
});
camera.updateOffset();
}
}
class VirtualPanel extends Panel {
/**
* `HTMLElement` that panel's referencing
* @readonly
*/
get element() {
return this._elProvider.element;
}
/**
* Cached innerHTML by the previous render function
* @readonly
*/
get cachedInnerHTML() {
return this._cachedInnerHTML;
}
/**
* A number for indexing which element it will be rendered on
* @readonly
*/
get elementIndex() {
const flicking = this._flicking;
const virtualElCount = flicking.panelsPerView + 1;
const panelCount = flicking.panelCount;
let index = this._index;
if (this._toggled) {
index = this._toggleDirection === DIRECTION.NEXT ? index + panelCount : index - panelCount;
}
return circulateIndex(index, virtualElCount);
}
/**
* @param options - {@link VirtualPanelOptions}
*/
constructor(options) {
super(options);
options.elementProvider.init(this);
this._elProvider = options.elementProvider;
this._cachedInnerHTML = null;
}
cacheRenderResult(result) {
this._cachedInnerHTML = result;
}
uncacheRenderResult() {
this._cachedInnerHTML = null;
}
render() {
const flicking = this._flicking;
const { renderPanel, cache } = flicking.virtual;
const element = this._elProvider.element;
const newInnerHTML = this._cachedInnerHTML || renderPanel(this, this._index);
if (newInnerHTML === element.innerHTML) return;
element.innerHTML = newInnerHTML;
if (cache) {
this.cacheRenderResult(newInnerHTML);
}
}
increaseIndex(val) {
this.uncacheRenderResult();
return super.increaseIndex(val);
}
decreaseIndex(val) {
this.uncacheRenderResult();
return super.decreaseIndex(val);
}
}
class VirtualRenderingStrategy {
renderPanels(flicking) {
const virtualManager = flicking.virtual;
const visiblePanels = flicking.visiblePanels;
const invisibleIndexes = range(flicking.panelsPerView + 1);
visiblePanels.forEach((panel) => {
const elementIndex = panel.elementIndex;
panel.render();
virtualManager.show(elementIndex);
invisibleIndexes[elementIndex] = -1;
});
invisibleIndexes.filter((val) => val >= 0).forEach((idx) => {
virtualManager.hide(idx);
});
}
getRenderingIndexesByOrder(flicking) {
const virtualManager = flicking.virtual;
const visiblePanels = [...flicking.visiblePanels].filter((panel) => panel.rendered).sort((panel1, panel2) => {
return panel1.position + panel1.offset - (panel2.position + panel2.offset);
});
if (visiblePanels.length <= 0) return virtualManager.elements.map((_, idx) => idx);
const visibleIndexes = visiblePanels.map((panel) => panel.elementIndex);
const invisibleIndexes = virtualManager.elements.map((el, idx) => __spreadProps(__spreadValues({}, el), { idx })).filter((el) => !el.visible).map((el) => el.idx);
return [...visibleIndexes, ...invisibleIndexes];
}
getRenderingElementsByOrder(flicking) {
const virtualManager = flicking.virtual;
const elements = virtualManager.elements;
return this.getRenderingIndexesByOrder(flicking).map((index) => elements[index].nativeElement);
}
updateRenderingPanels(flicking) {
const panels = flicking.renderer.panels;
const camera = flicking.camera;
const visibleIndexes = camera.visiblePanels.reduce((visibles, panel) => {
visibles[panel.index] = true;
return visibles;
}, {});
panels.forEach((panel) => {
if (panel.index in visibleIndexes || panel.loading) {
panel.markForShow();
} else {
panel.markForHide();
}
});
camera.updateOffset();
}
collectPanels(flicking) {
const align = parsePanelAlign(flicking.renderer.align);
return range(flicking.virtual.initialPanelCount).map(
(index) => new VirtualPanel({
index,
elementProvider: new VirtualElementProvider(flicking),
align,
flicking
})
);
}
createPanel(_el, options) {
return new VirtualPanel(__spreadProps(__spreadValues({}, options), {
elementProvider: new VirtualElementProvider(options.flicking)
}));
}
updatePanelSizes(flicking, size) {
flicking.virtual.elements.forEach((el) => {
setSize(el.nativeElement, size);
});
flicking.panels.forEach((panel) => panel.setSize(size));
}
}
const _Flicking = class _Flicking extends Component {
/** Creates a new Flicking instance
* @param root - A root HTMLElement to initialize Flicking on it. When it's a typeof `string`, it should be a css selector string
* @param options - A {@link FlickingOptions} object
* @throws {@link InitializationErrors}
* @example
* ```ts
* import Flicking from "@egjs/flicking";
*
* // Creating new instance of Flicking with HTMLElement
* const flicking = new Flicking(document.querySelector(".flicking-viewport"), { circular: true });
*
* // Creating new instance of Flicking with CSS selector
* const flicking2 = new Flicking(".flicking-viewport", { circular: true });
* ```
*/
constructor(root, options = {}) {
super();
this._scheduleResize = false;
const {
align = ALIGN.CENTER,
defaultIndex = 0,
horizontal = true,
circular = false,
circularFallback = CIRCULAR_FALLBACK.LINEAR,
bound = false,
adaptive = false,
panelsPerView = -1,
noPanelStyleOverride = false,
resizeOnContentsReady = false,
nested = false,
needPanelThreshold = 0,
preventEventsBeforeInit = true,
deceleration = 75e-4,
duration = 500,
easing = (x) => 1 - __pow(1 - x, 3),
inputType = ["mouse", "touch"],
moveType = "snap",
threshold = 40,
dragThreshold = 1,
interruptable = true,
bounce = "20%",
iOSEdgeSwipeThreshold = 30,
preventClickOnDrag = true,
preventDefaultOnDrag = false,
disableOnInit = false,
changeOnHold = false,
renderOnlyVisible = false,
virtual = null,
autoInit = true,
autoResize = true,
useResizeObserver = true,
resizeDebounce = 0,
observePanelResize = false,
maxResizeDebounce = 100,
useFractionalSize = false,
usePercentagePos = false,
externalRenderer = null,
renderExternal = null,
optimizeSizeUpdate = false,
animationThreshold = 0.5,
useCSSOrder = false
} = options;
this._initialized = false;
this._plugins = [];
this._isResizing = false;
this._align = align;
this._defaultIndex = defaultIndex;
this._horizontal = horizontal;
this._circular = circular;
this._circularFallback = circularFallback;
this._bound = bound;
this._adaptive = adaptive;
this._panelsPerView = panelsPerView;
this._noPanelStyleOverride = noPanelStyleOverride;
this._resizeOnContentsReady = resizeOnContentsReady;
this._nested = nested;
this._virtual = virtual;
this._needPanelThreshold = needPanelThreshold;
this._preventEventsBeforeInit = preventEventsBeforeInit;
this._deceleration = deceleration;
this._duration = duration;
this._easing = easing;
this._inputType = inputType;
this._moveType = moveType;
this._threshold = threshold;
this._dragThreshold = dragThreshold;
this._interruptable = interruptable;
this._bounce = bounce;
this._iOSEdgeSwipeThreshold = iOSEdgeSwipeThreshold;
this._preventClickOnDrag = preventClickOnDrag;
this._preventDefaultOnDrag = preventDefaultOnDrag;
this._disableOnInit = disableOnInit;
this._changeOnHold = changeOnHold;
this._renderOnlyVisible = renderOnlyVisible;
this._autoInit = autoInit;
this._autoResize = autoResize;
this._useResizeObserver = useResizeObserver;
this._resizeDebounce = resizeDebounce;
this._maxResizeDebounce = maxResizeDebounce;
this._observePanelResize = observePanelResize;
this._useFractionalSize = useFractionalSize;
this._usePercentagePos = usePercentagePos;
this._externalRenderer = externalRenderer;
this._renderExternal = renderExternal;
this._optimizeSizeUpdate = optimizeSizeUpdate;
this._animationThreshold = animationThreshold;
this._useCSSOrder = useCSSOrder;
this._viewport = new Viewport(this, getElement(root));
this._autoResizer = new AutoResizer(this);
this._renderer = this._createRenderer();
this._camera = this._createCamera();
this._control = this._createControl();
this._virtualManager = new VirtualManager(this, virtual);
if (this._autoInit) {
void this.init();
}
}
// Components
/**
* {@link Control} instance that manages user input and panel movement animations
* @remarks
* The concrete Control implementation is selected based on {@link FlickingOptions.moveType | moveType} option.
* @privateRemarks
* The control instance is created during construction by {@link Flicking._createControl}.
* @readonly
*/
get control() {
return this._control;
}
/**
* {@link Camera} instance that manages actual movement and positioning inside the viewport
* @remarks
* The concrete Camera implementation is selected based on {@link FlickingOptions.circular} and {@link FlickingOptions.bound} options.
* @privateRemarks
* The camera instance is created during construction by {@link Flicking._createCamera}.
* @readonly
*/
get camera() {
return this._camera;
}
/**
* {@link Renderer} instance that manages panels and their elements
* @remarks
* The concrete Renderer implementation is selected based on {@link Flicking.externalRenderer} and {@link FlickingOptions.virtual} options.
* @privateRemarks
* The renderer instance is created during construction by {@link Flicking._createRenderer}.
* @readonly
*/
get renderer() {
return this._renderer;
}
/**
* {@link Viewport} instance that manages viewport size and element
* @privateRemarks
* The viewport instance is created during construction by {@link Flicking} constructor.
* @readonly
*/
get viewport() {
return this._viewport;
}
/**
* {@link AutoResizer} instance that detects size changes and triggers resize when {@link FlickingOptions.autoResize | autoResize} option is enabled
* @privateRemarks
* The autoResizer instance is created during construction by {@link Flicking} constructor.
* @readonly
*/
get autoResizer() {
return this._autoResizer;
}
// Internal States
/**
* Whether Flicking's {@link Flicking.init} is called.
* @remarks
* This is `true` when {@link Flicking.init} is called, and is `false` after calling {@link Flicking.destroy}.
* Use this to check if Flicking is ready before calling certain methods that require initialization.
* @defaultValue false
* @readonly
* @example
* ```ts
* if (flicking.initialized) {
* flicking.setStatus(status);
* } else {
* await flicking.init();
* flicking.setStatus(status);
* }
* ```
*/
get initialized() {
return this._initialized;
}
/**
* Whether the circular mode is actually enabled.
* @remarks
* The {@link FlickingOptions.circular} option may not be enabled when the sum of panel sizes is too small.
* This property reflects the actual enabled state, which may differ from the {@link FlickingOptions.circular} option value.
* @defaultValue false
* @readonly
*/
get circularEnabled() {
return this._camera.circularEnabled;
}
/**
* Whether the virtual mode is actually enabled.
* @remarks
* The {@link FlickingOptions.virtual} option may not be enabled when {@link FlickingOptions.panelsPerView} is less than or equal to zero.
* This property reflects the actual enabled state, which may differ from the {@link FlickingOptions.virtual} option value.
* @defaultValue false
* @readonly
*/
get virtualEnabled() {
return this._panelsPerView > 0 && this._virtual != null;
}
/**
* Index of the currently active panel.
* @remarks
* Returns -1 when there is no active panel. This is a shorthand for `Flicking.currentPanel.index`.
* @readonly
*/
get index() {
return this._control.activeIndex;
}
/**
* The root viewport element (`.flicking-viewport`).
* @remarks
* This is the element passed to the Flicking constructor. It is a shorthand for `Flicking.viewport.element`.
* @readonly
*/
get element() {
return this._viewport.element;
}
/**
* The currently active panel.
* @remarks
* Returns `null` when there is no active panel. This is a shorthand for `Flicking.control.activePanel`.
* @readonly
*/
get currentPanel() {
return this._control.activePanel;
}
/**
* Array of all panels.
* @remarks
* This is a shorthand for `Flicking.renderer.panels`.
* @readonly
*/
get panels() {
return this._renderer.panels;
}
/**
* Total number of panels.
* @remarks
* This is a shorthand for `Flicking.renderer.panelCount`.
* @readonly
*/
get panelCount() {
return this._renderer.panelCount;
}
/**
* Array of panels that are currently visible in the viewport.
* @remarks
* This is a shorthand for `Flicking.camera.visiblePanels`.
* @readonly
*/
get visiblePanels() {
return this._camera.visiblePanels;
}
/**
* Whether Flicking is currently animating.
* @remarks
* This is a shorthand for `Flicking.control.animating`.
* @readonly
*/
get animating() {
return this._control.animating;
}
/**
* Whether the user is currently clicking or touching the viewport.
* @remarks
* This is a shorthand for `Flicking.control.holding`.
* @readonly
*/
get holding() {
return this._control.holding;
}
/**
* Array of currently activated plugins.
* @remarks
* Plugins are added via {@link Flicking.addPlugins} and removed via {@link Flicking.removePlugins}.
* @readonly
*/
get activePlugins() {
return this._plugins;
}
// Options Getter
// UI / LAYOUT
/** Current value of the {@link FlickingOptions.align | align} option. */
get align() {
return this._align;
}
/** Current value of the {@link FlickingOptions.defaultIndex | defaultIndex} option. */
get defaultIndex() {
return this._defaultIndex;
}
/** Current value of the {@link FlickingOptions.horizontal | horizontal} option. */
get horizontal() {
return this._horizontal;
}
/** Current value of the {@link FlickingOptions.circular | circular} option. */
get circular() {
return this._circular;
}
/**
* Current value of the {@link FlickingOptions.circularFallback | circularFallback} option.
* @since 4.5.0
*/
get circularFallback() {
return this._circularFallback;
}
/** Current value of the {@link FlickingOptions.bound | bound} option. */
get bound() {
return this._bound;
}
/** Current value of the {@link FlickingOptions.adaptive | adaptive} option. */
get adaptive() {
return this._adaptive;
}
/**
* Current value of the {@link FlickingOptions.panelsPerView | panelsPerView} option.
* @since 4.2.0
*/
get panelsPerView() {
return this._panelsPerView;
}
/** Current value of the {@link FlickingOptions.noPanelStyleOverride | noPanelStyleOverride} option. */
get noPanelStyleOverride() {
return this._noPanelStyleOverride;
}
/**
* Current value of the {@link FlickingOptions.resizeOnContentsReady | resizeOnContentsReady} option.
* @since 4.3.0
*/
get resizeOnContentsReady() {
return this._resizeOnContentsReady;
}
/**
* Current value of the {@link FlickingOptions.nested | nested} option.
* @since 4.7.0
*/
get nested() {
return this._nested;
}
// EVENTS
/** Current value of the {@link FlickingOptions.needPanelThreshold | needPanelThreshold} option. */
get needPanelThreshold() {
return this._needPanelThreshold;
}
/**
* Current value of the {@link FlickingOptions.preventEventsBeforeInit | preventEventsBeforeInit} option.
* @since 4.2.0
*/
get preventEventsBeforeInit() {
return this._preventEventsBeforeInit;
}
// ANIMATION
/** Current value of the {@link FlickingOptions.deceleration | deceleration} option. */
get deceleration() {
return this._deceleration;
}
/** Current value of the {@link FlickingOptions.easing | easing} option. */
get easing() {
return this._easing;
}
/** Current value of the {@link FlickingOptions.duration | duration} option. */
get duration() {
return this._duration;
}
// INPUT
/** Current value of the {@link FlickingOptions.inputType | inputType} option. */
get inputType() {
return this._inputType;
}
/** Current value of the {@link FlickingOptions.moveType | moveType} option. */
get moveType() {
return this._moveType;
}
/** Current value of the {@link FlickingOptions.threshold | threshold} option. */
get threshold() {
return this._threshold;
}
/** Current value of the {@link FlickingOptions.dragThreshold | dragThreshold} option. */
get dragThreshold() {
return this._dragThreshold;
}
/**
* Current value of the {@link FlickingOptions.animationThreshold | animationThreshold} option.
* @since 4.15.0
*/
get animationThreshold() {
return this._animationThreshold;
}
/**
* Current value of the {@link FlickingOptions.useCSSOrder | useCSSOrder} option.
* @since 4.15.0
*/
get useCSSOrder() {
return this._useCSSOrder;
}
/** Current value of the {@link FlickingOptions.interruptable | interruptable} option. */
get interruptable() {
return this._interruptable;
}
/** Current value of the {@link FlickingOptions.bounce | bounce} option. */
get bounce() {
return this._bounce;
}
/** Current value of the {@link FlickingOptions.iOSEdgeSwipeThreshold | iOSEdgeSwipeThreshold} option. */
get iOSEdgeSwipeThreshold() {
return this._iOSEdgeSwipeThreshold;
}
/** Current value of the {@link FlickingOptions.preventClickOnDrag | preventClickOnDrag} option. */
get preventClickOnDrag() {
return this._preventClickOnDrag;
}
/**
* Current value of the {@link FlickingOptions.preventDefaultOnDrag | preventDefaultOnDrag} option.
* @since 4.11.0
*/
get preventDefaultOnDrag() {
return this._preventDefaultOnDrag;
}
/** Current value of the {@link FlickingOptions.disableOnInit | disableOnInit} option. */
get disableOnInit() {
return this._disableOnInit;
}
/**
* Current value of the {@link FlickingOptions.changeOnHold | changeOnHold} option.
* @since 4.8.0
*/
get changeOnHold() {
return this._changeOnHold;
}
// PERFORMANCE
/** Current value of the {@link FlickingOptions.renderOnlyVisible | renderOnlyVisible} option. */
get renderOnlyVisible() {
return this._renderOnlyVisible;
}
/**
* {@link VirtualManager} instance that manages virtual panels
* @privateRemarks
* The virtualManager instance is created during construction by {@link Flicking} constructor.
* @readonly
*/
get virtual() {
return this._virtualManager;
}
// OTHERS
/** Current value of the {@link FlickingOptions.autoInit | autoInit} option. */
get autoInit() {
return this._autoInit;
}
/** Current value of the {@link FlickingOptions.autoResize | autoResize} option. */
get autoResize() {
return this._autoResize;
}
/**
* Current value of the {@link FlickingOptions.useResizeObserver | useResizeObserver} option.
* @since 4.4.0
*/
get useResizeObserver() {
return this._useResizeObserver;
}
/**
* Current value of the {@link FlickingOptions.observePanelResize | observePanelResize} option.
* @since 4.13.1
*/
get observePanelResize() {
return this._observePanelResize;
}
/**
* Current value of the {@link FlickingOptions.resizeDebounce | resizeDebounce} option.
* @since 4.6.0
*/
get resizeDebounce() {
return this._resizeDebounce;
}
/**
* Current value of the {@link FlickingOptions.maxResizeDebounce | maxResizeDebounce} option.
* @since 4.6.0
*/
get maxResizeDebounce() {
return this._maxResizeDebounce;
}
/**
* Current value of the {@link FlickingOptions.useFractionalSize | useFractionalSize} option.
* @since 4.9.0
*/
get useFractionalSize() {
return this._useFractionalSize;
}
/**
* Current value of the {@link FlickingOptions.usePercentagePos | usePercentagePos} option.
* @since 4.17.0
*/
get usePercentagePos() {
return this._usePercentagePos;
}
/** Current value of the {@link FlickingOptions.externalRenderer | externalRenderer} option. */
get externalRenderer() {
return this._externalRenderer;
}
/**
* @deprecated Use {@link Flicking.externalRenderer | externalRenderer} instead.
* Current value of the {@link FlickingOptions.renderExternal | renderExternal} option.
*/
get renderExternal() {
return this._renderExternal;
}
/** @internal */
get optimizeSizeUpdate() {
return this._optimizeSizeUpdate;
}
// Options Setter
// UI / LAYOUT
/**
* Sets {@link FlickingOptions.align}.
* @privateRemarks
* Setting this value updates the renderer and camera alignment, and triggers a resize operation.
*/
set align(val) {
this._align = val;
this._renderer.align = val;
this._camera.align = val;
void this.resize();
}
set defaultIndex(val) {
this._defaultIndex = val;
}
/**
* Sets {@link FlickingOptions.horizontal}.
* @privateRemarks
* Setting this value updates the control direction and triggers a resize operation.
*/
set horizontal(val) {
this._horizontal = val;
this._control.controller.updateDirection();
void this.resize();
}
/**
* Sets {@link FlickingOptions.circular}.
* @privateRemarks
* Setting this value triggers a resize operation to recalculate panel positions.
*/
set circular(val) {
this._circular = val;
void this.resize();
}
/**
* Sets {@link FlickingOptions.bound}.
* @privateRemarks
* Setting this value triggers a resize operation to recalculate panel positions.
*/
set bound(val) {
this._bound = val;
void this.resize();
}
/**
* Sets {@link FlickingOptions.adaptive}.
* @privateRemarks
* Setting this value triggers a resize operation to recalculate panel sizes.
*/
set adaptive(val) {
this._adaptive = val;
void this.resize();
}
/**
* Sets {@link FlickingOptions.panelsPerView}.
* @privateRemarks
* Setting this value triggers a resize operation to recalculate panel sizes.
*/
set panelsPerView(val) {
this._panelsPerView = val;
void this.resize();
}
/**
* Sets {@link FlickingOptions.noPanelStyleOverride}.
* @privateRemarks
* Setting this value triggers a resize operation to update panel styles.
*/
set noPanelStyleOverride(val) {
this._noPanelStyleOverride = val;
void this.resize();
}
/**
* Sets {@link FlickingOptions.resizeOnContentsReady}.
* @privateRemarks
* When set to `true`, immediately checks all panels for content readiness.
*/
set resizeOnContentsReady(val) {
this._resizeOnContentsReady = val;
if (val) {
this._renderer.checkPanelContentsReady(this._renderer.panels);
}
}
/**
* Sets {@link FlickingOptions.nested}.
* @privateRemarks
* Setting this value updates the control's axes options.
*/
set nested(val) {
this._nested = val;
const axes = this._control.controller.axes;
if (axes) {
axes.options.nested = val;
}
}
// EVENTS
set needPanelThreshold(val) {
this._needPanelThreshold = val;
}
set preventEventsBeforeInit(val) {
this._preventEventsBeforeInit = val;
}
// ANIMATION
/**
* Sets {@link FlickingOptions.deceleration}.
* @privateRemarks
* Setting this value updates the control's axes deceleration option.
*/
set deceleration(val) {
this._deceleration = val;
const axes = this._control.controller.axes;
if (axes) {
axes.options.deceleration = val;
}
}
/**
* Sets {@link FlickingOptions.easing}.
* @privateRemarks
* Setting this value updates the control's axes easing option.
*/
set easing(val) {
this._easing = val;
const axes = this._control.controller.axes;
if (axes) {
axes.options.easing = val;
}
}
set duration(val) {
this._duration = val;
}
// INPUT
/**
* Sets {@link FlickingOptions.inputType}.
* @privateRemarks
* Setting this value updates the control's pan input options.
*/
set inputType(val) {
this._inputType = val;
const panInput = this._control.controller.panInput;
if (panInput) {
panInput.options.inputType = val;
}
}
/**
* Sets {@link FlickingOptions.moveType}.
* @privateRemarks
* Setting this value creates a new Control instance based on the moveType and preserves the current panel position and progress.
*/
set moveType(val) {
this._moveType = val;
const prevControl = this._control;
const newControl = this._createControl();
const activePanel = prevControl.activePanel;
newControl.copy(prevControl);
const prevProgressInPanel = activePanel ? this._camera.getProgressInPanel(activePanel) : 0;
this._control = newControl;
this._control.updatePosition(prevProgressInPanel);
this._control.updateInput();
}
set threshold(val) {
this._threshold = val;
}
/**
* Sets {@link FlickingOptions.dragThreshold}.
* @privateRemarks
* Setting this value updates the control's pan input threshold option.
*/
set dragThreshold(val) {
this._dragThreshold = val;
const panInput = this._control.controller.panInput;
if (panInput) {
panInput.options.threshold = val;
}
}
/**
* Sets {@link FlickingOptions.animationThreshold}.
*/
set animationThreshold(val) {
this._animationThreshold = val;
}
/**
* Sets {@link FlickingOptions.useCSSOrder}.
*/
set useCSSOrder(val) {
this._useCSSOrder = val;
}
/**
* Sets {@link FlickingOptions.interruptable}.
* @privateRemarks
* Setting this value updates the control's axes interruptable option.
*/
set interruptable(val) {
this._interruptable = val;
const axes = this._control.controller.axes;
if (axes) {
axes.options.interruptable = val;
}
}
/**
* Sets {@link FlickingOptions.bounce}.
* @privateRemarks
* Setting this value updates the control input configuration.
*/
set bounce(val) {
this._bounce = val;
this._control.updateInput();
}
/**
* Sets {@link FlickingOptions.iOSEdgeSwipeThreshold}.
* @privateRemarks
* Setting this value updates the control's pan input iOS edge swipe threshold option.
*/
set iOSEdgeSwipeThreshold(val) {
this._iOSEdgeSwipeThreshold = val;
const panInput = this._control.controller.panInput;
if (panInput) {
panInput.options.iOSEdgeSwipeThreshold = val;
}
}
/**
* Sets {@link FlickingOptions.preventClickOnDrag}.
* @privateRemarks
* Setting this value adds or removes the prevent click handler from the control.
*/
set preventClickOnDrag(val) {
const prevVal = this._preventClickOnDrag;
if (val === prevVal) return;
const controller = this._control.controller;
if (val) {
controller.addPreventClickHandler();
} else {
controller.removePreventClickHandler();
}
this._preventClickOnDrag = val;
}
/**
* Sets {@link FlickingOptions.preventDefaultOnDrag}.
* @privateRemarks
* Setting this value updates the control's pan input preventDefaultOnDrag option.
*/
set preventDefaultOnDrag(val) {
this._preventDefaultOnDrag = val;
const panInput = this._control.controller.panInput;
if (panInput) {
panInput.options.preventDefaultOnDrag = val;
}
}
set disableOnInit(val) {
this._disableOnInit = val;
}
set changeOnHold(val) {
this._changeOnHold = val;
}
// PERFORMANCE
/**
* Sets {@link FlickingOptions.renderOnlyVisible}.
* @privateRemarks
* Setting this value triggers an immediate render operation.
*/
set renderOnlyVisible(val) {
this._renderOnlyVisible = val;
void this._renderer.render();
}
// OTHERS
/**
* Sets {@link FlickingOptions.autoResize}.
* @privateRemarks
* Setting this value enables or disables the auto resizer if Flicking is already initialized.
*/
set autoResize(val) {
this._autoResize = val;
if (!this._initialized) {
return;
}
if (val) {
this._autoResizer.enable();
} else {
this._autoResizer.disable();
}
}
/**
* Sets {@link FlickingOptions.useResizeObserver}.
* @privateRemarks
* Setting this value re-enables the auto resizer if Flicking is initialized and autoResize is enabled.
*/
set useResizeObserver(val) {
this._useResizeObserver = val;
if (this._initialized && this._autoResize) {
this._autoResizer.enable();
}
}
/**
* Sets {@link FlickingOptions.observePanelResize}.
* @privateRemarks
* Setting this value starts or stops observing panel sizes if Flicking is initialized and autoResize is enabled.
*/
set observePanelResize(val) {
this._observePanelResize = val;
if (this._initialized && this._autoResize) {
if (val) {
this._autoResizer.observePanels();
} else {
this._autoResizer.unobservePanels();
}
}
}
set optimizeSizeUpdate(val) {
this._optimizeSizeUpdate = val;
}
/**
* Sets {@link FlickingOptions.usePercentagePos}.
* @privateRemarks
* Setting this value immediately re-applies the camera element's transform with the new unit.
* The transform is not re-applied when Flicking is not initialized or the renderer is rendering.
*/
set usePercentagePos(val) {
this._usePercentagePos = val;
this._camera.applyTransform();
}
/**
* Initialize Flicking and move to the default index.
* @remarks
* This method is automatically called in the constructor when {@link FlickingOptions.autoInit | autoInit} is `true` (default).
* If Flicking is already initialized, this method returns immediately without doing anything.
* @fires {@link ReadyEvent}
* @returns Promise that resolves when initialization is complete
*/
init() {
if (this._initialized) return Promise.resolve();
const camera = this._camera;
const renderer = this._renderer;
const control = this._control;
const virtualManager = this._virtualManager;
const originalTrigger = this.trigger;
const preventEventsBeforeInit = this._preventEventsBeforeInit;
camera.init();
virtualManager.init();
renderer.init(this);
control.init(this);
if (preventEventsBeforeInit) {
this.trigger = () => this;
}
this._initialResize();
this._moveToInitialPanel();
if (this._autoResize) {
this._autoResizer.enable();
}
if (this._preventClickOnDrag) {
control.controller.addPreventClickHandler();
}
if (this._disableOnInit) {
this.disableInput();
}
renderer.checkPanelContentsReady(renderer.panels);
this._initialized = true;
return renderer.render().then(() => {
this._plugins.forEach((plugin) => plugin.init(this));
if (preventEventsBeforeInit) {
this.trigger = originalTrigger;
}
this.trigger(new ComponentEvent(EVENTS.READY));
});
}
/**
* Destroy Flicking and remove all event handlers.
* @remarks
* This method cleans up all resources including event handlers, components, and plugins.
* After calling this method, {@link Flicking.initialized} will be `false` and the instance should not be used.
*/
destroy() {
this.off();
this._autoResizer.disable();
this._control.destroy();
this._camera.destroy();
this._renderer.destroy();
this._plugins.forEach((plugin) => plugin.destroy());
this._scheduleResize = false;
this._initialized = false;
this._isResizing = false;
}
/**
* Move to the previous panel (current index - 1).
* @param duration - Duration of the panel movement animation (unit: ms). Defaults to {@link FlickingOptions.duration}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns Promise that resolves after reaching the previous panel
*/
prev(duration = this._duration) {
var _a, _b, _c;
return this.moveTo((_c = (_b = (_a = this._control.activePanel) == null ? void 0 : _a.prev()) == null ? void 0 : _b.index) != null ? _c : -1, duration, DIRECTION.PREV);
}
/**
* Move to the next panel (current index + 1).
* @param duration - Duration of the panel movement animation (unit: ms). Defaults to {@link FlickingOptions.duration}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns Promise that resolves after reaching the next panel
*/
next(duration = this._duration) {
var _a, _b, _c;
return this.moveTo((_c = (_b = (_a = this._control.activePanel) == null ? void 0 : _a.next()) == null ? void 0 : _b.index) != null ? _c : this._renderer.panelCount, duration, DIRECTION.NEXT);
}
/**
* Move to the panel with the given index.
* @param index - The index of the panel to move to
* @param duration - Duration of the animation (unit: ms). Defaults to {@link FlickingOptions.duration}
* @param direction - Direction to move (circular mode only). Defaults to {@link DIRECTION.NONE}
* @fires {@link MovementEvents}
* @throws {@link MovementErrors}
* @returns Promise that resolves after reaching the target panel
*/
moveTo(index, duration = this._duration, direction = DIRECTION.NONE) {
const renderer = this._renderer;
const panelCount = renderer.panelCount;
const panel = renderer.getPanel(index);
if (!panel) {
return Promise.reject(
new FlickingError(MESSAGE.INDEX_OUT_OF_RANGE(index, 0, panelCount - 1), CODE.INDEX_OUT_OF_RANGE)
);
}
if (this._control.animating) {
return Promise.reject(
new FlickingError(MESSAGE.ANIMATION_ALREADY_PLAYING, CODE.ANIMATION_ALREADY_PLAYING)
);
}
if (this._control.holding) {
this._control.controller.release();
}
return this._control.moveToPanel(panel, {
duration,
direction
});
}
/**
* Change the destination and duration of the animation currently playing.
* @remarks
* This method does nothing if no animation is currently playing.
* @param index - The index of the panel to move to
* @param duration - Duration of the animation (unit: ms)
* @param direction - Direction to move. Only available when {@link FlickingOptions.circular} is enabled
* @since 4.10.0
* @throws {@link AnimationUpdateErrors}
*/
updateAnimation(index, duration, direction) {
if (!this._control.animating) {
return;
}
const renderer = this._renderer;
const panelCount = renderer.panelCount;
const panel = renderer.getPanel(index);
if (!panel) {
throw new FlickingError(
MESSAGE.INDEX_OUT_OF_RANGE(index, 0, panelCount - 1),
CODE.INDEX_OUT_OF_RANGE
);
}
this._control.updateAnimation(panel, duration, direction);
}
/**
* Stop the animation currently playing.
* @remarks
* This method does nothing if no animation is currently playing.
* @since 4.10.0
* @fires {@link MoveEndEvent}
*/
stopAnimation() {
if (!this._control.animating) {
return;
}
this._control.stopAnimation();
}
/**
* Get the panel at the given index.
* @param index - The index of the panel to get
* @returns The panel at the given index, or `null` if it doesn't exist. This is a shorthand for `Flicking.renderer.getPanel(index)`.
* @example
* ```ts
* const panel = flicking.getPanel(0);
* // Which is a shorthand to...
* const samePanel = flicking.panels[0];
* ```
*/
getPanel(index) {
return this._renderer.getPanel(index);
}
/**
* Enable user input (mouse/touch).
* @remarks
* This is a shorthand for `Flicking.control.enable`.
* @returns The current instance for method chaining
*/
enableInput() {
this._control.enable();
return this;
}
/**
* Disable user input (mouse/touch).
* @remarks
* This is a shorthand for `Flicking.control.disable`.
* @returns The current instance for method chaining
*/
disableInput() {
this._control.disable();
return this;
}
/**
* Get the current Flicking status.
* @param options - {@link GetStatusParams}
* @returns Status object that can be used with {@link Flicking.setStatus} to restore the state
*/
getStatus(options = {}) {
var _a, _b;
const { index = true, position = true, includePanelHTML = false, visiblePanelsOnly = false } = options;
const camera = this._camera;
const panels = visiblePanelsOnly ? this.visiblePanels : this.panels;
const status = {
panels: panels.map((panel) => {
const panelInfo = { index: panel.index };
if (includePanelHTML) {
panelInfo.html = panel.element.outerHTML;
}
return panelInfo;
})
};
if (index) {
status.index = this.index;
}
if (position) {
const nearestAnchor = camera.findNearestAnchor(camera.position);
if (nearestAnchor) {
status.position = {
panel: nearestAnchor.panel.index,
progressInPanel: camera.getProgressInPanel(nearestAnchor.panel)
};
}
}
if (visiblePanelsOnly) {
const visiblePanels = this.visiblePanels;
status.visibleOffset = (_b = (_a = visiblePanels[0]) == null ? void 0 : _a.index) != null ? _b : 0;
}
return status;
}
/**
* Restore Flicking to the state of the given {@link Status}.
* @param status - {@link Status}
* @throws {@link StatusRestoreErrors}
*/
setStatus(status) {
var _a;
if (!this._initialized) {
throw new FlickingError(MESSAGE.NOT_INITIALIZED, CODE.NOT_INITIALIZED);
}
const { index, position, visibleOffset, panels } = status;
const renderer = this._renderer;
const control = this._control;
if (((_a = panels[0]) == null ? void 0 : _a.html) && !this._renderExternal) {
renderer.batchRemove({
index: 0,
deleteCount: this.panels.length,
hasDOMInElements: true
});
renderer.batchInsert({
index: 0,
elements: parseElement(panels.map((panel) => panel.html)),
hasDOMInElements: true
});
}
if (index != null) {
const panelIndex = visibleOffset ? index - visibleOffset : index;
void this.moveTo(panelIndex, 0).catch(() => void 0);
}
if (position && this._moveType === MOVE_TYPE.FREE_SCROLL) {
const { panel, progressInPanel } = position;
const panelIndex = visibleOffset ? panel - visibleOffset : panel;
const panelRange = renderer.panels[panelIndex].range;
const newCameraPos = panelRange.min + (panelRange.max - panelRange.min) * progressInPanel;
void control.moveToPosition(newCameraPos, 0).catch(() => void 0);
}
}
/**
* Add plugins to Flicking.
* @remarks
* Plugins are automatically initialized if Flicking is already initialized.
* @param plugins - {@link Plugin}
* @returns The current instance for method chaining
* @see https://github.com/naver/egjs-flicking-plugins
*/
addPlugins(...plugins) {
if (this._initialized) {
plugins.forEach((item) => item.init(this));
}
this._plugins.push(...plugins);
return this;
}
/**
* Remove plugins from Flicking.
* @param plugins - {@link Plugin}
* @returns The current instance for method chaining
* @see https://github.com/naver/egjs-flicking-plugins
*/
removePlugins(...plugins) {
plugins.forEach((item) => {
const foundIndex = findIndex(this._plugins, (val) => val === item);
if (foundIndex >= 0) {
item.destroy();
this._plugins.splice(foundIndex, 1);
}
});
return this;
}
/**
* Update viewport and panel sizes.
* @remarks
* This method does nothing if a resize is already in progress.
* @fires {@link ResizeEvents}
* @returns Promise that resolves when resize is complete
*/
resize() {
return __async(this, null, function* () {
if (!this._initialized) {
return;
}
if (this._isResizing) {
this._scheduleResize = true;
return;
}
this._scheduleResize = false;
this._isResizing = true;
const viewport = this._viewport;
const renderer = this._renderer;
const camera = this._camera;
const control = this._control;
const activePanel = control.activePanel;
const prevWidth = viewport.width;
const prevHeight = viewport.height;
const prevProgressInPanel = activePanel ? camera.getProgressInPanel(activePanel) : 0;
this.trigger(
new ComponentEvent(EVENTS.BEFORE_RESIZE, {
width: prevWidth,
height: prevHeight,
element: viewport.element
})
);
if (this._optimizeSizeUpdate) {
const measuredSize = viewport.measureSize();
if (this.horizontal && measuredSize.width !== prevWidth || !this.horizontal && measuredSize.height !== prevHeight) {
yield renderer.forceRenderAllPanels();
}
} else {
yield renderer.forceRenderAllPanels();
}
if (!this._initialized) {
return;
}
viewport.resize();
renderer.updatePanelSize();
camera.updateAlignPos();
camera.updateRange();
camera.updateAnchors();
camera.updateAdaptiveHeight();
camera.updatePanelOrder();
if (!control.animating) {
control.updatePosition(prevProgressInPanel);
}
camera.updateOffset();
yield renderer.render();
if (!this._initialized) {
return;
}
if (control.animating) ;
else {
control.updatePosition(prevProgressInPanel);
control.updateInput();
}
const newWidth = viewport.width;
const newHeight = viewport.height;
const sizeChanged = newWidth !== prevWidth || newHeight !== prevHeight;
this.trigger(
new ComponentEvent(EVENTS.AFTER_RESIZE, {
width: viewport.width,
height: viewport.height,
prev: {
width: prevWidth,
height: prevHeight
},
sizeChanged,
element: viewport.element
})
);
this._isResizing = false;
if (this._scheduleResize) {
void this.resize();
}
return;
});
}
/**
* Add new panels after the last panel.
* @param element - A new HTMLElement, outerHTML string, or an array of both
* @throws {@link DOMManipulationErrors}
* @returns Array of appended panels
* @example
* ```ts
* const flicking = new Flicking("#flick");
* flicking.append(document.createElement("div"));
* flicking.append("<div>Panel</div>");
* flicking.append(["<div>Panel</div>", document.createElement("div")]);
* ```
*/
append(element) {
return this.insert(this._renderer.panelCount, element);
}
/**
* Add new panels before the first panel.
* @remarks
* This will increase the index of existing panels by the number of panels added.
* @param element - A new HTMLElement, outerHTML string, or an array of both
* @throws {@link DOMManipulationErrors}
* @returns Array of prepended panels
* @example
* ```ts
* const flicking = new Flicking("#flick");
* flicking.prepend(document.createElement("div"));
* flicking.prepend("<div>Panel</div>");
* flicking.prepend(["<div>Panel</div>", document.createElement("div")]);
* ```
*/
prepend(element) {
return this.insert(0, element);
}
/**
* Insert new panels at the given index.
* @remarks
* This will increase the index of panels at or after the given index by the number of panels added.
* @param index - Index to insert new panels at
* @param element - A new HTMLElement, outerHTML string, or an array of both
* @throws {@link DOMManipulationErrors}
* @returns Array of inserted panels
* @example
* ```ts
* const flicking = new Flicking("#flick");
* flicking.insert(0, document.createElement("div"));
* flicking.insert(2, "<div>Panel</div>");
* flicking.insert(1, ["<div>Panel</div>", document.createElement("div")]);
* ```
*/
insert(index, element) {
if (this._renderExternal) {
throw new FlickingError(MESSAGE.NOT_ALLOWED_IN_FRAMEWORK, CODE.NOT_ALLOWED_IN_FRAMEWORK);
}
return this._renderer.batchInsert({
index,
elements: parseElement(element),
hasDOMInElements: true
});
}
/**
* Remove panels starting from the given index.
* @remarks
* This will decrease the index of panels after the removed ones by the number of panels removed.
* @param index - Index of the first panel to remove
* @param deleteCount - Number of panels to remove. Defaults to `1`
* @throws {@link DOMManipulationErrors}
* @returns Array of removed panels
*/
remove(index, deleteCount = 1) {
if (this._renderExternal) {
throw new FlickingError(MESSAGE.NOT_ALLOWED_IN_FRAMEWORK, CODE.NOT_ALLOWED_IN_FRAMEWORK);
}
return this._renderer.batchRemove({
index,
deleteCount,
hasDOMInElements: true
});
}
/**
* Factory method to create the appropriate Control implementation based on moveType option.
* @internal
* @privateRemarks
* Called during constructor and when moveType option is changed. The moveType option must be set before calling this method.
* Throws error if moveType is invalid.
*/
_createControl() {
var _a;
const moveType = this._moveType;
const moveTypes = Object.keys(MOVE_TYPE).map((key) => MOVE_TYPE[key]);
const moveTypeStr = Array.isArray(moveType) ? moveType[0] : moveType;
const moveTypeOptions = Array.isArray(moveType) ? (_a = moveType[1]) != null ? _a : {} : {};
if (!includes(moveTypes, moveTypeStr)) {
throw new FlickingError(
MESSAGE.WRONG_OPTION("moveType", JSON.stringify(moveType)),
CODE.WRONG_OPTION
);
}
switch (moveTypeStr) {
case MOVE_TYPE.SNAP:
return new SnapControl(moveTypeOptions);
case MOVE_TYPE.FREE_SCROLL:
return new FreeControl(moveTypeOptions);
case MOVE_TYPE.STRICT:
return new StrictControl(moveTypeOptions);
}
}
/**
* Factory method to create Camera instance for managing viewport movement and positioning.
* @internal
* @privateRemarks
* Called during constructor. The align option must be set before calling this method.
* Warns if both circular and bound options are enabled (bound is ignored).
*/
_createCamera() {
if (this._circular && this._bound) {
console.warn('"circular" and "bound" option cannot be used together, ignoring bound.');
}
return new Camera(this, {
align: this._align
});
}
/**
* Factory method to create the appropriate Renderer implementation based on externalRenderer and virtual options.
* @internal
* @privateRemarks
* Called during constructor. Selects ExternalRenderer if externalRenderer is provided, otherwise creates VanillaRenderer or ExternalRenderer based on renderExternal option.
* Warns if virtual is enabled without panelsPerView.
*/
_createRenderer() {
const externalRenderer = this._externalRenderer;
if (this._virtual && this._panelsPerView <= 0) {
console.warn('"virtual" and "panelsPerView" option should be used together, ignoring virtual.');
}
return externalRenderer ? externalRenderer : this._renderExternal ? this._createExternalRenderer() : this._createVanillaRenderer();
}
/**
* Factory method to create ExternalRenderer from renderExternal option (deprecated).
* @internal
* @privateRemarks
* Called by _createRenderer when renderExternal option is set. The renderExternal option must not be null and must contain renderer class and options.
*/
_createExternalRenderer() {
const { renderer, rendererOptions } = this._renderExternal;
return new renderer(__spreadValues({ align: this._align }, rendererOptions));
}
/**
* Factory method to create VanillaRenderer for vanilla JavaScript rendering.
* @internal
* @privateRemarks
* Called by _createRenderer when neither externalRenderer nor renderExternal is set.
* Uses VirtualRenderingStrategy if virtual is enabled, otherwise NormalRenderingStrategy.
*/
_createVanillaRenderer() {
const virtual = this.virtualEnabled;
return new VanillaRenderer({
align: this._align,
strategy: virtual ? new VirtualRenderingStrategy() : new NormalRenderingStrategy({
providerCtor: VanillaElementProvider
})
});
}
/**
* Move camera to the initial panel position based on defaultIndex option.
* @internal
* @privateRemarks
* Called during init() method, after _initialResize(). Requires camera, renderer, and control to be already initialized.
* Throws error if the initial panel position is not reachable.
*/
_moveToInitialPanel() {
const renderer = this._renderer;
const control = this._control;
const camera = this._camera;
const defaultPanel = renderer.getPanel(this._defaultIndex) || renderer.getPanel(0);
if (!defaultPanel) return;
const nearestAnchor = camera.findNearestAnchor(defaultPanel.position);
const initialPanel = nearestAnchor && defaultPanel.position !== nearestAnchor.panel.position && defaultPanel.index !== nearestAnchor.panel.index ? nearestAnchor.panel : defaultPanel;
control.setActive(initialPanel, null, false);
if (!nearestAnchor) {
throw new FlickingError(
MESSAGE.POSITION_NOT_REACHABLE(initialPanel.position),
CODE.POSITION_NOT_REACHABLE
);
}
let position = initialPanel.position;
if (!camera.canReach(initialPanel)) {
position = nearestAnchor.position;
}
camera.lookAt(position);
control.updateInput();
camera.updateOffset();
}
/**
* Calculate initial viewport and panel sizes during initialization.
* @internal
* @privateRemarks
* Called during init() method, before _moveToInitialPanel(). This is separate from the regular resize() to avoid triggering events before initialization is complete.
* Requires viewport, renderer, camera, and control to be already initialized. Triggers BEFORE_RESIZE and AFTER_RESIZE events.
*/
_initialResize() {
const viewport = this._viewport;
const renderer = this._renderer;
const camera = this._camera;
const control = this._control;
this.trigger(
new ComponentEvent(EVENTS.BEFORE_RESIZE, {
width: 0,
height: 0,
element: viewport.element
})
);
viewport.resize();
renderer.updatePanelSize();
camera.updateAlignPos();
camera.updateRange();
camera.updateAnchors();
camera.updateOffset();
control.updateInput();
const newWidth = viewport.width;
const newHeight = viewport.height;
const sizeChanged = newWidth !== 0 || newHeight !== 0;
this.trigger(
new ComponentEvent(EVENTS.AFTER_RESIZE, {
width: viewport.width,
height: viewport.height,
prev: {
width: 0,
height: 0
},
sizeChanged,
element: viewport.element
})
);
}
};
_Flicking.VERSION = "4.17.0";
let Flicking = _Flicking;
const SIDE_EVENTS = {
HOLD_START: "sideHoldStart",
HOLD_END: "sideHoldEnd",
MOVE_START: "sideMoveStart",
MOVE: "sideMove",
MOVE_END: "sideMoveEnd",
WILL_CHANGE: "sideWillChange",
CHANGED: "sideChanged",
WILL_RESTORE: "sideWillRestore",
RESTORED: "sideRestored"
};
class CrossFlicking extends Flicking {
constructor(root, options) {
super(root, options);
this._syncToCategory = (index, outerIndex) => {
if (this._disableIndexSync) {
return;
}
this.stopAnimation();
this._sideFlicking.forEach((child, i) => {
const { start, end } = this._sideState[i];
if (start <= index && end >= index && outerIndex !== i) {
child.stopAnimation();
void child.moveTo(index, 0);
void this.moveTo(i, 0);
}
});
};
this._setDraggable = (direction, draggable) => {
if (!this._disableSlideOnHold) {
return;
}
const dragThreshold = this._originalDragThreshold;
const threshold = draggable ? dragThreshold && dragThreshold >= 10 ? dragThreshold : 10 : Infinity;
if (direction === MOVE_DIRECTION.HORIZONTAL === this.horizontal) {
this.dragThreshold = threshold;
} else if (direction === MOVE_DIRECTION.VERTICAL === this.horizontal) {
this._sideFlicking.forEach((child) => {
child.dragThreshold = threshold;
});
}
};
this._setPreviousSideIndex = () => {
this._sideFlicking.forEach((child, i) => {
const { start, end } = this._sideState[i];
if (this._preserveIndex) {
if (this._nextIndex !== i) {
if (child.index < start) {
child.stopAnimation();
void child.moveTo(start, 0);
} else if (child.index > end) {
child.stopAnimation();
void child.moveTo(end, 0);
}
}
} else {
if (this._nextIndex !== i) {
void child.moveTo(start, 0);
}
}
});
};
this._addSideIndex = (e) => {
e.sideIndex = this._sideFlicking[e.index].index;
};
this._onHorizontalHoldStart = () => {
this._setDraggable(MOVE_DIRECTION.HORIZONTAL, true);
this._moveDirection = null;
};
this._onHorizontalMove = (e) => {
if (e.isTrusted && !this._moveDirection) {
this._setDraggable(MOVE_DIRECTION.VERTICAL, false);
this._moveDirection = MOVE_DIRECTION.HORIZONTAL;
}
};
this._onHorizontalMoveEnd = (e) => {
const visiblePanels = this.visiblePanels;
if (visiblePanels.length > 1) {
this._nextIndex = e.direction === "NEXT" ? visiblePanels[1].index : visiblePanels[0].index;
} else {
this._nextIndex = visiblePanels[0].index;
}
this._setDraggable(MOVE_DIRECTION.VERTICAL, true);
this._moveDirection = null;
requestAnimationFrame(() => this._setPreviousSideIndex());
if (e.isTrusted) {
this._syncToCategory(this._sideFlicking[this._nextIndex].index, this._nextIndex);
}
};
this._onSideHoldStart = () => {
this._setDraggable(MOVE_DIRECTION.VERTICAL, true);
this._moveDirection = null;
};
this._onSideMove = (e) => {
if (e.isTrusted && !this._moveDirection) {
this._setDraggable(MOVE_DIRECTION.HORIZONTAL, false);
this._moveDirection = MOVE_DIRECTION.VERTICAL;
}
};
this._onSideMoveEnd = () => {
this._setDraggable(MOVE_DIRECTION.HORIZONTAL, true);
this._moveDirection = null;
};
this._onSideChanged = (e) => {
if (this.visiblePanels.length < 2 && this._sideFlicking[this.index] === e.currentTarget) {
this._syncToCategory(e.index, this.index);
}
};
const { sideOptions = {}, preserveIndex = true, disableSlideOnHold = true, disableIndexSync = false } = options;
this._moveDirection = null;
this._nextIndex = 0;
this._originalDragThreshold = this.dragThreshold;
this._sideOptions = sideOptions;
this._preserveIndex = preserveIndex;
this._disableSlideOnHold = disableSlideOnHold;
this._disableIndexSync = disableIndexSync;
}
// Components
get sideFlicking() {
return this._sideFlicking;
}
get sideIndex() {
return this._sideFlicking.map((i) => i.index);
}
get sideState() {
return this._sideState;
}
// Options Getter
get sideOptions() {
return this._sideOptions;
}
get preserveIndex() {
return this._preserveIndex;
}
get disableSlideOnHold() {
return this._disableSlideOnHold;
}
get disableIndexSync() {
return this._disableIndexSync;
}
// Options Setter
set sideOptions(val) {
this._sideOptions = val;
}
set preserveIndex(val) {
this._preserveIndex = val;
}
set disableSlideOnHold(val) {
this._disableSlideOnHold = val;
}
set disableIndexSync(val) {
this._disableIndexSync = val;
}
init() {
return super.init().then(() => {
this._sideState = this._createSideState();
this._sideFlicking = this._createSideFlicking();
this._addComponentEvents();
});
}
destroy() {
this._sideFlicking.forEach((flicking) => {
flicking.destroy();
});
super.destroy();
}
_addComponentEvents() {
this.on(EVENTS.HOLD_START, this._onHorizontalHoldStart);
this.on(EVENTS.MOVE, this._onHorizontalMove);
this.on(EVENTS.MOVE_END, this._onHorizontalMoveEnd);
[EVENTS.CHANGED, EVENTS.WILL_CHANGE].forEach((event) => {
this.on(event, this._addSideIndex);
});
this._sideFlicking.forEach((flicking, mainIndex) => {
flicking.on(EVENTS.HOLD_START, this._onSideHoldStart);
flicking.on(EVENTS.MOVE, this._onSideMove);
flicking.on(EVENTS.MOVE_END, this._onSideMoveEnd);
flicking.on(EVENTS.CHANGED, this._onSideChanged);
Object.keys(SIDE_EVENTS).forEach((name) => {
flicking.on(EVENTS[name], (event) => {
this.trigger(
new ComponentEvent(SIDE_EVENTS[name], __spreadValues({
mainIndex
}, event))
);
});
});
});
}
_createSideState() {
const viewportEl = this.element;
const cameraEl = this.camera.element;
const panels = toArray(cameraEl.children);
const isCrossStructure = getDataAttributes(viewportEl, "data-cross-").structure;
let sideState = [];
if (!isCrossStructure) {
const groupPanels = this._getGroupFromAttribute(panels);
const groupKeys = Object.keys(groupPanels);
if (groupKeys.length) {
sideState = this._getSideStateFromGroup(groupPanels);
this.remove(0, this.panelCount - groupKeys.length);
} else {
sideState = this._getSideStateFromPanels(panels);
}
this._createCrossStructure(sideState);
} else {
sideState = this._getSideStateFromCrossStructure(panels);
}
void this.resize();
return sideState;
}
_createCrossStructure(sideState) {
const sideCamera = document.createElement("div");
let sidePanels = "";
sideCamera.classList.add(CLASS.CAMERA);
sideState.forEach((state, i) => {
const panel = this.camera.children[i];
sidePanels += state.element.innerHTML;
Array.from(panel.attributes).forEach((attribute) => panel.removeAttribute(attribute.name));
});
sideCamera.innerHTML = sidePanels;
sideState.forEach((_, i) => {
const panel = this.camera.children[i];
[CLASS.VIEWPORT, CLASS.VERTICAL].forEach((className) => {
if (!panel.classList.contains(className)) {
panel.classList.add(className);
}
});
panel.innerHTML = sideCamera.outerHTML;
});
this.element.setAttribute("data-cross-structure", "true");
}
_getGroupFromAttribute(panels) {
const groupKeys = [];
const groupPanels = {};
panels.forEach((panel) => {
const groupKey = getDataAttributes(panel, "data-cross-").groupkey;
if (groupKey && !includes(groupKeys, groupKey)) {
groupKeys.push(groupKey);
groupPanels[groupKey] = [panel];
} else if (groupKey) {
groupPanels[groupKey].push(panel);
}
});
return groupPanels;
}
_getSideStateFromGroup(groupPanels) {
return Object.keys(groupPanels).reduce((state, key) => {
const start = state.length ? +state[state.length - 1].end + 1 : 0;
const element = groupPanels[key].reduce((el, panel) => {
el.innerHTML += panel.outerHTML;
return el;
}, document.createElement("div"));
return [
...state,
{
key,
start,
end: start + groupPanels[key].length - 1,
element
}
];
}, []);
}
_getSideStateFromPanels(panels) {
return panels.reduce((state, panel, i) => {
const start = state.length ? +state[state.length - 1].end + 1 : 0;
return [
...state,
{
key: i.toString(),
start,
end: start + panel.children.length - 1,
element: panel
}
];
}, []);
}
_getSideStateFromCrossStructure(panels) {
const groupPanels = this._getGroupFromAttribute(panels);
return this._getSideStateFromGroup(groupPanels);
}
_createSideFlicking() {
return this.sideState.map((state, i) => {
return new Flicking(this.camera.children[i], __spreadProps(__spreadValues({}, this.sideOptions), {
horizontal: false,
panelsPerView: 1,
defaultIndex: state.start
}));
});
}
}
const getDefaultCameraTransform = (align = ALIGN.CENTER, horizontal = true, firstPanelSize) => {
const cameraAlign = getCameraAlign(align);
const panelAlign = getPanelAlign(align);
if (panelAlign == null) return "";
const camPosition = `calc(${cameraAlign} - (${firstPanelSize || "0px"} * ${panelAlign.percentage}) - ${panelAlign.absolute}px)`;
return horizontal ? `translate(${camPosition})` : `translate(0, ${camPosition})`;
};
const getCameraAlign = (align) => {
const alignVal = typeof align === "object" ? align.camera : align;
return parseAlign(alignVal);
};
const getPanelAlign = (align) => {
const alignVal = typeof align === "object" ? align.panel : align;
return parseArithmeticExpression(parseAlign(alignVal));
};
const parseAlign = (alignVal) => {
if (typeof alignVal === "number") {
return `${alignVal}px`;
}
switch (alignVal) {
case ALIGN.CENTER:
return "50%";
case ALIGN.NEXT:
return "100%";
case ALIGN.PREV:
return "0%";
default:
return alignVal;
}
};
const getRenderingPanels = (flicking, diffResult) => {
const removedPanels = diffResult.removed.reduce((map, idx) => {
map[idx] = true;
return map;
}, {});
const maintainedMap = diffResult.maintained.reduce((map, [prev, current]) => {
map[prev] = current;
return map;
}, {});
const renderingPanels = flicking.panels.filter((panel) => !removedPanels[panel.index]);
if (!flicking.useCSSOrder) {
renderingPanels.sort((panel1, panel2) => panel1.position + panel1.offset - (panel2.position + panel2.offset));
}
return [
...renderingPanels.map((panel) => diffResult.list[maintainedMap[panel.index]]),
...diffResult.added.map((idx) => diffResult.list[idx])
];
};
const sync = (flicking, diffResult, rendered) => {
const renderer = flicking.renderer;
const panels = renderer.panels;
const prevList = [...diffResult.prevList];
const added = [];
const removed = [];
if (diffResult.removed.length > 0) {
let endIdx = -1;
let prevIdx = -1;
diffResult.removed.forEach((removedIdx) => {
if (endIdx < 0) {
endIdx = removedIdx;
}
if (prevIdx >= 0 && removedIdx !== prevIdx - 1) {
removed.push(...batchRemove(renderer, prevIdx, endIdx + 1));
endIdx = removedIdx;
prevIdx = removedIdx;
} else {
prevIdx = removedIdx;
}
prevList.splice(removedIdx, 1);
});
removed.push(...batchRemove(renderer, prevIdx, endIdx + 1));
}
diffResult.ordered.forEach(([from, to]) => {
const prevPanel = panels.splice(from, 1)[0];
panels.splice(to, 0, prevPanel);
});
if (diffResult.ordered.length > 0) {
panels.forEach((panel, idx) => {
const indexDiff = idx - panel.index;
if (indexDiff > 0) {
panel.increaseIndex(indexDiff);
} else {
panel.decreaseIndex(-indexDiff);
}
});
panels.sort((panel1, panel2) => panel1.index - panel2.index);
panels.forEach((panel) => {
panel.updatePosition();
});
}
if (diffResult.added.length > 0) {
let startIdx = -1;
let prevIdx = -1;
const addedElements = rendered.slice(prevList.length);
diffResult.added.forEach((addedIdx, idx) => {
if (startIdx < 0) {
startIdx = idx;
}
if (prevIdx >= 0 && addedIdx !== prevIdx + 1) {
added.push(...batchInsert(renderer, diffResult, addedElements, startIdx, idx + 1));
startIdx = -1;
prevIdx = -1;
} else {
prevIdx = addedIdx;
}
});
if (startIdx >= 0) {
added.push(...batchInsert(renderer, diffResult, addedElements, startIdx));
}
}
if (diffResult.added.length > 0 || diffResult.removed.length > 0) {
renderer.updateAfterPanelChange(added, removed);
} else if (diffResult.ordered.length > 0) {
const camera = flicking.camera;
camera.updateRange();
camera.updateOffset();
camera.updateAnchors();
camera.resetNeedPanelHistory();
}
};
const batchInsert = (renderer, diffResult, addedElements, startIdx, endIdx) => {
return renderer.batchInsertDefer(
...diffResult.added.slice(startIdx, endIdx).map((index, elIdx) => ({ index, elements: [addedElements[elIdx]], hasDOMInElements: false }))
);
};
const batchRemove = (renderer, startIdx, endIdx) => {
const removed = renderer.panels.slice(startIdx, endIdx);
return renderer.batchRemoveDefer({ index: startIdx, deleteCount: removed.length, hasDOMInElements: false });
};
const withFlickingMethods = (prototype, flickingName) => {
[Component.prototype, Flicking.prototype].forEach((proto) => {
Object.getOwnPropertyNames(proto).filter((name) => !prototype[name] && name.indexOf("_") !== 0 && name !== "constructor").forEach((name) => {
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
if (descriptor.value) {
Object.defineProperty(prototype, name, {
value: function(...args) {
return descriptor.value.call(this[flickingName], ...args);
}
});
} else {
const getterDescriptor = {};
if (descriptor.get) {
getterDescriptor.get = function() {
var _a;
const flicking = this[flickingName];
return flicking && ((_a = descriptor.get) == null ? void 0 : _a.call(flicking));
};
}
if (descriptor.set) {
getterDescriptor.set = function(...args) {
var _a;
return (_a = descriptor.set) == null ? void 0 : _a.call(this[flickingName], ...args);
};
}
Object.defineProperty(prototype, name, getterDescriptor);
}
});
});
};
function keys(obj) {
return Object.keys(obj);
}
function isObject(val) {
return typeof val === "object";
}
function isFunction(val) {
return typeof val === "function";
}
var OBSERVERS_PATH = "__observers__";
var COMPUTED_PATH = "__computed__";
var CFCS_DETECTED_DEPENDENCIES_VERSION = 1;
var CFCS_DETECTED_DEPENDENCIES = "__CFCS_DETECTED_DEPENDENCIES__";
function __spreadArray(to, from, pack) {
if (arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
function getDetectedStack() {
Object[CFCS_DETECTED_DEPENDENCIES] = Object[CFCS_DETECTED_DEPENDENCIES] || {};
var versionList = Object[CFCS_DETECTED_DEPENDENCIES];
versionList[CFCS_DETECTED_DEPENDENCIES_VERSION] = versionList[CFCS_DETECTED_DEPENDENCIES_VERSION] || [];
return versionList[CFCS_DETECTED_DEPENDENCIES_VERSION];
}
function getCurrentDetected() {
var stack = getDetectedStack();
return stack[stack.length - 1];
}
var Observer = /* @__PURE__ */ (function() {
function Observer2(value) {
this._emitter = new Component();
this._current = value;
}
var __proto = Observer2.prototype;
Object.defineProperty(__proto, "current", {
/**
* return the current value.
*/
get: function() {
var currentDetected = getCurrentDetected();
currentDetected === null || currentDetected === void 0 ? void 0 : currentDetected.push(this);
return this._current;
},
set: function(value) {
this._setCurrent(value);
},
enumerable: false,
configurable: true
});
__proto.subscribe = function(callback) {
this.current;
this._emitter.on("update", callback);
return this;
};
__proto.unsubscribe = function(callback) {
this._emitter.off("update", callback);
return this;
};
__proto._setCurrent = function(value) {
var prevValue = this._current;
var isUpdate = value !== prevValue;
this._current = value;
if (isUpdate) {
this._emitter.trigger("update", value, prevValue);
}
};
__proto.toString = function() {
return "".concat(this.current);
};
__proto.valueOf = function() {
return this.current;
};
return Observer2;
})();
function injectObserve(prototype, memberName, publicName) {
if (publicName === void 0) {
publicName = memberName;
}
var nextAttributes = {
configurable: true,
get: function() {
return getObserver(this, publicName).current;
},
set: function(value) {
getObserver(this, publicName, value).current = value;
}
};
Object.defineProperty(prototype, memberName, nextAttributes);
if (publicName !== memberName) {
Object.defineProperty(prototype, publicName, {
configurable: true,
get: function() {
return getObserver(this, publicName).current;
}
});
}
}
function Observe() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
if (args.length > 1) {
return injectObserve(args[0], args[1]);
}
return function(prototype, memberName) {
return injectObserve(prototype, memberName, args[0]);
};
}
function injectReactiveSubscribe(object) {
object["subscribe"] = function(name, callback) {
this[name];
getObserver(this, name).subscribe(callback);
};
object["unsubscribe"] = function(name, callback) {
var _this = this;
if (!name) {
keys(getObservers(this)).forEach(function(observerName) {
_this.unsubscribe(observerName);
});
return;
}
if (!(name in this)) {
return;
}
getObserver(this, name).unsubscribe(callback);
};
}
function makeReactiveObject(setup, all) {
var result = isFunction(setup) ? setup() : setup;
var reactiveObject = {};
defineObservers(reactiveObject);
keys(result).forEach(function(name) {
var value = result[name];
if (isObserver(value)) {
setObserver(reactiveObject, name, value);
} else {
setObserver(reactiveObject, name, observe(value));
}
Observe(name)(reactiveObject, name);
});
injectReactiveSubscribe(reactiveObject);
return reactiveObject;
}
function reactive(setup) {
return makeReactiveObject(setup);
}
function observe(defaultValue) {
return new Observer(defaultValue);
}
function withReactiveMethods(ref, methods) {
var obj = {};
if (!methods) {
return obj;
}
methods.forEach(function(name) {
obj[name] = function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var current = ref.current || ref.value;
return current[name].apply(current, args);
};
});
return obj;
}
function defineObservers(instance) {
var observers = {};
Object.defineProperty(instance, OBSERVERS_PATH, {
get: function() {
return observers;
}
});
return observers;
}
function getObservers(instance, isComputed) {
var _a, _b;
if (!instance[OBSERVERS_PATH]) {
defineObservers(instance);
}
var observers = instance[OBSERVERS_PATH];
{
var computedList = (_b = (_a = instance === null || instance === void 0 ? void 0 : instance.constructor) === null || _a === void 0 ? void 0 : _a.prototype) === null || _b === void 0 ? void 0 : _b[COMPUTED_PATH];
if (computedList) {
computedList.forEach(function(name) {
if (!(name in observers) && name in instance) {
instance[name];
}
});
}
}
return observers;
}
function getObserver(instance, name, defaultValue) {
var observers = getObservers(instance);
if (!observers[name]) {
observers[name] = observe(defaultValue);
}
return observers[name];
}
function setObserver(instance, name, observer) {
var observers = getObservers(instance);
observers[name] = observer;
}
function isObserver(val) {
return val && isObject(val) && "current" in val && "subscribe" in val && "unsubscribe" in val;
}
function adaptReactive(adapter, props) {
var objectAdapter = isFunction(adapter) ? {
setup: adapter
} : adapter;
function getProps() {
var _a, _b, _c, _d, _e;
return (_e = (_c = (_a = props === null || props === void 0 ? void 0 : props()) !== null && _a !== void 0 ? _a : (_b = objectAdapter.props) === null || _b === void 0 ? void 0 : _b.call(objectAdapter)) !== null && _c !== void 0 ? _c : (_d = objectAdapter.data) === null || _d === void 0 ? void 0 : _d.call(objectAdapter)) !== null && _e !== void 0 ? _e : {};
}
var eventEmitter = new Component();
var mountedHooks = [];
var initHooks = [];
var destroyHooks = [];
var onHooks = [];
var instanceRef = {
current: null
};
var offHooksList = [];
var initialState = null;
var eventNames = [];
var methodNames = [];
var onMounted = function(callback) {
mountedHooks.push(callback);
};
var onInit = function(callback) {
initHooks.push(callback);
};
var onDestroy = function(callback) {
destroyHooks.push(callback);
};
var on = function(callback) {
onHooks.push(callback);
};
var emit = function(eventName) {
var params = [];
for (var _i = 1; _i < arguments.length; _i++) {
params[_i - 1] = arguments[_i];
}
eventEmitter.trigger.apply(eventEmitter, __spreadArray([eventName], params, false));
};
var setInitialState = function(state) {
initialState = state;
};
var setEvents = function(events) {
eventNames = events;
};
var setMethods = function(methods) {
methodNames = methods;
};
if (objectAdapter.setup) {
instanceRef.current = objectAdapter.setup({
getProps,
setInitialState,
setEvents,
setMethods,
onMounted,
onDestroy,
onInit,
emit,
on
}) || null;
}
if (objectAdapter.created) {
instanceRef.current = objectAdapter.created(getProps()) || null;
}
if (objectAdapter.events) {
setEvents(objectAdapter.events);
}
if (objectAdapter.state) {
setInitialState(objectAdapter.state);
}
if (objectAdapter.methods) {
setMethods(objectAdapter.methods);
}
if (objectAdapter.mounted) {
onMounted(objectAdapter.mounted);
}
if (objectAdapter.destroy) {
destroyHooks.push(objectAdapter.destroy);
}
if (objectAdapter.init) {
initHooks.push(objectAdapter.init);
}
if (objectAdapter.on) {
onHooks.push(function(instance, eventName, listener) {
var off = objectAdapter.on(instance, eventName, listener);
return function() {
var _a;
off && off();
(_a = objectAdapter.off) === null || _a === void 0 ? void 0 : _a.call(objectAdapter, instance, eventName, listener);
};
});
}
return {
events: function() {
return eventNames;
},
state: function() {
var inst = instanceRef.current;
if (initialState) {
return initialState;
}
if (inst) {
var observers_1 = getObservers(inst);
setInitialState(keys(observers_1).reduce(function(prev, cur) {
prev[cur] = observers_1[cur].current;
return prev;
}, {}));
}
return initialState || {};
},
instance: function() {
return instanceRef.current;
},
mounted: function() {
var props2 = getProps();
mountedHooks.forEach(function(hook) {
instanceRef.current = hook(props2, instanceRef.current) || instanceRef.current;
});
},
init: function() {
var instance = instanceRef.current;
var props2 = getProps();
offHooksList = eventNames.map(function(eventName) {
var listener = function() {
var _a;
var params = [];
for (var _i = 0; _i < arguments.length; _i++) {
params[_i] = arguments[_i];
}
(_a = eventEmitter).trigger.apply(_a, __spreadArray([eventName], params, false));
};
var instance2 = instanceRef.current;
return onHooks.map(function(hook) {
return hook(instance2, eventName, listener);
}).filter(Boolean);
});
initHooks.forEach(function(hook) {
hook(instance, props2);
});
},
destroy: function() {
offHooksList.forEach(function(offHooks) {
offHooks.forEach(function(hook) {
hook();
});
});
eventEmitter.off();
var instance = instanceRef.current;
var props2 = getProps();
destroyHooks.forEach(function(hook) {
hook(instance, props2);
});
},
methods: function() {
return withReactiveMethods(instanceRef, methodNames);
},
on: function(eventName, listener) {
eventEmitter.on(eventName, listener);
},
off: function(eventName, listener) {
eventEmitter.off(eventName, listener);
}
};
}
const getIsReachStart = (flicking) => !flicking.circular && flicking.index === 0;
const getIsReachEnd = (flicking) => !flicking.circular && flicking.index === flicking.panelCount - 1;
const getTotalPanelCount = (flicking) => flicking.panelCount;
const getCurrentPanelIndex = (flicking) => flicking.index;
const getProgress = (flicking) => {
const cam = flicking.camera;
const progressRatio = (cam.position - cam.range.min) / (cam.range.max - cam.range.min);
const percent = Math.min(Math.max(progressRatio, 0), 1) * 100;
return percent;
};
const getIndexProgress = (flicking) => {
const cam = flicking.camera;
const anchorPoints = cam.anchorPoints;
const length = anchorPoints.length;
const cameraPosition = cam.position;
const isCircular = flicking.circularEnabled;
let indexProgress = 0;
const { min, max } = cam.range;
const firstAnchorPoint = anchorPoints[0];
const lastAnchorPoint = anchorPoints[length - 1];
const distanceLastToFirst = max - lastAnchorPoint.position + (firstAnchorPoint.position - min);
anchorPoints.some((anchorPoint, index) => {
const anchorPosition = anchorPoint.position;
const nextAnchorPoint = anchorPoints[index + 1];
if (index === 0 && cameraPosition <= anchorPosition) {
if (isCircular) {
indexProgress = (cameraPosition - anchorPosition) / distanceLastToFirst;
} else {
indexProgress = (cameraPosition - anchorPosition) / anchorPoint.panel.size;
}
} else if (index === length - 1 && cameraPosition >= anchorPosition) {
if (isCircular) {
indexProgress = index + (cameraPosition - anchorPosition) / distanceLastToFirst;
} else {
indexProgress = index + (cameraPosition - anchorPosition) / anchorPoint.panel.size;
}
} else if (nextAnchorPoint && anchorPosition <= cameraPosition && cameraPosition <= nextAnchorPoint.position) {
indexProgress = index + (cameraPosition - anchorPosition) / (nextAnchorPoint.position - anchorPosition);
} else {
return false;
}
return true;
});
return indexProgress;
};
const flickingReactiveAPIAdapter = ({ onInit, onDestroy, setMethods, getProps }) => {
var _a, _b, _c;
let flicking;
const moveTo = (i) => {
if (flicking == null) {
return Promise.reject(new Error("Flicking instance is not available"));
}
if (flicking == null ? void 0 : flicking.animating) {
return Promise.resolve();
}
return flicking.moveTo(i);
};
setMethods(["moveTo"]);
const options = getProps().options;
const reactiveObj = reactive({
isReachStart: (options == null ? void 0 : options.defaultIndex) ? (options == null ? void 0 : options.defaultIndex) === 0 : true,
isReachEnd: (options == null ? void 0 : options.totalPanelCount) && (options == null ? void 0 : options.defaultIndex) ? options.defaultIndex === options.totalPanelCount - 1 : false,
totalPanelCount: (_a = options == null ? void 0 : options.totalPanelCount) != null ? _a : 0,
currentPanelIndex: (_b = options == null ? void 0 : options.defaultIndex) != null ? _b : 0,
progress: 0,
indexProgress: (_c = options == null ? void 0 : options.defaultIndex) != null ? _c : 0,
moveTo
});
const onChanged = () => {
if (flicking === void 0) return;
reactiveObj.isReachStart = getIsReachStart(flicking);
reactiveObj.isReachEnd = getIsReachEnd(flicking);
reactiveObj.currentPanelIndex = getCurrentPanelIndex(flicking);
};
const onPanelChange = () => {
if (flicking === void 0) return;
onChanged();
reactiveObj.totalPanelCount = getTotalPanelCount(flicking);
};
const onMove = () => {
if (flicking === void 0) return;
reactiveObj.progress = getProgress(flicking);
reactiveObj.indexProgress = getIndexProgress(flicking);
};
onInit((inst, data) => {
flicking = data.flicking;
if (flicking === void 0) return;
reactiveObj.isReachStart = getIsReachStart(flicking);
reactiveObj.isReachEnd = getIsReachEnd(flicking);
reactiveObj.currentPanelIndex = getCurrentPanelIndex(flicking);
reactiveObj.progress = getProgress(flicking);
reactiveObj.totalPanelCount = getTotalPanelCount(flicking);
flicking == null ? void 0 : flicking.on("changed", onChanged);
flicking == null ? void 0 : flicking.on("panelChange", onPanelChange);
flicking == null ? void 0 : flicking.on("move", onMove);
});
onDestroy(() => {
flicking == null ? void 0 : flicking.off("changed", onChanged);
flicking == null ? void 0 : flicking.off("panelChange", onPanelChange);
flicking == null ? void 0 : flicking.off("move", onMove);
});
return reactiveObj;
};
const connectFlickingReactiveAPI = (flicking, options) => {
const obj = adaptReactive(flickingReactiveAPIAdapter, () => ({ flicking, options }));
obj.mounted();
const instance = obj.instance();
obj.init();
return instance;
};
export {
ALIGN,
AnchorPoint,
AnimatingState,
AxesController,
BoundCameraMode,
CIRCULAR_FALLBACK,
CLASS,
CODE,
Camera,
CircularCameraMode,
Control,
CrossFlicking,
DIRECTION,
DisabledState,
DraggingState,
CODE as ERROR_CODE,
EVENTS,
ExternalRenderer,
FlickingError,
FreeControl,
HoldingState,
IdleState,
LinearCameraMode,
MESSAGE,
MOVE_DIRECTION,
MOVE_TYPE,
NormalRenderingStrategy,
ORDER,
Panel,
Renderer,
SIDE_EVENTS,
SnapControl,
State,
StateMachine,
StrictControl,
VanillaElementProvider,
VanillaRenderer,
Viewport,
VirtualElementProvider,
VirtualManager,
VirtualPanel,
VirtualRenderingStrategy,
camelize,
checkExistence,
circulateIndex,
circulatePosition,
clamp,
connectFlickingReactiveAPI,
Flicking as default,
find,
findIndex,
findRight,
flickingReactiveAPIAdapter,
getDataAttributes,
getDefaultCameraTransform,
getDirection,
getElement,
getElementSize,
getFlickingAttached,
getMinusCompensatedIndex,
getProgress$1 as getProgress,
getRenderingPanels,
getStyle,
includes,
isBetween,
isString,
merge,
parseAlign$1 as parseAlign,
parseArithmeticExpression,
parseArithmeticSize,
parseBounce,
parseCSSSizeValue,
parseElement,
parsePanelAlign,
range,
setPrototypeOf,
setSize,
sync,
toArray,
withFlickingMethods
};
//# sourceMappingURL=flicking.esm.js.map