UNPKG

@egjs/flicking

Version:

Everyday 30 million people experience. It's reliable, flexible and extendable carousel.

757 lines (649 loc) 27.2 kB
/* * Copyright (c) 2015 NAVER Corp. * egjs projects are licensed under the MIT license */ import { ComponentEvent } from "@egjs/component"; import Flicking, { FlickingOptions } from "../Flicking"; import FlickingError from "../core/FlickingError"; import Panel from "../core/panel/Panel"; import AnchorPoint from "../core/AnchorPoint"; import * as ERROR from "../const/error"; import { ALIGN, CIRCULAR_FALLBACK, DIRECTION, EVENTS, ORDER } from "../const/external"; import { checkExistence, find, getFlickingAttached, getProgress, getStyle, includes, parseAlign, toArray } from "../utils"; import { ValueOf } from "../type/internal"; import { CameraMode, BoundCameraMode, CircularCameraMode, LinearCameraMode } from "./mode"; export interface CameraOptions { align: FlickingOptions["align"]; } /** * A component that manages actual movement inside the viewport * @ko 뷰포트 내에서의 실제 움직임을 담당하는 컴포넌트 */ class Camera { // Options private _align: FlickingOptions["align"]; // Internal states private _flicking: Flicking; private _mode: CameraMode; private _el: HTMLElement; private _transform: string; private _position: number; private _alignPos: number; private _offset: number; private _circularOffset: number; private _circularEnabled: boolean; private _range: { min: number; max: number }; private _visiblePanels: Panel[]; private _anchors: AnchorPoint[]; private _needPanelTriggered: { prev: boolean; next: boolean }; private _panelOrder: ValueOf<typeof ORDER>; // Internal states getter /** * The camera element(`.flicking-camera`) * @ko 카메라 엘리먼트(`.flicking-camera`) * @type {HTMLElement} * @readonly */ public get element() { return this._el; } /** * An array of the child elements of the camera element(`.flicking-camera`) * @ko 카메라 엘리먼트(`.flicking-camera`)의 자식 엘리먼트 배열 * @type {HTMLElement[]} * @readonly */ public get children() { return toArray(this._el.children) as HTMLElement[]; } /** * Current position of the camera * @ko Camera의 현재 좌표 * @type {number} * @readonly */ public get position() { return this._position; } /** * Align position inside the viewport where {@link Panel}'s {@link Panel#alignPosition alignPosition} should be located at * @ko 패널의 정렬 기준 위치. 뷰포트 내에서 {@link Panel}의 {@link Panel#alignPosition alignPosition}이 위치해야 하는 곳입니다 * @type {number} * @readonly */ public get alignPosition() { return this._alignPos; } /** * Position offset, used for the {@link Flicking#renderOnlyVisible renderOnlyVisible} option * @ko Camera의 좌표 오프셋. {@link Flicking#renderOnlyVisible renderOnlyVisible} 옵션을 위해 사용됩니다. * @type {number} * @default 0 * @readonly */ public get offset() { return this._offset - this._circularOffset; } /** * Whether the `circular` option is enabled. * The {@link Flicking#circular circular} option can't be enabled when sum of the panel sizes are too small. * @ko {@link Flicking#circular circular} 옵션이 활성화되었는지 여부를 나타내는 멤버 변수. * {@link Flicking#circular circular} 옵션은 패널의 크기의 합이 충분하지 않을 경우 비활성화됩니다. * @type {boolean} * @default false * @readonly */ public get circularEnabled() { return this._circularEnabled; } /** * A current camera mode * @type {CameraMode} * @readonly */ public get mode() { return this._mode; } /** * A range that Camera's {@link Camera#position position} can reach * @ko Camera의 {@link Camera#position position}이 도달 가능한 범위 * @type {object} * @property {number} min A minimum position<ko>최소 위치</ko> * @property {number} max A maximum position<ko>최대 위치</ko> * @readonly */ public get range() { return this._range; } /** * A difference between Camera's minimum and maximum position that can reach * @ko Camera가 도달 가능한 최소/최대 좌표의 차이 * @type {number} * @readonly */ public get rangeDiff() { return this._range.max - this._range.min; } /** * An array of visible panels from the current position * @ko 현재 보이는 패널들의 배열 * @type {Panel[]} * @readonly */ public get visiblePanels() { return this._visiblePanels; } /** * A range of the visible area from the current position * @ko 현재 위치에서 보이는 범위 * @type {object} * @property {number} min A minimum position<ko>최소 위치</ko> * @property {number} min A maximum position<ko>최대 위치</ko> * @readonly */ public 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 * @ko 카메라가 도달 가능한 {@link AnchorPoint}의 목록 * @type {AnchorPoint[]} * @readonly */ public get anchorPoints() { return this._anchors; } /** * A current parameters of the Camera for updating {@link AxesController} * @ko {@link AxesController}를 업데이트하기 위한 현재 Camera 패러미터들 * @type {ControlParams} * @readonly */ public 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 * @ko 현재 카메라가 도달 가능한 범위의 최소 혹은 최대점을 넘어섰는지를 나타냅니다 * @type {boolean} * @readonly */ public get atEdge() { return this._position <= this._range.min || this._position >= this._range.max; } /** * Return the size of the viewport * @ko 뷰포트 크기를 반환합니다 * @type {number} * @readonly */ public 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 * Range is from 0 to last panel's index * @ko 첫번째 패널로부터 마지막 패널까지의 카메라 위치의 진행도를 반환합니다 * 범위는 0부터 마지막 패널의 인덱스까지입니다 * @type {number} * @readonly */ public 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]; // Looped if (prevPosition > panelPos) { prevPosition -= rangeDiff; } return nearestPanel.index - 1 + getProgress(position, prevPosition, panelPos); } else { const nextPanel = nearestPanel.next(); let nextPosition = nextPanel ? nextPanel.position + nextPanel.offset : nextRange + bounceSize[1]; // Looped if (nextPosition < panelPos) { nextPosition += rangeDiff; } return nearestPanel.index + getProgress(position, panelPos, nextPosition); } } /** * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/direction direction} CSS property applied to the camera element(`.flicking-camera`) * @ko 카메라 엘리먼트(`.flicking-camera`)에 적용된 {@link https://developer.mozilla.org/en-US/docs/Web/CSS/direction direction} CSS 속성 * @type {string} * @readonly */ public get panelOrder() { return this._panelOrder; } // Options Getter /** * A value indicating where the {@link Camera#alignPosition alignPosition} should be located at inside the viewport element * @ko {@link Camera#alignPosition alignPosition}이 뷰포트 엘리먼트 내의 어디에 위치해야 하는지를 나타내는 값 * @type {ALIGN | string | number} */ public get align() { return this._align; } // Options Setter public set align(val: FlickingOptions["align"]) { this._align = val; } /** */ public constructor(flicking: Flicking, { align = ALIGN.CENTER }: Partial<CameraOptions> = {}) { this._flicking = flicking; this._resetInternalValues(); // Options this._align = align; } /** * Initialize Camera * @ko Camera를 초기화합니다 * @throws {FlickingError} * {@link ERROR_CODE VAL_MUST_NOT_NULL} If the camera element(`.flicking-camera`) does not exist inside viewport element * <ko>{@link ERROR_CODE VAL_MUST_NOT_NULL} 뷰포트 엘리먼트 내부에 카메라 엘리먼트(`.flicking-camera`)가 존재하지 않을 경우</ko> * @return {this} */ public init(): this { const viewportEl = this._flicking.viewport.element; checkExistence(viewportEl.firstElementChild, "First element child of the viewport element"); this._el = viewportEl.firstElementChild as HTMLElement; this._checkTranslateSupport(); this._updateMode(); this.updatePanelOrder(); return this; } /** * Destroy Camera and return to initial state * @ko Camera를 초기 상태로 되돌립니다 * @return {void} */ public destroy(): this { this._resetInternalValues(); return this; } /** * Move to the given position and apply CSS transform * @ko 해당 좌표로 이동하고, CSS transform을 적용합니다 * @param {number} pos A new position<ko>움직일 위치</ko> * @throws {FlickingError} * {@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} When {@link Camera#init init} is not called before * <ko>{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} {@link Camera#init init}이 이전에 호출되지 않은 경우</ko> * @return {this} */ public lookAt(pos: number): void { 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(); }); } else { this.applyTransform(); } } /** * Return a previous {@link AnchorPoint} of given {@link AnchorPoint} * If it does not exist, return `null` instead * @ko 주어진 {@link AnchorPoint}의 이전 {@link AnchorPoint}를 반환합니다 * 존재하지 않을 경우 `null`을 반환합니다 * @param {AnchorPoint} anchor A reference {@link AnchorPoint}<ko>기준 {@link AnchorPoint}</ko> * @return {AnchorPoint | null} The previous {@link AnchorPoint}<ko>이전 {@link AnchorPoint}</ko> */ public getPrevAnchor(anchor: AnchorPoint): AnchorPoint | null { 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} * If it does not exist, return `null` instead * @ko 주어진 {@link AnchorPoint}의 다음 {@link AnchorPoint}를 반환합니다 * 존재하지 않을 경우 `null`을 반환합니다 * @param {AnchorPoint} anchor A reference {@link AnchorPoint}<ko>기준 {@link AnchorPoint}</ko> * @return {AnchorPoint | null} The next {@link AnchorPoint}<ko>다음 {@link AnchorPoint}</ko> */ public getNextAnchor(anchor: AnchorPoint): AnchorPoint | null { 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 * 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 * @ko 현재 카메라 아래 패널에서의 위치 진행도를 반환합니다 * 반환값은 카메라가 패널 내부에 있을 경우 0부터 1까지의 값을 갖습니다 * 패널의 margin 영역에 있을 경우 0보다 작거나 1보다 큰 값을 반환할 수 있습니다 */ public getProgressInPanel(panel: Panel) { const panelRange = panel.range; return (this._position - panelRange.min) / (panelRange.max - panelRange.min); } /** * Return {@link AnchorPoint} that includes given position * If there's no {@link AnchorPoint} that includes the given position, return `null` instead * @ko 주어진 좌표를 포함하는 {@link AnchorPoint}를 반환합니다 * 주어진 좌표를 포함하는 {@link AnchorPoint}가 없을 경우 `null`을 반환합니다 * @param {number} position A position to check<ko>확인할 좌표</ko> * @return {AnchorPoint | null} The {@link AnchorPoint} that includes the given position<ko>해당 좌표를 포함하는 {@link AnchorPoint}</ko> */ public findAnchorIncludePosition(position: number): AnchorPoint | null { return this._mode.findAnchorIncludePosition(position); } /** * Return {@link AnchorPoint} nearest to given position * If there're no {@link AnchorPoint}s, return `null` instead * @ko 해당 좌표에서 가장 가까운 {@link AnchorPoint}를 반환합니다 * {@link AnchorPoint}가 하나도 없을 경우 `null`을 반환합니다 * @param {number} position A position to check<ko>확인할 좌표</ko> * @return {AnchorPoint | null} The {@link AnchorPoint} nearest to the given position<ko>해당 좌표에 가장 인접한 {@link AnchorPoint}</ko> */ public findNearestAnchor(position: number): AnchorPoint | null { return this._mode.findNearestAnchor(position); } /** * Return {@link AnchorPoint} that matches {@link Flicking#currentPanel} * @ko 현재 {@link Flicking#currentPanel}에 해당하는 {@link AnchorPoint}를 반환합니다 * @return {AnchorPoint | null} */ public findActiveAnchor(): AnchorPoint | null { const flicking = getFlickingAttached(this._flicking); const activePanel = flicking.control.activePanel; if (!activePanel) return null; return find(this._anchors, anchor => anchor.panel.index === activePanel.index) ?? this.findNearestAnchor(activePanel.position); } /** * Clamp the given position between camera's range * @ko 주어진 좌표를 Camera가 도달 가능한 범위 사이의 값으로 만듭니다 * @param {number} position A position to clamp<ko>범위를 제한할 좌표</ko> * @return {number} A clamped position<ko>범위 제한된 좌표</ko> */ public clampToReachablePosition(position: number): number { return this._mode.clampToReachablePosition(position); } /** * Check whether the given panel is inside of the Camera's range * @ko 해당 {@link Panel}이 Camera가 도달 가능한 범위 내에 있는지를 반환합니다 * @param panel An instance of {@link Panel} to check<ko>확인할 {@link Panel}의 인스턴스</ko> * @return {boolean} Whether the panel's inside Camera's range<ko>도달 가능한 범위 내에 해당 패널이 존재하는지 여부</ko> */ public canReach(panel: Panel): boolean { return this._mode.canReach(panel); } /** * Check whether the given panel element is visible at the current position * @ko 현재 좌표에서 해당 패널 엘리먼트를 볼 수 있는지 여부를 반환합니다 * @param panel An instance of {@link Panel} to check<ko>확인할 {@link Panel}의 인스턴스</ko> * @return Whether the panel element is visible at the current position<ko>현재 위치에서 해당 패널 엘리먼트가 보이는지 여부</ko> */ public canSee(panel: Panel): boolean { return this._mode.canSee(panel); } /** * Update {@link Camera#range range} of Camera * @ko Camera의 {@link Camera#range range}를 업데이트합니다 * @method * @abstract * @memberof Camera * @instance * @name updateRange * @chainable * @throws {FlickingError} * {@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} When {@link Camera#init init} is not called before * <ko>{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} {@link Camera#init init}이 이전에 호출되지 않은 경우</ko> * @return {this} */ public 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} * @ko Camera의 {@link Camera#alignPosition alignPosition}을 업데이트합니다 * @chainable * @return {this} */ public updateAlignPos(): this { const align = this._align; const alignVal = typeof align === "object" ? (align as { camera: string | number }).camera : align; this._alignPos = parseAlign(alignVal, this.size); return this; } /** * Update Camera's {@link Camera#anchorPoints anchorPoints} * @ko Camera의 {@link Camera#anchorPoints anchorPoints}를 업데이트합니다 * @throws {FlickingError} * {@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} When {@link Camera#init init} is not called before * <ko>{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} {@link Camera#init init}이 이전에 호출되지 않은 경우</ko> * @chainable * @return {this} */ public updateAnchors(): this { this._anchors = this._mode.getAnchors(); return this; } /** * Update Viewport's height to active panel's height * @ko 현재 선택된 패널의 높이와 동일하도록 뷰포트의 높이를 업데이트합니다 * @throws {FlickingError} * {@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} When {@link Camera#init init} is not called before * <ko>{@link ERROR_CODE NOT_ATTACHED_TO_FLICKING} {@link Camera#init init}이 이전에 호출되지 않은 경우</ko> * @chainable * @return {this} */ public updateAdaptiveHeight() { const flicking = getFlickingAttached(this._flicking); const activePanel = flicking.control.activePanel; if (!flicking.horizontal || !flicking.adaptive || !activePanel) return; flicking.viewport.setSize({ height: activePanel.height }); } /** * Update current offset of the camera * @ko 현재 카메라의 오프셋을 업데이트합니다 * @chainable * @return {this} */ public updateOffset(): this { 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 * @ko 카메라 엘리먼트에 적용된 {@link https://developer.mozilla.org/en-US/docs/Web/CSS/direction direction} CSS 속성에 맞게 방향을 업데이트합니다 * @return {this} */ public updatePanelOrder(): this { 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 * @ko 발생한 {@link Flicking#event:needPanel needPanel} 이벤트들을 초기화하여 다시 발생할 수 있도록 합니다 * @chainable * @return {this} */ public resetNeedPanelHistory(): this { this._needPanelTriggered = { prev: false, next: false }; return this; } /** * Apply "transform" style with the current position to camera element * @ko 현재 위치를 기준으로한 transform 스타일을 카메라 엘리먼트에 적용합니다. * @return {this} */ public applyTransform(): this { 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; } private _resetInternalValues() { this._position = 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 }; } private _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: Panel[] = newVisiblePanels.filter(panel => !includes(prevVisiblePanels, panel)); const removed: Panel[] = 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 })); }); } } private _checkNeedPanel(): void { 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; } } } private _checkReachEnd(prevPos: number, newPos: number): void { const flicking = getFlickingAttached(this._flicking); const range = this._range; const wasBetweenRange = prevPos > range.min && prevPos < range.max; const isBetweenRange = newPos > range.min && newPos < range.max; if (!wasBetweenRange || isBetweenRange) return; const direction = newPos <= range.min ? DIRECTION.PREV : DIRECTION.NEXT; flicking.trigger(new ComponentEvent(EVENTS.REACH_EDGE, { direction })); } private _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(ERROR.MESSAGE.TRANSFORM_NOT_SUPPORTED, ERROR.CODE.TRANSFORM_NOT_SUPPORTED); } this._transform = transformName; }; private _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; } } private _togglePanels(prevPos: number, pos: number): boolean { 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); } } export default Camera;