Source

components/shared/TLayout/TLayout.vue

<template lang="pug">
.layout-wrapper.layout
  .layout(
    :class="{ menu_collapsed: isMenuCollapsed, with_sidebar: isSidebarVisible }" ref="layout")
    .minimum_width 
      b-alert(variant="warning", show, v-text="$t('error.responsive_minimum_width')")
    //.hamburger__wrapper.fixed(
      :class="{ extended: isSidebarExtended }",
      v-show="isSidebarVisible"
      )
      em.fas.fa-bars.text-center.hamburger(
        @click="toggleSidebarExtended(!isSidebarExtended)"
      )
    .sidebar(
      ref="sidebar",
      :class="{ extended: isSidebarExtended }",
      v-show="isSidebarVisible"
    )
      .sidebar__overlay(@click="toggleSidebarExtended(false)")
      .sidebar__inner
        .hamburger__wrapper(
          :class="{ extended: isSidebarExtended }",
          v-show="isSidebarVisible"
          @click="toggleSidebarExtended(!isSidebarExtended)"
        )
          SimplicitiLogo.text-center.hamburger()
        slot(name="sidebar")
          component(:is="getLayoutComponent(['sidebar', 'sidebarComponent'])")
    .menu(
      ref="menu",
      v-show="isMenuVisible",
      :class="{ full_collapse: willMenuCollapseCompletely }"
    )
      .menu_inner
        slot(name="menu")
          component(:is="getLayoutComponent(['menu', 'menuComponent'])")
    .main(:style="mainStyle()", ref="main")
      .toggle_menu(v-if="isMenuVisible&&isMenuToggleVisible")
          button(@click="toggleMenuCollapsed(!isMenuCollapsed)")
            em.fas.fa-chevron-right(v-show="isMenuCollapsed")
            em.fas.fa-chevron-left(v-show="!isMenuCollapsed")
      .floating_layout(
        :class="{ has_inner: isRightMenuVisible || isWideMenuVisible }",
        v-show="isSubMenuVisible||isRightMenuVisible||isWideMenuVisible"
      )
        .fl__inner(
          v-show="isSubMenuVisible||isRightMenuVisible||isWideMenuVisible"
        )
          .sub_menu(v-if="isSubMenuVisible")
            .sub_menu__header
              button.close_btn(@click="toggleSubMenu(false)")
                em.fas.fa-times
            .sub_menu__content
              slot(name="sub_menu")
                component(
                  :is="getLayoutComponent(['sub_menu', 'subMenu', 'subMenuComponent'])"
                )
          .wide_menu_overlay(v-if="isWideMenuVisible" :style="mainStyle()")
            .wide_menu
              button.close_btn(@click="toggleWideMenu(false)")
                em.fas.fa-times 
              .wide_menu__inner
                slot(name="wide_menu")
                  component(
                    :is="getLayoutComponent(['wide_menu', 'wideMenuComponent'])"
                  )
          .fl__right(v-if="isRightMenuVisible")
            .fl__right__header(v-if="isRightMenuCloseButtonVisible")
              button.close_btn(@click="toggleRightMenu(false)")
                em.fas.fa-times
            .fl__right__inner(
              :class="{ has_bottom: isRightMenuBottomVisible }"
            ) 
              .top.right_menu__top
                slot(name="right_menu")
                  component.right_menu__top__component(
                    :is="getLayoutComponent(['right_menu', 'right_menu_top', 'rightMenuTop', 'rightMenuTopComponent'])"
                  )
              .bottom.right_menu__bottom(v-if="isRightMenuBottomVisible")
                slot(name="right_menu_bottom")
                  component(
                    :is="getLayoutComponent(['right_menu_bottom', 'rightMenuBottom', 'rightMenuBottomComponent'])"
                  )
      slot(name="main")
        //Loading text
</template>
<script>
import Vue from 'vue'
import $ from 'jquery'
import { mapGetters } from 'vuex'
import SimplicitiLogo from '@/components/shared/SimplicitiLogo/SimplicitiLogo'

Vue.use({
  install() {
    Vue.prototype.$layout = Vue.$layout = {
      components: {},
      /**
       * Clear a component from a layout section (sidebar, menu, etc)
       */
      clearComponent(type) {
        this.components[type] = null
        this.vm && this.vm.$forceUpdate()
      },
      bind(vm) {
        this.vm = vm
      },
    }
  },
})

/**
 * Used by MapToolbox to hide itself if Table+Carto (i.g Location module)
 * (Module context pattern)
 */
export const layoutStaticContext = (() => {
  const context = {
    vm: null, //access to current layout vm
  }
  Object.defineProperty(context, 'isRightMenuVisible', {
    enumerable: true,
    get: () => context.vm.isRightMenuVisible,
  })
  Object.defineProperty(context, 'isRightMenuBottomVisible', {
    enumerable: true,
    get: () => context.vm.isRightMenuBottomVisible,
  })
  Object.defineProperty(context, '$data', {
    enumerable: true,
    get: () => context.vm.$data,
  })
  return context
})()

export default {
  name: 'TLayout',
  components: {
    SimplicitiLogo,
  },
  provide() {
    let self = this
    return {
      layoutData() {
        return self.$data
      },
      layoutVM() {
        return self
      },
    }
  },
  props: {
    sidebar: {
      type: Boolean,
      default: false,
    },
    wideMenu: {
      type: Boolean,
      default: false,
    },
    menu: {
      type: Boolean,
      default: false,
    },
    menuCollapsed: {
      type: Boolean,
      default: false,
    },
    menuToggle: {
      type: Boolean,
      default: true,
    },
    menuFullCollapse: {
      type: Boolean,
      default: true,
    },
    rightMenu: {
      type: Boolean,
      default: false,
    },
    rightMenuBottom: {
      type: Boolean,
      default: false,
    },
    /** Dynamic layout, default */
    syncWithVuex: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      /**
       * Customize current layout name
       * Used by V3 - Location Module - Table+Map mode -> To preserve the map if details windows is closed
       */
      currentLayoutName: '',
      mainOffsetLeft: 0,
      windowWidth: 0,
      isSidebarVisible: this.sidebar,
      isSidebarExtended: false,
      isMenuVisible: this.menu,
      isMenuCollapsed: this.menuCollapsed,
      isMenuToggleVisible: this.menuToggle,
      willMenuCollapseCompletely: this.menuFullCollapse,
      isWideMenuVisible: this.wideMenu,
      isSubMenuVisible: false,
      isRightMenuVisible: this.rightMenu,
      isRightMenuBottomVisible: this.rightMenuBottom,
      isRightMenuCloseButtonVisible: false,

      //On Firefox, the main div needs to recalculate width manually
      isFirefox: navigator.userAgent.toLowerCase().indexOf('firefox') > -1,
      bodyOffsetWidth: document.body.offsetWidth,
      layoutOffsetWidthExceptMainTotal: 0,
    }
  },
  computed: {
    ...mapGetters({
      layoutComponent: 'app/layoutComponent',
      vuexIsSidebarExtended: 'app/isSidebarExtended',
    }),
    mainWidth() {
      return this.windowWidth > 968
        ? `calc(100vw - ${this.mainOffsetLeft}px)`
        : `calc(100vw)`
    },
  },
  watch: {
    menu() {
      this.isMenuVisible = this.menu
    },
    menuCollapsed() {
      this.isMenuCollapsed = this.menuCollapsed
    },
    menuToggle() {
      this.isMenuToggleVisible = this.menuToggle
    },
    menuFullCollapse() {
      this.willMenuCollapseCompletely = this.menuFullCollapse
    },
    rightMenu() {
      this.isRightMenuVisible = this.rightMenu
    },
    rightMenuBottom() {
      this.isRightMenuBottomVisible = this.rightMenuBottom
    },
    isSidebarVisible() {
      this.recalculateMainOffsetLeft()
    },
    isMenuVisible() {
      this.recalculateMainOffsetLeft()
    },
    isMenuCollapsed() {
      this.recalculateMainOffsetLeft()
    },
    isSidebarExtended() {
      this.recalculateMainOffsetLeft()
    },
    /**
     * @todo Improve: This is a hotfix for: Expanded sidebar do not close itself after sidebar link click (alerts/identification)
     */
    vuexIsSidebarExtended() {
      this.isSidebarExtended = this.vuexIsSidebarExtended
    },
  },
  created() {
    layoutStaticContext.vm = this
    //On Firefox, the main div needs to recalculate width manually
    if (this.isFirefox) {
      this.documentBodyOffsetWidthInterval = setInterval(() => {
        this.bodyOffsetWidth = document.body.offsetWidth
        if (this.$refs.layout) {
          this.layoutOffsetWidthExceptMainTotal = Array.from(
            this.$refs.layout.childNodes
          ).reduce(
            (a, v) => (v.className.includes('main') ? a : a + v.offsetWidth),
            0
          )
        }
      }, 1000)
    }
  },
  mounted() {
    if (this.syncWithVuex) {
      this.$layout.bind(this)
    }

    this.windowWidth = window.innerWidth
    $(window).on(
      'resize',
      (this.onResize = () => {
        this.windowWidth = window.innerWidth
        this.recalculateMainOffsetLeft()
        this.onChange(null, false)
      })
    )
    this.$nextTick(() => this.recalculateMainOffsetLeft())
  },
  destroyed() {
    $(window).off('resize', this.onResize)
    if (this.isFirefox) {
      clearInterval(this.documentBodyOffsetWidthInterval)
    }
  },
  methods: {
    mainStyle() {
      let maxWidth = ''

      //On Firefox, the main div needs to recalculate width manually
      if (this.isFirefox && !!this.$refs.layout) {
        maxWidth = this.bodyOffsetWidth - this.layoutOffsetWidthExceptMainTotal
      }

      if (maxWidth === 0) {
        return ''
      }

      return `max-width:${maxWidth}px;`
    },
    getLayoutComponent(names) {
      let n = names.find((n) => this.layoutComponent(n))
      return n ? this.layoutComponent(n) : null
    },
    onChange(key, updateStore = true) {
      this.recalculateMainOffsetLeft()
      if (updateStore) {
        this.$store.dispatch('app/updateLayout', {
          ...this.$data,
        })
      }
    },
    recalculateMainOffsetLeft() {
      this.$nextTick(() => {
        this.mainOffsetLeft = (this.$refs.main || {}).offsetLeft || 0
      })
    },
    /**
     *
     * Called from app/index.js store
     *
     * */
    configure(values = {}) {
      if (!this.syncWithVuex) {
        return
      }
      //this.$log.debug("configure", values);
      let matchTable = {
        sidebar: 'isSidebarVisible',
        sidebar_extended: 'isSidebarExtended',
        menu: 'toggleMenu',
        menu_collapsed: 'toggleMenuCollapsed',
        menu_full_collapse: 'willMenuCollapseCompletely',
        right_menu: 'toggleRightMenu',
        right_menu_bottom: 'toggleRightMenuBottom',
        right_menu_close_btn: 'isRightMenuCloseButtonVisible',
        sub_menu: 'toggleSubMenu',
        wide_menu: 'toggleWideMenu',
        menu_toggle: 'isMenuToggleVisible',
        isMenuToggleVisible: 'isMenuToggleVisible',
        isSidebarVisible: 'isSidebarVisible',
        isSidebarExtended: 'isSidebarExtended',
        isMenuVisible: 'toggleMenu',
        isMenuCollapsed: 'toggleMenuCollapsed',
        willMenuCollapseCompletely: 'willMenuCollapseCompletely',
        isWideMenuVisible: 'toggleWideMenu',
        isSubMenuVisible: 'toggleSubMenu',
        isRightMenuVisible: 'toggleRightMenu',
        isRightMenuBottomVisible: 'toggleRightMenuBottom',
      }
      Object.keys(values).forEach((key) => {
        if (matchTable[key]) {
          if (typeof this[matchTable[key]] === 'function') {
            this[matchTable[key]](values[key], false)
          } else {
            this[matchTable[key]] = values[key]
            this.onChange(key, false)
          }
        } else {
          if (this.$data[key] !== undefined) {
            this[key] = values[key]
          }
        }
      })

      //Update at end
      this.$store.dispatch('app/updateLayout', {
        ...this.$data,
      })
    },
    /**
     * Internal
     * */
    toggleWideMenu(value = true, updateStore = true) {
      this.isWideMenuVisible = value
      this.$emit('toggleWideMenu')
      this.onChange('wide_menu', updateStore)
    },
    /**
     * Internal
     * */
    toggleSidebar(value = true, updateStore = true) {
      this.isSidebarVisible = value
      this.onChange('sidebar', updateStore)
    },
    /**
     * Internal
     * */
    toggleSidebarExtended(value = true, updateStore = true) {
      this.isSidebarExtended = value
      this.onChange('sidebar_extended', updateStore)
      this.$store.dispatch('sidebar/setIsExpanded', this.isSidebarExtended)
    },
    /**
     * Internal
     * */
    toggleMenu(value = false, updateStore = true) {
      this.isMenuVisible = value
      this.onChange('menu', updateStore)
      this.$emit('onMenuToggle', value)
    },
    /**
     * Internal
     * */
    toggleMenuCollapsed(value = false, updateStore = true) {
      this.isMenuCollapsed = value
      this.onChange('menu_collapsed', updateStore)
      if (this.willMenuCollapseCompletely) {
        this.$emit('onMenuToggle', !value)
      }
      this.$emit('onMenuCollapsed', value)
    },
    /**
     * Internal
     * */
    toggleSubMenu(value = true, updateStore = true) {
      this.isSubMenuVisible = value
      this.onChange('sub_menu', updateStore)
    },
    /**
     * Internal
     * */
    toggleRightMenu(value = true, updateStore = true) {
      this.isRightMenuVisible = value
      this.onChange('right_menu', updateStore)
    },
    /**
     * Internal
     * */
    toggleRightMenuBottom(value = true, updateStore = true) {
      this.isRightMenuBottomVisible = value
      this.onChange('right_menu_bottom', updateStore)
    },
  },
}
</script>
<style lang="scss" scoped>
.layout,
.layout > * {
  box-sizing: border-box;
  @media (max-width: 359px) {
    display: none;
  }
}
.layout .minimum_width {
  display: none;
  @media (max-width: 359px) {
    position: fixed;
    display: flex;
    justify-content: center;
    align-items: center;
    min-width: calc(100vw);
    align-self: center;
    z-index: 10;
  }
}
.layout {
  display: flex;
  height: calc(100vh);
  width: 100%;
  width: -moz-available; /* WebKit-based browsers will ignore this. */
  width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
  width: fill-available;

  @media (max-width: 359px) {
    align-items: center;
  }

  @media (max-width: 1400px) {
  }
}
.sidebar {
  min-width: 60px;
  max-width: 60px;
  z-index: 1000000;
}
.sidebar__inner {
  padding-top: 0px;
  box-shadow: 1px 0px 6px 0px #a9a9a991;
  background-color: white;
  height: calc(100vh);
  overflow-y: auto;
  scrollbar-color: white;
  scrollbar-width: thin;
}
.sidebar__inner::-webkit-scrollbar {
  width: 3px;
}
.sidebar__inner::-webkit-scrollbar-track {
  box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}

.sidebar__inner::-webkit-scrollbar-thumb {
  background-color: rgba(169, 169, 169, 0.267);
  outline: 0.5px solid rgba(169, 169, 169, 0.267);
}
.sidebar.extended {
  display: block;
  position: fixed;
  left: 0px;
  top: 0px;
  width: calc(100vw);
  max-width: none;
  z-index: 10000000;

  & .sidebar__overlay {
    display: block;
    position: fixed;
    left: 0px;
    top: 0px;
    width: calc(100vw);
    height: calc(100vh);
    background-color: rgba(47, 79, 79, 0.4);
    z-index: -1;
  }
  & .sidebar__inner {
    height: calc(100vh);
    width: 300px;
  }
}
.sidebar__hamburger {
  padding: 10px;
}
.hamburger__wrapper.fixed {
  position: fixed;
  top: 0px;
  left: 0px;
  z-index: 10;
  display: none;
}
.hamburger__wrapper {
  background-color: #0a71ae;
  height: 50px;
  margin: 0 auto;
}
.hamburger__wrapper.extended {
  box-shadow: none;
  border-right: 0;
}
.hamburger {
  cursor: pointer;
  margin: 0 auto;
  display: block;
  font-size: 30px;
}

.main {
  position: relative;
  max-height: calc(100vh);
  overflow-y: auto;
  scrollbar-color: white;
  scrollbar-width: thin;
  width: calc(100%);
  z-index: 1000000;
}
.main::-webkit-scrollbar {
  width: 10px;
}
.main::-webkit-scrollbar-track {
  background-color: white;
  border-radius: 10px;
}
.main::-webkit-scrollbar-thumb {
  background-color: var(--color-tundora);
  height: 15px;
  border-radius: 10px;
}

.menu {
  flex-shrink: 0;
  width: 468px;
  max-width: 468px;
  background-color: white;
  box-shadow: 3px 4px 6px 0px #a9a9a991;
  border-right: 1px solid #a9a9a991;
  z-index: 2;
  max-height: calc(100vh);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: white;
  @media (max-width: 968px) {
    max-width: 350px;
  }
  @media (min-width: 969px) and (max-width: 1400px) {
    max-width: 350px;
  }
  @media (min-width: 1401px) and (max-width: 1600px) {
    max-width: 400px;
  }
}

.menu::-webkit-scrollbar {
  width: 5px;
}
.menu::-webkit-scrollbar-track {
  background-color: white;
  border-radius: 10px;
}
.menu::-webkit-scrollbar-thumb {
  background-color: var(--color-tundora);
  height: 15px;
  border-radius: 10px;
}

.menu_collapsed .menu {
  max-width: 110px !important;
}
.menu_collapsed .menu.full_collapse {
  display: none;
}

.menu_inner {
  position: relative;
}
.menu__hamburger {
  display: none;
  @media (max-width: 968px) {
    display: flex;
    margin: 0 auto;
  }
}

.floating_layout {
  position: absolute;
  height: 100%;
  z-index: 10000;
  &.has_inner {
    width: inherit;
  }
}

.toggle_menu {
  position: fixed;
  top: 50%;
  z-index: 100000;
}
.toggle_menu button {
  height: 50px;
  border: 0px;
  border-radius: 0px 5px 5px 0px;
  background-color: white;
  border-right: 2px solid #00000069;
  position: relative;
  left: -1px;
}
.toggle_menu em {
  color: var(--color-black);
}
.toggle_menu button:hover,
.toggle_menu button:focus,
.toggle_menu button:active {
  outline: 0;
}

.sub_menu {
  min-width: 400px;
  max-width: 400px;
  max-height: calc(100vh);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: white;
  background-color: var(--color-wild-sand);
  position: relative;
  @media (max-width: 968px) {
    min-width: 350px;
    max-width: 350px;
  }
  @media (min-width: 969px) and (max-width: 1400px) {
    min-width: calc(25vw);
    max-width: 350px;
  }
  @media (min-width: 1401px) and (max-width: 1600px) {
    min-width: 350px;
    max-width: 350px;
  }
}

.sub_menu__header {
  background-color: var(--color-dark-blue);
  height: 30px;
  right: 5px;
  position: absolute;
  top: 5px;
}
.sub_menu__header .close_btn {
  background-color: transparent;
}
.sub_menu__header .close_btn em {
  color: white;
}
.sub_menu__content {
  margin: 0px;
}

.wide_menu_overlay {
  background-color: rgba(255, 255, 255, 0.8);
  position: absolute;
  z-index: 10000;
  height: inherit;
  width: 100%;
}
.wide_menu {
  height: 100%;
  position: relative;
  background-color: white;
  width: 90%;
}
.wide_menu__inner {
  height: 100%;
}

.fl__inner {
  display: flex;
  height: 100%;
}

.fl__right {
  background-color: transparent;
  height: 100%;
  width: calc(100%);
}
.fl__right__header {
  height: 26px;
  margin-right: 20px;
  position: absolute;
  right: -10px;
  z-index: 99999;
  top: 0px;
}
.close_btn {
  float: right;
  border: 0px;
  font-weight: 500;
  font-size: 16px;
  background-color: white;
  border-radius: 5px;
}
.close_btn em {
  color: var(--color-dark-blue);
}
.close_btn:hover,
.close_btn:active,
.close_btn:focus {
  outline: 0;
}
.wide_menu .close_btn {
  position: absolute;
  z-index: 1;
  right: 5px;
  top: 5px;
}
.fl__right__inner {
  display: grid;
  grid-template-rows: 100%;
  /*margin: 0px 10px 0px 22px;*/
  height: 100%;
}
.fl__right__inner.has_bottom {
  grid-template-rows: 1fr 400px;
}
.fl__right__inner .top {
  /*border-radius: 5px;*/
  background-color: white;
}
.fl__right__inner .bottom {
  background-color: white;
  max-width: 100%;
  overflow-x: auto;
}

.right_menu__top {
  overflow-x: auto;
  scrollbar-color: white;
  scrollbar-width: thin;
}
.right_menu__top::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
.right_menu__top::-webkit-scrollbar-track {
  background-color: white;
  border-radius: 10px;
}
.right_menu__top::-webkit-scrollbar-thumb {
  background-color: var(--color-tundora);
  height: 15px;
  border-radius: 10px;
}
.right_menu__top__component {
  height: calc(100%);
}

.right_menu__bottom,
.right_menu__top,
.sub_menu {
  z-index: 1001;
}

.layout-simple {
  width: 100%;
}
</style>