Last active
January 7, 2023 00:56
-
-
Save enwin/589ac5ee48b733f4bcd83af99fd9e5c4 to your computer and use it in GitHub Desktop.
Class to watch UI changes: width, height, scrollY, breakpoint based on the passed layout, page load, inputType
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default [ | |
['small', `(min-width: ${576 / 16}rem)`], | |
['medium', `(min-width: ${768 / 16}rem)`], | |
['large', `(min-width: ${992 / 16}rem)`], | |
['xlarge', `(min-width: ${1200 / 16}rem)`], | |
['2xlarge', `(min-width: ${1400 / 16}rem)`], | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class UI { | |
constructor(layouts) { | |
this.properties = { | |
breakpoint: '', | |
height: undefined, | |
inputType: '', | |
loaded: false, | |
scrollY: 0, | |
width: undefined, | |
}; | |
this.layouts = layouts; | |
this.tick = {}; | |
this.observers = {}; | |
// listen to browser resize | |
window.addEventListener('resize', () => { | |
this.resized(); | |
}); | |
// listen to page scroll | |
window.addEventListener( | |
'scroll', | |
() => { | |
this.scrolled(); | |
}, | |
{ | |
passive: true, | |
} | |
); | |
// listen to pointer to detect current use of the page | |
if (window.PointerEvent) { | |
window.addEventListener( | |
'pointerdown', | |
(event) => { | |
this.detectInputType(event); | |
}, | |
{ | |
passive: true, | |
} | |
); | |
window.addEventListener( | |
'pointermove', | |
(event) => { | |
this.detectInputType(event); | |
}, | |
{ | |
passive: true, | |
} | |
); | |
// fallback for old browsers | |
} else { | |
window.addEventListener( | |
'mousemove', | |
(event) => { | |
this.detectInputTypeFallback(event); | |
}, | |
{ | |
passive: true, | |
} | |
); | |
window.addEventListener( | |
'touchstart', | |
(event) => { | |
this.detectInputTypeFallback(event); | |
}, | |
{ | |
passive: true, | |
} | |
); | |
} | |
// listen to keyboard to detect current use of the page | |
window.addEventListener( | |
'keydown', | |
(event) => { | |
this.handleKeys(event); | |
}, | |
{ | |
passive: true, | |
} | |
); | |
// listen to page load event | |
if (document.readyState === 'complete') { | |
this.handleLoad(); | |
} else { | |
window.addEventListener( | |
'load', | |
() => { | |
this.handleLoad(); | |
}, | |
{ once: true } | |
); | |
} | |
// register breakpoints | |
this.breakpoints = this.layouts | |
.map(([name, mqString]) => { | |
const mqBreakpoint = window.matchMedia(mqString); | |
mqBreakpoint.name = name; | |
if ('addEventListener' in mqBreakpoint) { | |
mqBreakpoint.addEventListener('change', () => this.matched()); | |
} else { | |
// old listener syntax | |
mqBreakpoint.addListener(() => this.matched()); | |
} | |
return mqBreakpoint; | |
}) | |
.reverse(); | |
// trigger the callbacks on setup | |
this.matched(); | |
this.resized(); | |
this.scrolled(); | |
} | |
detectInputType(event) { | |
this.inputType = event.pointerType; | |
} | |
detectInputTypeFallback(event) { | |
this.inputType = event.type === 'mousemove' ? 'mouse' : 'touch'; | |
} | |
handleKeys() { | |
this.inputType = 'keyboard'; | |
} | |
handleLoad() { | |
this.loaded = true; | |
} | |
/** | |
* loop over the breakpoints to get which one matches or fallback to the 'root' breakpoint | |
*/ | |
matched() { | |
const matchingMql = this.breakpoints.find((mql) => mql.matches); | |
const name = matchingMql ? matchingMql.name : 'root'; | |
this.breakpoint = name; | |
} | |
/** | |
* method to listen for property change | |
* @param {string} property width | height | scrollY | breakpoint | inputType | loaded | |
* @param {function} callback function to call when the property changes | |
* @param {boolean} immediate call the callback with the current value of the property | |
* @returns function to call to stop listening to the property change | |
*/ | |
observe(property, callback, immediate = false) { | |
// skip unobservable properties | |
if (!this.properties[property]) { | |
return; | |
} | |
// create the property observer list if it doesnt exist | |
if (!this.observers[property]) { | |
this.observers[property] = []; | |
} | |
// add the callback to the propery observer list | |
const length = this.observers[property].push(callback); | |
// create the function to use to stop listening to the property | |
const unobserver = () => { | |
this.observers[property].splice(length - 1, 1); | |
}; | |
// trigger the callback with the current value | |
if (immediate && this.properties[property]) { | |
callback(this.properties[property]); | |
} | |
return unobserver; | |
} | |
resized() { | |
this.height = window.innerHeight; | |
this.width = window.innerWidth; | |
} | |
scrolled() { | |
this.scrollY = window.scrollY; | |
} | |
/** | |
* trigger the callbacks for a given property | |
* @param {string} property property to trigger | |
* @param {string|number} value value to send to the callback | |
*/ | |
signal(property, value) { | |
// skip unobserved properties | |
if (!this.observers[property] || !this.observers[property].length) { | |
return; | |
} | |
// only trigger one callback per animation frame | |
if (this.tick[property]) { | |
window.cancelAnimationFrame(this.tick[property]); | |
} | |
// request the next frame to trigger the callbacks | |
this.tick[property] = window.requestAnimationFrame(() => { | |
if (this.observers[property]) { | |
this.observers[property].forEach((callback) => callback(value)); | |
} | |
this.tick[property] = null; | |
}); | |
} | |
set breakpoint(value) { | |
if (value === this.properties.breakpoint) { | |
return; | |
} | |
this.properties.breakpoint = value; | |
this.signal('breakpoint', value); | |
} | |
get breakpoint() { | |
return this.properties.breakpoint; | |
} | |
set height(value) { | |
if (value === this.properties.height) { | |
return; | |
} | |
this.properties.height = value; | |
this.signal('height', value); | |
} | |
get height() { | |
return this.properties.height; | |
} | |
set loaded(value) { | |
if (value === this.properties.loaded) { | |
return; | |
} | |
this.properties.loaded = value; | |
this.signal('loaded', value); | |
} | |
get loaded() { | |
return this.properties.loaded; | |
} | |
set inputType(value) { | |
if (value === this.properties.inputType) { | |
return; | |
} | |
this.properties.inputType = value; | |
// set the custom data-input attribute so CSS can use it | |
document.documentElement.setAttribute('data-input', value); | |
this.signal('inputType', value); | |
} | |
get inputType() { | |
return this.properties.inputType; | |
} | |
set scrollY(value) { | |
if (value === this.properties.scrollY) { | |
return; | |
} | |
this.properties.scrollY = value; | |
this.signal('scrollY', value); | |
} | |
get scrollY() { | |
return this.properties.scrollY; | |
} | |
set width(value) { | |
if (value === this.properties.width) { | |
return; | |
} | |
this.properties.width = value; | |
this.signal('width', value); | |
} | |
get width() { | |
return this.properties.width; | |
} | |
} | |
export default UI; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UI from './ui.js'; | |
import layouts from './layouts.js'; | |
export default new UI(layouts); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Now any script can do: