core/modules/resizer.js

import eventBus from '@/core/modules/eventBus'
import raf from 'raf'
import * as R from 'ramda'

/**
 * @namespace resizer
 */

export const resizer = (function() {
	// get the :after style from the body
	const getStyle = () => window.getComputedStyle(document.body, ':after')

	// get the content prop and give it a clean
	const query = R.compose(R.replace(/'|"/g, ''), item =>
		item.getPropertyValue('content')
	)

	// compose the functions together to return the current breakpoint
	const getCurrentMediaQuery = R.compose(query, getStyle)

	const windowMatch = breakpoint => window.matchMedia(breakpoint).matches

	/**
	 * @memberof resizer
	 * @function getWindowProps
	 * @return {Object} with the current width/height/breakpoint
	 */
	function getWindowProps() {
		const width = window.innerWidth
		const height = window.innerHeight
		const query = getCurrentMediaQuery()

		return {
			width,
			height,
			query
		}
	}

	/**
	 * @memberof resizer
	 * @function mapEventsToResize
	 * @param {Array} events - array of functions to test/trigger
	 * @return {Object} with the current width/height/breakpoint
	 */
	function mapEventsToResize(events) {
		return R.compose(
			R.map(([breakpoint, fn]) => {
				// <REFACTOR></REFACTOR> me thinks this could be written in
				// a way that it can be defined outside of the loop
				const once = (arg, fn) => {
					if (once.value === arg) return
					fn({ match: arg, ...getWindowProps() })
					once.value = arg
				}

				// <REFACTOR></REFACTOR> me thinks this could be written in
				// a way that it can be defined outside of the loop
				const test = (breakpoint, fn) => {
					const state = windowMatch(breakpoint)
					once(state, fn)
				}

				const matchQueryTest = test.bind(null, breakpoint, fn)
				// if (windowMatch(breakpoint)) matchQueryTest()
				eventBus.on('window:resize', matchQueryTest)

				return matchQueryTest
			}),
			Object.entries
		)(events)
	}

	// setup a handle reference
	let handle

	// reference the current, will become to previous one
	let last = getCurrentMediaQuery()

	/**
	 * @description The throttled window resize event
	 * @memberof resizer
	 * @function windowResizeEvent
	 *
	 * @return {void}
	 */
	function windowResizeEvent() {
		const { width, height, query } = getWindowProps()

		eventBus.emit('window:resize', {
			width,
			height,
			query
		})

		if (query !== last) {
			last = query
			eventBus.emit('window:breakpoint', {
				width,
				height,
				query
			})
		}
	}

	/**
	 * @description add the window event - return a destroy method
	 * @memberof resizer
	 * @function addWindowResizeEvent
	 *
	 * @return {Object}
	 */
	function addWindowResizeEvent() {
		if (addWindowResizeEvent.isRunning) return
		addWindowResizeEvent.isRunning = true
		handle = raf.bind(null, windowResizeEvent)

		window.addEventListener('resize', handle, false)

		return {
			destroy() {
				if (!addWindowResizeEvent.isRunning) return
				addWindowResizeEvent.isRunning = false
				window.removeEventListener('resize', handle, false)
				handle.cancel()
			}
		}
	}

	addWindowResizeEvent.isRunning = false

	return (events = {}) => {
		addWindowResizeEvent()

		// store the current events
		let bank = mapEventsToResize(events)

		return {
			get breakpoint() {
				return getCurrentMediaQuery()
			},

			get width() {
				return window.innerWidth
			},

			get height() {
				return window.innerHeight
			},

			destroy() {
				bank.forEach(fn => eventBus.off('window:resize', fn))
				bank = []
			},

			...eventBus
		}
	}
})()

/**
 * Create a router
 * @memberof Behaviour
 * @mixin ScreenMixin
 * @description class used to handle window resize events
 * @example
 * import ScreenMixin, { mix } from '@/core/ScreenMixin'
 * import {
 * 	ScreenMixin,
 * } from '@/core/modules/'
 *
 * export default class ExampleWithAllTheThings extends mix(Behaviour).with(
 * 	ScreenMixin
 * ) {
 * 	mount = () => {
 * 		this.$$screen.on('window:resize', () => {})
 * 	}
 *
 *	screens = {
 *		'(min-width: 1024px)': ({ match, ...rest }) => {
 *			if (match) {
 *				log(rest)
 *			}
 *		}
 *	}
 * }
 * @return {RefsMixin}
 */
export const ScreenMixin = superclass =>
	class extends superclass {
		init() {
			this.$$screen = resizer(this.screens)
			if (super.init) super.init()
			return this
		}

		destroy() {
			this.$$screen.destroy()
			if (super.destroy) super.destroy()
		}
	}