🐭

LINEリッチメニューのboundsオブジェクトをマウスドラッグで設定する

2024/12/25に公開

はじめに

LINEリッチメニューとは、LINEのトーク画面下部に固定表示されるメニューです。このメニューには、クーポンやショップカードなどのLINE公式アカウントの機能に加え、ECサイトや予約サイトなどの外部サイトへのリンクを設定できます。
https://campus.line.biz/line-official-account/courses/features/lessons/richmenu

Messaging APIを使ってリッチメニューを設定する際、タップ可能な領域はboundsオブジェクトで定義する必要があります。
あらかじめテンプレートを用意する方法では、用途に応じてテンプレートを増やす必要があり、管理が煩雑になりがちです。

そこで、画面上から自由にドラッグ操作を行い、選択した領域の座標(x, y)やサイズ(width, height)を取得・設定できるように実装を行いました。

全体像

この機能の目的は以下のとおりです。

  1. ドラッグで領域を選択

  2. 選択した領域の座標(x, y)やサイズ(width, height)を取得

概要

  1. 選択範囲の状態を管理
    • ドラッグ中かどうかをuseRef、選択範囲の情報をuseStateで管理
  2. マウスボタンが押下された時の処理
    • 選択範囲の初期位置を設定
  3. マウスボタンが離された時の処理
    • 選択範囲の状態をリセット
  4. マウスポインターが移動した時の処理
    • 選択範囲のサイズを計算

実装

  1. 選択範囲の状態について
// 選択範囲の状態を管理
const [selectionRect, setSelectionRect] = useState<SelectionRectState>(null);
// ドラッグ中かどうかを追跡するref
const isDraggingRef = useRef(false);
  • selectionRectで現在選択中の領域の座標とサイズを保持します
  • isDraggingRefでドラッグ中かどうかを管理します
    • refを使用することで値の更新時に再レンダリングが発生しないようにしています
  1. マウスボタンが押下された時の処理
/** マウスボタンが押下された時の処理 */
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
  // 左クリック以外は無視
  if (e.button !== 0) return;
  isDraggingRef.current = true;

  const containerRect = e.currentTarget.getBoundingClientRect();
  // クリック位置を基準に選択範囲の初期位置を設定
  const startX = e.clientX - containerRect.x;
  const startY = e.clientY - containerRect.y;

  setSelectionRect({ x: startX, y: startY, width: 0, height: 0 });
};
  • e.button !== 0で左クリック以外のマウス操作を無視します
  • isDraggingRef.current = trueでドラッグ中の状態を設定します
  • getBoundingClientRect()でコンテナ要素の位置情報を取得します
  • マウスクリック位置(e.clientX, e.clientY)からコンテナの位置(containerRect.x, containerRect.y)を引くことで、コンテナ内での相対座標を計算します
  • setSelectionRectで選択範囲の初期状態を設定します
    • 開始位置(x, y)は計算した相対座標
    • 初期サイズ(width, height)は0
  1. マウスボタンが離された時の処理
/** マウスボタンが離された時の処理 */
const handlePointerUp = () => {
  isDraggingRef.current = false;
};
  • isDraggingRef.current = falseでドラッグ中の状態をリセットします
  1. マウスポインターが移動した時の処理
/** ポインターの移動時の処理 */
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
  if (selectionRect == null || !isDraggingRef.current) return;

  const containerRect = e.currentTarget.getBoundingClientRect();

  // 現在のポインター位置を取得
  const currentX = e.clientX - containerRect.x;
  const currentY = e.clientY - containerRect.y;

  // 選択範囲の計算
  const newRect = calculateSelectionRect(
    currentX,
    currentY,
    selectionRect.x,
    selectionRect.y,
  );
  setSelectionRect(newRect);
};
  • selectionRect == null || !isDraggingRef.currentで選択範囲が存在しないか、ドラッグ中でない場合は処理を終了します
  • getBoundingClientRect()でコンテナ要素の位置情報を取得します
  • マウスポインター位置(e.clientX, e.clientY)からコンテナの位置(containerRect.x, containerRect.y)を引くことで、コンテナ内での相対座標を計算します
  • calculateSelectionRectで選択範囲の新しい状態を計算します
  • setSelectionRectで選択範囲の新しい状態を設定します
コードの全体像はこちら
'use client';
import { useRef, useState } from 'react';

export const SelectionRect = () => {
  // 選択範囲の状態を管理
  const [selectionRect, setSelectionRect] = useState<SelectionRectState>(null);
  // ドラッグ中かどうかを追跡するref
  const isDraggingRef = useRef(false);

  /** マウスボタンが押下された時の処理 */
  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
    // 左クリック以外は無視
    if (e.button !== 0) return;

    isDraggingRef.current = true;
    const containerRect = e.currentTarget.getBoundingClientRect();

    // クリック位置を基準に選択範囲の初期位置を設定
    const startX = e.clientX - containerRect.x;
    const startY = e.clientY - containerRect.y;

    setSelectionRect({ x: startX, y: startY, width: 0, height: 0 });
  };

  /** ポインターの移動時の処理 */
  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    if (selectionRect == null || !isDraggingRef.current) return;

    const containerRect = e.currentTarget.getBoundingClientRect();

    // 現在のポインター位置を取得
    const currentX = e.clientX - containerRect.x;
    const currentY = e.clientY - containerRect.y;

    // 選択範囲の計算
    const newRect = calculateSelectionRect(
      currentX,
      currentY,
      selectionRect.x,
      selectionRect.y,
    );
    setSelectionRect(newRect);
  };

  /** マウスボタンが離された時の処理 */
  const handlePointerUp = () => {
    isDraggingRef.current = false;
  };

  /** 選択範囲を計算する */
  const calculateSelectionRect = (
    currentX: number,
    currentY: number,
    startX: number,
    startY: number,
  ) => {
    return {
      x: Math.min(currentX, startX),
      y: Math.min(currentY, startY),
      width: Math.abs(currentX - startX),
      height: Math.abs(currentY - startY),
    };
  };

  return (
    <div className='flex flex-col gap-4'>
      {/* 選択エリア */}
      <div className='relative m-auto h-[400px] w-[600px] border border-blacka-12'>
        <div
          onPointerDown={handlePointerDown}
          onPointerMove={handlePointerMove}
          onPointerUp={handlePointerUp}
          className='size-full'
        >
          {/* 選択範囲の表示 */}
          {selectionRect && (
            <div
              style={{
                left: selectionRect.x,
                top: selectionRect.y,
                width: selectionRect.width,
                height: selectionRect.height,
              }}
              className='absolute border-2 border-red-11 bg-red-5 opacity-50'
            />
          )}
        </div>
      </div>

      {/* 選択範囲の情報表示 */}
      <div>
        <p>x座標: {selectionRect?.x}</p>
        <p>y座標: {selectionRect?.y}</p>
        <p>幅: {selectionRect?.width}</p>
        <p>高さ: {selectionRect?.height}</p>
      </div>
    </div>
  );
};

実際にドラッグ操作を行うことで、添付画像のように選択範囲の座標やサイズが取得できることを確認できます。

まとめ

onPointerDown, onPointerMove, onPointerUpを使用することで、画面上でドラッグ操作を行い、選択した領域の座標とサイズを取得することができました。
ただし、現状では以下のような課題が残っています。

  • PCでの操作を前提としている
  • 選択済みの領域を編集できない
  • 選択した領域の重複を防ぐことができない
  • 選択済みの領域を移動可能にする

これらの課題を解決することで、より実用的な機能を提供できるようになるため、引き続き課題解決のための実装を検討していきたいと思います。

chot Inc. tech blog

Discussion