/* * Copyright (c) 2015 NAVER Corp. * egjs projects are licensed under the MIT license */ import { $, isCssPropsFromAxes, setCssProps, revertCssProps } from "../utils"; import { ActiveEvent, ElementType, InputEventType } from "../types"; import { DIRECTION_ALL } from "../const"; import { toAxis, convertInputType, InputType, InputTypeObserver, } from "./InputType"; export interface PinchInputOption { scale?: number; threshold?: number; inputType?: string[]; touchAction?: string; } /** * @typedef {Object} PinchInputOption The option object of the eg.Axes.PinchInput module * @ko eg.Axes.PinchInput 모듈의 옵션 객체 * @param {Number} [scale=1] Coordinate scale that a user can move사용자의 동작으로 이동하는 좌표의 배율 * @param {Number} [threshold=0] Minimal scale before recognizing 사용자의 Pinch 동작을 인식하기 위해산 최소한의 배율 * @param {String[]} [inputType=["touch", "pointer"]] Types of input devices * - touch: Touch screen * - pointer: Mouse and touch 입력 장치 종류 * - touch: 터치 입력 장치 * - pointer: 마우스 및 터치 * @param {String} [touchAction="none"] Value that overrides the element's "touch-action" css property. It is set to "none" to prevent scrolling during touch. 엘리먼트의 "touch-action" CSS 속성을 덮어쓰는 값. 터치 도중 스크롤을 방지하기 위해 "none" 으로 설정되어 있다. **/ /** * A module that passes the amount of change to eg.Axes when two pointers are moving toward (zoom-in) or away from each other (zoom-out). use one axis. * @ko 2개의 pointer를 이용하여 zoom-in하거나 zoom-out 하는 동작의 변화량을 eg.Axes에 전달하는 모듈. 한 개 의 축을 사용한다. * @example * ```js * const pinch = new eg.Axes.PinchInput("#area", { * scale: 1 * }); * * // Connect 'something' axis when two pointers are moving toward (zoom-in) or away from each other (zoom-out). * axes.connect("something", pinch); * ``` * @param {HTMLElement|String|jQuery} element An element to use the eg.Axes.PinchInput module eg.Axes.PinchInput 모듈을 사용할 엘리먼트 * @param {PinchInputOption} [options] The option object of the eg.Axes.PinchInput moduleeg.Axes.PinchInput 모듈의 옵션 객체 */ export class PinchInput implements InputType { public options: PinchInputOption; public axes: string[] = []; public element: HTMLElement = null; private _observer: InputTypeObserver; private _pinchFlag = false; private _enabled = false; private _originalCssProps: { [key: string]: string }; private _activeEvent: ActiveEvent = null; private _baseValue: number; private _isOverThreshold = false; /** * */ public constructor(el: ElementType, options?: PinchInputOption) { this.element = $(el); this.options = { scale: 1, threshold: 0, inputType: ["touch", "pointer"], touchAction: "none", ...options, }; this._onPinchStart = this._onPinchStart.bind(this); this._onPinchMove = this._onPinchMove.bind(this); this._onPinchEnd = this._onPinchEnd.bind(this); } public mapAxes(axes: string[]) { this.axes = axes; } public connect(observer: InputTypeObserver): InputType { if (this._activeEvent) { this._detachEvent(); } this._attachEvent(observer); this._originalCssProps = setCssProps( this.element, this.options, DIRECTION_ALL ); return this; } public disconnect() { this._detachEvent(); if (!isCssPropsFromAxes(this._originalCssProps)) { revertCssProps(this.element, this._originalCssProps); } return this; } /** * Destroys elements, properties, and events used in a module. * @ko 모듈에 사용한 엘리먼트와 속성, 이벤트를 해제한다. */ public destroy() { this.disconnect(); this.element = null; } /** * Enables input devices * @ko 입력 장치를 사용할 수 있게 한다 * @return {PinchInput} An instance of a module itself 모듈 자신의 인스턴스 */ public enable() { this._enabled = true; return this; } /** * Disables input devices * @ko 입력 장치를 사용할 수 없게 한다. * @return {PinchInput} An instance of a module itself 모듈 자신의 인스턴스 */ public disable() { this._enabled = false; return this; } /** * Returns whether to use an input device * @ko 입력 장치를 사용 여부를 반환한다. * @return {Boolean} Whether to use an input device 입력장치 사용여부 */ public isEnabled() { return this._enabled; } private _onPinchStart(event: InputEventType) { const activeEvent = this._activeEvent; const pinchEvent = activeEvent.onEventStart(event); if (!pinchEvent || !this._enabled || activeEvent.getTouches(event) !== 2) { return; } this._baseValue = this._observer.get(this)[this.axes[0]]; this._observer.hold(this, event); this._pinchFlag = true; this._isOverThreshold = false; activeEvent.prevEvent = pinchEvent; } private _onPinchMove(event: InputEventType) { const threshold = this.options.threshold; const activeEvent = this._activeEvent; const pinchEvent = activeEvent.onEventMove(event); if ( !pinchEvent || !this._pinchFlag || !this._enabled || activeEvent.getTouches(event) !== 2 ) { return; } const distance = this._getDistance(pinchEvent.scale); const offset = this._getOffset( pinchEvent.scale, activeEvent.prevEvent.scale ); if (this._isOverThreshold || distance >= threshold) { this._isOverThreshold = true; this._observer.change(this, event, toAxis(this.axes, [offset])); } activeEvent.prevEvent = pinchEvent; } private _onPinchEnd(event: InputEventType) { const activeEvent = this._activeEvent; activeEvent.onEventEnd(event); if ( !this._pinchFlag || !this._enabled || activeEvent.getTouches(event) >= 2 ) { return; } activeEvent.onRelease(); this._observer.release(this, event, [0], 0); this._baseValue = null; this._pinchFlag = false; } private _attachEvent(observer: InputTypeObserver) { const activeEvent = convertInputType(this.options.inputType); const element = this.element; if (!activeEvent) { return; } if (!element) { throw new Error("Element to connect input does not exist."); } this._observer = observer; this._enabled = true; this._activeEvent = activeEvent; activeEvent.start.forEach((event) => { element.addEventListener(event, this._onPinchStart, false); }); activeEvent.move.forEach((event) => { element.addEventListener(event, this._onPinchMove, false); }); activeEvent.end.forEach((event) => { element.addEventListener(event, this._onPinchEnd, false); }); } private _detachEvent() { const activeEvent = this._activeEvent; const element = this.element; if (element) { activeEvent?.start.forEach((event) => { element.removeEventListener(event, this._onPinchStart, false); }); activeEvent?.move.forEach((event) => { element.removeEventListener(event, this._onPinchMove, false); }); activeEvent?.end.forEach((event) => { element.removeEventListener(event, this._onPinchEnd, false); }); } this._enabled = false; this._observer = null; } private _getOffset(pinchScale: number, prev: number = 1): number { return this._baseValue * (pinchScale - prev) * this.options.scale; } private _getDistance(pinchScale: number): number { return Math.abs(pinchScale - 1); } }