feat: render headings in outliner

This commit is contained in:
dragonwocky 2024-01-31 00:12:14 +11:00
parent ff1e5f7550
commit 6661c5559b
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
6 changed files with 88 additions and 122 deletions

View File

@ -63,19 +63,18 @@ const isEnabled = async (id) => {
}; };
const modDatabase = async (id) => { const modDatabase = async (id) => {
const optionDefaults = const optionDefaults = (await getMods())
(await getMods()) .find((mod) => mod.id === id)
.find((mod) => mod.id === id) ?.options?.map?.((opt) => {
?.options?.map?.((opt) => { let value = opt.value;
let value = opt.value; value ??= opt.values?.[0]?.value;
value ??= opt.values?.[0]?.value; value ??= opt.values?.[0];
value ??= opt.values?.[0]; return [opt.key, value];
return [opt.key, value]; })
}) ?.filter?.(([, value]) => typeof value !== "undefined");
?.filter?.(([, value]) => typeof value !== "undefined") ?? {};
return globalThis.__enhancerApi.initDatabase( return globalThis.__enhancerApi.initDatabase(
[await getProfile(), id], [await getProfile(), id],
Object.fromEntries(optionDefaults) Object.fromEntries(optionDefaults ?? [])
); );
}; };

View File

@ -58,11 +58,11 @@ const insertMenu = async (api, db) => {
let _contentWindow; let _contentWindow;
const updateMenuTheme = () => { const updateMenuTheme = () => {
const darkMode = document.body.classList.contains("dark"), const darkMode = document.body.classList.contains("dark"),
notionTheme = darkMode ? "dark" : "light"; notionTheme = darkMode ? "dark" : "light";
menuPing.theme = notionTheme; menuPing.theme = notionTheme;
_contentWindow?.postMessage?.(menuPing, "*"); _contentWindow?.postMessage?.(menuPing, "*");
}; };
const $modal = html`<${Modal}> const $modal = html`<${Modal}>
<${Frame} <${Frame}
@ -122,14 +122,13 @@ const insertMenu = async (api, db) => {
const insertPanel = async (api, db) => { const insertPanel = async (api, db) => {
const notionFrame = ".notion-frame", const notionFrame = ".notion-frame",
togglePanelHotkey = await db.get("togglePanelHotkey"), togglePanelHotkey = await db.get("togglePanelHotkey"),
{ addMutationListener, removeMutationListener } = api, { html, setState, addMutationListener, removeMutationListener } = api;
{ html, setState, addPanelView } = api;
const $panel = html`<${Panel} const $panel = html`<${Panel}
hotkey="${togglePanelHotkey}" hotkey="${togglePanelHotkey}"
...${Object.assign( ...${Object.assign(
...["Width", "Open", "View"].map((key) => ({ ...["Width", "Open", "View"].map((key) => ({
[`_get${key}`]: () => db.get(`sidePanel${key}`), [`_get${key}`]: () => db.get(`panel${key}`),
[`_set${key}`]: async (value) => { [`_set${key}`]: async (value) => {
await db.set(`panel${key}`, value); await db.set(`panel${key}`, value);
setState({ rerender: true }); setState({ rerender: true });
@ -146,27 +145,6 @@ const insertPanel = async (api, db) => {
}; };
addMutationListener(notionFrame, appendToDom); addMutationListener(notionFrame, appendToDom);
appendToDom(); appendToDom();
const $helloThere = html`<div class="p-[16px]">hello there</div>`,
$generalKenobi = html`<div class="p-[16px]">general kenobi</div>`;
addPanelView({
title: "outliner",
// prettier-ignore
$icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<circle cx="5" cy="7" r="2.8"/>
<circle cx="5" cy="17" r="2.79"/>
<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
<path d="M17,10.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,9.6,17.55,10.05,17,10.05z"/>
<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
</svg>`,
$view: $helloThere,
});
addPanelView({
title: "word counter",
$icon: "type",
$view: $generalKenobi,
});
}; };
export default async (api, db) => export default async (api, db) =>

View File

@ -1,22 +1,26 @@
/** /**
* notion-enhancer: outliner * notion-enhancer: outliner
* (c) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill) * (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/) * (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
export default async function ({ web, components }, db) { "use strict";
const dbNoticeText = 'Open a page to see its table of contents.',
pageNoticeText = 'Click on a heading to jump to it.',
$notice = web.html`<p id="outliner--notice">${dbNoticeText}</p>`;
const $headingList = web.html`<div></div>`; export default async (api, db) => {
const { html, addMutationListener, addPanelView } = api,
let viewFocused = false, frame = ".notion-sidebar-container + div",
$page; page = ".notion-page-content",
await components.addPanelView({ headings = [
id: '87e077cc-5402-451c-ac70-27cc4ae65546', ".notion-header-block",
icon: web.html`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> ".notion-sub_header-block",
".notion-sub_sub_header-block",
],
$view = html`<div></div>`;
addPanelView({
title: "Outliner",
// prettier-ignore
$icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<circle cx="5" cy="7" r="2.8"/> <circle cx="5" cy="7" r="2.8"/>
<circle cx="5" cy="17" r="2.79"/> <circle cx="5" cy="17" r="2.79"/>
<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/> <path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
@ -24,61 +28,47 @@ export default async function ({ web, components }, db) {
<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" /> <path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/> <path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
</svg>`, </svg>`,
title: 'Outliner', $view,
$content: web.render(web.html`<div></div>`, $notice, $headingList),
onFocus: () => {
viewFocused = true;
$page = document.getElementsByClassName('notion-page-content')[0];
updateHeadings();
},
onBlur: () => {
viewFocused = false;
},
}); });
await web.whenReady();
function updateHeadings() { function Heading({ indent, ...props }, ...children) {
if (!$page) return; return html`<div
$notice.innerText = pageNoticeText; role="button"
$headingList.style.display = ''; class="notion-enhancer--outliner-heading
const $headerBlocks = $page.querySelectorAll('[class^="notion-"][class*="header-block"]'), block cursor-pointer select-none text-[14px]
$fragment = web.html`<div></div>`; m-px py-[6px] pr-[2px] pl-[${indent * 18}px]
let indent = 0; decoration-(2 [color:var(--theme--fg-border)])
for (const $header of $headerBlocks) { hover:bg-[color:var(--theme--bg-hover)]
const id = $header.dataset.blockId.replace(/-/g, ''), underline-(& offset-4)"
placeholder = $header.querySelector('[placeholder]').getAttribute('placeholder'), ...${props}
headerDepth = +[...placeholder].reverse()[0]; >
indent = (headerDepth-1) * 18; ${children}
const $outlineHeader = web.render( </div>`;
web.html`<a href="#${id}" class="outliner--header"
placeholder="${web.escape(placeholder)}"
style="--outliner--indent:${indent}px;"></a>`,
$header.innerText
);
$outlineHeader.addEventListener('click', (event) => {
location.hash = '';
});
web.render($fragment, $outlineHeader);
}
if ($fragment.innerHTML !== $headingList.innerHTML) {
web.render(web.empty($headingList), ...$fragment.children);
}
} }
const pageObserver = () => {
if (!viewFocused) return; const getHeadings = () => {
if (document.contains($page)) { return [...document.querySelectorAll(headings.join(", "))];
updateHeadings(); },
} else { getHeadingLevel = ($heading) => {
$page = document.querySelector('.notion-page-content'); for (let i = 0; i < headings.length; i++)
if (!$page) { if ($heading.matches(headings[i])) return i + 1;
$notice.innerText = dbNoticeText; },
$headingList.style.display = 'none'; updateHeadings = () => {
} else updateHeadings(); $view.innerHTML = "";
} for (const $heading of getHeadings()) {
}; const title = $heading.innerText,
web.addDocumentObserver(pageObserver, [ indent = getHeadingLevel($heading);
'.notion-page-content', if (!title) continue;
'.notion-collection_view_page-block', const $h = html`<${Heading} indent=${indent} onclick=${() => {
]); // todo: scroll into view
pageObserver(); }}>${title}</p>`;
} $view.append($h);
}
};
let $page;
addMutationListener(page, () => {
if (!document.contains($page)) $page = document.querySelector(page);
if ($page) updateHeadings();
});
};

View File

@ -1,23 +1,21 @@
{ {
"name": "outliner", "name": "Outliner",
"version": "0.5.0",
"id": "87e077cc-5402-451c-ac70-27cc4ae65546", "id": "87e077cc-5402-451c-ac70-27cc4ae65546",
"version": "0.4.0", "description": "Adds a table of contents to the side panel to overview and navigate the current page's headings and subheadings.",
"description": "adds a table of contents to the side panel.", "thumbnail": "outliner.png",
"preview": "outliner.png", "tags": ["panel"],
"tags": ["extension", "panel"],
"authors": [ "authors": [
{
"name": "dragonwocky",
"homepage": "https://dragonwocky.me/",
"avatar": "https://dragonwocky.me/avatar.jpg"
},
{ {
"name": "CloudHill", "name": "CloudHill",
"email": "rh.cloudhill@gmail.com",
"homepage": "https://github.com/CloudHill", "homepage": "https://github.com/CloudHill",
"avatar": "https://avatars.githubusercontent.com/u/54142180" "avatar": "https://avatars.githubusercontent.com/u/54142180"
} }
], ],
"js": { "clientScripts": ["client.mjs"]
"client": ["client.mjs"]
},
"css": {
"client": ["client.css"]
},
"options": []
} }

View File

@ -27,7 +27,7 @@
"type": "number", "type": "number",
"key": "distanceScrolledUntilShown", "key": "distanceScrolledUntilShown",
"description": "How far down a page you must be scrolled for the scroll to top button to appear.", "description": "How far down a page you must be scrolled for the scroll to top button to appear.",
"value": 50 "value": 0
}, },
{ {
"type": "select", "type": "select",

View File

@ -2,6 +2,7 @@
"core", "core",
"extensions/titlebar", "extensions/titlebar",
"extensions/topbar", "extensions/topbar",
"extensions/outliner",
"extensions/scroll-to-top", "extensions/scroll-to-top",
"themes/classic-dark" "themes/classic-dark"
] ]