0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScriptでタブ表示の動的生成

Last updated at Posted at 2021-11-21

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

test_tabs_simple.gif

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

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を使用いたします。
  • createTabsメソッドがエクスポートされます。
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)の使用例(詳細)

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

上のサンプルの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 here. Thank you very much.
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?