Selecto.jsの内部実装メカニズム:要素選択アルゴリズムの詳細分析

Selecto.jsの内部実装メカニズム:要素選択アルゴリズムの詳細分析

Selecto.jsはマウスまたはタッチ操作でドラッグ領域内の要素を選択できるコンポーネントです。このライブラリはシンプルなリスト選択から複雑なグラフィックエディタまで幅広い用途に使用され、直感的な要素選択機能を提供します。本稿ではSelecto.jsの内部動作原理を深く掘り下げ、要素選択アルゴリズムの実装構造を詳細に解説します。

Selecto.jsの主な機能と利用シーン

Selecto.jsの中心機能は直感的なドラッグ選択体験の提供であり、複数の選択モードとインタラクティブな操作方法をサポートしています。以下のシナリオで特に有効です:

  • 画像ギャラリーでの一括選択
  • データテーブルでの複数行選択
  • グラフィックエディタでのオブジェクト選択
  • ファイルマネージャーでのファイル選択

基本的な処理フロー

Selecto.jsの処理フローは初期化、ドラッグ開始、ドラッグ中、ドラッグ終了の4つの主要フェーズに分かれます。各フェーズには固有のタスクとアルゴリズムが存在します。

初期化プロセス

初期化段階では、Selecto.jsは必要なイベントリスナーとDOM要素を設定します。主なコードはpackages/selecto/src/SelectoManager.tsxのコンストラクタにあります。非表示の選択ボックス要素を作成し、ドラッグ関連のイベントハンドラを初期化します。

ドラッグ開始処理

ユーザーがマウスを押下または画面をタッチすると、ドラッグ開始プロセスが起動します。このとき、Selecto.jsは初期座標を記録し、選択操作を開始すべきか判断します。このロジックは_onDragStartメソッドで実装されています。

ドラッグ中の処理

ドラッグ中は、Selecto.jsがリアルタイムで選択ボックスの位置とサイズを更新し、選択範囲内に含まれる要素を計算します。この段階のコアアルゴリズムは要素ヒットテストであり、後述の章で詳しく説明します。_onDragおよび_checkSelectedメソッドに関連コードがあります。

ドラッグ終了処理

マウスの解放またはタッチの終了により、ドラッグ終了プロセスが開始されます。Selecto.jsは最終的な選択結果を確定し、適切なイベントを発火させます。このロジックは_onDragEndメソッドで実装されています。

要素選択アルゴリズムの詳細分析

Selecto.jsの核となるのは高効率な要素選択アルゴリズムです。このアルゴリズムは主に2つの重要なステップで構成されます:領域分割とヒットテスト。

領域分割戦略

選択効率を向上させるため、Selecto.jsはグリッドベースの領域分割戦略を採用しています。全体の選択領域を複数の小さなグリッドに分割し、各グリッドを要素グループに対応させます。この方法により、ヒットテストが必要な要素数を大幅に削減できます。

// 領域分割の核心コード
private _updateElementGroups(configuration: IObject<any>) {
    const widthDimension = configuration.innerWidth;
    const heightDimension = configuration.innerHeight;
    const elementPositions: number[][][] = configuration.selectablePoints;

    if (!widthDimension || !heightDimension) {
        configuration.innerGroups = null;
    } else {
        const groupCollection: Record<string | number, Record<string | number, number[]>> = {};

        elementPositions.forEach((positionArray, index) => {
            let leftBound = Infinity;
            let rightBound = -Infinity;
            let topBound = Infinity;
            let bottomBound = -Infinity;

            positionArray.forEach(coordinates => {
                const gridX = Math.floor(coordinates[0] / widthDimension);
                const gridY = Math.floor(coordinates[1] / heightDimension);

                leftBound = Math.min(gridX, leftBound);
                rightBound = Math.max(gridX, rightBound);
                topBound = Math.min(gridY, topBound);
                bottomBound = Math.max(gridY, bottomBound);
            });

            for (let x = leftBound; x <= rightBound; ++x) {
                for (let y = topBound; y <= bottomBound; ++y) {
                    groupCollection[x] = groupCollection[x] || {};
                    groupCollection[x][y] = groupCollection[x][y] || [];
                    groupCollection[x][y].push(index);
                }
            }
        });

        configuration.innerGroups = groupCollection;
    }
}

高速ヒットテストアルゴリズム

ヒットテストは要素が選択領域内に含まれているかどうかを判定するプロセスです。Selecto.jsは幾何学計算に基づいた高速なヒットテストアルゴリズムを使用し、要素の形状と選択領域の重複度を考慮します。

// ヒットテストの核心コード
private performHitDetection(
    selectionRectangle: Rect,
    configuration: any,
    isDragging: boolean,
    interactionEvent: any,
) {
    // ...一部コード省略...
    const checkHitStatus = (vertexList: number[][], targetElement: Element) => {
        const hitThresholdValue =
            typeof threshold === "function"
                ? splitUnit(`${threshold(targetElement)}`)
                : splitUnit(`${threshold}`);

        const withinBounds = ignoreClick
            ? false
            : isInside([mouseX, mouseY], vertexList);

        if (!isDragging && enableClickSelection && withinBounds) {
            return true;
        }
        const intersectionVertices = calculateIntersection(rectPoints, vertexList);

        if (!intersectionVertices.length) {
            return false;
        }
        let intersectionArea = computeArea(intersectionVertices);

        // 線分の場合
        let baseArea = 0;

        if (intersectionArea === 0 && computeArea(vertexList) === 0) {
            baseArea = calculateLineLength(vertexList);
            intersectionArea = calculateLineLength(intersectionVertices);
        } else {
            baseArea = computeArea(vertexList);
        }

        if (thresholdValue.unit === "px") {
            return intersectionArea >= thresholdValue.value;
        } else {
            const percentage = between(
                Math.round((intersectionArea / baseArea) * 100),
                0,
                100
            );

            return percentage >= Math.min(100, thresholdValue.value);
        }
    };
    // ...一部コード省略...
}

このアルゴリズムは要素が選択領域内に完全に収まっている場合だけでなく、部分的に重なっているケースも考慮します。hitRateパラメータにより、要素が選択されるために必要な最小重複割合を制御できます。

主要な設定オプションの説明

Selecto.jsは豊富な設定オプションを提供し、開発者が具体的な要件に応じて選択動作をカスタマイズできます。以下は重要な設定オプションです:

selectableTargets

選択可能な対象要素を指定します。CSSセレクタ、DOM要素配列、または要素配列を返す関数を設定できます。

selectByClick

trueに設定すると、ドラッグ選択ボックスではなく単一要素をクリックして選択できるようになります。

continueSelect

trueに設定すると、CtrlやShiftキーを押さずに複数要素を選択し続けられるようになります。

hitRate

要素が選択されるために必要な最小重複割合を制御します。パーセンテージ(例:50%)またはピクセル値(例:10px)を指定できます。

scrollOptions

自動スクロール動作を設定し、選択ボックスがコンテナの端に近づいたときにトリガーされます。

実際の応用例

以下はSelecto.jsの簡単な応用例で、初期化と使用方法を示しています:

import Selecto from "selecto";

const selectorInstance = new Selecto({
    container: document.body,
    selectableTargets: [".selectable-item"],
    selectByClick: true,
    continueSelect: false,
    hitRate: 60,
});

selectorInstance.on("select", ({ added, removed }) => {
    added.forEach(element => element.classList.add("active-selection"));
    removed.forEach(element => element.classList.remove("active-selection"));
});

この例では選択器を作成し、ユーザーがドラッグまたはクリックで"selectable-item"クラスを持つ要素を選択できるようにします。選択状態が変化すると、"active-selection"クラスが追加または削除されます。

イベントシステムの詳細

Selecto.jsは豊富なイベントシステムを提供し、開発者がさまざまな選択関連イベントに応答できるようにします:

  • selectStart: 選択開始時に発火
  • select: 選択状態が変化したときに発火
  • selectEnd: 選択終了時に発火
  • dragStart: ドラッグ開始時に発火
  • drag: ドラッグ中に発火
  • dragEnd: ドラッグ終了時に発火

これらのイベントは追加された要素、削除された要素、選択領域の座標などの詳細情報を提供し、開発者が複雑な選択インタラクションを実現できるようにします。

パフォーマンス最適化のテクニック

多数の要素を含むシナリオでもSelecto.jsが高パフォーマンスを維持するために、以下の最適化テクニックが使用できます:

  1. 選択可能要素数の制限:実際に選択が必要な要素のみを対象とする
  2. 適切なhitRate値の使用:要素サイズとレイアウトに応じてhitRateを調整し、不要な計算を削減
  3. 領域分割の活用:Selecto.jsの領域分割戦略は既に選択パフォーマンスを最適化しているが、要素リストの更新頻度に注意する
  4. イベントデリゲーションの使用:動的に追加される要素にはイベントデリゲーションを使用し、選択対象リストの頻繁な更新を避ける

複数フレームワーク対応

Selecto.jsはコアJavaScriptライブラリを提供するだけでなく、人気のあるフロントエンドフレームワーク向けに専用実装も提供しています:

  • React: packages/react-selecto/
  • Vue: packages/vue-selecto/ および packages/vue3-selecto/
  • Angular: packages/ngx-selecto/
  • Svelte: packages/svelte-selecto/
  • Preact: packages/preact-selecto/
  • Lit: packages/lit-selecto/

これらのフレームワーク固有の実装は、それぞれのエコシステムとの統合性を高め、コンポーネント化されたAPIとリアクティブな状態管理を提供します。

タグ: javascript dom-manipulation event-handling algorithm-design performance-optimization

6月3日 23:03 投稿