/* * Copyright (c) 2015 NAVER Corp. * egjs projects are licensed under the MIT license */ import { ALIGN, DIRECTION } from "./constants/values"; import * as ERROR from "./error/codes"; import FlickingError from "./error/FlickingError"; import Flicking, { FlickingOptions } from "./Flicking"; import { ElementLike } from "./types/external"; import { LiteralUnion, Merged, ValueOf } from "./types/internal"; // eslint-disable-next-line @typescript-eslint/ban-types export const merge = (target: From, ...sources: To[]): Merged => { sources.forEach(source => { Object.keys(source).forEach(key => { target[key] = source[key] as unknown; }); }); return target as Merged; }; 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 = (iterable: ArrayLike): T[] => [].slice.call(iterable) as T[]; export const parseAlign = (align: LiteralUnion> | 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; 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 => { 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; }; // Elements that can execute script or load remote content on their own const XSS_UNSAFE_TAGS = ["SCRIPT", "IFRAME", "OBJECT", "EMBED", "LINK", "META", "BASE"]; // An attribute that can run script: an event handler (on*) or a `javascript:` URL const isUnsafeAttribute = (name: string, value: string): boolean => { const lower = name.toLowerCase(); if (lower.indexOf("on") === 0) { return true; } return (lower === "src" || lower === "href" || lower === "xlink:href") && /^\s*javascript:/i.test(value); }; const sanitizeElementNode = (node: Element): void => { toArray(node.attributes).forEach(attr => { if (isUnsafeAttribute(attr.name, attr.value)) { node.removeAttribute(attr.name); } }); toArray(node.children).forEach(child => { if (includes(XSS_UNSAFE_TAGS, child.tagName.toUpperCase())) { child.remove(); } else { sanitizeElementNode(child); } }); }; /** * Parse an HTML string into elements with sanitization, used by {@link Flicking.setStatus} to restore * panels from a serialized `html` string. The string is parsed inside a `