HTMLの中にタブ表示を作ります。
タブは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.