JavaScriptでタブ表示の動的生成

# HTMLの中にタブ表示を作ります。

![test_tabs_simple.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/ef13175c-1174-4bea-e8d1-abb8075a1c9d.gif)

<!--<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/a1cdfc53-0757-51b3-2a7d-308eb0e7df1d.png">-->

# タブはJavaScript内に記述したデータから動的に作成されます。

```html:test_tabs_simple.html
<!DOCTYPE html>

<head>
    <meta charset="utf-8">
</head>

<body>
    <!-- タブ表示を行うエレメントを生成しておく。 -->
    <div id="tabs1"></div>
</body>
<!-- scriptタグは type='module' とする。-->
<script type="module">
    // javascriptファイルの読み込み
    import { createTabs } from "./tabs.mjs";
    const tabs = createTabs(document.getElementById("tabs1"));

    // 1つ目のタブを生成
    const tab1 = { "title": document.createElement("span"), "content": document.createElement("span") };
    tab1.title.innerText = "タブ1";
    tab1.content.innerText = "タブ1の内容、ないよう、ナイヨウ、NAIYOU、ないよー😭、にゃいよー🐱";
    tabs.add(tab1.title, tab1.content);

    // 2つ目のタブを生成
    const tab2 = { "title": document.createElement("span"), "content": document.createElement("span") };
    tab2.title.innerText = "タブ2";
    tab2.content.innerHTML = "<a href='https://qiita.com/'>qiita</a>";
    tabs.add(tab2.title, tab2.content);
</script>

</html>
```

# タブを生成するJavaScriptライブラリ (tabs.mjs)


- uuidを生成するために、[uuid v4 cdn](https://unpkg.com/uuid@latest/dist/umd/uuidv4.min.js)を使用いたします。
- createTabsメソッドがエクスポートされます。

```javascript:tabs.mjs
await import("https://unpkg.com/uuid@latest/dist/umd/uuidv4.min.js");
export const createTabs = (element, listener) => {
    const tabs = Object.create(Tabs);
    tabs.uuid = uuidv4();
    tabs.parent = element;
    tabs.parent.classList.add(`tabs_${tabs.uuid}`);
    tabs.buttons = document.createElement("div");
    tabs.buttons.appendChild(
        [e => e.classList.add("content0", "lamp"), e => e.id = `lamp_${tabs.uuid}`].
            reduce((a, e) => { e(a); return a; }, document.createElement("div"))
    );
    tabs.parent.appendChild(
        [e => e.id = `tab-buttons_${tabs.uuid}`].
            reduce((a, e) => { e(a); return a; }, tabs.buttons)
    );
    tabs.contents = document.createElement("div");
    tabs.parent.appendChild(
        [e => e.id = `tab-content_${tabs.uuid}`].
            reduce((a, e) => { e(a); return a; }, tabs.contents)
    );
    listener === undefined || tabs.add_listener(listener);
    return tabs;
};
const Tabs = {
    // タブが選択されたときに呼び出されるリスナー
    listeners: [],
    add_listener: function (f) {
        this.listeners.push(f);
    },
    // cssの生成
    update_css: function () {
        const id = `style_${this.uuid}`;
        const n = this.buttons.children.length - 1;
        const w = Number(100.0 / n).toFixed(2);
        document.getElementById(id)?.remove();
        const style = document.createElement("style");
        style.id = id;
        style.innerText = `.tabs_${this.uuid} {margin:10px auto;position:relative;}` +
            `#tab-buttons_${this.uuid} span{cursor:pointer;border-bottom:2px solid #ddd;display:block;float:left;text-align:center;height:40px;line-height:40px;width:${w}%;}` +
            `#tab-content_${this.uuid} {border-bottom:3px solid #ddd;padding:15px;display:inline-block;}` +
            `#lamp_${this.uuid} {height:2px;background:#333;display:block;position:absolute;top:40px;transition:all .3s ease-in;width:${w}%;}` +
            [...Array(n).keys()].map(i => `#lamp_${this.uuid}.content${i} {left:${Number(i * 100 / n).toFixed(2)}%;}`).join(" ");
        document.head.appendChild(style);
    },
    // tabがクリックされたときの処理
    onclick_tab: function (e) {
        const clist = document.getElementById(`lamp_${this.uuid}`).classList;
        [...clist].filter(c => c.startsWith("content")).forEach(c => clist.remove(c));
        const content_name = [...e.classList].filter(c => c.startsWith("content"))[0];
        clist.add(content_name);

        const contents = document.getElementById(`tab-content_${this.uuid}`);
        if (contents.children.length > 0) {
            Array.from(contents.children).forEach(c => c.style.display = "none");
            contents.querySelectorAll(`.${e.className}`)[0].style.display = "block";
        }
        const title = this.buttons.querySelector(`.${content_name}:not(.lamp)`);
        this.listeners.forEach(l => { l({ name: content_name, title: title }) });
    },
    // タブの追加
    add: function (label, content) {
        const cno = Number([...([...this.buttons.querySelectorAll(".lamp ~ *")].slice(-1)[0]?.classList || ["content-1"])].filter(e=>e.startsWith("content"))[0].replace("content",""))+1;
        const name = `content${cno}`;
        this.buttons.appendChild(
            [
                e => e.classList.add(name),
                e => e.addEventListener("click", ev => this.onclick_tab(ev.currentTarget)),
            ].reduce((a, e) => { e(a); return a; }, label)
        );
        content === undefined || this.contents.appendChild(
            [e => e.classList.add(name),].
                reduce((a, e) => { e(a); return a; }, content)
        );
        this.update_css();
        const current_name = [...document.getElementById(`lamp_${this.uuid}`).classList].filter(c => c.startsWith("content"))[0];

        this.onclick_tab(this.buttons.querySelector(`.${current_name}:not(.lamp)`));
        return name;
    },
    remove: function(name){
        return this.remove_from_name(name);
    },
    remove_from_name: function(name){
        const e = this.get_title_element_from_name(name);
        e?.remove();
        this.update_css();
    },
    remove_from_content_no: function(cno){
        const e = this.get_title_element_from_content_no(name);
        e?.remove();
        this.update_css();
    },
    // タブの変更
    replace: function (cno, label) {
        const e = this.get_title_element_from_content_no(cno);
        if (e != null) {
            this.buttons.insertBefore(
                [
                    e => e.classList.add(cno),
                    e => e.addEventListener("click", ev => this.onclick_tab(ev.currentTarget)),
                ].reduce((a, e) => { e(a); return a; }, label), e
            );
            e.remove();
        }
    },
    // タイトルのHTMLエレメント取得
    get_title_element_from_content_no: function (cno) {
        return this.buttons.querySelector(`.${cno}:not(.lamp)`);
    },
    get_title_element_from_name: function(name){
        return [...this.buttons.querySelectorAll(".lamp ~ *")].filter(e=>e.innerText.match(new RegExp(name)))[0];
    },
    // タブ数の取得
    length: function () {
        return this.buttons.children.length - 1;
    },
};
```

# ライブラリ(tabs.mjs)の使用例（詳細）

以下のようにタブグループを複数作成したり、動的にタブを追加したりすることができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/75534ce6-cf59-18a8-8985-c66d93f7b7b1.png">


上のサンプルのHTMLファイルです。

```html:test_tabs.html
<!DOCTYPE html>

<head>
    <meta charset="utf-8">
    <script src="https://kit.fontawesome.com/13ddd76903.js" crossorigin="anonymous"></script>
</head>

<body>
    <div id="tabs1"></div>

    <div style="width:80%;">
        <div id="tabs2"></div>
    </div>
    <button id="bt0">タブの動的追加</button>
    <button id="bt1">タブの動的削除</button>

</body>
<script type="module">

    import { createTabs } from "./tabs.mjs";
    let tabs = undefined;
    tabs = createTabs(document.getElementById("tabs1"));
    [
        ["使用方法", "createTabsメソッドにてタブを管理する変数（タブコントロールと呼ぶことにします）を取得します。そのタブコントロールのaddメソッドを呼び出して、タブを追加します。addメソッドの引数は、タブのタイトル、タブ内のコンテンツともElementの形式で指定してください。createTabsメソッドによりタブコントロールを複数作成することもできます。"],
        ["制限事項", "<ul><li>画面の横幅が小さいと表示が崩れます。</li><li>画面表示後にタブを追加することはできますが、削除することはできません。</li><li>html内のscriptタグにはtype='module'を指定する必要があります。</li><li>importを使用しているためfile:プロトコルだとCORSエラーで動作しません。</li></ul>"],
        ["参考", "The design and other structures are based on <a href='https://codepen.io/hamzadhamiya/pen/bltnA'>here</a>. Thank you very much."],
    ].forEach(p => tabs.add(
        [e => e.innerHTML = p[0]].reduce((a, e) => { e(a); return a; }, document.createElement("span")),
        [e => e.innerHTML = p[1]].reduce((a, e) => { e(a); return a; }, document.createElement("span"))
    ));
    tabs = createTabs(document.getElementById("tabs2"));
    [["準備", "タブコントロールを挿入するHTMLエレメントの作成、importによるtabs.mjsの読み込みなどです。このHTMLファイルを参考にしてみてください。"],
    ["<span style='font-size:small;width:100%;'><i class='far fa-copy'></i> タブの動的追加</span>", "タブコントロールに新しいタブを追加します。</span>"]].forEach(p => tabs.add(
        [e => e.innerHTML = p[0]].reduce((a, e) => { e(a); return a; }, document.createElement("span")),
        [e => e.innerHTML = p[1]].reduce((a, e) => { e(a); return a; }, document.createElement("span"))
    ));
    document.querySelector("#bt0").addEventListener("click", e => {
        const n = tabs.length() + 1;
        [[`新規 ${n}`, `タブ内のコンテンツ ${n}`]].forEach(p => tabs.add(
            [e => e.textContent = p[0]].reduce((a, e) => { e(a); return a; }, document.createElement("span")),
            [e => e.textContent = p[1]].reduce((a, e) => { e(a); return a; }, document.createElement("span"))
        ));
    })
    document.querySelector("#bt1").addEventListener("click", e => {
        tabs.remove_from_name("タブの動的追加");
    })
</script>

</html>
```



# 参考

- The design and other structures are based on <a href='https://codepen.io/hamzadhamiya/pen/bltnA'>here</a>. Thank you very much.