Vue implements floating draggable buttons on the mobile side

need:

  1. The button is displayed floating on the side of the page;
  2. Click the button to expand multiple shortcut buttons from bottom to top.
  3. Long press the button to allow dragging to change the button position, and the button is in a non-expanded state;
  4. When the button movement is completed and your finger is released, the distance to the left and right sides is calculated and automatically moved to the side display;
  5. After moving to the side, the expansion style is changed based on the specific left and right positions;
  6. Handle special cases when moving to non-visible areas.

Show results:

Implementation:

<template>
  <div class="shortcut" @touchstart="touchstart($event)" @touchmove="touchMove($event)" @touchend="touchEnd($event)" v-if="isMobile">
    <div class="shortcut__container">
      <transition name="fade">
        <div class="shadow" v-if="showPopover" @click.stop="showPopover = false"></div>
      </transition>
      <transition name="sub-fade">
        <div :class="['shortcut__list', `${type}`]" v-if="showPopover">
          <div class="shortcut__list_item">
            <div class="icon-box"><img src="@/images/common/ic_question.png" alt=""></div>
            Investment and financial management
          </div>
          <div class="shortcut__list_item">
            <div class="icon-box"><img src="@/images/common/ic_question.png" alt=""></div>
            my assets
          </div>
          <div class="shortcut__list_item">
            <div class="icon-box"><img src="@/images/common/ic_question.png" alt=""></div>
            Consult us
          </div>
        </div>
      </transition>
      <div class="shortcut__btn" :class="{ anim: showPopover }" @click.stop="handleBtn()"> + </div>
    </div>
  </div>
</template>

<script>
const TIME = 50
export default {<!-- -->
  data () {<!-- -->
    return {<!-- -->
      isMobile: /Mobi|Android|iPhone/i.test(navigator.userAgent),
      showPopover: false,
      timeOutEvent: 0,
      longClick: 0,
      //Finger original position
      oldMousePos: {<!-- -->},
      //original position of element
      oldNodePos: {<!-- -->},
      type: 'right',
    }
  },
  methods: {<!-- -->
    touchstart (ev) {<!-- -->
      // The timer controls the long press time, and dragging starts after {TIME} milliseconds.
      this.timeOutEvent = setTimeout(() => {<!-- -->
        this.longClick = 1
      }, TIME)
      const selectDom = ev.currentTarget
      const {<!-- --> pageX, pageY } = ev.touches[0] // Finger position
      const {<!-- --> offsetLeft, offsetTop } = selectDom // element position
      //Finger original position
      this.oldMousePos = {<!-- -->
        x: pageX,
        y: pageY,
      }
      //original position of element
      this.oldNodePos = {<!-- -->
        x: offsetLeft,
        y: offsetTop,
      }
      this.handleMoving()
      selectDom.style.left = `${<!-- -->offsetLeft}px`
      selectDom.style.top = `${<!-- -->offsetTop}px`
    },
    touchMove (ev) {<!-- -->
      // If you move before {TIME} milliseconds, the long press will not be triggered and the timer will be cleared.
      clearTimeout(this.timeOutEvent)
      if (this.longClick === 1) {<!-- -->
        this.handleMoving()
        this.showPopover = false

        const selectDom = ev.currentTarget
        // x-axis offset
        const lefts = this.oldMousePos.x - this.oldNodePos.x
        // y-axis offset
        const tops = this.oldMousePos.y - this.oldNodePos.y
        const {<!-- --> pageX, pageY } = ev.touches[0] // Finger position
        selectDom.style.left = `${<!-- -->pageX - lefts}px`
        selectDom.style.top = `${<!-- -->pageY - tops}px`
      }
    },
    touchEnd (ev) {<!-- -->
      //Clear the timer
      clearTimeout(this.timeOutEvent)
      if (this.longClick === 1) {<!-- -->
        this.longClick = 0
        const selectDom = ev.currentTarget
        const {<!-- --> innerWidth, innerHeight } = window
        const {<!-- --> offsetLeft, offsetTop } = selectDom
        selectDom.style.left = offsetLeft + 50 > innerWidth / 2 ? 'calc(100% - 55px)' : '15px'
        if (offsetTop < 150) {<!-- -->
          selectDom.style.top = '150px'
        } else if (offsetTop + 150 > innerHeight) {<!-- -->
          selectDom.style.top = `${<!-- -->innerHeight - 150}px`
        }
        this.type = offsetLeft + 50 > innerWidth / 2 ? 'right' : 'left'

        setTimeout(() => {<!-- -->
          document.body.style.overflow = 'auto'
          document.body.style.userSelect = 'auto'
        }, 1000)
      }
    },
    handleMoving () {<!-- -->
      // Disable body scrolling
      document.body.style.overflow = 'hidden'
      // Disable body text selection
      document.body.style.userSelect = 'none'
    },
    handleBtn () {<!-- -->
      this.showPopover = !this.showPopover
    }
  },
}
</script>

<style scoped lang="less">
.icon-box {<!-- -->
  background: #fff;
  width: .8rem;
  height: .8rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.shortcut {<!-- -->
  position: fixed;
  z-index: 9999;
  left: calc(100% - 55px);
  top: calc(100% - 150px);
  user-select: none;

   & amp;__container {<!-- -->
    position: relative;
  }

   & amp;__list {<!-- -->
    position: absolute;
    bottom: .8rem;
    z-index: 8;
     & amp;_item {<!-- -->
      color: #fff;
      display: flex;
      flex-direction: row;
      align-items: center;
      white-space: nowrap;
      margin-bottom: .15rem;

      .icon-box {<!-- -->
        margin: 0 .1rem 0 0;

        img {<!-- -->
          width: 0.36rem;
          height: 0.36rem;
        }
      }
    }

     & amp;.left {<!-- -->
      left: 0;
    }

     & amp;.right {<!-- -->
      right: 0;
      .shortcut__list_item {<!-- -->
        flex-direction: row-reverse;
        .icon-box {<!-- -->
          margin: 0 0 0 .1rem;
        }
      }
    }
  }

   & amp;__btn {<!-- -->
    background: #fff;
    width: .8rem;
    height: .8rem;
    border-radius: 50%;
    text-align: center;
    line-height: .7rem;
    color: #3356D9;
    font-size: .5rem;
    position: relative;
    z-index: 8;
    border: 1px solid #3356D9;
    transition: all .3s linear;

     & amp;.anim {<!-- -->
      transform: rotate(135deg);
    }
  }
}
.shadow {<!-- -->
  width: 100%;
  max-width: 1024px;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 1;
  margin: 0 auto;
}
.sub-fade-leave-active,.sub-fade-enter-active {<!-- -->
  transition: max-height 0.3s linear;
}
.sub-fade-enter,.sub-fade-leave-to {<!-- -->
  max-height: 0;
  overflow: hidden;
}
.sub-fade-enter-to,.sub-fade-leave {<!-- -->
  max-height: 2.56rem;
  overflow: hidden;
}
</style>