[Vue 3.0] Popper.js 로 우클릭 메뉴 (Context Menu) 구현하기 (작성중)

개요

우클릭 메뉴(Context Menu)는 상황에 따라서 유용합니다. 모바일같은 경우 꾹 누르면 우클릭 메뉴가 뜨는 것처럼 할 수 있습니다.

우클릭 메뉴 예시 사진

popper.js 는 가볍고 강력한, 특정 요소에 붙어 있는 팝업 요소(popover)를 만들기 좋은 라이브러리입니다. 이것을 이용해보도록 하겠습니다.

설계

우클릭 메뉴를 설계할 때 고려해야 할 사항은 다음과 같습니다. UI 자체야 더 고급스럽고 사용성 좋게 만들려면 한도 끝도 없지만, 아래 기능은 무조건 충족시키도록 해보자구요.

  1. 우클릭 메뉴가 활성화되는 지점(캔버스)이 정해져 있어야 합니다. 아무데서나 우클릭해서 전부 우클릭 메뉴가 뜬다면 UI 로써 기능이 좀 거시기하겠죠..?
  2. 다음과 같을 때 우클릭 메뉴가 사라져야 합니다.
    1. 우클릭 메뉴가 활성화된 상태에서 메뉴의 바깥을 눌렀을 때
    2. 우클릭 메뉴 안에 있는 링크(버튼)을 눌렀을 때
  3. 우클릭 메뉴의 항목을 클릭했을 때의 동작을 커스터마이징 하기 쉬워야 합니다.
  4. 단순히 우클릭 메뉴 (Container) 안에 또 다른 컴포넌트 (Item)를 넣기만 해도 자연스럽게 의존 관계가 생길 수 있도록 해봅시다. (예를 들어 Container의 props 를 통해 Item 용 class 를 일괄 적용시킬 수 있도록 하기)

기능 적인 요소 외로 그냥 사용하기 편하도록 하려면 어떻게 해야 할까요? 자잘한 고려사항을 아래와 같이 정해보았습니다.

  1. 메뉴를 가져다쓰는 입장에서 모든 스타일을 지정할 수 있도록, 처음에는 아무런 스타일을 지정하지 않습니다. 클래스 명이나 css 속성을 통해 스타일을 마음대로 주무를 수 있도록 했습니다.

목표 코드

실제로 해당 컴포넌트를 어떻게 사용할 건지 먼저 정하고, 컴포넌트를 만들어 가는 것도 괜찮은 방법입니다. 아래는 App.vue 입니다. 템플릿 부분, 스크립트 부분, CSS 부분을 나눠서 보겠습니다.

<template>
  <div>
    <div
      v-ec-contextmenu:my-context
      :style="{ width: '200px', height: '200px', backgroundColor: '#eee' }"
    >
      이 칸에서 우클릭을 해보세요.
    </div>
    <ec-container
      class="context-menu-container"
      style="text-align: left"
      id="my-context"
      :itemStyle="{ display: 'block' }"
      itemClass="font-bold"
      v-slot="api"
    >
      <ec-item @click="log('첫 번째 메뉴가 눌렸습니다.')">첫 번째 메뉴</ec-item>
      <ec-item @click="log('두 번째 메뉴가 눌렸습니다.')">두 번째 메뉴</ec-item>
      <ec-item @click="log('세 번째 메뉴가 눌렸습니다.')">세 번째 메뉴</ec-item>
      <div>안녕하십니까!!</div>
      <button @click="api.close">이 버튼을 클릭하면 닫습니다.</button>
      <pre>state: {{ api.state }}</pre>
    </ec-container>
  </div>
</template>
import { defineComponent } from "vue";
import { EcContainer, EcItem, EcDirective } from "./components/MyContextMenu";

export default defineComponent({
  name: "App",

  directives: {
    EcContextmenu: EcDirective,
  },
  components: {
    EcContainer,
    EcItem,
  },
  setup() {
    return {
      log: console.log,
    };
  },
});
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
.context-menu-container {
  background-color: #fff;
  border: 1px solid #ddd;
  padding: 15px;
}
.font-bold {
  font-weight: bold;
}

위 코드에서 우리가 설계했던 부분이 어떤 부분인지 한번 짚겠습니다.

  1. 우클릭이 활성화되는 지점v-ec-contextmenu:my-context 로 지정했습니다. my-context 는 우클릭 메뉴의 id 값입니다. v-ec-contextmenu 는 vue에서 directive 라는 기능으로, App 컴포넌트 정의에서 directives: { EcContextmenu: EcDirective, } 로 지정해주고 있습니다. 정리하면 다음과 같습니다.
    1. 우클릭이 활성화되는 지점은 v-ec-contextmenu directive 가 있는 HTML 엘리먼트이다.
    2. 우클릭을 눌렀을 때 my-context 를 찾아 우클릭 메뉴를 동작시키도록 한다.
  2. 메뉴 항목을 클릭했을 때의 동작은 @click="..." 를 통해 지정해줍니다. 동작을 커스터마이징하기 쉽습니다.
  3. ec-container 컴포넌트에서 itemStyleitemClass props 를 통해 하위의 모든 ec-item 컴포넌트가 영향을 받고 있습니다. 이로부터 위치만 내부에 있는 컴포넌트가 의존성을 가질 수 있다는 걸 확인할 수 있습니다.

구현

저장 공간 만들기

우클릭 했을 때

각각의 컴포넌트마다 만들어진 이벤트를 수신하기 위해, 한 단계를 거쳐나가야 합니다.

한계

popper.js 는 객체의 나타나고 안나타나고를 css의 inset(left, right, top, bottom 등)transform 을 이용합니다. 그래서 만약 부드러운 애니메이션을 만들고 싶지만 그러한 속성을 사용하기 어려울 수 있습니다. 그래서 상위 엘리먼트든 하위 앨리먼트든 만들어서 transformtransition 등을 적절히 적용해야 할 것으로 보입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다

Scroll to top