デイリーページに、特定のページへのリンクを持つページ一覧を表示し、さらに内容を開閉できるようにする
- *見なくなる対策として、デイリーページの下部に位置付け可視化ページへのリンク一覧を表示するのがいいのではないか?*をさらにリンク一覧だけでなく、そのリンク先の内容の一部まで表示できるようにしたもの。
- 「【kasten】(kastenページ)」では、記述のブロックごとに3桁の番号を振っているので、その行を含む行を抜き出す仕様。
- Zettelkastenページには「探究用ノート」というリンクを記述しており、
- 探究用ノートというリンクを持つページ一覧とその内容を取得
- デイリーページ下部にそのページへのリンク一覧を表示
- リンク一覧には右横に「+」と書いてあり、そこをクリックすると番号を降っている行を表示
- もう一度押すと表示したものを閉じる
- というもの。
- 閉じた状態
- 開いた状態
code:script.js
- requestAnimationFrame(() => {
- if (window.KCS?.role === 'SV') return;
- cosense.on('lines:changed', ({ by }) => {
- if (by === 'edit') updateLinkedPages();
- });
- cosense.on('lines:changed', ({ by }) => {
- async function updateLinkedPages() {
- if (cosense.Layout !== 'page') return;
- if (!/^\d{8}/.test(cosense.Page.title)) return; // 8桁数字タイトルのみ対象
- async function updateLinkedPages() {
- 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 url =
- }
- async function fetchPage(title) {
- const linkify = (text, project = scrapbox.Project.name) =>
- text.replace(/[([^]]+)]/g, (_, t) =>
<a href="https://scrapbox.io/${project}/${encodeURIComponent(t.trim())}" target="_blank">${t}</a>
- );
- text.replace(/[([^]]+)]/g, (_, t) =>
- const linkify = (text, project = scrapbox.Project.name) =>
- // 行頭の半角空白・全角空白・タブ1文字を削除
- function normalizeLine(t) {
- return t.replace(/^( | |\t)/, "");
- }
- async function renderList(titles) {
- const container = document.querySelector(".editor");
- if (!container) return;
- async function renderList(titles) {
- 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();
- const url =
- }
- // 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>);
- text.replace(/[([^]]+)]/g, (_, t) =>
- // メインのリスト描画処理
- 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で + / - を切り替え - } - }; - });- }
- });