サイドバーの幅をドラッグで変更できるようにする - HTMLページでのスプリッタの作成 - JavaScript

HTMLページでサイドバーの幅をドラッグで変更できるようにするコードを紹介します。

概要

2段組みのHTMLページでサイドバーの幅をドラッグで変更できるようにしたい場合があります。
WindowsアプリケーションではSplitterコントロールを利用すると簡単に実現できます。 HTMLページの場合は、フレームを利用すれば比較的簡単に実現できますが、1つのページでサイドバーの幅を可変にしたい場合は、独自の実装が必要になります。
この記事では、フレームを使わずに1つのHTMLページでサイドバーの幅をドラッグで変更できるコードを紹介します。

方針

段組みは、Flexboxで実装します。3段組の枠を作成し、左の枠をサイドバーに、 中央の枠は幅を狭くしてスプリッタに、右側の枠をコンテンツ枠とします。左の枠にflex-basisプロパティを設定し、初期の幅を設定します。
中央の枠をドラッグするタイミングで、左側の枠のflex-basisプロパティを変更することで、ドラッグでサイドバーの幅が変化します。

実装

コード

splitter-simple-01.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="splitter-simple-01.css"/>
  <script type="text/javascript">
    let StartDrag = false;

    window.onload = onLoad;

    function onLoad() {
      const splitter = document.querySelector("#Splitter1");
      const leftFrame = document.querySelector("#LeftFrame1");

      splitter.addEventListener("mousedown", (event) => {

        document.addEventListener("mousemove", resize, false);
        document.addEventListener("mouseup", () => {
          document.removeEventListener("mousemove", resize, false);
        }, false);
      });

      function resize(e) {
        const size = `${e.x}px`;
        leftFrame.style.flexBasis = size;
      }
    }
  </script>
</head>
<body>
  <div class="Container">
    <div id="LeftFrame1" class="LeftFrame">
      <p>Item1</p>
      <p>Item2</p>
      <p>Item3</p>
      <p>Item4</p>
      <p>Item5</p>
    </div>
    <div id="Splitter1" class="Splitter">
    </div>
    <div class="RightFrame">
      <p>Content1</p>
      <p>Content2</p>
      <p>Content3</p>
      <p>Content4</p>
      <p>Content5</p>
    </div>
   </div>
</body>
</html>
splitter-simple-01.css
body {
  margin:0;
  padding:0;
}

.Container {
  display: flex;
  border: 1px solid #808080;
}

.LeftFrame {
  background-color: #F0F0F0;
  flex-basis: 160px;
}
.RightFrame {
}

.Splitter {
  flex-basis: 16px;
  cursor: col-resize; 
  background-color: #360090;
}

解説

ページロード時に以下のJavaScriptを実行します。
id="Splitter1"の要素に対して、mousedownのイベントリスナーを設定します。
  function onLoad() {
    const splitter = document.querySelector("#Splitter1");
    const leftFrame = document.querySelector("#LeftFrame1");

    splitter.addEventListener("mousedown", (event) => {

      document.addEventListener("mousemove", resize, false);
      document.addEventListener("mouseup", () => {
        document.removeEventListener("mousemove", resize, false);
      }, false);
    });

    function resize(e) {
      const size = `${e.x}px`;
      leftFrame.style.flexBasis = size;
    }
  }

Splitterの枠でマウスのボタンが押されると、ドキュメント全体に対して、mousemove と mouseup のイベントリスナーを設定します。 マウスポインタの移動が発生すると、resize()メソッドを呼び出し、左側のサイドバーの枠をリサイズします。 マウスのボタンがUPされ、ドラッグが終了すると、ドキュメント全体に設定されていた、mousemove のイベントリスナを削除します。
    splitter.addEventListener("mousedown", (event) => {

      document.addEventListener("mousemove", resize, false);
      document.addEventListener("mouseup", () => {
        document.removeEventListener("mousemove", resize, false);
      }, false);
    });

resize関数では、左側のサイドバーのflexBasisプロパティの値をマウスポインタの座標位置のxの値に更新します。
    function resize(e) {
      const size = `${e.x}px`;
      leftFrame.style.flexBasis = size;
    }

実行結果

上記のHTMLファイルをWebブラウザで表示します。下図のページが表示されます。


紫色のスプリッタの枠にマウスポインタを移動します。マウスポインタの形状が横方向のサイズ変更ポインタに変化します。


ドラッグすると、左側の枠がリサイズできます。




縦のスプリッタを実装できました。

補足:別の実装

先のコードでは、スプリッタでmousedownイベントが発生した際にドキュメント全体のmousemove, mouseupイベントリスナーを設定しましたが、 以下のコードでは、あらかじめ、ドキュメント全体のmousemove, mouseupイベントリスナーを設定しておきドラッグされているかのフラグを判定して、 ドラッグ処理を実装する方法です。
splitter-simple-02.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
  <link rel="stylesheet" href="splitter-simple-02.css"/>
  <script type="text/javascript">
    let StartDrag = false;

    window.onload = onLoad;

    function onLoad() {
      const splitter = document.querySelector("#Splitter1");
      const leftFrame = document.querySelector("#LeftFrame1");

      splitter.addEventListener('mousedown', e => {
        StartDrag = true;
      })
      document.addEventListener('mouseup', e => {
        if (StartDrag == true) {
          StartDrag = false;
          const size = `${e.x}px`;
          leftFrame.style.flexBasis = size;
        }
      })
      document.addEventListener('mousemove', e => {
        if (StartDrag == true) {
          const size = `${e.x}px`;
          leftFrame.style.flexBasis = size;
        }
      })
    }
  </script>
</head>
<body>
  <div class="Container">
    <div id="LeftFrame1" class="LeftFrame">
      <p>Item1</p>
      <p>Item2</p>
      <p>Item3</p>
      <p>Item4</p>
      <p>Item5</p>
    </div>
    <div id="Splitter1" class="Splitter">
    </div>
    <div class="RightFrame">
      <p>Content1</p>
      <p>Content2</p>
      <p>Content3</p>
      <p>Content4</p>
      <p>Content5</p>
    </div>
   </div>
</body>
</html>

splitter-simple-02.css は先の例の splitter-simple-01.css と同じファイルです。

補足:mouseup, mousemoveイベントリスナーをスプリッタの枠に設定する方法でも動作するのではないか?

先に紹介した例では、mouseup, mousemoveイベントリスナーをドキュメント全体に設定していますが、 下記のコードのように、mouseup, mousemoveイベントリスナーをスプリッタの枠に設定しても動作するように思えますが、 このコードはうまく動作しません。スプリッタ内でマウスダウンするまでは良いですが、マウスを少し速く移動させてしまうと、 スプリッタの枠外にマウスポインタが出てしまい、その際にmousedownイベントが発生せず正しく動作しないです。
  //うまく動作しない例

  splitter.addEventListener('mousedown', e => {
    StartDrag = true;
  })
  splitter.addEventListener('mouseup', e => {
    StartDrag = false;
    const size = `${e.x}px`;
    leftFrame.style.flexBasis = size;
  })
  splitter.addEventListener('mousemove', e => {
    if (StartDrag == true) {
      const size = `${e.x}px`;
      leftFrame.style.flexBasis = size;
    }
  })

ドラッグ中にマウスポインタがデフォルトに戻らないようにする

先に紹介したHTMLファイルでは、ドラッグ中のマウスポインタの形状がデフォルトの矢印の形状に戻ってしまいます。
この現象は、マウス移動直後に左側のサイドバーのリサイズをした直後に、マウスポインタがスプリッタの枠の外側に出てしまうためです。



対処法は、リサイズ時にマウスポインタがスプリッタの枠の外側に出ないよう、スプリッタの幅の半分の値を引いてサイドバーの flexBasis プロパティを設定します。
splitter-simple-03.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="splitter-simple-03.css"/>
  <script type="text/javascript">
    let StartDrag = false;

    window.onload = onLoad;

    function onLoad() {
      const splitter = document.querySelector("#Splitter1");
      const leftFrame = document.querySelector("#LeftFrame1");

      splitter.addEventListener("mousedown", (event) => {

        document.addEventListener("mousemove", resize, false);
        document.addEventListener("mouseup", () => {
          document.removeEventListener("mousemove", resize, false);
        }, false);
      });

      function resize(e) {
        const size = `${e.x-8}px`; // 8 = スプリッタの幅の半分
        leftFrame.style.flexBasis = size;
      }
    }
  </script>
</head>
<body>
  <div class="Container">
    <div id="LeftFrame1" class="LeftFrame">
      <p>Item1</p>
      <p>Item2</p>
      <p>Item3</p>
      <p>Item4</p>
      <p>Item5</p>
    </div>
    <div id="Splitter1" class="Splitter">
    </div>
    <div class="RightFrame">
      <p>Content1</p>
      <p>Content2</p>
      <p>Content3</p>
      <p>Content4</p>
      <p>Content5</p>
    </div>
   </div>
</body>
</html>

splitter-simple-03.css は先の例の splitter-simple-01.css と同じファイルです。

実行結果

上記のHTMLファイルをWebブラウザで表示し、スプリッタのエリアをドラッグすると、ドラッグ中もマウスポインタが元の矢印の形状に戻らずにリサイズできます。


スプリッタの領域にマージンがある場合

スプリッタでサイドバーの幅を可変にできる領域の外側にマージンのある場合の例です。
splitter-simple-04.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
  <link rel="stylesheet" href="splitter-simple-04.css"/>
  <script type="text/javascript">
    let StartDrag = false;

    window.onload = onLoad;

    function onLoad() {
      const splitter = document.querySelector("#Splitter1");
      const leftFrame = document.querySelector("#LeftFrame1");

      splitter.addEventListener("mousedown", (event) => {
        document.addEventListener("mousemove", resize, false);
        document.addEventListener("mouseup", () => {
          document.removeEventListener("mousemove", resize, false);
        }, false);
      });

      function resize(e) {
        const size = `${e.x -8}px`; // 8 = スプリッタの幅の半分
        leftFrame.style.flexBasis = "calc("+size+" - 4rem)";
      }
    }
  </script>
</head>
<body>
  <div class="Container">
    <div id="LeftFrame1" class="LeftFrame">
      <p>Item1</p>
      <p>Item2</p>
      <p>Item3</p>
      <p>Item4</p>
      <p>Item5</p>
    </div>
    <div id="Splitter1" class="Splitter">
    </div>
    <div class="RightFrame">
      <p>Content1</p>
      <p>Content2</p>
      <p>Content3</p>
      <p>Content4</p>
      <p>Content5</p>
    </div>
   </div>
  <div id="output"></div>
</body>
</html>
splitter-simple-04.css
body {
  margin:4rem 2rem 2rem 4rem;
  padding:0;
}

.Container {
  display: flex;
  border: 1px solid #808080;
}

.LeftFrame {
  background-color: #F0F0F0;
  /*resize: vertical;*/
  flex-basis: 160px;
}
.RightFrame {
}

.Splitter {
  flex-basis: 16px;
  cursor: col-resize; 
  background-color: #360090;
}

解説

HTMLをWebブラウザで表示すると下図の画面になります。



修正前の下記のresize 関数のコードを利用した場合の動作を確認します。
  function resize(e) {
    const size = `${e.x-8}px`; // 8 = スプリッタの幅の半分
    leftFrame.style.flexBasis = size;
  }

スプリッタにマウスポインタを重ねます。マウスポインタの形状が変化します。


マウスボタンを押して、マウスを移動させると、スプリッタの位置が右側に飛びます。
マウスポインタの位置を左サイドバーのflexBasisプロパティに設定しているため、マージンを含めた値が設定されてしまうため、 左サイドバーがマージンのサイズだけ広くなってしまい、スプリッタの位置が右にずれてしまいます。


resize関数を下記のコードに変更します。
サイドバーのサイズが変更できる親の枠がページに対して4remのマージンがついているため、 flexBasis を設定する際に 4rem引きます。ピクセルから 4remを引く場合には、CSSのcalc関数を利用します。
マウスポインタの座標値が 240pxの場合、次の文字列をflexBasisに設定します。
CSSのcalc関数の詳細はこちらの記事を参照してください。
calc(240px - 4rem)

文字列を整形してflexBasisに代入するロジックが以下となります。
  function resize(e) {
    const size = `${e.x -8}px`; // 8 = スプリッタの幅の半分
    leftFrame.style.flexBasis = "calc("+size+" - 4rem)";
  }

body {
  margin:4rem 2rem 2rem 4rem;
  padding:0;
}

修正後のコードの動作を確認します。ページを表示します。スプリッタ上にマウスポインタを移動します。


ドラッグします。スプリッタの位置が飛ばずにサイズが変更できます。



親要素にマージンがある場合でのサイドバーのドラッグによるリサイズが実装できました。
著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2023-04-12
作成日: 2023-04-11
iPentec all rights reserverd.