◆ タイトルどおりのコンポーネントを作ってみました

flexbox で上下や左右に並べたとき パーセントや flex-grow などの自動調整だけではなく ユーザがドラッグしてサイズ調整できるようにしたいことありますよね
コンポーネントにしておくと便利そうだったので作ってみました

使い方

基本的なこと

split-container 要素の子要素に分割したそれぞれのエリアに配置する要素を配置します
slot 属性で first と second を指定します

<split-container direction="row" position="30">
<div slot="first">aaaa</div>
<div slot="second">bbbb</div>
</split-container>

split-container の direction 属性が row なら横に並び column なら縦にならびます
row の場合は first が左側で second が右側です
column の場合は first が上側で second が下側です

split-container は中に splitter みたいな分割するタグを自由に配置はできず 「2 つに分割する」 という要素です
複数に分割したい場合はネストさせます

<split-container direction="row">
<split-container slot="first" direction="column">
<split-container slot="first" direction="row">
<div slot="first">A1</div>
<div slot="second">A2</div>
</split-container>
<split-container slot="second" direction="row">
<div slot="first">B1</div>
<div slot="second">B2</div>
</split-container>
</split-container>
<split-container slot="second" direction="column">
<split-container slot="first" direction="row">
<div slot="first">X1</div>
<div slot="second">X2</div>
</split-container>
<split-container slot="second" direction="row">
<div slot="first">Y1</div>
<div slot="second">Y2</div>
</split-container>
</split-container>
</split-container>

<split-container direction="row" position="20">
<div slot="first">A</div>
<split-container slot="second" direction="row" position="20">
<div slot="first">B</div>
<split-container slot="second" direction="row" position="20">
<div slot="first">C</div>
<split-container slot="second" direction="row">
<div slot="first">D</div>
<div slot="second">E</div>
</split-container>
</split-container>
</split-container>
</split-container>

ネストの違い

2 つに分割してそれぞれを更に分割 とするか 片方をどんどん分割していくかはどちらでもできます
ただし split-container を含む要素のサイズが変わったときに内側のリサイズのされ方に違いはあります

<split-container direction="row">
<split-container slot="first" direction="row">
<div slot="first">A</div>
<div slot="second">B</div>
</split-container>
<split-container slot="second" direction="row">
<div slot="first">C</div>
<div slot="second">D</div>
</split-container>
</split-container>

<split-container direction="row">
<div slot="first">A</div>
<split-container slot="second" direction="row">
<div slot="first">B</div>
<split-container slot="second" direction="row">
<div slot="first">C</div>
<div slot="second">D</div>
</split-container>
</split-container>
</split-container>

とした場合 どちらも 4 つに分割されて 3 の境界線があります
真ん中の境界線をドラッグで動かす場合 上側の例だと真ん中の境界線で区切られた両方に split-container があります
「A+B | C+D」 となっているので A+B と C+D のコンテナサイズが変わり A,B,C,D すべてのサイズに影響します
下側の例だと 「A | B+C+D」 となった中に 「B | C+D」 があります
真ん中の境界線はこの 「B | C+D」 のところなので ここを動かすと B,C,D に影響しますが A のサイズには影響しません

この違いでどっちにするか選べばよいです

サイズ

デフォルトの境界線の位置は position 属性で指定します
first に割り当てるサイズをパーセントで指定します
20 を指定すると first が 20% で second が 80% のサイズになります
デフォルトは 50 で境界線を中央に配置します

各エリアに最低限のサイズを指定したい場合は first-min と second-min 属性を使います
これらは row/column に応じて min-width か min-height を設定します
px など単位付きで CSS として有効なフォーマットでサイズを指定できます

制限

属性値は初期化時に使用され 以降は変更しても反映されません
ユーザが変更する場所なのでプログラムから操作しないでしょうし 対応するのが面倒なので対応してません
プロパティとしての書き換えもなしです

プレビューで動かせる例です

<!doctype html>

<script>
customElements.define("split-container", class extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) return
const direction = this.getAttribute("direction") === "row" ? "row" : "column"
this.removeAttribute("direction")
const position = ~~this.getAttribute("position") || 50
this.removeAttribute("position")
const first_min = this.getAttribute("first-min") || 0
this.removeAttribute("first-min")
const second_min = this.getAttribute("second-min") || 0
this.removeAttribute("second-min")

this.attachShadow({ mode: "open" }).innerHTML = `
<style>
* {
box-sizing: border-box;
}
#container {
width: 100%;
height: 100%;
display: flex;
}
.column {
flex-flow: column;
}
#splitter {
min-width:3px;
min-height: 3px;
background: silver;
use-select: none;
}
.row #splitter {
cursor: col-resize;
}
.column #splitter {
cursor: row-resize;
}
#splitter:hover {
background: deepskyblue;
}
#first,#second {
flex: 1 0 0;
overflow: auto;
}
</style>
<div id="container" class="${direction}">
<div id="first">
<slot name="first"></slot>
</div>
<div id="splitter"></div>
<div id="second">
<slot name="second"></slot>
</div>
</div>
`

const container = this.shadowRoot.querySelector("#container")
const first = this.shadowRoot.querySelector("#first")
const second = this.shadowRoot.querySelector("#second")
const splitter = this.shadowRoot.querySelector("#splitter")

first.style.flexGrow = position
second.style.flexGrow = 100 - position
if (direction === "row") {
first.style.minWidth = first_min
second.style.minWidth = second_min
} else {
first.style.minHeight = first_min
second.style.minHeight = second_min
}

let handling = false
splitter.addEventListener("mousedown", (event) => {
handling = true
event.preventDefault()
})
window.addEventListener("mousemove", (event) => {
if (handling) {
const { x, y } = container.getBoundingClientRect()
if (direction === "row") {
const left = event.clientX - x
first.style.flexGrow = left
second.style.flexGrow = container.clientWidth - left
} else {
const top = event.clientY - y
first.style.flexGrow = top
second.style.flexGrow = container.clientHeight - top
}
}
})
window.addEventListener("mouseup", () => {
handling = false
})
}
})

</script>

<style>
* {
box-sizing: border-box;
}
.split {
border: 1px solid silver;
margin: 30px;
display: block;
width: 300px;
height: 300px;
}
.split2 {
border: 1px solid silver;
margin: 30px;
display: block;
width: 500px;
height: 500px;
}
.split2 split-container {
width: 100%;
height: 100%;
}
</style>

<split-container class="split" direction="row" position="30">
<div slot="first">aaaa</div>
<div slot="second">bbbb</div>
</split-container>

<split-container class="split" direction="column" position="30">
<div slot="first">aaaa</div>
<div slot="second">bbbb</div>
</split-container>

<split-container class="split" direction="row" first-min="100px">
<div slot="first">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</div>
<div slot="second">bbbb</div>
</split-container>

<split-container class="split" direction="column" second-min="100px">
<div slot="first">aaaa</div>
<div slot="second">bbbb</div>
</split-container>

<split-container class="split2" direction="row">
<split-container slot="first" direction="column">
<split-container slot="first" direction="row">
<div slot="first">A1</div>
<div slot="second">A2</div>
</split-container>
<split-container slot="second" direction="row">
<div slot="first">B1</div>
<div slot="second">B2</div>
</split-container>
</split-container>
<split-container slot="second" direction="column">
<split-container slot="first" direction="row">
<div slot="first">X1</div>
<div slot="second">X2</div>
</split-container>
<split-container slot="second" direction="row">
<div slot="first">Y1</div>
<div slot="second">Y2</div>
</split-container>
</split-container>
</split-container>

<split-container class="split" direction="row" position="20">
<div slot="first">A</div>
<split-container slot="second" direction="row" position="20">
<div slot="first">B</div>
<split-container slot="second" direction="row" position="20">
<div slot="first">C</div>
<split-container slot="second" direction="row">
<div slot="first">D</div>
<div slot="second">E</div>
</split-container>
</split-container>
</split-container>
</split-container>