@egjs/flicking
Version:
Everyday 30 million people experience. It's reliable, flexible and extendable carousel.
352 lines (288 loc) • 9.71 kB
text/typescript
/*
* Copyright (c) 2015 NAVER Corp.
* egjs projects are licensed under the MIT license
*/
import Flicking, { FlickingOptions } from "./Flicking";
import FlickingError from "./core/FlickingError";
import * as ERROR from "./const/error";
import { ALIGN, DIRECTION } from "./const/external";
import { LiteralUnion, Merged, ValueOf } from "./type/internal";
import { ElementLike } from "./type/external";
// eslint-disable-next-line @typescript-eslint/ban-types
export const merge = <From extends object, To extends object>(target: From, ...sources: To[]): Merged<From, To> => {
sources.forEach(source => {
Object.keys(source).forEach(key => {
target[key] = source[key] as unknown;
});
});
return target as Merged<From, To>;
};
export const getElement = (el: HTMLElement | string | null, parent?: HTMLElement): HTMLElement => {
let targetEl: HTMLElement | null = null;
if (isString(el)) {
const parentEl = parent ? parent : document;
const queryResult = parentEl.querySelector(el);
if (!queryResult) {
throw new FlickingError(ERROR.MESSAGE.ELEMENT_NOT_FOUND(el), ERROR.CODE.ELEMENT_NOT_FOUND);
}
targetEl = queryResult as HTMLElement;
} else if (el && el.nodeType === Node.ELEMENT_NODE) {
targetEl = el;
}
if (!targetEl) {
throw new FlickingError(ERROR.MESSAGE.WRONG_TYPE(el, ["HTMLElement", "string"]), ERROR.CODE.WRONG_TYPE);
}
return targetEl;
};
export const checkExistence = (value: any, nameOnErrMsg: string) => {
if (value == null) {
throw new FlickingError(ERROR.MESSAGE.VAL_MUST_NOT_NULL(value, nameOnErrMsg), ERROR.CODE.VAL_MUST_NOT_NULL);
}
};
export const clamp = (x: number, min: number, max: number) => Math.max(Math.min(x, max), min);
export const getFlickingAttached = (val: Flicking | null): Flicking => {
if (!val) {
throw new FlickingError(ERROR.MESSAGE.NOT_ATTACHED_TO_FLICKING, ERROR.CODE.NOT_ATTACHED_TO_FLICKING);
}
return val;
};
export const toArray = <T>(iterable: ArrayLike<T>): T[] => [].slice.call(iterable) as T[];
export const parseAlign = (align: LiteralUnion<ValueOf<typeof ALIGN>> | number, size: number): number => {
let alignPoint: number | null;
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(ERROR.MESSAGE.WRONG_OPTION("align", align), ERROR.CODE.WRONG_OPTION);
}
}
} else {
alignPoint = align as number;
}
return alignPoint;
};
export const parseBounce = (bounce: FlickingOptions["bounce"], size: number): number[] => {
let parsedBounce: Array<number | null>;
if (Array.isArray(bounce)) {
parsedBounce = (bounce as string[]).map(val => parseArithmeticSize(val, size));
} else {
const parsedVal = parseArithmeticSize(bounce, size);
parsedBounce = [parsedVal, parsedVal];
}
return parsedBounce.map(val => {
if (val == null) {
throw new FlickingError(ERROR.MESSAGE.WRONG_OPTION("bounce", bounce), ERROR.CODE.WRONG_OPTION);
}
return val;
});
};
export const parseArithmeticSize = (cssValue: number | string, base: number): number | null => {
const parsed = parseArithmeticExpression(cssValue);
if (parsed == null) return null;
return parsed.percentage * base + parsed.absolute;
};
export const parseArithmeticExpression = (cssValue: number | string): { percentage: number; absolute: number } | null => {
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 || "+";
}
// Return default value for values not in good form
if (!sign) {
return null;
}
const signMultiplier = sign === "+" ? 1 : -1;
if (unit === "%") {
parsed.percentage += signMultiplier * (parsedValue / 100);
} else {
parsed.absolute += signMultiplier * parsedValue;
}
// Match next occurrence
++idx;
matchResult = cssRegex.exec(cssValue);
}
// None-matched
if (idx === 0) {
return null;
}
return parsed;
};
export const parseCSSSizeValue = (val: string | number): string => isString(val) ? val : `${val}px`;
export const parsePanelAlign = (align: FlickingOptions["align"]) => typeof align === "object"
? (align as { panel: string | number }).panel
: align;
export const getDirection = (start: number, end: number): ValueOf<typeof DIRECTION> => {
if (start === end) return DIRECTION.NONE;
return start < end ? DIRECTION.NEXT : DIRECTION.PREV;
};
export const parseElement = (element: ElementLike | ElementLike[]): HTMLElement[] => {
if (!Array.isArray(element)) {
element = [element];
}
const elements: HTMLElement[] = [];
element.forEach(el => {
if (isString(el)) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = el;
elements.push(...toArray(tempDiv.children) as HTMLElement[]);
while (tempDiv.firstChild) {
tempDiv.removeChild(tempDiv.firstChild);
}
} else if (el && el.nodeType === Node.ELEMENT_NODE) {
elements.push(el);
} else {
throw new FlickingError(ERROR.MESSAGE.WRONG_TYPE(el, ["HTMLElement", "string"]), ERROR.CODE.WRONG_TYPE);
}
});
return elements;
};
export const getMinusCompensatedIndex = (idx: number, max: number) => idx < 0 ? clamp(idx + max, 0, max) : clamp(idx, 0, max);
export const includes = <T>(array: T[], target: any): target is T => {
for (const val of array) {
if (val === target) return true;
}
return false;
};
export const isString = (val: any): val is string => typeof val === "string";
export const circulatePosition = (pos: number, min: number, max: number) => {
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;
};
export const find = <T>(array: T[], checker: (val: T) => boolean): T | null => {
for (const val of array) {
if (checker(val)) {
return val;
}
}
return null;
};
export const findRight = <T>(array: T[], checker: (val: T) => boolean): T | null => {
for (let idx = array.length - 1; idx >= 0; idx--) {
const val = array[idx];
if (checker(val)) {
return val;
}
}
return null;
};
export const findIndex = <T>(array: T[], checker: (val: T) => boolean): number => {
for (let idx = 0; idx < array.length; idx++) {
if (checker(array[idx])) {
return idx;
}
}
return -1;
};
export const getProgress = (pos: number, prev: number, next: number) => (pos - prev) / (next - prev);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
export const getStyle = (el: HTMLElement): CSSStyleDeclaration => window.getComputedStyle(el) || (el as any).currentStyle as CSSStyleDeclaration;
export const setSize = (el: HTMLElement, { width, height }: Partial<{
width: number | string;
height: number | string;
}>) => {
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`;
}
}
};
export const isBetween = (val: number, min: number, max: number) => val >= min && val <= max;
export const circulateIndex = (index: number, max: number): number => {
if (index >= max) {
return index % max;
} else if (index < 0) {
return getMinusCompensatedIndex((index + 1) % max - 1, max);
} else {
return index;
}
};
export const range = (end: number): number[] => {
const arr = new Array(end);
for (let i = 0; i < end; i++) {
arr[i] = i;
}
return arr;
};
export const getElementSize = ({
el,
horizontal,
useFractionalSize,
useOffset,
style
}: {
el: HTMLElement;
horizontal: boolean;
useFractionalSize: boolean;
useOffset: boolean;
style: CSSStyleDeclaration;
}): number => {
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);
};
export const setPrototypeOf = Object.setPrototypeOf || ((obj, proto) => {
obj.__proto__ = proto;
return obj;
});