Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
@skava/state / src / application.ts
Size: Mime:
/**
 * @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