app/javascript/js/controllers/sidebar_controller.js

Summary

Maintainability
A
1 hr
Test Coverage
import { Controller } from '@hotwired/stimulus'
import { enter, leave, toggle } from 'el-transition'
import Cookies from 'js-cookie'

// Detect whether an element is in view inside a parent element.
// Original here: https://gist.github.com/jjmu15/8646226
function isInViewport(element, parentElement) {
  const rect = element.getBoundingClientRect()
  const html = document.documentElement
  const parent = parentElement.getBoundingClientRect()

  return (
    rect.top >= 0
    && rect.left >= 0
    && rect.bottom <= (parent.height || window.innerHeight || html.clientHeight)
    && rect.right <= (parent.width || window.innerWidth || html.clientWidth)
  )
}

// Used on initial page load to scroll to the first active sidebar item if it's not in view.
function scrollSidebarMenuItemIntoView() {
  const activeSidebarItem = document.querySelector('.avo-sidebar .mac-styled-scrollbar a.active')
  const sidebarScrollingArea = document.querySelector('.avo-sidebar .mac-styled-scrollbar')
  if (activeSidebarItem && !isInViewport(activeSidebarItem, sidebarScrollingArea)) {
    activeSidebarItem.scrollIntoView({ block: 'end', inline: 'nearest' })
  }
}

export default class extends Controller {
  static targets = ['sidebar', 'mobileSidebar', 'mainArea']

  static values = {
    open: Boolean,
  }

  get cookieKey() {
    return `${window.Avo.configuration.cookies_key}.sidebar.open`
  }

  get sidebarOpen() {
    return Cookies.get(this.cookieKey) === '1'
  }

  get sidebarScrollPosition() {
    return window.Avo.localStorage.get('sidebar.sidebarScrollPosition')
  }

  set sidebarScrollPosition(value) {
    window.Avo.localStorage.set('sidebar.sidebarScrollPosition', value)
  }

  set cookie(state) {
    Cookies.set(this.cookieKey, state === true ? 1 : 0)
  }

  connect() {
    this.attachScrollVisibilityAnchor()

    // Restore sidebar scroll position
    if (this.sidebarScrollPosition && window.Avo.configuration.preserve_sidebar_scroll) {
      document.querySelector('.avo-sidebar .mac-styled-scrollbar').scrollTo({
        top: this.sidebarScrollPosition,
        behavior: 'instant',
      })
    }
    this.rememberScrollPosition()
  }

  rememberScrollPosition() {
    let handler

    document.addEventListener('turbo:visit', handler = () => {
      // Remeber sidebar scroll position before changing pages.
      this.sidebarScrollPosition = document.querySelector('.avo-sidebar .mac-styled-scrollbar').scrollTop
      // remove event handler after disconnection
      document.removeEventListener('turbo:visit', handler)
    })
  }

  attachScrollVisibilityAnchor() {
    if (window.Avo.configuration.focus_sidebar_menu_item) {
      scrollSidebarMenuItemIntoView()
    }
  }

  markSidebarClosed() {
    Cookies.set(this.cookieKey, '0')
    this.openValue = false
    leave(this.sidebarTarget)
    this.mainAreaTarget.classList.remove('sidebar-open')
  }

  markSidebarOpen() {
    Cookies.set(this.cookieKey, '1')
    this.openValue = true
    enter(this.sidebarTarget)
    this.mainAreaTarget.classList.add('sidebar-open')
  }

  toggleSidebar() {
    if (this.openValue) {
      this.markSidebarClosed()
    } else {
      this.markSidebarOpen()
    }
  }

  toggleSidebarOnMobile() {
    toggle(this.mobileSidebarTarget)
  }
}