diff --git a/src/common/events.js b/src/common/events.js
index a87ef9e..81aa5a6 100644
--- a/src/common/events.js
+++ b/src/common/events.js
@@ -64,16 +64,20 @@ const _state = {},
let documentObserver,
mutationListeners = [];
const mutationQueue = [],
- addMutationListener = (selector, callback, attributesOnly = false) => {
- mutationListeners.push([selector, callback, attributesOnly]);
+ addMutationListener = (selector, callback, subtree = true) => {
+ mutationListeners.push([selector, callback, subtree]);
},
removeMutationListener = (callback) => {
mutationListeners = mutationListeners.filter(([, c]) => c !== callback);
},
- selectorMutated = (mutation, selector, attributesOnly) => {
- const matchesTarget = mutation.target?.matches(selector);
- if (attributesOnly) return matchesTarget;
- const descendsFromTarget = mutation.target?.matches(`${selector} *`),
+ selectorMutated = (mutation, selector, subtree) => {
+ const target =
+ mutation.type === "characterData"
+ ? mutation.target.parentElement
+ : mutation.target,
+ matchesTarget = target?.matches(selector);
+ if (!subtree) return matchesTarget;
+ const descendsFromTarget = target?.matches(`${selector} *`),
addedToTarget = [...(mutation.addedNodes || [])].some(
(node) =>
node instanceof HTMLElement &&
@@ -85,8 +89,8 @@ const mutationQueue = [],
handleMutations = () => {
while (mutationQueue.length) {
const mutation = mutationQueue.shift();
- for (const [selector, callback, attributesOnly] of mutationListeners) {
- const matches = selectorMutated(mutation, selector, attributesOnly);
+ for (const [selector, callback, subtree] of mutationListeners) {
+ const matches = selectorMutated(mutation, selector, subtree);
if (matches) callback(mutation);
}
}
@@ -99,6 +103,7 @@ const mutationQueue = [],
});
documentObserver.observe(document.body, {
attributes: true,
+ characterData: true,
childList: true,
subtree: true,
});
diff --git a/src/core/client.mjs b/src/core/client.mjs
index 8376611..75f3d37 100644
--- a/src/core/client.mjs
+++ b/src/core/client.mjs
@@ -99,7 +99,7 @@ const insertMenu = async (api, db) => {
Configure the notion-enhancer and its mods
/>`.attach($button, "right");
addMutationListener(notionSidebar, appendToDom);
- addMutationListener(".notion-app-inner", updateMenuTheme, true);
+ addMutationListener(".notion-app-inner", updateMenuTheme, false);
appendToDom();
addKeyListener(openMenuHotkey, (event) => {
diff --git a/src/core/menu/islands/Checkbox.mjs b/src/core/menu/islands/Checkbox.mjs
index 96c2889..0d7fc36 100644
--- a/src/core/menu/islands/Checkbox.mjs
+++ b/src/core/menu/islands/Checkbox.mjs
@@ -13,7 +13,7 @@ function Checkbox({ _get, _set, _requireReload = true, ...props }) {
type="checkbox"
class="hidden checked:&+div:(px-px
bg-[color:var(--theme--accent-primary)])
- not-checked:&+div:(&>div:text-transparent
+ not-checked:&+div:(&>i:text-transparent
border-(& [color:var(--theme--fg-primary)])
hover:bg-[color:var(--theme--bg-hover)])"
...${props}
diff --git a/src/extensions/outliner/client.css b/src/extensions/outliner/client.css
deleted file mode 100644
index a37d938..0000000
--- a/src/extensions/outliner/client.css
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * notion-enhancer: outliner
- * (c) 2020 CloudHill (https://github.com/CloudHill)
- * (c) 2021 dragonwocky (https://dragonwocky.me/)
- * (https://notion-enhancer.github.io/) under the MIT license
- */
-
-#outliner--notice {
- color: var(--theme--text_secondary);
- font-size: 14px;
- margin-top: 0;
- margin-bottom: 1rem;
-}
-
-.outliner--header {
- position: relative;
- margin: 0 -1rem;
- padding: 0 1rem;
- display: block;
- font-size: 14px;
- line-height: 2.2;
- white-space: nowrap;
- overflow: hidden;
- user-select: none;
- text-overflow: ellipsis;
- text-decoration: none;
- text-indent: var(--outliner--indent);
- color: inherit;
- cursor: pointer !important;
- transition: background 20ms ease-in;
-}
-.outliner--header:hover {
- background: var(--theme--ui_interactive-hover);
-}
-
-.outliner--header:empty::after {
- color: var(--theme--text_secondary);
- content: attr(placeholder);
-}
-
-/* indentation lines */
-.outliner--header:not([style='--outliner--indent:0px;'])::before {
- content: '';
- height: 100%;
- position: absolute;
- left: calc((1rem + var(--outliner--indent)) - 11px);
-}
diff --git a/src/extensions/outliner/client.mjs b/src/extensions/outliner/client.mjs
index 6a7d51a..bdbcf38 100644
--- a/src/extensions/outliner/client.mjs
+++ b/src/extensions/outliner/client.mjs
@@ -8,15 +8,16 @@
"use strict";
export default async (api, db) => {
- const { html, addMutationListener, addPanelView } = api,
- frame = ".notion-sidebar-container + div",
+ const { html, debounce, addMutationListener, addPanelView } = api,
+ behavior = (await db.get("smoothScrolling")) ? "smooth" : "auto",
+ scroller = ".notion-frame > .notion-scroller",
page = ".notion-page-content",
headings = [
".notion-header-block",
".notion-sub_header-block",
".notion-sub_sub_header-block",
],
- $view = html``;
+ $toc = html``;
addPanelView({
title: "Outliner",
// prettier-ignore
@@ -28,7 +29,15 @@ export default async (api, db) => {
`,
- $view,
+ $view: html`
+
+ Click on a heading to jump to it.
+
+ ${$toc}
+ `,
});
function Heading({ indent, ...props }, ...children) {
@@ -36,9 +45,9 @@ export default async (api, db) => {
role="button"
class="notion-enhancer--outliner-heading
block cursor-pointer select-none text-[14px]
- m-px py-[6px] pr-[2px] pl-[${indent * 18}px]
decoration-(2 [color:var(--theme--fg-border)])
hover:bg-[color:var(--theme--bg-hover)]
+ py-[6px] pr-[2px] pl-[${indent * 18}px]
underline-(& offset-4)"
...${props}
>
@@ -46,29 +55,38 @@ export default async (api, db) => {
`;
}
+ let $page;
+ const updatePage = () => {
+ if (document.contains($page)) return;
+ $page = document.querySelector(page);
+ updateHeadings();
+ };
+
const getHeadings = () => {
- return [...document.querySelectorAll(headings.join(", "))];
+ return [...$page.querySelectorAll(headings.join(", "))];
},
getHeadingLevel = ($heading) => {
for (let i = 0; i < headings.length; i++)
if ($heading.matches(headings[i])) return i + 1;
},
- updateHeadings = () => {
- $view.innerHTML = "";
+ updateHeadings = debounce(() => {
+ $toc.innerHTML = "";
+ if (!$page) return;
+ const $frag = document.createDocumentFragment();
for (const $heading of getHeadings()) {
const title = $heading.innerText,
indent = getHeadingLevel($heading);
if (!title) continue;
const $h = html`<${Heading} indent=${indent} onclick=${() => {
- // todo: scroll into view
+ const $scroller = document.querySelector(scroller);
+ $scroller.scrollTo({ top: $heading.offsetTop - 24, behavior });
}}>${title}
`;
- $view.append($h);
+ $frag.append($h);
}
- };
+ $toc.append($frag);
+ });
- let $page;
- addMutationListener(page, () => {
- if (!document.contains($page)) $page = document.querySelector(page);
- if ($page) updateHeadings();
- });
+ const semanticHeadings = '[class$="header-block"] :is(h2, h3, h4)';
+ addMutationListener(`${page} ${semanticHeadings}`, updateHeadings);
+ addMutationListener(`${page}, ${scroller}`, updatePage, false);
};
diff --git a/src/extensions/outliner/mod.json b/src/extensions/outliner/mod.json
index 2b2de33..6ebc5e5 100644
--- a/src/extensions/outliner/mod.json
+++ b/src/extensions/outliner/mod.json
@@ -17,5 +17,13 @@
"avatar": "https://avatars.githubusercontent.com/u/54142180"
}
],
+ "options": [
+ {
+ "type": "toggle",
+ "key": "smoothScrolling",
+ "description": "Animates scrolling to a heading smoothly. Disable this to jump to a heading instantly when clicking it in the Outliner's table of contents.",
+ "value": true
+ }
+ ],
"clientScripts": ["client.mjs"]
}
diff --git a/src/extensions/scroll-to-top/client.mjs b/src/extensions/scroll-to-top/client.mjs
index 67d1f1c..674bd2a 100644
--- a/src/extensions/scroll-to-top/client.mjs
+++ b/src/extensions/scroll-to-top/client.mjs
@@ -19,7 +19,7 @@ export default async (api, db) => {
let $scroller;
const $btn = html`<${FloatingButton}
- onclick=${() => $scroller?.scroll({ top: 0, left: 0, behavior })}
+ onclick=${() => $scroller?.scroll({ top: 0, behavior })}
aria-label="Scroll to top"
>
/>`,
@@ -38,6 +38,6 @@ export default async (api, db) => {
$scroller?.addEventListener("scroll", onScroll);
onScroll();
};
- addMutationListener(scroller, setup, true);
+ addMutationListener(scroller, setup, false);
setup();
};
diff --git a/src/extensions/scroll-to-top/mod.json b/src/extensions/scroll-to-top/mod.json
index f279483..f13b372 100644
--- a/src/extensions/scroll-to-top/mod.json
+++ b/src/extensions/scroll-to-top/mod.json
@@ -20,7 +20,7 @@
{
"type": "toggle",
"key": "smoothScrolling",
- "description": "Animates the return to the top of the page smoothly. Disable this to jump instantly to the top of the page when clicking the scroll to top button.",
+ "description": "Animates the return to the top of the page smoothly. Disable this to jump to the top of the page instantly when clicking the scroll to top button.",
"value": true
},
{
diff --git a/src/extensions/topbar/client.mjs b/src/extensions/topbar/client.mjs
index 3095e23..bd62d39 100644
--- a/src/extensions/topbar/client.mjs
+++ b/src/extensions/topbar/client.mjs
@@ -26,77 +26,82 @@ export default async function (api, db) {
$btn.style.padding = "0px 8px";
$btn.innerHTML = $btn.ariaLabel;
},
- displayIcon = ($btn, icon) => {
- if ($btn.innerHTML === icon) return;
+ displayIcon = ($btn, $icon) => {
+ if ($btn.contains($icon)) return;
$btn.style.width = "33px";
$btn.style.padding = "0px";
$btn.style.justifyContent = "center";
- $btn.innerHTML = icon;
+ $btn.innerHTML = "";
+ $btn.append($icon);
};
// share button is text by default
const shareSelector = ".notion-topbar-share-menu",
shareButton = await db.get("shareButton"),
- shareIcon = await db.get("shareIcon");
+ shareIcon = await db.get("shareIcon"),
+ $shareIcon = shareIcon
+ ? html(shareIcon.content)
+ : html``;
addMutationListener(shareSelector, () => {
const $btn = document.querySelector(shareSelector);
- let icon = shareIcon?.content;
- icon ??= ``;
if (!$btn) return;
- if (shareButton === "Icon") displayIcon($btn, icon);
+ if (shareButton === "Icon") displayIcon($btn, $shareIcon);
if (shareButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const commentsSelector = ".notion-topbar-comments-button",
commentsButton = await db.get("commentsButton"),
- commentsIcon = await db.get("commentsIcon");
+ commentsIcon = await db.get("commentsIcon"),
+ $commentsIcon = commentsIcon ? html(commentsIcon.content) : undefined;
addMutationListener(commentsSelector, () => {
- const $btn = document.querySelector(commentsSelector),
- icon = commentsIcon?.content;
+ const $btn = document.querySelector(commentsSelector);
if (!$btn) return;
if (commentsButton === "Text") displayLabel($btn);
- if (commentsButton === "Icon" && icon) displayIcon($btn, icon);
+ if (commentsButton === "Icon" && commentsIcon)
+ displayIcon($btn, $commentsIcon);
if (commentsButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const updatesSelector = ".notion-topbar-updates-button",
updatesButton = await db.get("updatesButton"),
- updatesIcon = await db.get("updatesIcon");
+ updatesIcon = await db.get("updatesIcon"),
+ $updatesIcon = updatesIcon ? html(updatesIcon.content) : undefined;
addMutationListener(updatesSelector, () => {
- const $btn = document.querySelector(updatesSelector),
- icon = updatesIcon?.content;
+ const $btn = document.querySelector(updatesSelector);
if (!$btn) return;
if (updatesButton === "Text") displayLabel($btn);
- if (updatesButton === "Icon" && icon) displayIcon($btn, icon);
+ if (updatesButton === "Icon" && updatesIcon)
+ displayIcon($btn, $updatesIcon);
if (updatesButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const favoriteSelector = ".notion-topbar-favorite-button",
favoriteButton = await db.get("favoriteButton"),
- favoriteIcon = await db.get("favoriteIcon");
+ favoriteIcon = await db.get("favoriteIcon"),
+ $favoriteIcon = favoriteIcon ? html(favoriteIcon.content) : undefined;
addMutationListener(favoriteSelector, () => {
- const $btn = document.querySelector(favoriteSelector),
- icon = favoriteIcon?.content;
+ const $btn = document.querySelector(favoriteSelector);
if (!$btn) return;
if (favoriteButton === "Text") displayLabel($btn);
- if (favoriteButton === "Icon" && icon) displayIcon($btn, icon);
+ if (favoriteButton === "Icon" && favoriteIcon)
+ displayIcon($btn, $favoriteIcon);
if (favoriteButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const moreSelector = ".notion-topbar-more-button",
moreButton = await db.get("moreButton"),
- moreIcon = await db.get("moreIcon");
+ moreIcon = await db.get("moreIcon"),
+ $moreIcon = moreIcon ? html(moreIcon.content) : undefined;
addMutationListener(moreSelector, () => {
- const $btn = document.querySelector(moreSelector),
- icon = moreIcon?.content;
+ const $btn = document.querySelector(moreSelector);
if (!$btn) return;
- $btn.ariaLabel = "More";
+ if (!$btn.ariaLabel) $btn.ariaLabel = "More";
if (moreButton === "Text") displayLabel($btn);
- if (moreButton === "Icon" && icon) displayIcon($btn, icon);
+ if (moreButton === "Icon" && moreIcon) displayIcon($btn, $moreIcon);
if (moreButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});