デイリーページに、特定のページへのリンクを持つページ一覧を表示し、さらに内容を開閉できるようにする

  • *見なくなる対策として、デイリーページの下部に位置付け可視化ページへのリンク一覧を表示するのがいいのではないか?*をさらにリンク一覧だけでなく、そのリンク先の内容の一部まで表示できるようにしたもの。
  • 「【kasten】(kastenページ)」では、記述のブロックごとに3桁の番号を振っているので、その行を含む行を抜き出す仕様。
  • Zettelkastenページには「探究用ノート」というリンクを記述しており、
    • 探究用ノートというリンクを持つページ一覧とその内容を取得
    • デイリーページ下部にそのページへのリンク一覧を表示
    • リンク一覧には右横に「+」と書いてあり、そこをクリックすると番号を降っている行を表示
    • もう一度押すと表示したものを閉じる
  • というもの。
  • 閉じた状態
  • 開いた状態

code:script.js

  • requestAnimationFrame(() => {
    • if (window.KCS?.role === 'SV') return;
    • cosense.on('lines:changed', ({ by }) => {
      • if (by === 'edit') updateLinkedPages();
    • });
    • async function updateLinkedPages() {
      • if (cosense.Layout !== 'page') return;
      • if (!/^\d{8}/.test(cosense.Page.title)) return; // 8桁数字タイトルのみ対象
  •   - try {
          - const { relatedPages } = await fetchPage("【kasten】");
          - const titles = (relatedPages?.links1hop || []).map(p => p.title).sort();
          - await renderList(titles);
      - } catch (err) {
          - console.error("リンク取得エラー:", err);
      - }
    
    • }
    • async function fetchPage(title) {
      • const url = https://scrapbox.io/api/pages/${scrapbox.Project.name}/${encodeURIComponent(title)};
      • const res = await fetch(url);
      • if (!res.ok) throw new Error(${res.status} ${res.statusText});
      • return res.json();
    • }
    • const linkify = (text, project = scrapbox.Project.name) =>
      • text.replace(/[([^]]+)]/g, (_, t) =>
        • <a href="https://scrapbox.io/${project}/${encodeURIComponent(t.trim())}" target="_blank">${t}</a>
      • );
    • // 行頭の半角空白・全角空白・タブ1文字を削除
    • function normalizeLine(t) {
      • return t.replace(/^( | |\t)/, "");
    • }
    • async function renderList(titles) {
      • const container = document.querySelector(".editor");
      • if (!container) return;
  •   - let div = document.getElementById("tempRINKList");
      - if (!div) {
          - div = Object.assign(document.createElement("div"), { id: "tempRINKList" });
          - Object.assign(div.style, {
              - background: "floralwhite", padding: "1em", borderRadius: "5px",
              - marginTop: "1em", border: "1px solid [[ccc",]] fontSize: "14px", lineHeight: "1.6"
          - });
          - container.appendChild(div);
      - }
    
  •   - if (!document.getElementById("rink-style")) {
          - const style = document.createElement("style");
          - style.id = "rink-style";
          - style.textContent = `
              - [[tempRINKList]] ul { padding-left: 0; margin: 0; }
              - [[tempRINKList]] li {
                  - list-style: none; position: relative;
                  - padding: 0.2em 1.5em 0.2em 1.2em;
              - }
    
  •           - [[tempRINKList]] li::before {
                  - content: "•";
                  - position: absolute; left: 0; top: 0.55em;
                  - font-size: 1em; line-height: 1; color: black;
              - }
    
  •           - [[tempRINKList]] .toggle-btn {
                  - position: absolute; right: 0; top: 0.3em;
                  - width: 1.2em; height: 1.2em;
                  - cursor: pointer;
              - }
              - [[tempRINKList]] .toggle-btn::before,
              - [[tempRINKList]] .toggle-btn::after {
                  - content: ""; position: absolute;
                  - background: [[666;]] left: 50%; top: 50%;
                  - transform: translate(-50%, -50%);
              - }
              - [[tempRINKList]] .toggle-btn::before { width: 70%; height: 1.5px; }
              - [[tempRINKList]] .toggle-btn::after { width: 1.5px; height: 70%; }
              - [[tempRINKList]] .toggle-btn.open::after { display: none; }
    
  •           - [[tempRINKList]] .page-link { text-decoration: none; color: [[0645ad;]] }
    
  •           - [[tempRINKList]] .sub-lines {
                  - margin-top: .5em; padding-left: 1em;
                  - border-left: 2px solid [[ddd;]]
                  - font-size: 13px; display: none;
              - }
              - [[tempRINKList]] .sub-lines.open { display: block; }
    
  •           - [[tempRINKList]] .sub-line-item {
                  - margin: 0.2em 0;
                  - padding-left: 0.5em;
              - }
    
  •           - [[tempRINKList]] .sub-line-item::before {
                  - content: "• ";
                  - color: [[333;]]
              - }
          - `;
          - document.head.appendChild(style);
      - }
    
  •   - let html = "<ul>";
      - for (const title of titles) {
          - const url = `/${scrapbox.Project.name}/${encodeURIComponent(title)}`;
          - let subLines = "";
    
  •       - try {
              - const { lines = [] } = await fetchPage(title);
    
  •           - // ★★★ 抽出条件:半角空白1 / 全角空白1 / タブ1のみ ★★★
              - const matched = lines
                  - .map(l => l.text)
                  - .filter(t => /^( | |\t)(?! | |\t)/.test(t));
    
  •           - if (matched.length) {
                  - subLines = `
                      - <div class="sub-lines">
                          - ${matched.map(l => {
                              - const clean = normalizeLine(l);
                              - return `<div class="sub-line-item">${linkify(clean)}</div>`;
                          - }).join("")}
                      - </div>`;
              - }
          - } catch (err) {
              - console.error(`ページ取得失敗: ${title}`, err);
          - }
    
  •       - html += `
              - <li data-title="${title}">
                  - <a href="${url}" target="_blank" class="page-link">${title}</a>
                  - <span class="toggle-btn"></span>
                  - ${subLines}
              - </li>`;
      - }
      - html += "</ul>";
    
  •   - div.innerHTML = html;
    
  •   - div.querySelectorAll(".toggle-btn").forEach(btn => {
          - btn.onclick = () => {
              - const sub = btn.closest("li").querySelector(".sub-lines");
              - if (sub) {
                  - const open = sub.classList.toggle("open");
                  - btn.classList.toggle("open", open);
              - }
          - };
      - });
    
    • }
  • });

:script.js

  • requestAnimationFrame(() => {

    • if (window.KCS?.role === 'SV') return;

      • // ページの内容が編集されたときにフック
      • cosense.on('lines:changed', ({ by }) => {
        • if (by === 'edit') updateLinkedPages();
      • });
      • // 【kasten】からリンクされているページを収集
      • async function updateLinkedPages() {
        • if (cosense.Layout !== 'page') return;
        • if (!/^\d{8}/.test(cosense.Page.title)) return; // 8桁数字タイトルのみ対象
  •     - try {
            - const { relatedPages } = await fetchPage("【kasten】");
            - const titles = (relatedPages?.links1hop || []).map(p => p.title).sort();
            - await renderList(titles);
        - } catch (err) {
            - console.error("リンク取得エラー:", err);
        - }
    - }
    
      • // Scrapbox API からページ JSON を取得
      • async function fetchPage(title) {
        • const url = https://scrapbox.io/api/pages/${scrapbox.Project.name}/${encodeURIComponent(title)};
        • const res = await fetch(url);
        • if (!res.ok) throw new Error(${res.status} ${res.statusText});
        • return res.json();
      • }
      • // Scrapbox リンク記法を HTML の aタグに変換
      • const linkify = (text, project = scrapbox.Project.name) =>
        • text.replace(/[([^]]+)]/g, (_, t) =>
          • <a href="https://scrapbox.io/${project}/${encodeURIComponent(t.trim())}" target="_blank">${t}</a>);
      • // メインのリスト描画処理
      • async function renderList(titles) {
        • const container = document.querySelector(".editor");
        • if (!container) return;
    •   - // リスト表示領域を生成 or 取得
        - let div = document.getElementById("tempRINKList");
        - if (!div) {
            - div = Object.assign(document.createElement("div"), { id: "tempRINKList" });
            - Object.assign(div.style, {
                - background: "floralwhite", padding: "1em", borderRadius: "5px",
                - marginTop: "1em", border: "1px solid [[ccc",]] fontSize: "14px", lineHeight: "1.6"
            - });
            - container.appendChild(div);
        - }
      
    •   - // 初回のみ CSS を追加
        - if (!document.getElementById("rink-style")) {
            - const style = document.createElement("style");
            - style.id = "rink-style";
            - style.textContent = `
                - [[tempRINKList]] ul { padding-left: 0; margin: 0; }
                - [[tempRINKList]] li {
                    - list-style: none; position: relative;
                    - padding: 0.2em 1.5em 0.2em 1.2em;
                - }
                - /* 自作バレット */
                - [[tempRINKList]] li::before {
                    - content: "•"; position: absolute; left: 0; top: 0.5em;
                    - font-size: 1em; line-height: 1; color: black;
                - }
                - /* トグルボタンの描画 (CSSだけで+/-を表現) */
                - [[tempRINKList]] .toggle-btn {
                    - position: absolute; right: 0; top: 0.3em;
                    - width: 1.2em; height: 1.2em;
                    - cursor: pointer;
                - }
                - [[tempRINKList]] .toggle-btn::before,
                - [[tempRINKList]] .toggle-btn::after {
                    - content: ""; position: absolute;
                    - background: [[666;]] left: 50%; top: 50%;
                    - transform: translate(-50%, -50%);
                - }
                - /* 横棒 */
                - [[tempRINKList]] .toggle-btn::before {
                    - width: 70%; height: 1.5px;
                    - border-radius: 1px;
                - }
                - /* 縦棒(閉じているときだけ表示) */
                - [[tempRINKList]] .toggle-btn::after {
                    - width: 1.5px; height: 70%;
                    - border-radius: 1px;
                - }
                - /* 開いているときは縦棒を消してマイナス記号に */
                - [[tempRINKList]] .toggle-btn.open::after { display: none; }
      
    •           - [[tempRINKList]] .page-link { text-decoration: none; color: [[0645ad;]] }
                - [[tempRINKList]] .sub-lines {
                    - margin-top: .5em; padding-left: 1em; border-left: 2px solid [[ddd;]]
                    - font-size: 13px; display: none;
                - }
                - [[tempRINKList]] .sub-lines.open { display: block; }
            - `;
            - document.head.appendChild(style);
        - }
      
    •   - // リストHTML生成
        - let html = "<ul>";
        - for (const title of titles) {
            - const url = `/${scrapbox.Project.name}/${encodeURIComponent(title)}`;
            - let subLines = "";
      
    •       - try {
                - const { lines = [] } = await fetchPage(title);
                - const matched = lines.map(l => l.text).filter(t => /^\d{3}/.test(t));
                - if (matched.length) {
                    - subLines = `<div class="sub-lines">${matched.map(l => `<div>${linkify(l)}</div>`).join("")}</div>`;
                - }
            - } catch (err) {
                - console.error(`ページ取得失敗: ${title}`, err);
            - }
      
    •       - html += `
                - <li data-title="${title}">
                    - <a href="${url}" target="_blank" class="page-link">${title}</a>
                    - <span class="toggle-btn"></span>
                    - ${subLines}
                - </li>`;
        - }
        - html += "</ul>";
      
    •   - div.innerHTML = html;
      
    •   - // トグル動作: サブラインを開閉
        - div.querySelectorAll(".toggle-btn").forEach(btn => {
            - btn.onclick = () => {
                - const sub = btn.closest("li").querySelector(".sub-lines");
                - if (sub) {
                    - const open = sub.classList.toggle("open");
                    - btn.classList.toggle("open", open); // CSSで + / - を切り替え
                - }
            - };
        - });
      
      • }
    • });