備忘録としてまとめておきます。
document.addEventListener('DOMContentLoaded', function () {
/*───────────────────────────────────────────────
1. ヘッダーナビのリンク先を決定
───────────────────────────────────────────────*/
const currentPathname = window.location.pathname;
const pathSegments = currentPathname.split('/');
const currentDir = pathSegments[pathSegments.length - 2]; // 現在のディレクトリ名
const currentFilePath = window.location.pathname;
let homeLink, blogLink, sonotaLink, gijyutuLink;
if (currentDir === 'blog') {
homeLink = '../index.html';
blogLink = 'blog.html';
sonotaLink = '../sonota/sonota.html';
gijyutuLink = '../gijyutu/gijyutu.html';
} else if (currentDir === 'kihon') {
homeLink = '../index.html';
blogLink = '../blog/blog.html';
sonotaLink = '../sonota/sonota.html';
gijyutuLink = '../gijyutu/gijyutu.html';
} else if (currentDir === 'sonota') {
homeLink = '../index.html';
blogLink = '../blog/blog.html';
sonotaLink = 'sonota.html';
gijyutuLink = '../gijyutu/gijyutu.html';
} else if (currentDir === 'gijyutu') {
homeLink = '../index.html';
blogLink = '../blog/blog.html';
sonotaLink = '../sonota/sonota.html';
gijyutuLink = 'gijyutu.html';
} else if (currentDir === 'zakki') {
homeLink = '../index.html';
blogLink = '../blog/blog.html';
sonotaLink = '../sonota/sonota.html';
gijyutuLink = '../gijyutu/gijyutu.html';
} else {
homeLink = 'index.html';
blogLink = 'blog/blog.html';
sonotaLink = 'sonota/sonota.html';
gijyutuLink = 'gijyutu/gijyutu.html';
}
/*───────────────────────────────────────────────
2. Windows パスを取得(UNC 対応)
‑ file:///D:/foo/bar.html → D:\foo\bar.html
‑ file://server/share/foo → \\server\share\foo\bar.html
───────────────────────────────────────────────*/
function getWindowsFullPath () {
const url = new URL(window.location.href);
if (url.protocol !== 'file:') return '';
// UNC (file://server/share/...)
if (url.host) {
return '\\\\' + url.host + decodeURIComponent(url.pathname).replace(/\//g, '\\');
}
// ローカルドライブ (file:///D:/...)
return decodeURIComponent(url.pathname)
.replace(/\//g, '\\') // / → \
.replace(/^\\/, ''); // 先頭の \ を除去
}
const fullPathWin = getWindowsFullPath(); // 例: D:\work\index.html or \\server\share\index.html
const dirPathWin = fullPathWin.substring(0, fullPathWin.lastIndexOf('\\'));
const VsPathWin = `vscode://file${currentFilePath}`;
const VsDirPathWin = `vscode://file${currentFilePath.substring(0, currentFilePath.lastIndexOf('/'))}`;
/*───────────────────────────────────────────────
3. VS Code / メモ帳 / Explorer 用 URI を生成
───────────────────────────────────────────────*/
const toPosix = p => p.replace(/\\/g, '/');
const vscodeFileUri = `vscode://file${toPosix(currentFilePath)}`;
const vscodeFolderUri = `vscode://file${toPosix(currentFilePath.substring(0, currentFilePath.lastIndexOf('/')))}`;
const notepadFileUri = `note:${fullPathWin}`;
const notepadFolderUri= `note:${dirPathWin}`;
const explorerFolderUri = dirPathWin.toLowerCase().startsWith('explorer:')
? dirPathWin
: `explorer:${dirPathWin}`;
/*───────────────────────────────────────────────
4. ヘッダとサイドバーを描画
───────────────────────────────────────────────*/
const headerContent = `
<header class="main-header">
<div class="container">
<div class="header-left">
<button class="hamburger-menu" aria-label="メニューを開く">
<span class="bar"></span><span class="bar"></span><span class="bar"></span>
</button>
<div class="logo"><a href="${homeLink}">manual</a></div>
</div>
<nav class="main-nav" id="main-nav-menu">
<ul>
<li><a href="${sonotaLink}">その他</a></li>
<li><a href="${blogLink}">ブログ</a></li>
<li><a href="${gijyutuLink}">技術</a></li>
<!-- <li class="nav-when-sidebar"><a href="/kyoyubunsyo/manual-black/zakki/zakki.html">雑記</a></li> -->
</ul>
</nav>
<div class="header-right" style="margin-left:auto; display:flex; align-items:center; gap:6px;">
<div class="header-icons">
<a href="https://www.google.co.jp" target="_blank" class="icon-btn" title="Google">
<img src="https://www.google.com/favicon.ico" alt="Google" style="width:18px; height:18px; margin-right:7px;">
</a>
<a href="https://chatgpt.com/" target="_blank" class="icon-btn" title="ChatGPT">
<img src="https://chat.openai.com/favicon.ico" alt="ChatGPT" style="width:18px; height:18px; margin-right:7px;">
</a>
<!-- TODOリスト(Dアイコン) -->
<a href="/kyoyubunsyo/manual-black/kihon/todo.html"
class="icon-btn"
title="TODOリスト"
style="margin-right:6px; color:#fff;">
<svg width="18" height="18" viewBox="0 0 24 24">
<!-- 枠(紙) -->
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/>
<!-- Dの縦棒 -->
<line x1="8" y1="7" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<!-- Dの右側カーブ -->
<path d="M8 7h4a4 4 0 0 1 0 10h-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</a>
<!-- 4マスメモ -->
<a href="/kyoyubunsyo/manual-black/kihon/4masu-memo.html" class="icon-btn" title="4マスメモ" style="margin-right:-0px; color:#fff;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="#fff">
<rect x="3" y="3" width="8" height="8" rx="1"></rect>
<rect x="13" y="3" width="8" height="8" rx="1"></rect>
<rect x="3" y="13" width="8" height="8" rx="1"></rect>
<rect x="13" y="13" width="8" height="8" rx="1"></rect>
</svg>
</a>
</div>
<button id="fav-toggle" class="icon-btn" title="お気に入り & 最近開いたページ" aria-expanded="false" style="margin-right:6px; color:#fff;">
<!-- 星アイコン(塗り) -->
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 17.27 18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
</svg>
</button>
<!-- ★ ここにメモ帳アイコンを追加 -->
<a href="${notepadFileUri}" class="icon-btn" title="メモ帳で開く" style="margin-right:0px; color:#fff;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<!-- メモ帳っぽいアイコン(例: 四角+線) -->
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/>
<line x1="7" y1="7" x2="17" y2="7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="7" y1="15" x2="13" y2="15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</a>
<button class="memo-toggle" aria-label="メモを開く" title="メモ">
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 4h8a2 2 0 0 1 2 2v7.17a2 2 0 0 1-.59 1.41l-3.83 3.83A2 2 0 0 1 12.17 19H8a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z" fill="currentColor"/>
<path d="M13 19v-3a2 2 0 0 1 2-2h3" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
</div>
</header>`;
document.getElementById('header-placeholder').innerHTML = headerContent;
const sidebarContent = `
<ul>
<!-- <li><a href="${vscodeFolderUri}">VS Codeでフォルダを開く</a></li> -->
<li><a href="${notepadFileUri}">メモ帳で開く</a></li>
<li><a href="${explorerFolderUri}">フォルダを開く</a></li>
<li><a href="${vscodeFileUri}">VS Codeで開く</a></li>
<li><a href="/◯◯◯◯/manual-black/kihon/todo.html">TODOリスト</a></li>
<li><a href="https://◯◯◯◯.html">カレンダー</a></li>
<li><a href="/◯◯◯◯/◯◯◯◯/kihon/4masu-memo.html">4マスメモ</a></li>
<!--<li><a href="/◯◯◯◯/◯◯◯◯/kihon/calc.html">計算支援ツール</a></li>-->
<li><a href="/◯◯◯◯/◯◯◯◯/zakki/zakki.html">雑記</a></li>
<li><a href="/◯◯◯◯/◯◯◯◯/gijyutu/manual-kensaku-page.html">マニュアル内検索</a></li>
<li><a href="https://chatgpt.com/">ChatGPT</a></li>
<li><a href="https://google.com" target="_blank">Google検索</a></li>
</ul>`;
document.getElementById('sidebar').innerHTML = sidebarContent;
/*───────────────────────────────────────────────
5. sidebar に未完了 TODO を表示(API連携版)
──────────────────────────────────────────────*/
const sidebarElement = document.getElementById('sidebar');
const sidebarTodoContainer = document.createElement('div');
sidebarTodoContainer.style.padding = '10px 15px';
sidebarTodoContainer.style.borderTop = '1px solid #777';
sidebarTodoContainer.style.marginTop = '15px';
sidebarTodoContainer.style.color = '#ccc';
sidebarTodoContainer.innerHTML = `
<div style="font-size:13px;">TODOリストの未完項目</div>
<ul id="sidebar-todo-list" style="
list-style-type: disc;
list-style-position: outside;
margin-left: 16px;
padding-left: 0;
margin-top: 8px;
"></ul>
`;
sidebarElement.appendChild(sidebarTodoContainer);
const sidebarTodoList = document.getElementById('sidebar-todo-list');
// ===== サーバーAPIからTODOを取得して表示 =====
async function loadSidebarTodos() {
const API_URL = "https://bellblog.org/todo/api.php"; // ←変更してください
sidebarTodoList.innerHTML = '';
try {
const res = await fetch(API_URL);
if (!res.ok) throw new Error("読み込み失敗");
const todos = await res.json();
const incomplete = todos.filter(t => !t.completed).slice(0, 5);
if (incomplete.length > 0) {
incomplete.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo.text;
li.style.fontSize = '12px';
sidebarTodoList.appendChild(li);
});
} else {
const li = document.createElement('li');
li.textContent = '未完了なし';
li.style.fontSize = '12px';
sidebarTodoList.appendChild(li);
}
} catch (e) {
console.error("サイドバーTODO取得エラー:", e);
const li = document.createElement('li');
li.textContent = '読み込みエラー';
li.style.color = 'red';
li.style.fontSize = '12px';
sidebarTodoList.appendChild(li);
}
}
// ページ読み込み時に実行
loadSidebarTodos();
/*───────────────────────────────────────────────
6. クイックメモの表示
───────────────────────────────────────────────*/
// ヘッダー/サイドバー挿入の直後あたりに追記
const memoPanel = document.createElement('aside');
memoPanel.id = 'memo-panel';
memoPanel.innerHTML = `
<div class="memo-header">
<span>クイックメモ</span>
<div class="memo-actions">
<button id="memo-expand" class="icon-btn" aria-label="メモを広くする" title="メモを広くする" aria-pressed="false">
<!-- 拡張アイコン(左右に伸びる) -->
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
<path d="M3 12h18" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M7 8l-4 4 4 4M17 8l4 4-4 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button id="memo-close" class="icon-btn" aria-label="メモを閉じる" title="閉じる">×</button>
</div>
</div>
<textarea id="memo-text" placeholder="ここにメモを書けます(自動保存)"></textarea>
`;
document.body.appendChild(memoPanel);
// ===== Memo panel state & content =====
const MEMO_OPEN_KEY = 'memoOpen';
const MEMO_TEXT_KEY = 'memoText';
const MEMO_WIDE_KEY = 'memoWide';
// 要素取得
const memoToggleBtn = document.querySelector('.memo-toggle');
const memoPanelEl = document.getElementById('memo-panel');
const memoTextEl = document.getElementById('memo-text');
const memoCloseBtn = document.getElementById('memo-close');
const memoExpandBtn = document.getElementById('memo-expand');
// 初期復元(開閉状態のチラつき防止)
(function restoreMemoState() {
if (!memoPanelEl) return;
const savedOpen = localStorage.getItem(MEMO_OPEN_KEY) === 'true';
const savedText = localStorage.getItem(MEMO_TEXT_KEY) || '';
const savedWide = localStorage.getItem(MEMO_WIDE_KEY) === 'true';
// 初回はアニメーション無効にしてから状態反映
const prev = memoPanelEl.style.transition;
memoPanelEl.style.transition = 'none';
memoPanelEl.classList.toggle('active', savedOpen);
memoPanelEl.style.transition = prev || '';
memoTextEl.value = savedText;
document.body.classList.toggle('memo-wide', savedWide);
if (memoExpandBtn) memoExpandBtn.setAttribute('aria-pressed', String(savedWide));
})();
// トグル(ボタン/クローズ×)
function setMemoOpen(next) {
memoPanelEl.classList.toggle('active', next);
localStorage.setItem(MEMO_OPEN_KEY, String(next));
}
if (memoToggleBtn && memoPanelEl) {
memoToggleBtn.addEventListener('click', () => {
const willOpen = !memoPanelEl.classList.contains('active');
setMemoOpen(willOpen);
});
}
if (memoCloseBtn) {
memoCloseBtn.addEventListener('click', () => setMemoOpen(false));
}
// ワイド切替
function setMemoWide(next) {
document.body.classList.toggle('memo-wide', next);
localStorage.setItem(MEMO_WIDE_KEY, String(next));
if (memoExpandBtn) memoExpandBtn.setAttribute('aria-pressed', String(next));
}
if (memoExpandBtn) {
memoExpandBtn.addEventListener('click', () => {
const willWide = !document.body.classList.contains('memo-wide');
setMemoWide(willWide);
});
}
// 入力の自動保存(300ms デバウンス)
let memoSaveTimer = null;
if (memoTextEl) {
memoTextEl.addEventListener('input', () => {
clearTimeout(memoSaveTimer);
memoSaveTimer = setTimeout(() => {
localStorage.setItem(MEMO_TEXT_KEY, memoTextEl.value || '');
}, 300);
});
}
// 複数タブ同期(開閉のみ同期。本文は最後の入力が勝つ想定)
window.addEventListener('storage', (ev) => {
if (!memoPanelEl) return;
if (ev.key === MEMO_OPEN_KEY) {
const open = ev.newValue === 'true';
memoPanelEl.classList.toggle('active', open);
} else if (ev.key === MEMO_TEXT_KEY && memoTextEl) {
memoTextEl.value = ev.newValue || '';
} else if (ev.key === MEMO_WIDE_KEY) {
const wide = ev.newValue === 'true';
document.body.classList.toggle('memo-wide', wide);
if (memoExpandBtn) memoExpandBtn.setAttribute('aria-pressed', String(wide));
}
});
const SIDEBAR_STORAGE_KEY = 'sidebarOpen'; // "true" / "false"
// 初期状態を localStorage から復元(初回描画のチラつき防止つき)
(function restoreSidebarState() {
const saved = localStorage.getItem(SIDEBAR_STORAGE_KEY);
const shouldOpen = saved === 'true'; // 未保存なら false(閉)
const hamburger = document.querySelector('.hamburger-menu');
const sidebar = document.getElementById('sidebar');
const contentWrapper = document.querySelector('.content-wrapper');
if (!hamburger || !sidebar || !contentWrapper) return;
// ★ 初期復元時のアニメーション無効化
const sidebarPrev = sidebar.style.transition;
const contentPrev = contentWrapper.style.transition;
sidebar.style.transition = 'none';
contentWrapper.style.transition = 'none';
// クラス付与で見た目を復元(ここでは一切アニメーションしない)
hamburger.classList.toggle('active', shouldOpen);
sidebar.classList.toggle('active', shouldOpen);
contentWrapper.classList.toggle('sidebar-active', shouldOpen);
document.body.classList.toggle('sidebar-open', shouldOpen);
hamburger.setAttribute('aria-expanded', String(shouldOpen));
// ★ 次フレームで transition を元に戻す(以降の操作は通常どおりアニメーション)
requestAnimationFrame(() => {
sidebar.style.transition = sidebarPrev || '';
contentWrapper.style.transition = contentPrev || '';
});
})();
/*───────────────────────────────────────────────
7. ハンバーガーメニュー制御
───────────────────────────────────────────────*/
const hamburger = document.querySelector('.hamburger-menu');
const sidebar = document.getElementById('sidebar');
const contentWrapper = document.querySelector('.content-wrapper');
if (hamburger && sidebar && contentWrapper) {
hamburger.addEventListener('click', () => {
const nowOpen = !sidebar.classList.contains('active');
hamburger.classList.toggle('active', nowOpen);
sidebar.classList.toggle('active', nowOpen);
contentWrapper.classList.toggle('sidebar-active', nowOpen);
document.body.classList.toggle('sidebar-open', nowOpen);
// 状態を保存
// ARIA 更新
hamburger.setAttribute('aria-expanded', String(nowOpen));
});
}
/*───────────────────────────────────────────────
9. ☆パネル(お気に入りのみ)
──────────────────────────────────────────────*/
(function setupFavPanel() {
// 1) パネルDOMを用意
const panel = document.createElement('div');
panel.id = 'fav-panel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-label', 'お気に入り');
panel.innerHTML = `
<div class="fav-panel-inner">
<div class="fav-section">
<div class="fav-section-title">★ お気に入り</div>
<ul id="fav-list" class="fav-list"></ul>
</div>
</div>
`;
document.body.appendChild(panel);
const favToggle = document.getElementById('fav-toggle');
const favListEl = panel.querySelector('#fav-list');
// 2) お気に入り描画
function renderFavorites() {
favListEl.innerHTML = '';
const favs = (window.manualFavorites || []);
if (!favs.length) {
const li = document.createElement('li');
li.className = 'fav-empty';
li.textContent = 'favorites-data.js を用意して manualFavorites を定義してください。';
favListEl.appendChild(li);
return;
}
favs.forEach(({ title, url }) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = url;
a.textContent = title || url;
li.appendChild(a);
favListEl.appendChild(li);
});
}
function openPanel() {
renderFavorites();
panel.classList.add('open');
favToggle?.setAttribute('aria-expanded', 'true');
positionPanel();
}
function closePanel() {
panel.classList.remove('open');
favToggle?.setAttribute('aria-expanded', 'false');
}
// 3) トグル
favToggle?.addEventListener('click', (e) => {
e.stopPropagation();
if (panel.classList.contains('open')) closePanel(); else openPanel();
});
// 4) 外側クリックで閉じる
document.addEventListener('click', (e) => {
if (!panel.classList.contains('open')) return;
const withinPanel = panel.contains(e.target);
const withinBtn = favToggle?.contains(e.target);
if (!withinPanel && !withinBtn) closePanel();
});
// 5) スクロール/リサイズ時に位置補正
window.addEventListener('resize', positionPanel);
window.addEventListener('scroll', positionPanel);
function positionPanel() {
const btn = favToggle;
if (!btn) return;
// 一旦表示して幅を取得(非表示だと offsetWidth が 0 になるため)
const wasOpen = panel.classList.contains('open');
if (!wasOpen) panel.style.display = 'block';
const rect = btn.getBoundingClientRect();
const panelWidth = panel.offsetWidth || 360; // 取れない場合の保険
const gap = 12; // 右端との余白
const tweak = 14; // さらに左へ寄せたい量
// 基本はボタンの左より少し左に出す
let left = rect.left - 12 - tweak;
// 画面からはみ出さないように補正
const maxLeft = window.innerWidth - panelWidth - gap;
const minLeft = 8;
left = Math.min(left, maxLeft);
left = Math.max(left, minLeft);
panel.style.top = `${rect.bottom + 6}px`;
panel.style.left = `${left}px`;
if (!wasOpen) panel.style.display = '';
}
})();
});
/*───────────────────────────────────────────────
↑ ヘッダーの描画処理の終わり
───────────────────────────────────────────────*/
/*───────────────────────────────────────────────
10. トーストを表示するヘルパー関数
───────────────────────────────────────────────*/
function showToast(message, duration = 2000) {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
document.body.appendChild(toast);
}
toast.textContent = message;
// もし前回の hide クラスが残っていたらクリア
toast.classList.remove('hide');
// show クラスを付与してフェードイン
toast.classList.add('show');
// duration 経過後にフェードアウト
setTimeout(() => {
toast.classList.remove('show');
toast.classList.add('hide');
// フェードアウト終了後に要素を消したければ、以下も追加できます:
toast.addEventListener('animationend', function onEnd(e) {
if (e.animationName === 'toast-fade-out') {
toast.removeEventListener('animationend', onEnd);
toast.remove();
}
});
}, duration);
}
// クリック時に呼び出し(変更不要)
document.querySelectorAll('a.flink').forEach(a => {
a.addEventListener('click', () => {
showToast('処理準備中', 2000);
});
});
/*───────────────────────────────────────────────
11. フォルダを開く処理
───────────────────────────────────────────────*/
document.addEventListener('DOMContentLoaded', () => {
const debugDiv = document.getElementById('debug');
document.querySelectorAll('a.openfolder').forEach(a => {
const raw = a.dataset.path || '';
// セグメントごとに encodeURIComponent して openfolder: URI を生成
const uri = 'openfolder:' +
raw.replace(/\\/g, '/')
.split('/')
.map(encodeURIComponent)
.join('/');
a.href = uri;
// デバッグ出力(#debug があるときだけ)
if (debugDiv) {
debugDiv.innerHTML += `<p>元のパス: ${raw}<br>変換後URI: ${uri}</p>`;
}
// クリック時ログ(任意)
a.addEventListener('click', () => console.log('クリック:', uri));
});
});
コメント