UNPKG

vue-responsiveness

Version:

A tiny, performant, and intuitive Vue 3 plugin for working with responsive breakpoints and media queries at runtime.

151 lines (139 loc) 5.22 kB
import type { VueResponsivenessBreakpoints, VueResponsivenessMatches } from './' import type { App } from 'vue' import { Presets } from './' import { reactive, computed } from 'vue' type Orientation = 'portrait' | 'landscape' type Hover = 'none' | 'hover' type PrefersColorScheme = 'dark' | 'light' type PrefersContrast = 'more' | 'less' | 'custom' | 'no-preference' type PrefersReducedMotion = 'reduce' | 'no-preference' type QueryConfig = | ['orientation', readonly Orientation[]] | ['hover', readonly (Hover)[]] | ['prefers-color-scheme', readonly PrefersColorScheme[], 'colorScheme'] | [ 'prefers-contrast', readonly (PrefersContrast)[], 'contrast' ] | [ 'prefers-reduced-motion', readonly PrefersReducedMotion[], 'reducedMotion' ] let matches: VueResponsivenessMatches export const VueResponsiveness = { install( app: App, breakpoints: VueResponsivenessBreakpoints = Presets.Bootstrap_5 ): void { const intervals = Object.entries(breakpoints) .sort(([, a], [, b]) => Number(a) - Number(b)) .reduce( (out, [key, min], i, arr) => { out[key] = { min: min ? `(min-width: ${min}px)` : '', max: arr[i + 1]?.[1] ? `(max-width: ${(arr[i + 1][1] as number) - 0.1}px)` : '' } return out }, {} as Record<string, { min: string; max: string }> ) matches = reactive({ isMin: computed(() => (key: string) => matches[key]?.min || false), isMax: computed(() => (key: string) => matches[key]?.max || false), isOnly: computed(() => (key: string) => matches[key]?.only || false), current: computed( () => Object.keys(intervals).find((key) => matches[key].only) || '' ), hover: 'hover', orientation: 'landscape' as Orientation, prefers: { colorScheme: 'light' as PrefersColorScheme, contrast: 'no-preference' as PrefersContrast, reducedMotion: 'no-preference' as PrefersReducedMotion }, ...Object.keys(intervals).reduce((acc, key) => { acc[key] = { min: false, max: false, only: false } return acc }, {} as Partial<VueResponsivenessMatches>) }) as VueResponsivenessMatches if (typeof window !== 'undefined') { Object.entries(intervals).forEach(([interval, mediaQueries]) => { const queryLists = { min: window.matchMedia(mediaQueries.min), max: window.matchMedia(mediaQueries.max) } Object.entries(queryLists).forEach(([key, mediaQueryList]) => { const listener = ({ matches: val }: MediaQueryListEvent | MediaQueryList) => { const { min, max } = { ...matches[interval], [key]: val } as { min: boolean; max: boolean } matches[interval] = { min, max, only: min && max } } mediaQueryList.addEventListener('change', listener) listener(mediaQueryList) }) }) const additionalQueries: QueryConfig[] = [ ['orientation', ['portrait', 'landscape']], ['hover', ['none', 'hover']], ['prefers-color-scheme', ['dark', 'light'], 'colorScheme'], [ 'prefers-contrast', ['more', 'less', 'custom', 'no-preference'], 'contrast' ], ['prefers-reduced-motion', ['reduce', 'no-preference'], 'reducedMotion'] ] additionalQueries.forEach(([query, values, path]) => { values.forEach((value) => { const mediaQuery = window.matchMedia(`(${query}: ${value})`) const updateMatches = (l: MediaQueryListEvent | MediaQueryList) => { if (l.matches) { switch (query) { case 'orientation': { matches[query] = value as 'portrait' | 'landscape' break } case 'hover': { matches[query] = value as 'none' | 'hover' break } default: { switch (path) { case 'colorScheme': matches.prefers[path] = value as 'dark' | 'light' break case 'contrast': matches.prefers[path] = value as | 'more' | 'less' | 'custom' | 'no-preference' break case 'reducedMotion': matches.prefers[path] = value as | 'reduce' | 'no-preference' break default: } } } } } mediaQuery.addEventListener('change', updateMatches) updateMatches(mediaQuery) }) }) } app.config.globalProperties.$matches = matches } } export const useMatches = () => matches