Source

client/core/modules/screen.js

/**
 * 
 * ## A simple screen size manager
 * 
 * - keeps track of screen size and ratio changes
 * - helper to compute a component dimensions based on other components: getHeightMinus(...), getWidthMinus(...)
 * - observe screen size changes and publish them to the PubSub on the EVT_WINDOW_RESIZED channel
 * - observe when container components (Block, Panel) are resized to propagate the change on children
 * 
 * @namespace
 */
kiss.screen = {
	isMobile: false,

	/**
	 * Check if a screen is horizontal (= landscape)
	 * 
	 * @returns {boolean}
	 */
	isHorizontal: () => kiss.screen.current.width > kiss.screen.current.height,

	/**
	 * Check if a screen is vertical (= portrait)
	 * 
	 * @returns {boolean}
	 */
	isVertical: () => kiss.screen.current.width < kiss.screen.current.height,

	/**
	 * Check if a screen is a touch screen
	 * 
	 * @returns {boolean}
	 */
	isTouch: () => {
		try {
			const hasTouchEvents = "ontouchstart" in window
			const hasTrueTouchPoints = navigator.maxTouchPoints > 0 && matchMedia("(pointer: coarse)").matches

			return hasTouchEvents || hasTrueTouchPoints
		} catch {
			return false
		}
	},

	/**
	 * Store the last click position on the screen
	 */
	lastClick: {
		x: 0,
		y: 0
	},

	/**
	 * Previous dimensions and ratio
	 * 
	 * @example
	 * kiss.screen.previous.height
	 * kiss.screen.previous.ratio
	 */
	previous: {
		width: window.innerWidth,
		height: window.innerHeight,
		ratio: window.innerWidth / window.innerHeight
	},

	/**
	 * Current dimensions and ratio
	 * 
	 * @example
	 * kiss.screen.current.width
	 * kiss.screen.current.ratio
	 */
	current: {
		width: window.innerWidth,
		height: window.innerHeight,
		ratio: window.innerWidth / window.innerHeight
	},

	/**
	 * Init the screen size observer
	 */
	init() {
		// Startup test to check if it's a mobile environment
		if (kiss.tools.isMobile()) kiss.screen.isMobile = true

		// Init screen size listener
		kiss.screen.observe()
	},

	/**
	 * Update the current screen size and cache the previous one
	 * 
	 * @private
	 * @ignore
	 */
	_update() {
		kiss.screen.previous = kiss.screen.current
		kiss.screen.current = {
			width: window.innerWidth,
			height: window.innerHeight,
			ratio: window.innerWidth / window.innerHeight
		}
	},

	/**
	 * Compute the delta between previous and new screen size
	 * 
	 * @private
	 * @ignore
	 * @returns {object} For example: {width: 100, height: 50}
	 */
	_delta() {
		let deltaWidth = kiss.screen.current.width - kiss.screen.previous.width
		let deltaHeight = kiss.screen.current.height - kiss.screen.previous.height
		let delta = {
			width: deltaWidth,
			height: deltaHeight
		}
		return delta
	},

	/**
	 * Compute the remaining window's height in pixels (= window's height minus a computed delta)
	 * 
	 * @param {...*} something - Either a number, or a CSS height in pixels, or an array of ids of the items to consider while computing the remaining height
	 * @returns {number} The remaining height, in pixels
	 * 
	 * @example
	 * // With number:
	 * kiss.screen.getHeightMinus(60)
	 * 
	 * // With a CSS size:
	 * kiss.screen.getHeightMinus("60px")
	 * 
	 * // With a component id:
	 * kiss.screen.getHeightMinus("top-bar")
	 * 
	 * // With multiple component ids:
	 * kiss.screen.getHeightMinus("top-bar", "button-bar")
	 * 
	 * // With multiple numbers:
	 * kiss.screen.getHeightMinus(60, 20, $("top-bar").offsetHeight)
	 * 
	 * // Mixed stuff:
	 * kiss.screen.getHeightMinus("top-bar", 20, "60px")
	 */
	getHeightMinus(...something) {
		let delta = 0
		something.forEach(function (item) {
			// Item given as a number: 60
			if (typeof item == "number") {
				delta += item
			} else {
				// Item given as CSS size: "60px"
				if (item.indexOf("px") != -1) {
					delta += Number(item.substring(0, item.indexOf("px")))
				}
				// Item given as a DOM Element id
				else {
					let node = $(item)
					if (node) {
						if (!node.offsetHeight) node = node.firstElementChild
						delta += node.offsetHeight
					}
				}
			}
		})

		return (window.innerHeight - delta)
	},

	/**
	 * Compute the remaining window's width in pixels (= window's width minus a computed delta)
	 * 
	 * @param {...*} something - Either a number, or a CSS height in pixels, or an array of ids of the items to consider while computing the remaining width
	 * @returns {number} The remaining height, in pixels
	 * 
	 * @example
	 * // With number:
	 * kiss.screen.getWidthMinus(60)
	 * 
	 * // With a CSS size:
	 * kiss.screen.getWidthMinus("60px")
	 * 
	 * // With a component id:
	 * kiss.screen.getWidthMinus("left-nav")
	 * 
	 * // With multiple component ids:
	 * kiss.screen.getWidthMinus("left-nav", "left-nav-margin")
	 * 
	 * // With multiple numbers:
	 * kiss.screen.getWidthMinus(60, 20, $("left-nav").offsetWidth)
	 * 
	 * // Mixed stuff:
	 * kiss.screen.getWidthMinus("left-nav", 20, "60px")
	 */
	getWidthMinus(...something) {
		let delta = 0
		something.forEach(function (item) {
			// Item given as a number: 60
			if (typeof item == "number") {
				delta += item
			} else {
				// Item given as CSS size: "60px"
				if (item.indexOf("px") != -1) {
					delta += Number(item.substring(0, item.indexOf("px")))
				}
				// Item given as a DOM Element id
				else {
					let node = $(item)
					if (node) {
						if (!node.offsetWidth) node = node.firstElementChild
						delta += node.offsetWidth
					}
				}
			}
		})

		return (window.innerWidth - delta)
	},

	/**
	 * Get the screen orientation
	 * 
	 * @returns {string} "vertical" or "horizontal"
	 */
	getOrientation() {
		return (kiss.screen.current.height > kiss.screen.current.width) ? "vertical" : "horizontal"
	},

	/**
	 * Debounce a function which occurs at a high frequency, and force it to occur at a specific interval (in milliseconds)
	 * 
	 * @private
	 * @ignore
	 * @param {integer} interval - Interval in milliseconds used to call the debounced function
	 * @param {function} fn - The function to debounce
	 * @returns {function} A function calling the function to debounce, passing it the {event} that occured.
	 * 
	 * @example
	 * window.addEventListener("resize", kiss.screen._debounce(function(event) { console.log(event) })
	 */
	_debounce(interval, fn) {
		let timer
		return function (event) {
			if (timer) clearTimeout(timer)
			timer = setTimeout(fn, interval, event)
		}
	},

	/**
	 * Observe the window resize event, and publish the changes in the EVT_WINDOW_RESIZED pubsub channel.
	 * 
	 * @example
	 * kiss.screen.observe()
	 */
	observe() {
		window.addEventListener("resize", kiss.screen._debounce(100, function () {
			kiss.screen._update()
			kiss.pubsub.publish("EVT_WINDOW_RESIZED", {
				previous: kiss.screen.previous,
				current: kiss.screen.current,
				delta: kiss.screen._delta()
			})
		}))

		// Observe the mouse position
		window.addEventListener("mousemove", (evt) => {
			kiss.screen.mousePosition = {
				x: evt.clientX,
				y: evt.clientY
			}
		})
	},

	/**
	 * Observe when container components (Block, Panel) are resized.
	 * Propagate the event EVT_CONTAINERS_RESIZED with the list of ids of the resized containers
	 * 
	 * @ignore
	 * @param {function} callback to execute when the observer is triggered
	 */
	getResizeObserver() {
		if (kiss.screen.resize) return kiss.screen.resize

		kiss.screen.resize = new ResizeObserver(kiss.screen._debounce(100, function (entries) {
			let elements = Array.from(entries)
				.filter(entry => entry.borderBoxSize[0].blockSize != 0)
				.map(entry => entry.target.id)

			// Propagate the list of container ids
			if (elements.length > 0) kiss.pubsub.publish("EVT_CONTAINERS_RESIZED", elements)
		}))

		return kiss.screen.resize
	}
}