<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>複数タブ切り替え</title>
<style>
:root { --bg:#0b1020; --panel:#121a2e; --ink:#e8ecf3; --muted:#98a2b3; --accent:#3b82f6; --border:#243049; }
*{ box-sizing:border-box; }
html,body{ height:100%; }
body{ margin:0; background:var(--bg); color:var(--ink); font-family:system-ui,-apple-system,Segoe UI,Roboto,"Noto Sans JP",sans-serif; }
header{ padding:10px 12px; border-bottom:1px solid var(--border); background:linear-gradient(180deg,#0f172a,#0b1020); position:sticky; top:0; z-index:10; }
.row{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.brand{ font-weight:700; margin-right:auto; letter-spacing:.02em; }
button, .ghost {
background:#1b2541; color:var(--ink); border:1px solid var(--border);
padding:8px 10px; border-radius:10px; cursor:pointer; font-size:14px;
}
button:hover{ background:#223059; }
.ghost{ background:transparent; }
input[type="text"]{ background:#0f172a; color:var(--ink); border:1px solid var(--border); padding:8px; border-radius:10px; min-width:260px; }
.tabs{ display:flex; gap:6px; padding:8px 12px; overflow:auto; border-bottom:1px solid var(--border); background:#0e1527; }
.tab{
display:flex; align-items:center; gap:8px; max-width:260px;
background:#16213c; border:1px solid var(--border); color:var(--ink);
padding:6px 10px; border-radius:10px; cursor:pointer; user-select:none;
}
.tab.active{ background:linear-gradient(180deg,#1b2b55,#172649); border-color:#355093; }
.tab .title{ white-space:nowrap; text-overflow:ellipsis; overflow:hidden; }
.tab .badge{ font-size:11px; color:var(--muted); border:1px solid var(--border); padding:0 6px; border-radius:999px; }
.tab .close{ margin-left:4px; opacity:.8; }
.tab .close:hover{ opacity:1; }
.dropzone{
display:none; /* 初期は非表示。トグルで表示 */
margin:10px 12px; padding:16px; border:2px dashed var(--border); border-radius:12px;
color:var(--muted); text-align:center; background:#0f172a;
}
.dropzone.dragover{ border-color:var(--accent); color:var(--ink); }
.view{ position:fixed; inset:120px 0 0 0; }
iframe{
width:100%; height:100%; border:0; background:#fff;
}
details.howto{ margin:10px 12px; color:var(--muted); }
code{ background:#0f172a; padding:2px 6px; border-radius:6px; border:1px solid var(--border); }
</style>
</head>
<body>
<header>
<div class="row">
<div class="brand"><small>ローカルHTMLタブビューア<a data-path="◯◯◯" class="openfolder"◯◯フォルダ</a></small></div>
<button id="btnAddPreset" title="プリセットに記述した相対パスを開く">プリセットを読み込む</button>
<input id="urlBox" type="text" placeholder="(参考)URLを入力して開く ※多くのサイトは埋め込み不可" />
<button id="btnOpenURL" class="ghost">URLをタブで開く</button>
<input id="filePicker" type="file" accept=".html,.htm" multiple style="display:none" />
<button id="btnPick">ファイルを選択</button>
<button id="btnToggleDrop" class="ghost">ドラッグ&ドロップ欄</button>
<button id="btnClearAll" class="ghost" title="タブをすべて閉じる">すべて閉じる</button>
</div>
</header>
<div id="dropzone" class="dropzone">ここにローカルの <strong>.html / .htm</strong> をドラッグ&ドロップ</div>
<div id="tabs" class="tabs" role="tablist" aria-label="tabs"></div>
<div class="view">
<iframe id="viewer" sandbox="allow-scripts allow-forms allow-same-origin"></iframe>
</div>
<details class="howto">
<summary>使い方&注意点</summary>
<ol>
<li><b>プリセット方式(推奨)</b>:このファイルと同じフォルダにあるHTMLを、下の <code>PRESET_TABS</code> に相対パスで追記し、「プリセットを読み込む」を押してください。画像やCSSの相対パスもそのまま機能します。</li>
<li><b>ファイル選択/D&D方式</b>:その場でHTMLを読み込んで表示します(<code>srcdoc</code>で注入)。<u>そのHTML内の相対リンクやCSS・画像パスは動きません</u>(ブラウザの制限のため)。</li>
<li>外部サイト(例:Google、X など)は多くが <code>X-Frame-Options / CSP</code> で <b>iframe 埋め込みを拒否</b>しており表示できません。</li>
<li>このビューアをダブルクリックで <b>file://</b> として開くのが簡単です。同一フォルダ配下の相対パスであればプリセット方式が一番安定します。</li>
</ol>
<p>プリセット編集例:</p>
<pre><code>// 例:このtabs-viewer.htmlと同じフォルダに a.html と docs/b.html がある場合
const PRESET_TABS = [
{ title: "Aページ", src: "./a.html" }, // 相対パス
{ title: "Bページ", src: "./docs/b.html" } // サブフォルダ
];</code></pre>
</details>
<script>
/* =========================
1) プリセット(相対パスで安定)
========================= */
const PRESET_TABS = [
{ title: "◯◯◯, src: "4masu-memo.html" },
{ title: "◯◯◯", src: "◯◯◯.html" },
{ title: "◯◯◯", src: "◯◯◯.html" },
{ title: "◯◯◯", src: "◯◯◯.html" },
{ title: "◯◯◯", src: "◯◯◯.html" },
{ title: "◯◯◯", src: "◯◯◯.html" },
{ title: "◯◯◯", src: "◯◯◯.html" },
// ここを書き換えて使ってください(例)
// { title: "サンプル1", src: "./sample1.html" },
// { title: "サンプル2", src: "./sub/sample2.html" },
];
/* =========================
タブ管理
========================= */
const tabsEl = document.getElementById('tabs');
const viewer = document.getElementById('viewer');
const filePick = document.getElementById('filePicker');
const urlBox = document.getElementById('urlBox');
const dropzone = document.getElementById('dropzone');
let tabs = []; // { id, title, kind: 'preset'|'srcdoc'|'url', src?, html? }
let activeId = null;
let idSeq = 1;
function addTabFromPreset(title, src){
const id = 't' + (idSeq++);
tabs.push({ id, title, kind:'preset', src });
renderTabs();
activate(id);
}
function addTabFromHTMLFile(file){
const reader = new FileReader();
reader.onload = () => {
const html = String(reader.result||'');
const id = 't' + (idSeq++);
tabs.push({ id, title: file.name, kind:'srcdoc', html });
renderTabs();
activate(id);
};
reader.readAsText(file, 'utf-8');
}
function addTabFromURL(url){
const id = 't' + (idSeq++);
tabs.push({ id, title: url, kind:'url', src: url });
renderTabs();
activate(id);
}
function closeTab(id){
const idx = tabs.findIndex(t => t.id === id);
if(idx === -1) return;
const wasActive = (activeId === id);
tabs.splice(idx,1);
renderTabs();
if(!tabs.length){
activeId = null;
viewer.removeAttribute('src');
viewer.removeAttribute('srcdoc');
return;
}
if(wasActive){
const next = tabs[Math.max(0, idx-1)];
activate(next.id);
}
}
function renderTabs(){
tabsEl.innerHTML = '';
for(const t of tabs){
const el = document.createElement('div');
el.className = 'tab' + (t.id===activeId ? ' active' : '');
el.setAttribute('role', 'tab');
el.setAttribute('aria-selected', t.id===activeId ? 'true' : 'false');
el.title = t.title;
const title = document.createElement('div');
title.className = 'title';
title.textContent = t.title;
const badge = document.createElement('div');
badge.className = 'badge';
badge.textContent = t.kind;
const close = document.createElement('button');
close.className = 'close ghost';
close.textContent = '✕';
close.title = '閉じる';
close.onclick = (e)=>{ e.stopPropagation(); closeTab(t.id); };
el.onclick = ()=> activate(t.id);
el.append(title, badge, close);
tabsEl.appendChild(el);
}
}
function activate(id){
activeId = id;
renderTabs();
const t = tabs.find(x => x.id === id);
if(!t) return;
// 切り替え時は viewer を都度セットし直す
viewer.removeAttribute('src');
viewer.removeAttribute('srcdoc');
if(t.kind === 'preset' || t.kind === 'url'){
viewer.src = t.src; // 相対パス or URL
}else if(t.kind === 'srcdoc'){
viewer.srcdoc = t.html;
}
}
/* =========================
UIイベント
========================= */
document.getElementById('btnAddPreset').onclick = ()=>{
if(!PRESET_TABS.length){
alert('PRESET_TABS が空です。ファイル内のコメントを参考に相対パスを追記してください。');
return;
}
// 重複しないように、一旦全タブ閉じるか聞く
const addToExisting = tabs.length && confirm('既存タブを残したままプリセットを追加しますか?\n(キャンセル=既存タブを全て閉じてから読み込み)');
if(!addToExisting){ tabs = []; activeId = null; }
for(const p of PRESET_TABS){
addTabFromPreset(p.title || p.src, p.src);
}
};
document.getElementById('btnPick').onclick = ()=> filePick.click();
filePick.onchange = (e)=>{
const files = Array.from(e.target.files||[]);
files.forEach(f => addTabFromHTMLFile(f));
filePick.value = ''; // 連続選択可
};
document.getElementById('btnOpenURL').onclick = ()=>{
const url = urlBox.value.trim();
if(!url) return;
addTabFromURL(url);
};
document.getElementById('btnClearAll').onclick = ()=>{
if(!tabs.length) return;
if(confirm('すべてのタブを閉じますか?')){
tabs = [];
activeId = null;
renderTabs();
viewer.removeAttribute('src');
viewer.removeAttribute('srcdoc');
}
};
document.getElementById('btnToggleDrop').onclick = ()=>{
dropzone.style.display = (dropzone.style.display==='block' ? 'none' : 'block');
};
['dragenter','dragover'].forEach(ev=>{
dropzone.addEventListener(ev, e => { e.preventDefault(); dropzone.classList.add('dragover'); });
});
['dragleave','drop'].forEach(ev=>{
dropzone.addEventListener(ev, e => { e.preventDefault(); dropzone.classList.remove('dragover'); });
});
dropzone.addEventListener('drop', e=>{
const files = Array.from(e.dataTransfer.files||[]).filter(f => /\.html?$/i.test(f.name));
if(!files.length){ alert('HTMLファイル(.html / .htm)をドロップしてください。'); return; }
files.forEach(f => addTabFromHTMLFile(f));
});
</script>
<script src="foloder-test.js"></script>
</body>
</html>
目次
コメント