<template lang="pug">
.w-drawer-wrap(v-if="showWrapper || pushContent" :class="wrapperClasses")
  //- Pushing content.
  .w-drawer-wrap__track(v-if="pushContent" :style="trackStyles")
    .w-drawer-wrap__pushable
      w-overlay(
        v-if="!noOverlay"
        ref="overlay"
        v-model="showDrawer"
        @click="onOutsideClick"
        :persistent="persistent"
        persistent-no-animation
        :bg-color="overlayColor || 'transparent'"
        :opacity="overlayOpacity")
      slot(name="pushable")
    transition(
      name="fade"
      @before-leave="onBeforeClose"
      @after-leave="onClose")
      component.w-drawer(
        v-if="showDrawer"
        ref="drawer"
        :is="tag || 'aside'"
        :class="drawerClasses"
        :style="styles"
        :tabindex="noOverlay ? 0 : null"
        @keydown.escape.stop="noOverlay && !persistent && onOutsideClick()")
        slot
  //- Other cases.
  template(v-else)
    w-overlay(
      v-if="!noOverlay"
      ref="overlay"
      v-model="showDrawer"
      @click="onOutsideClick"
      :persistent="persistent"
      persistent-no-animation
      :bg-color="overlayColor"
      :opacity="overlayOpacity")
    transition(
      :name="transitionName"
      appear
      @before-leave="onBeforeClose"
      @after-leave="onClose")
      component.w-drawer(
        v-if="showDrawer"
        ref="drawer"
        :is="tag || 'aside'"
        :class="drawerClasses"
        :style="styles"
        :tabindex="noOverlay ? 0 : null"
        @keydown.escape.stop="noOverlay && !persistent && onOutsideClick()")
        slot
</template>

<script>
import { focusElement } from '../utils/focus'
// The complexity in this component is on close:
// we must keep the wrapper in the DOM until the drawer transition is finished.
// Then emit the modelValue update that will trigger the removal of the wrapper from the DOM.

const oppositeSides = { left: 'right', right: 'left', top: 'down', bottom: 'up' }

export default {
  name: 'w-drawer',
  expose: ['focus'],

  props: {
    modelValue: { default: true },
    left: { type: Boolean },
    right: { type: Boolean },
    top: { type: Boolean },
    bottom: { type: Boolean },
    persistent: { type: Boolean },
    persistentNoAnimation: { type: Boolean },
    fitContent: { type: Boolean },
    width: { type: [Number, String, Boolean] },
    height: { type: [Number, String, Boolean] },
    zIndex: { type: [Number, String, Boolean] },
    color: { type: String },
    bgColor: { type: String },
    noOverlay: { type: Boolean },
    pushContent: { type: Boolean },
    absolute: { type: Boolean },
    overlayColor: { type: String },
    overlayOpacity: { type: [Number, String, Boolean] },
    drawerClass: { type: String },
    tag: { type: String, default: 'aside' },
    dark: { type: Boolean },
    light: { type: Boolean }
  },

  emits: ['input', 'update:modelValue', 'before-close', 'close'],

  provide () {
    return {
      // If a detachable is used inside a w-drawer without an appendTo, default to the drawer element
      // instead of the w-app.
      detachableDefaultRoot: () => this.$refs.drawer || null
    }
  },

  data () {
    return {
      showWrapper: this.modelValue,
      showDrawer: this.modelValue,
      persistentAnimate: false
    }
  },

  computed: {
    // Return the width or height value if defined, or false otherwise.
    size () {
      let size = this.width || this.height
      // If a number is passed without units, append `px`.
      if (size && parseInt(size) == size) size += 'px'
      return size || false
    },
    // Return `width` or `height`, `width` by default (position right by default).
    sizeProperty () {
      return (['left', 'right'].includes(this.position) && 'width') || 'height'
    },
    position () {
      return (
        (this.left && 'left') || (this.right && 'right') ||
        (this.top && 'top') || (this.bottom && 'bottom') ||
        'right'
      )
    },
    wrapperClasses () {
      return {
        'w-drawer-wrap--fixed': !this.absolute && !this.pushContent,
        'w-drawer-wrap--absolute': this.absolute,
        'w-drawer-wrap--push-content': this.pushContent
      }
    },
    drawerClasses () {
      return {
        [this.drawerClass]: true,
        [this.color]: this.color,
        [`${this.bgColor}--bg`]: this.bgColor,
        'w-drawer--open': !!this.showDrawer,
        [`w-drawer--${this.position}`]: true,
        'w-drawer--fit-content': this.fitContent,
        'w-drawer--persistent': this.persistent,
        'w-drawer--persistent-animate': this.persistent && this.persistentAnimate,
        'w-drawer--dark': this.dark,
        'w-drawer--light': this.light
      }
    },
    // The track is a wrapper around the pushable content and drawer.
    // It moves inside the overflow hidden outer wrap.
    trackStyles () {
      return this.pushContent && this.showDrawer && {
        transform: `translateX(${this.position === 'left' ? '' : '-'}${this.size || '200px'})`
      }
    },
    styles () {
      return {
        [`max-${this.sizeProperty}`]: this.size || null,
        zIndex: this.zIndex || this.zIndex === 0 || null
      }
    },
    // In case of pushing content, the showWrapper variable doesn't reflect the behavior:
    // unmount the drawer (remove from DOM) is what it does when showWrapper is false.
    unmountDrawer () {
      return !this.showWrapper
    },
    transitionName () {
      return `slide-${oppositeSides[this.position]}`
    }
  },

  methods: {
    focus () {
      if (this.$refs.overlay) this.$refs.overlay.focus()
      else focusElement(this.$refs.drawer?.$el || this.$refs.drawer)
    },

    onBeforeClose () {
      this.$emit('before-close')
    },
    onClose () {
      this.showWrapper = false
      this.$emit('update:modelValue', false)
      this.$emit('input', false)
      this.$emit('close')
    },
    onOutsideClick () {
      if (!this.persistent) {
        // The close method is called on animation end.
        this.showDrawer = false
      }
      else if (!this.persistentNoAnimation) {
        this.persistentAnimate = true
        setTimeout(() => (this.persistentAnimate = false), 200) // Must match CSS animation duration.
      }
    }
  },

  watch: {
    modelValue (value) {
      // If value is true, mount the wrapper in DOM and open the drawer.
      // If value is false, keep the wrapper in DOM and close the drawer;
      // At the end of the drawer transition the value is updated and wrapper
      // removed from the DOM.
      if (value) this.showWrapper = true
      this.showDrawer = value
    }
  }
}
</script>

<style lang="scss">
.w-drawer-wrap {
  &--fixed {position: fixed;z-index: 500;}

  &--absolute {
    position: absolute;
    inset: 0;
    overflow: hidden;
  }

  .w-overlay {z-index: 1;position: inherit;}

  &--push-content {
    position: relative;
    overflow: hidden;
    height: 100%;
    flex: 1;
    display: flex;
    flex-direction: column;

    .w-overlay {
      position: absolute;
      inset: 0;
      z-index: 2;
    }
    .w-drawer {position: absolute;}
    .w-drawer--left {right: 100%;left: auto !important;}
    .w-drawer--right {left: 100%;}
  }

  &__track {
    display: flex;
    flex: 1;
    height: 100%;
    @include default-transition;
  }

  &__pushable {
    position: relative;
    flex-grow: 1;
  }
}

.w-drawer {
  position: inherit;
  display: flex;
  z-index: 1;
  background: var(--w-drawer-bg-color);
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.3);

  @include themeable;

  &--left, &--right {
    top: 0;
    bottom: 0;
    width: 100%;
    max-width: var(--w-drawer-max-size);
  }
  &--top, &--bottom {
    left: 0;
    right: 0;
    height: 100%;
    max-height: var(--w-drawer-max-size);
  }
  &--fit-content {width: auto;height: auto;}

  &--left {left: 0;}
  &--right {right: 0;}
  &--top {top: 0;}
  &--bottom {bottom: 0;}

  &--persistent-animate {animation: 0.2s w-drawer-pop cubic-bezier(0.6, -0.28, 0.74, 0.05);}
}

@keyframes w-drawer-pop {
  0%, 100% {transform: scale(1);}
  50% {transform: scale(1.05);}
}
</style>
