@egjs/flicking
Version:
Everyday 30 million people experience. It's reliable, flexible and extendable carousel.
1,469 lines (1,468 loc) • 242 kB
JavaScript
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory(require("@egjs/component"), require("@egjs/axes"), require("@egjs/imready")) : typeof define === "function" && define.amd ? define(["@egjs/component", "@egjs/axes", "@egjs/imready"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.Flicking = factory(global.eg.Component, global.eg.Axes, global.eg.ImReady));
})(this, (function(Component, Axes, ImReady) {
"use strict";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());
});
};
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"
};
const Values = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
ALIGN,
CIRCULAR_FALLBACK,
CLASS,
DIRECTION,
MOVE_DIRECTION,
MOVE_TYPE,
ORDER
}, Symbol.toStringTag, { value: "Module" }));
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"
};
const Events = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
EVENTS
}, Symbol.toStringTag, { value: "Module" }));
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;
};
const Utils = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
camelize,
checkExistence,
circulateIndex,
circulatePosition,
clamp,
find,
findIndex,
findRight,
getDataAttributes,
getDirection,
getElement,
getElementSize,
getFlickingAttached,
getMinusCompensatedIndex,
getProgress: getProgress$1,
getStyle,
includes,
isBetween,
isString,
merge,
parseAlign: parseAlign$1,
parseArithmeticExpression,
parseArithmeticSize,
parseBounce,
parseCSSSizeValue,
parseElement,
parsePanelAlign,
range,
setPrototypeOf,
setSize,
toArray
}, Symbol.toStringTag, { value: "Module" }));
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;
}
}
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 };
}
}
let Camera$1 = 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;
el.style[this._transform] = flicking.horizontal ? `translate(${this._panelOrder === ORDER.RTL ? actualPosition : -actualPosition}px)` : `translate(0, ${-actualPosition}px)`;
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 Component.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 Component.ComponentEvent(EVENTS.NEED_PANEL, { direction: DIRECTION.PREV }));
needPanelTriggered.prev = true;
}
if (!needPanelTriggered.next) {
flicking.trigger(new Component.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.