MENU

フォルダリンクを登録できるページ

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>エクスプローラリンク集</title>
  <link rel="stylesheet" href="../style.css">
  <style>
    /* ページ内ミニスタイル(必要なら削除可) */
    .toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:12px 0}
    .toolbar input[type="text"]{padding:8px;border:1px solid #e5e7eb;border-radius:8px;min-width:240px}
    .toolbar button{padding:8px 10px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;cursor:pointer}
    .toolbar .right{margin-left:auto;display:flex;gap:8px}
    .form-grid{display:grid;grid-template-columns:1fr 2fr 2fr;gap:8px;margin:12px 0}
    .form-grid input, .form-grid textarea{padding:8px;border:1px solid #e5e7eb;border-radius:8px;width:100%}
    .form-grid label{font-size:12px;color:#6b7280}
  .list{
    display:block; /* 1列にする */
    margin:16px 0;
  }
  .card{
    border:1px solid #e5e7eb;
    border-radius:12px;
    padding:12px;
    background:#fff;
    display:flex;               /* 横並び */
    justify-content:space-between;
    align-items:center;
    margin-bottom:12px;         /* 各行に余白 */
    width:100%;                 /* 横幅いっぱい */
    box-sizing:border-box;
  }
  .card .actions{
    display:flex;
    gap:6px;
    flex-shrink:0;              /* ボタンを縮めない */
  }
  .card .nowrap{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .card .meta{font-size:12px;color:#6b7280;margin-top:6px;word-break:break-all}
    .actions{display:flex;gap:6px}
    .actions button{padding:6px 8px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;cursor:pointer}
    .dragger{cursor:grab;font-size:12px;color:#6b7280}
    .badge{display:inline-block;border:1px solid #e5e7eb;border-radius:999px;padding:2px 8px;font-size:12px;margin-left:8px}
    .hidden{display:none}
    .muted{color:#6b7280}
    .danger{border-color:#ef4444;color:#ef4444}
    .ok{border-color:#10b981;color:#10b981}
    @media (prefers-color-scheme: dark){
      .card, .toolbar button{background:#0f172a;border-color:#243049;color:#e8ecf3}
      .form-grid input,.form-grid textarea,.toolbar input[type="text"]{background:#0b1020;border-color:#243049;color:#e8ecf3}
      .badge{border-color:#243049}
      .meta{color:#98a2b3}
    }
  </style>
</head>
<body>
<div id="header-placeholder"></div>
<div class="page-wrapper">
  <aside id="sidebar">
    <!-- Sidebar content will be injected here by JS -->
  </aside>

  <div class="content-wrapper">
    <div class="main">
      <h1 class="hyoudai" style="margin-bottom:0;">エクスプローラリンク集</h1>

      <!-- 入力フォーム -->
      <div class="form">
        <div class="form-grid">
          <div>
            <label for="name">表示名</label>
            <input id="name" type="text" placeholder="例)ドキュメント(社内共有)">
          </div>
          <div>
            <label for="path">パス(ドライブ/UNC)</label>
            <input id="path" type="text" placeholder="例)D:\work\docs  または  \\server\share\team">
          </div>
          <div>
            <label for="note">メモ(任意)</label>
            <input id="note" type="text" placeholder="任意の補足を書けます">
          </div>
        </div>
        <div class="toolbar">
          <button id="addBtn">追加</button>
          <button id="clearFormBtn" class="muted">クリア</button>
          <span id="editModeBadge" class="badge hidden">編集モード</span>
          <div class="right">
            <input id="q" type="text" placeholder="検索(表示名・パス・メモ)">
            <button id="exportBtn">エクスポート</button>
            <button id="importBtn">インポート</button>
            <input id="importFile" type="file" accept="application/json" class="hidden">
            <button id="resetBtn" class="danger">全削除</button>
          </div>
        </div>
      </div>

      <!-- 一覧 -->
      <div id="list" class="list"></div>

      <p class="muted" id="emptyMsg" style="margin-top:12px;">まだ登録がありません。「表示名」「パス」を入力して「追加」を押してください。</p>
    </div>
  </div>
</div>

<script>
(() => {
  const LS_KEY = 'explorerLinksV1';

  /** @type {{id:string,name:string,path:string,note?:string,createdAt:number,updatedAt?:number}[]} */
  let data = [];
  let filter = '';
  let dragSrcId = null;
  let editingId = null;

  // 初期ロード
  function load() {
    try {
      const raw = localStorage.getItem(LS_KEY);
      if (raw) {
        data = JSON.parse(raw);
      } else {
        // 初回のみ例データを1件(空なら)
        data = [{
          id: crypto.randomUUID(),
          name: 'サンプル:共有ドキュメント',
          path: 'D:\\◯◯◯\\テスト',
          note: '初期サンプル。不要なら削除してください。',
          createdAt: Date.now()
        }];
        save();
      }
    } catch(e) {
      console.error(e);
      data = [];
    }
  }

  function save() {
    localStorage.setItem(LS_KEY, JSON.stringify(data));
  }

  function createExplorerHref(p){
    if (!p) return '#';
    // explorer: はバックスラッシュOK。スペース等は encodeURI で軽くエスケープ(\ は維持される)
    return 'explorer:' + encodeURI(p);
  }

  function render(){
    const listEl = document.getElementById('list');
    const empty = document.getElementById('emptyMsg');
    listEl.innerHTML = '';

    const rows = data
      .filter(item => {
        if (!filter) return true;
        const hay = (item.name + ' ' + item.path + ' ' + (item.note||'')).toLowerCase();
        return hay.includes(filter.toLowerCase());
      });

    empty.style.display = rows.length ? 'none' : '';

    for (const item of rows) {
      const card = document.createElement('div');
      card.className = 'card';
      card.draggable = true;
      card.dataset.id = item.id;

      // 左側(内容)
      const left = document.createElement('div');
      const title = document.createElement('div');
      title.className = 'nowrap';
      title.innerHTML = `<strong>${escapeHtml(item.name)}</strong>`;

      const link = document.createElement('div');
      link.innerHTML = `<a class="open" href="${createExplorerHref(item.path)}" title="エクスプローラで開く">${escapeHtml(item.path)}</a>`;

      const meta = document.createElement('div');
      meta.className = 'meta';
      const created = new Date(item.createdAt);
      const updated = item.updatedAt ? new Date(item.updatedAt) : null;
      meta.textContent = (item.note ? (item.note + ' — ') : '') +
        `登録: ${fmt(created)}` + (updated ? ` / 更新: ${fmt(updated)}` : '');

      left.appendChild(title);
      left.appendChild(link);
      left.appendChild(meta);

      // 右側(操作)
      const right = document.createElement('div');
      right.className = 'actions';
      right.innerHTML = `
        <span class="dragger" title="ドラッグで並べ替え">↕</span>
        <button data-act="copy" data-id="${item.id}">コピー</button>
        <button data-act="edit" data-id="${item.id}">編集</button>
        <button data-act="del" data-id="${item.id}" class="danger">削除</button>
      `;

      card.appendChild(left);
      card.appendChild(right);

      // D&D
      card.addEventListener('dragstart', e => {
        dragSrcId = item.id;
        e.dataTransfer.effectAllowed = 'move';
      });
      card.addEventListener('dragover', e => {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
      });
      card.addEventListener('drop', e => {
        e.preventDefault();
        if (!dragSrcId) return;
        const dstId = item.id;
        if (dragSrcId === dstId) return;
        reorder(dragSrcId, dstId);
        dragSrcId = null;
      });
      card.addEventListener('dragend', () => dragSrcId = null);

      listEl.appendChild(card);
    }
  }

  function reorder(srcId, dstId){
    const from = data.findIndex(x => x.id === srcId);
    const to = data.findIndex(x => x.id === dstId);
    if (from < 0 || to < 0) return;
    const [moved] = data.splice(from, 1);
    data.splice(to, 0, moved);
    save(); render();
  }

  function fmt(d){
    const pad = n => String(n).padStart(2,'0');
    return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
  }

  function escapeHtml(s){
    return String(s ?? '').replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
  }

  // 追加 / 更新
  function onSubmit(){
    const name = document.getElementById('name').value.trim();
    const path = document.getElementById('path').value.trim();
    const note = document.getElementById('note').value.trim();
    if (!name || !path){
      alert('「表示名」と「パス」は必須です。');
      return;
    }
    // パスの簡易整形:全角スペース除去、末尾バックスラッシュは残す
    const normPath = path.replace(/\u3000/g,' ').replace(/[ ]+$/,'');
    if (editingId){
      const idx = data.findIndex(x => x.id === editingId);
      if (idx >= 0){
        data[idx] = { ...data[idx], name, path: normPath, note, updatedAt: Date.now() };
      }
      editingId = null;
      document.getElementById('editModeBadge').classList.add('hidden');
      document.getElementById('addBtn').textContent = '追加';
    } else {
      data.push({ id: crypto.randomUUID(), name, path: normPath, note, createdAt: Date.now() });
    }
    save(); clearForm(); render();
  }

  function clearForm(){
    document.getElementById('name').value = '';
    document.getElementById('path').value = '';
    document.getElementById('note').value = '';
    editingId = null;
    document.getElementById('editModeBadge').classList.add('hidden');
    document.getElementById('addBtn').textContent = '追加';
  }

  // 操作(コピー/編集/削除)
  function onAction(e){
    const btn = e.target.closest('button');
    if (!btn) return;
    const act = btn.dataset.act;
    const id = btn.dataset.id;
    if (!act || !id) return;

    const item = data.find(x => x.id === id);
    if (!item) return;

    if (act === 'copy'){
      const text = item.path;
      if (navigator.clipboard?.writeText){
        navigator.clipboard.writeText(text).then(() => {
          toast('パスをコピーしました');
        }).catch(() => {
          fallbackCopy(text);
        });
      } else {
        fallbackCopy(text);
      }
    } else if (act === 'edit'){
      document.getElementById('name').value = item.name;
      document.getElementById('path').value = item.path;
      document.getElementById('note').value = item.note || '';
      editingId = id;
      document.getElementById('editModeBadge').classList.remove('hidden');
      document.getElementById('addBtn').textContent = '更新';
      window.scrollTo({top:0, behavior:'smooth'});
    } else if (act === 'del'){
      if (confirm(`「${item.name}」を削除しますか?`)){
        data = data.filter(x => x.id !== id);
        save(); render();
      }
    }
  }

  function fallbackCopy(text){
    const ta = document.createElement('textarea');
    ta.value = text;
    document.body.appendChild(ta);
    ta.select();
    try { document.execCommand('copy'); toast('パスをコピーしました'); }
    catch { alert('コピーに失敗しました'); }
    finally { document.body.removeChild(ta); }
  }

  // 検索
  function onSearch(e){
    filter = e.target.value || '';
    render();
  }

  // エクスポート / インポート / 全削除
  function onExport(){
    const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = `explorer_links_${new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')}.json`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
  }
  function onImportClick(){ document.getElementById('importFile').click(); }
  function onImportFile(e){
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try{
        const arr = JSON.parse(reader.result);
        if (!Array.isArray(arr)) throw new Error('JSONは配列ではありません');
        // ざっくり検証
        const mapped = arr.map(x => ({
          id: x.id || crypto.randomUUID(),
          name: String(x.name ?? ''),
          path: String(x.path ?? ''),
          note: x.note ? String(x.note) : '',
          createdAt: Number(x.createdAt || Date.now()),
          updatedAt: x.updatedAt ? Number(x.updatedAt) : undefined
        })).filter(x => x.name && x.path);
        if (!mapped.length) throw new Error('有効な項目がありません');
        data = mapped;
        save(); render();
        toast('インポートしました');
      } catch(err){
        alert('インポートに失敗しました: ' + err.message);
      } finally {
        e.target.value = '';
      }
    };
    reader.readAsText(file, 'utf-8');
  }
  function onReset(){
    if (confirm('すべての登録を削除します。よろしいですか?')){
      data = []; save(); render();
    }
  }

  function toast(msg){
    // 簡易トースト(必要なら既存のトーストに置換)
    const div = document.createElement('div');
    div.textContent = msg;
    div.style.position='fixed'; div.style.bottom='20px'; div.style.left='50%';
    div.style.transform='translateX(-50%)';
    div.style.padding='10px 14px'; div.style.background='#111827'; div.style.color='#fff';
    div.style.borderRadius='10px'; div.style.boxShadow='0 4px 14px rgba(0,0,0,.2)';
    div.style.zIndex='9999'; div.style.opacity='0'; div.style.transition='opacity .2s';
    document.body.appendChild(div);
    requestAnimationFrame(()=>div.style.opacity='1');
    setTimeout(()=>{ div.style.opacity='0'; setTimeout(()=>div.remove(), 200); }, 1500);
  }

  // イベント束ね
  function bind(){
    document.getElementById('addBtn').addEventListener('click', onSubmit);
    document.getElementById('clearFormBtn').addEventListener('click', clearForm);
    document.getElementById('list').addEventListener('click', onAction);
    document.getElementById('q').addEventListener('input', onSearch);
    document.getElementById('exportBtn').addEventListener('click', onExport);
    document.getElementById('importBtn').addEventListener('click', onImportClick);
    document.getElementById('importFile').addEventListener('change', onImportFile);
    document.getElementById('resetBtn').addEventListener('click', onReset);

    // Enter で追加
    const inputs = ['name','path','note'].map(id=>document.getElementById(id));
    inputs.forEach(el => el.addEventListener('keydown', e=>{
      if (e.key === 'Enter'){
        e.preventDefault();
        onSubmit();
      }
    }));
  }

  // 起動
  load(); bind(); render();
})();
</script>

<script src="../favorites-data.js"></script>
<script src="../load_header.js"></script>
</body>
</html>
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次