Source: rotate.js

/**
 * Copyright (c) 2015 NAVER Corp.
 * egjs projects are licensed under the MIT license
 */
import {window, document} from "./browser";

export default (() => {
	let beforeScreenWidth = -1;
	let beforeVertical = null;
	const USER_LISTENERS = []; // user's event listener

	const agent = (() => {
		const ua = window.navigator.userAgent;
		const match = ua.match(/(iPhone OS|CPU OS|Android)\s([^\s;-]+)/); // fetch Android & iOS env only
		const res = {
			os: "",
			version: "",
			ua
		};

		if (match) {
			res.os = match[1].replace(/(?:CPU|iPhone)\sOS/, "ios").toLowerCase();
			res.version = match[2].replace(/\D/g, ".");
		}

		return res;
	})();

	const isMobile = /android|ios/.test(agent.os) || /Mobi/.test(agent.ua);

	// for non-mobile, will return an empty function methods
	if (!isMobile) {
		const fn = () => false;

		return {
			on: fn,
			off: fn,
			isVertical: fn
		};
	}

	/**
	 * Return event name string for orientationChange according browser support
	 */
	const ORIENTATION_CHANGE_EVENT = (() => {
		let type;

		/**
		 * Some platform/broswer returns previous widht/height state value. For workaround, give some delays.
		 *
		 * Android bug:
		 * - Andorid 2.3 - Has orientationchange with bug. Needs 500ms delay.
		 *
		 *   Note: Samsung's branded Android 2.3
		 *   When check orientationchange using resize event, could cause browser crash if user binds resize event on window
		 *
		 * - Android 2.2 - orientationchange fires twice(at first time width/height are not updated, but second returns well)
		 * - Lower than 2.2 - use resize event
		 *
		 * InApp bug:
		 * - Set 200ms delay
		 */
		if ((agent.os === "android" && agent.version === "2.1")) {
			type = "resize";
		} else {
			type = "onorientationchange" in window ? "orientationchange" : "resize";
		}

		return type;
	})();

	/**
	 * When viewport orientation is portrait, return true otherwise false
	 */
	function isVertical() {
		let screenWidth;
		let degree;
		let vertical;

		if (ORIENTATION_CHANGE_EVENT === "resize") {
			screenWidth = document.documentElement.clientWidth;

			if (beforeScreenWidth === -1) { // first call isVertical
				vertical = screenWidth < document.documentElement.clientHeight;
			} else {
				if (screenWidth < beforeScreenWidth) {
					vertical = true;
				} else if (screenWidth === beforeScreenWidth) {
					vertical = beforeVertical;
				} else {
					vertical = false;
				}
			}
		} else {
			degree = window.orientation;

			if (degree === 0 || degree === 180) {
				vertical = true;
			} else if (degree === 90 || degree === -90) {
				vertical = false;
			}
		}
		return vertical;
	}

	/**
	 * Trigger rotate event
	 */
	function triggerRotate(e) {
		const currentVertical = isVertical();

		if (isMobile) {
			if (beforeVertical !== currentVertical) {
				beforeVertical = currentVertical;
				beforeScreenWidth = document.documentElement.clientWidth;

				USER_LISTENERS.forEach(v => v(e, {
					isVertical: beforeVertical
				}));
			}
		}
	}

	/**
	 * Trigger event handler
	 */
	function handler(e) {
		let rotateTimer = null;

		if (ORIENTATION_CHANGE_EVENT === "resize") {
			window.setTimeout(() => triggerRotate(e), 0);
		} else {
			if (agent.os === "android") {
				const screenWidth = document.documentElement.clientWidth;

				if (e.type === "orientationchange" && screenWidth === beforeScreenWidth) {
					window.setTimeout(() => handler(e), 500);

					// When width value wasn't changed after firing orientationchange, then call handler again after 300ms.
					return false;
				}
			}

			rotateTimer && window.clearTimeout(rotateTimer);
			rotateTimer = window.setTimeout(() => triggerRotate(e), 300);
		}

		return undefined;
	}

	/**
	 * Tiny custom rotate event binder.
	 * > **NOTE:**
	 * > - It works for mobile environment only.
	 * > - For non-mobile environment, every methods will return 'false'.
	 *
	 * > **참고:**
	 * > - 모바일 환경에서만 동작 합니다.
	 * > - 비모바일 환경에서는 모든 메서드들은 'false'를 반환합니다.
	 * @ko 기기 회전에 따른 rotate 커스텀 이벤트 바인더
	 * @namespace eg.rotate
	 * @param {Event} e Native event object<ko>네이티브 이벤트 객체</ko>
	 * @param {Object} info The object of data to be sent when the event is fired<ko>이벤트가 발생할 때 전달되는 데이터 객체</ko>
	 * @param {Boolean} info.isVertical The orientation of the device (true: portrait, false: landscape) <ko>기기의 화면 방향(true: 수직 방향, false: 수평 방향)</ko>
	 * @support { "ios" : "7+", "an" : "2.1+ (except 3.x)"}
	 * @example
	 * var handler = function(e, info){
	 *      info.isVertical;
	 * }
	 * // bind
	 * eg.rotate.on(handler);
	 *
	 * // unbind
	 * eg.rotate.off(handler);
	 *
	 * // unbind all event attached (call without listener param)
	 * eg.rotate.off();
	 */
	return {
		/**
		 * Bind rotate event
		 * @ko rotate 이벤트 바인딩
		 * @memberof eg.rotate
		 * @static
		 * @param {Function} listener listener function <ko>이벤트 핸들러 함수</ko>
		 */
		on(listener) {
			if (typeof(listener) !== "function") {
				return;
			}

			beforeVertical = isVertical();
			beforeScreenWidth = document.documentElement.clientWidth;
			USER_LISTENERS.push(listener);

			// only attach once
			USER_LISTENERS.length === 1 &&
				window.addEventListener(ORIENTATION_CHANGE_EVENT, handler);
		},

		/**
		 * Unbind rotate event
		 * Without param, will unbind all binded listeners
		 * @ko rotate 이벤트 바인딩 해제. 파라미터 없이 호출되는 경우, 바인딩된 모든 이벤트를 해제한다.
		 * @memberof eg.rotate
		 * @static
		 * @param {Function} [listener] listener function <ko>이벤트 핸들러 함수</ko>
		 */
		off(listener) {
			if (typeof(listener) === "function") {
				// remove given listener from list
				for (let i = 0, el; (el = USER_LISTENERS[i]); i++) {
					if (el === listener) {
						USER_LISTENERS.splice(i, 1);
						break;
					}
				}
			}

			// detach when the condition is met
			if (!listener || USER_LISTENERS.length === 0) {
				USER_LISTENERS.splice(0);
				window.removeEventListener(ORIENTATION_CHANGE_EVENT, handler);
			}
		},

		/**
		 * Native event name used to detect rotate
		 * @ko roate 이벤트를 위해 사용된 네이티브 이벤트 명
		 * @memberof eg.rotate
		 * @property {String} event event name <ko>이벤 명</ko>
		 * @private
		 */
		orientationChange: ORIENTATION_CHANGE_EVENT,

		/**
		 * Get device is in vertical mode
		 * @ko 화면이 수직 방향인지 여부
		 * @memberof eg.rotate
		 * @static
		 * @method
		 * @return {Boolean} The orientation of the device (true: portrait, false: landscape) <ko>기기의 화면 방향(true: 수직 방향, false: 수평 방향)</ko>
		 * @example
		 *   eg.rotate.isVertical();  // Check if device is in portrait mode
		 */
		isVertical,

		/**
		 * Trigger rotate event
		 * @memberof eg.rotate
		 * @private
		 */
		triggerRotate,

		/**
		 * Event handler function
		 * @memberof eg.rotate
		 * @private
		 */
		handler
	};
})();
comments powered by Disqus