mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-08 06:29:03 +00:00
feat: scroll to heading on click in outliner
This commit is contained in:
parent
6661c5559b
commit
ba8c660442
@ -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,
|
||||
});
|
||||
|
@ -99,7 +99,7 @@ const insertMenu = async (api, db) => {
|
||||
<b>Configure the notion-enhancer and its mods</b>
|
||||
<//>`.attach($button, "right");
|
||||
addMutationListener(notionSidebar, appendToDom);
|
||||
addMutationListener(".notion-app-inner", updateMenuTheme, true);
|
||||
addMutationListener(".notion-app-inner", updateMenuTheme, false);
|
||||
appendToDom();
|
||||
|
||||
addKeyListener(openMenuHotkey, (event) => {
|
||||
|
@ -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}
|
||||
|
@ -1,47 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: outliner
|
||||
* (c) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
|
||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (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);
|
||||
}
|
@ -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`<div></div>`;
|
||||
$toc = html`<div></div>`;
|
||||
addPanelView({
|
||||
title: "Outliner",
|
||||
// prettier-ignore
|
||||
@ -28,7 +29,15 @@ export default async (api, 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="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,
|
||||
$view: html`<section>
|
||||
<p
|
||||
class="py-[12px] pl-[18px]
|
||||
text-([color:var(--theme--fg-secondary)] [13px])"
|
||||
>
|
||||
Click on a heading to jump to it.
|
||||
</p>
|
||||
${$toc}
|
||||
</section>`,
|
||||
});
|
||||
|
||||
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) => {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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}</p>`;
|
||||
$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);
|
||||
};
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"
|
||||
><i class="i-chevrons-up" />
|
||||
<//>`,
|
||||
@ -38,6 +38,6 @@ export default async (api, db) => {
|
||||
$scroller?.addEventListener("scroll", onScroll);
|
||||
onScroll();
|
||||
};
|
||||
addMutationListener(scroller, setup, true);
|
||||
addMutationListener(scroller, setup, false);
|
||||
setup();
|
||||
};
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
@ -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`<i class="i-share2 size-[20px]"></i>`;
|
||||
addMutationListener(shareSelector, () => {
|
||||
const $btn = document.querySelector(shareSelector);
|
||||
let icon = shareIcon?.content;
|
||||
icon ??= `<i class="i-share2 size-[20px]"></i>`;
|
||||
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";
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user