Repository URL to install this package:
|
Version:
3.0.2 ▾
|
/**
* @todo isAtBottom & isAtTop & isBottomVisible
* @todo onKeyDown?
* @todo all click boundaries re-use this?
*/
/**
* @see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/13679
* @see https://github.com/Microsoft/TypeScript/issues/5073
*/
import debounce from 'lodash/debounce'
import { fromMapToObj, fromArgumentsToArray, isArray } from 'exotic'
import { observable, computed, action, observe } from 'xmobx/mobx'
import { isTouchDevice, isEdgeOrInternetExplorer } from './deps'
import {
ApplicationTypes,
CurriedSet,
ApplicationScreen,
ApplicationScroll,
ApplicationSnackbar,
} from './typings'
const IS_BROWSER = typeof window === 'object'
/**
* @todo method like @connectTo('prop1', 'prop2')
* @todo debounce with requestAnimationFrame
*/
class ApplicationStore implements ApplicationTypes {
store = observable.map()
snackbar: ApplicationSnackbar = {}
@observable.ref
prev = {
screen: {
height: 0,
width: 0,
},
}
// struct makes sure observer won't be signaled unless diff is large...
// @observable.struct
@observable
screen: ApplicationScreen = {
height: IS_BROWSER ? window.innerHeight : 0,
width: IS_BROWSER ? window.innerWidth : 0,
availHeight: IS_BROWSER ? window.screen.availHeight : 0,
availWidth: IS_BROWSER ? window.screen.availWidth : 0,
isGrowing: undefined,
isShrinking: undefined,
}
// @observable.struct
@observable
scroll: ApplicationScroll = {
vertical: IS_BROWSER ? window.scrollY : 0,
horizontal: IS_BROWSER ? window.scrollX : 0,
direction: 'none',
}
@observable
hasError = false
@observable
orientation = 'portrait'
// does not need to be observable or computed - does not change
isTouchDevice = IS_BROWSER && isTouchDevice()
isModernBrowser = !isEdgeOrInternetExplorer()
isOldBadBrowser = isEdgeOrInternetExplorer()
/**
* @note - was this - but now is observe direct since we have typings
* observe(properties: string | Array<string>, subscriber: Function) {
* return observe(this, properties, subscriber)
* }
*/
observe = observe.bind(undefined, this)
// =========
// to always trigger
get isLoading() {
return this.store.get('isLoading')
}
set isLoading(isLoading: boolean) {
this.store.set('isLoading', isLoading)
}
get isLoadingBig(): boolean {
return this.isLoading && this.store.get('isLoadingBig')
}
@action
showLoadingGauge(isLoading: boolean, duration = 3500) {
this.setIsLoading(true)
setTimeout(() => this.setIsLoading(false), duration)
return this
}
@action
setIsLoadingBig(isLoading: boolean) {
this.setIsLoading(isLoading)
this.store.set('isLoadingBig', isLoading)
return this
}
@action
setLoadingTimer(isLoading: boolean = true, duration = 3500) {
this.setIsLoading(isLoading)
this.store.set('isLoadingBig', isLoading)
setTimeout(() => this.setIsLoadingBig(false), duration)
return this
}
@action
setIsLoading(isLoading: boolean) {
if (isLoading !== this.isLoading) {
console.log('setIsLoading', isLoading)
this.isLoading = isLoading
}
return this
}
// =========
@action
setError(message: string, meta = Object) {
const error = { message, meta }
this.store.set('error', error)
return this
}
@action
snack(message: string, meta: Object) {
this.snackbar.message = message
this.snackbar.meta = meta
return this
}
/**
* @todo @name isTheatre
* (small, medium, large) - 2200, 2400, 6000
*
* @todo @name isDesktop
* (small, medium, large) - 1440, 1600, 1920
*/
@computed
get isSuperSize() {
return this.screen.width >= 1200
}
/**
* @todo @name isLaptop
* (small, medium, large) - 1024, 1280, 1366
*/
@computed
get isDesktop() {
return this.screen.width > 1024
}
// get isMediumDesktop()
// get isLargeDesktop()
// get isSmallDesktop()
// get isSmallNotebook()
// get isMediumNotebook()
// get isLargeNotebook()
// get isLargeTablet()
// get isSmallTablet()
// get isMediumTablet()
// get isTablet()
// we aren't really checking tablet-desktop < -.-
@computed
get isTablet() {
return this.screen.width >= 768 && this.screen.width <= 1024
}
@computed
get isTabletOrLarger() {
// return this.isTablet || this.isPhone
return this.screen.width <= 1024
}
@computed
get isTabletOrSmaller() {
return this.isTablet || this.isPhone
}
@computed
get isMobile() {
return this.isPhone
}
@computed
get isMobileOrLarger() {
return this.isPhone
}
@computed
get isPhone() {
return this.screen.width < 768
}
@computed
get isDesktopOrLarger() {
return this.screen.width >= 1024
}
@computed
get deviceType() {
return this.isDesktop ? 'desktop' : this.isTablet ? 'tablet' : 'mobile'
}
/**
* @private
*/
@action.bound
handleResize(event: UIEvent) {
if (this.isTouchDevice) {
console.info('[state:application] handleResize ... LIAR')
return
}
this.updateDimensions()
}
/**
* @private
*/
@action
updateDimensions() {
// if we trigger this like this, it will always trigger an update
// since we already debounce, struct is not 100% needed
this.prev = {
screen: {
height: this.screen.height,
width: this.screen.width,
},
}
this.screen = {
height: window.innerHeight,
width: window.innerWidth,
availHeight: window.screen.availHeight,
availWidth: window.screen.availWidth,
// clientWidth: document.body.clientWidth,
}
}
/**
* @private
*/
@action.bound
handleScroll(event: Event) {
this.updateScrollPosition(event)
}
@action.bound
handleOrientation(event?: Event) {
this.updateDimensions()
}
/**
* @private
*/
@action
updateScrollPosition(event: Event) {
// minMovementInPixels
const minimum = 100
// @todo - react event?
const scrollTop = (event.srcElement as any).body.scrollTop || window.scrollY
const scrollSide = window.scrollX
/**
* higher value = lower down the page
*/
const scrollDirection = this.scroll.vertical < scrollTop ? 'down' : 'up'
// or in reverse
const verticalDifference = Math.abs(this.scroll.vertical - scrollTop)
// ignoring horizontal for now
// const horizontalDifference = Math.abs(this.scroll.horizontal - scrollSide)
// minimum < horizontalDifference &&
if (scrollDirection === this.scroll.direction) {
if (minimum < verticalDifference) {
// console.debug('application_scroll_ignored')
// ignore - we barely scrolled, and in the same direction
return
}
}
// console.info('scrolling', {
// scrollTop,
// scrollDirection,
// verticalDifference,
// })
this.scroll = {
vertical: scrollTop,
horizontal: scrollSide,
direction: scrollDirection,
}
// @example
// if (scrollDirection === 'up') // hide
// else // show
}
@computed
get isScrollingUp(): boolean {
return this.scroll.direction === 'up'
}
@computed
get isScrollingDown(): boolean {
if (this.scroll.vertical >= 50 && this.scroll.direction === 'down') {
return true
} else {
return false
}
}
// =========
@action.bound
toggle(key: string) {
const value = this.store.get(key)
const inverseValue = !value
this.store.set(key, inverseValue)
return this
}
@action.bound
toggleFor(key: string) {
return (event?: any) => this.toggle(key)
}
@action.bound
set(key: string, value?: any): ApplicationStore | CurriedSet {
// make setters easily
if (arguments.length === 1) {
return (lateValue: any): ApplicationStore => {
this.set(key, lateValue)
return this
}
}
// could also Object.define an observable property here...
this.store.set(key, value)
return this
}
/**
* @example isFilterOpen
*/
@action.bound
get(key: string | Array<string>): any {
if (arguments.length > 1) {
return fromArgumentsToArray.apply(undefined, arguments)
}
/**
* @example .get('isFilterOpen', 'isLoading')
* => {isFilterOpen: true, isLoading: false}
*/
if (isArray(key)) {
const obj = {}
const keyList = key as Array<string>
keyList.forEach(name => {
obj[name] = this.store.get(name)
})
return obj
}
return this.store.get(key)
}
// @computed
entries() {
return fromMapToObj(this.store)
}
}
const applicationContainer = new ApplicationStore()
function handleResize(event: UIEvent) {
// console.debug('[application] resize subscribed')
applicationContainer.handleResize(event)
}
function handleScroll(event: Event) {
// console.debug('[application] scroll subscribed')
applicationContainer.handleScroll(event)
}
function handleOrientation(event: Event) {
// console.debug('[application] orientation subscribed')
// screen.orientation.angle
applicationContainer.handleOrientation(event)
}
function subscribe() {
// maybe no need to debounce if we use the struct?
// window.addEventListener('resize', debounce(applicationContainer.handleResize, 60))
// window.addEventListener('scroll', debounce(applicationContainer.handleScroll, 60))
// window.addEventListener('resize', handleResize)
// not debouncing, handling logic
window.addEventListener('scroll', handleScroll)
// https://jira.skava.net/browse/SKREACT-3957
// https://jira.skava.net/browse/SKREACT-3939
if (isTouchDevice() === false) {
window.addEventListener('resize', debounce(handleResize, 60))
}
// window.addEventListener('scroll', debounce(handleScroll, 60))
/**
* @see https://developer.mozilla.org/en-US/docs/Web/Events/orientationchange
*/
if (isTouchDevice()) {
window.addEventListener('orientationchange', handleOrientation)
}
}
if (IS_BROWSER) {
subscribe()
/**
* @note renamed from @name DEVTOOLS_GLOBALS_APPLICATION_ENABELD
*/
if (process.env.DEVTOOLS_GLOBALS_APPLICATION_ENABLED) {
window.application = applicationContainer
}
}
export { ApplicationStore }
export { applicationContainer as application }
export { applicationContainer }
export default applicationContainer