chore: perf optimisations, overflow topbar to the left when comments & updates sidebars are open

This commit is contained in:
dragonwocky 2024-01-25 11:44:28 +11:00
parent 1a709f5a84
commit aae4c7e663
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
15 changed files with 113 additions and 68 deletions

View File

@ -58,30 +58,36 @@ const _state = {},
callback(state);
}
return state;
};
},
dumpState = () => _state;
let documentObserver,
mutationListeners = [];
const mutationQueue = [],
addMutationListener = (selector, callback) => {
mutationListeners.push([selector, callback]);
addMutationListener = (selector, callback, attributesOnly = false) => {
mutationListeners.push([selector, callback, attributesOnly]);
},
removeMutationListener = (callback) => {
mutationListeners = mutationListeners.filter(([, c]) => c !== callback);
},
onSelectorMutated = (mutation, selector) =>
mutation.target?.matches(`${selector}, ${selector} *`) ||
[...(mutation.addedNodes || [])].some(
(node) =>
node instanceof HTMLElement &&
(node?.matches(`${selector}, ${selector} *`) ||
node?.querySelector(selector))
),
selectorMutated = (mutation, selector, attributesOnly) => {
const matchesTarget = mutation.target?.matches(selector);
if (attributesOnly) return matchesTarget;
const descendsFromTarget = mutation.target?.matches(`${selector} *`),
addedToTarget = [...(mutation.addedNodes || [])].some(
(node) =>
node instanceof HTMLElement &&
(node?.matches(`${selector}, ${selector} *`) ||
node?.querySelector(selector))
);
return matchesTarget || descendsFromTarget || addedToTarget;
},
handleMutations = () => {
while (mutationQueue.length) {
const mutation = mutationQueue.shift();
for (const [selector, callback] of mutationListeners) {
if (onSelectorMutated(mutation, selector)) callback(mutation);
for (const [selector, callback, attributesOnly] of mutationListeners) {
const matches = selectorMutated(mutation, selector, attributesOnly);
if (matches) callback(mutation);
}
}
},
@ -169,6 +175,7 @@ Object.assign((globalThis.__enhancerApi ??= {}), {
debounce,
setState,
useState,
dumpState,
addMutationListener,
removeMutationListener,
addKeyListener,

View File

@ -11,6 +11,7 @@ import { sendTelemetryPing } from "./sendTelemetry.mjs";
import { Modal, Frame } from "./islands/Modal.mjs";
import { MenuButton } from "./islands/MenuButton.mjs";
import { TopbarButton } from "./islands/TopbarButton.mjs";
import { Tooltip } from "./islands/Tooltip.mjs";
import { Panel } from "./islands/Panel.mjs";
const shouldLoadThemeOverrides = async (api, db) => {
@ -45,8 +46,8 @@ const shouldLoadThemeOverrides = async (api, db) => {
const insertMenu = async (api, db) => {
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`,
notionSettingsAndMembers = `${notionSidebar} > [role="button"]:nth-child(3)`,
{ html, addKeyListener, addMutationListener } = api,
{ platform, enhancerUrl, onMessage } = api,
{ html, addMutationListener, removeMutationListener } = api,
{ addKeyListener, platform, enhancerUrl, onMessage } = api,
menuButtonIconStyle = await db.get("menuButtonIconStyle"),
openMenuHotkey = await db.get("openMenuHotkey"),
menuPing = {
@ -59,19 +60,11 @@ const insertMenu = async (api, db) => {
const updateMenuTheme = () => {
const darkMode = document.body.classList.contains("dark"),
notionTheme = darkMode ? "dark" : "light";
if (menuPing.theme === notionTheme) return;
menuPing.theme = notionTheme;
_contentWindow?.postMessage?.(menuPing, "*");
},
triggerMenuRender = (contentWindow) => {
_contentWindow ??= contentWindow;
if (!$modal.hasAttribute("open")) return;
_contentWindow?.focus?.();
delete menuPing.theme;
updateMenuTheme();
};
const $modal = html`<${Modal} onopen=${triggerMenuRender}>
const $modal = html`<${Modal}>
<${Frame}
title="notion-enhancer menu"
src="${enhancerUrl("core/menu/index.html")}"
@ -81,7 +74,8 @@ const insertMenu = async (api, db) => {
const apiKey = "__enhancerApi";
this.contentWindow[apiKey] = globalThis[apiKey];
}
triggerMenuRender(this.contentWindow);
_contentWindow = this.contentWindow;
updateMenuTheme();
}}
/>
<//>`,
@ -95,12 +89,17 @@ const insertMenu = async (api, db) => {
>notion-enhancer
<//>`;
const appendToDom = () => {
if (!document.contains($modal)) document.body.append($modal);
if (!document.querySelector(notionSidebar)?.contains($button))
document.querySelector(notionSettingsAndMembers)?.after($button);
const $settings = document.querySelector(notionSettingsAndMembers);
document.body.append($modal);
$settings?.after($button);
const appended = document.contains($modal) && document.contains($button);
if (appended) removeMutationListener(appendToDom);
};
html`<${Tooltip}>
<b>Configure the notion-enhancer and its mods</b>
<//>`.attach($button, "right");
addMutationListener(notionSidebar, appendToDom);
addMutationListener("body", updateMenuTheme);
addMutationListener(".notion-app-inner", updateMenuTheme, true);
appendToDom();
addKeyListener(openMenuHotkey, (event) => {
@ -108,7 +107,6 @@ const insertMenu = async (api, db) => {
event.stopPropagation();
$modal.open();
});
window.addEventListener("focus", triggerMenuRender);
window.addEventListener("message", (event) => {
// from embedded menu
if (event.data?.channel !== "notion-enhancer") return;
@ -124,8 +122,8 @@ const insertMenu = async (api, db) => {
const insertPanel = async (api, db) => {
const notionFrame = ".notion-frame",
togglePanelHotkey = await db.get("togglePanelHotkey"),
{ addPanelView, addMutationListener } = api,
{ html, setState } = api;
{ addMutationListener, removeMutationListener } = api,
{ html, setState, addPanelView } = api;
const $panel = html`<${Panel}
hotkey="${togglePanelHotkey}"
@ -142,9 +140,9 @@ const insertPanel = async (api, db) => {
appendToDom = () => {
const $frame = document.querySelector(notionFrame);
if (!$frame) return;
if (!$frame.contains($panel)) $frame.append($panel);
if (!$frame.style.flexDirection !== "row")
$frame.style.flexDirection = "row";
$frame.append($panel);
$frame.style.flexDirection = "row";
removeMutationListener(appendToDom);
};
addMutationListener(notionFrame, appendToDom);
appendToDom();

View File

@ -38,11 +38,10 @@ function Modal(props, ...children) {
await new Promise(requestAnimationFrame);
}
$modal.setAttribute("open", "");
$modal.onopen?.();
setTimeout(() => $modal.onopen?.(), 200);
};
$modal.close = () => {
_openQueued = false;
$modal.onbeforeclose?.();
$modal.removeAttribute("open");
if ($modal.contains(document.activeElement)) {
document.activeElement.blur();

View File

@ -144,7 +144,7 @@ function Panel({
icon="panel-right"
/>`,
addToTopbar = () => {
if (document.contains($topbarToggle)) return;
if (document.contains($topbarToggle)) removeMutationListener(addToTopbar);
document.querySelector(topbarFavorite)?.after($topbarToggle);
};
$panelToggle.onclick = $topbarToggle.onclick = () => $panel.toggle();
@ -158,7 +158,7 @@ function Panel({
panelIcon = await topbarDatabase.get("panelIcon");
if (panelButton === "Text") {
$topbarToggle.innerHTML = `<span>${$topbarToggle.ariaLabel}</span>`;
} else if (panelIcon.content) $topbarToggle.innerHTML = panelIcon.content;
} else if (panelIcon?.content) $topbarToggle.innerHTML = panelIcon.content;
});
let preDragWidth, dragStartX, _animatedAt;

View File

@ -73,7 +73,11 @@ function Tooltip(props, ...children) {
y = () => {
const rect = $target.getBoundingClientRect();
if (["left", "right"].includes(alignment)) {
return event.clientY - $tooltip.clientHeight / 2;
// match mouse alignment if hovering over large
// target e.g. panel resize handle, otherwise centre
return rect.height > $tooltip.clientHeight * 2
? event.clientY - $tooltip.clientHeight / 2
: rect.top + rect.height / 2 - $tooltip.clientHeight / 2;
} else if (alignment === "top") {
return rect.top - $tooltip.clientHeight - 6;
} else if (alignment === "bottom") {

View File

@ -7,7 +7,7 @@
"use strict";
function TopbarButton({ icon, ...props }, ...children) {
const { html, extendProps, addMutationListener } = globalThis.__enhancerApi;
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
tabindex: 0,
role: "button",

View File

@ -35,10 +35,6 @@
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 79px"></div>
</div>
<div class="row">
<div class="shimmer icon"></div>
<div class="shimmer" style="width: 51px"></div>
</div>
<div class="row row-group">
<div class="shimmer" style="width: 53px"></div>
</div>

View File

@ -58,7 +58,8 @@ body > #skeleton .row {
display: flex;
align-items: center;
padding: 0 15px;
height: 30px;
margin: 2px 0;
height: 27px;
}
body > #skeleton .shimmer {
height: 14px;
@ -88,7 +89,7 @@ body > #skeleton .shimmer::before {
);
}
body > #skeleton .row-group {
height: 21px;
height: 24px;
margin-top: 18px;
}
body > #skeleton .row-group:first-child {

View File

@ -126,7 +126,10 @@ const renderMenu = async () => {
categories=${categories}
/>`,
$main = html`
<main class="flex-(& col) overflow-hidden transition-[height]" style="height: calc(100% + 65px)">
<main
class="flex-(& col) overflow-hidden transition-[height]"
style="height: calc(100% + 65px)"
>
<!-- wrappers necessary for transitions and breakpoints -->
<div class="grow overflow-auto">
<div class="relative h-full w-full">
@ -197,19 +200,14 @@ const importApi = () => {
useState(["rerender"], renderMenu);
};
window.addEventListener("focus", async () => {
await importApi().then(hookIntoState);
const { setState } = globalThis.__enhancerApi;
setState({ focus: true, rerender: true });
});
window.addEventListener("message", async (event) => {
if (event.data?.channel !== "notion-enhancer") return;
await importApi().then(hookIntoState);
const { setState, useState } = globalThis.__enhancerApi;
setState({
rerender: true,
hotkey: event.data?.hotkey ?? useState(["hotkey"]),
theme: event.data?.theme ?? useState(["theme"]),
icon: event.data?.icon ?? useState(["icon"]),
hotkey: event.data?.hotkey ?? useState(["hotkey"])[0],
theme: event.data?.theme ?? useState(["theme"])[0],
icon: event.data?.icon ?? useState(["icon"])[0],
});
});

View File

@ -63,7 +63,7 @@ const createWindowButtons = async () => {
window.addEventListener("resize", resizeWindow);
resizeWindow();
return html`<div>${$minimize}${$maximize}${$unmaximize}${$close}</div>`;
return html`<div class="flex flex-nowrap">${$minimize}${$maximize}${$unmaximize}${$close}</div>`;
};
if (globalThis.IS_TABS) {

View File

@ -12,12 +12,12 @@ export default async (api, db) => {
const titlebarStyle = await db.get("titlebarStyle");
if (titlebarStyle === "Disabled") return;
const { onMessage, addMutationListener } = api,
const { onMessage, addMutationListener, removeMutationListener } = api,
$buttons = await createWindowButtons(),
topbarMore = ".notion-topbar-more-button",
addToTopbar = () => {
if (document.contains($buttons)) return;
document.querySelector(topbarMore)?.after($buttons)
if (document.contains($buttons)) removeMutationListener(addToTopbar);
document.querySelector(topbarMore)?.after($buttons);
},
showIfNoTabBar = async () => {
const { isShowingTabBar } = await __electronApi.electronAppFeatures.get();

View File

@ -0,0 +1,28 @@
/**
* notion-enhancer: topbar
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
/* overflow topbar to the left when comments & updates sidebars are open */
.notion-topbar-action-buttons {
flex-direction: row-reverse;
}
.notion-topbar-action-buttons > div {
flex-wrap: nowrap;
}
.notion-topbar-action-buttons > :nth-child(1) {
order: 2;
}
/* position comments & updates sidebars below topbar */
.notion-cursor-listener
aside[style*="align-self: flex-end"][style*="height: 100vh"] {
height: calc(100vh - 45px) !important;
top: 45px !important;
}
.notion-cursor-listener
aside[style*="align-self: flex-end"][style*="height: 100vh"]
[style*="margin-top: 45px"] {
margin-top: 0 !important;
}

View File

@ -16,7 +16,8 @@ const pinLabel = "Pin always on top",
unpinTooltip = "Unpin window from always on top";
export default async function (api, db) {
const { html, sendMessage, addMutationListener } = api,
const { html, sendMessage } = api,
{ addMutationListener, removeMutationListener } = api,
displayLabel = ($btn) => {
if ($btn.innerHTML === $btn.ariaLabel) return;
$btn.style.width = "auto";
@ -42,7 +43,8 @@ export default async function (api, db) {
let icon = shareIcon?.content;
icon ??= `<i class="i-share2 w-[20px] h-[20px]"></i>`;
if (shareButton === "Icon") displayIcon($btn, icon);
if (shareButton === "Disabled") $btn.style.display = "none";
if (shareButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const commentsSelector = ".notion-topbar-comments-button",
@ -53,7 +55,8 @@ export default async function (api, db) {
icon = commentsIcon?.content;
if (commentsButton === "Text") displayLabel($btn);
if (commentsButton === "Icon" && icon) displayIcon($btn, icon);
if (commentsButton === "Disabled") $btn.style.display = "none";
if (commentsButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const updatesSelector = ".notion-topbar-updates-button",
@ -64,7 +67,8 @@ export default async function (api, db) {
icon = updatesIcon?.content;
if (updatesButton === "Text") displayLabel($btn);
if (updatesButton === "Icon" && icon) displayIcon($btn, icon);
if (updatesButton === "Disabled") $btn.style.display = "none";
if (updatesButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const favoriteSelector = ".notion-topbar-favorite-button",
@ -75,7 +79,8 @@ export default async function (api, db) {
icon = favoriteIcon?.content;
if (favoriteButton === "Text") displayLabel($btn);
if (favoriteButton === "Icon" && icon) displayIcon($btn, icon);
if (favoriteButton === "Disabled") $btn.style.display = "none";
if (favoriteButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const moreSelector = ".notion-topbar-more-button",
@ -87,7 +92,8 @@ export default async function (api, db) {
$btn.ariaLabel = "More";
if (moreButton === "Text") displayLabel($btn);
if (moreButton === "Icon" && icon) displayIcon($btn, icon);
if (moreButton === "Disabled") $btn.style.display = "none";
if (moreButton === "Disabled" && $btn.style.display !== "none")
$btn.style.display = "none";
});
const alwaysOnTopButton = await db.get("alwaysOnTopButton");
@ -122,8 +128,10 @@ export default async function (api, db) {
icon="pin-off"
/>`,
addToTopbar = () => {
if (document.contains($pin) && document.contains($unpin)) return;
document.querySelector(topbarFavorite)?.after($pin, $unpin);
const $topbarFavorite = document.querySelector(topbarFavorite);
if (!$topbarFavorite) return;
$topbarFavorite.after($pin, $unpin);
removeMutationListener(addToTopbar);
};
html`<${Tooltip}><b>${pinTooltip}</b><//>`.attach($pin, "bottom");
html`<${Tooltip}><b>${unpinTooltip}</b><//>`.attach($unpin, "bottom");

View File

@ -118,6 +118,7 @@
"extensions": ["svg"]
}
],
"clientStyles": ["client.css"],
"clientScripts": ["client.mjs"],
"electronScripts": [[".webpack/main/index", "electron.cjs"]]
}

View File

@ -43,6 +43,7 @@ export default (async () => {
// their local states will be cleared (e.g.,
// references to registered hotkeys)
const _state = globalThis.__enhancerApi?.dumpState?.();
await Promise.all([
// i.e. if (not_menu) or (is_menu && not_electron), then import
!(!IS_MENU || !IS_ELECTRON) || import(enhancerUrl("assets/icons.svg.js")),
@ -55,6 +56,10 @@ export default (async () => {
import(enhancerUrl("common/events.js")),
import(enhancerUrl("common/markup.js")),
]);
// copy across state from prev. module imports if
// useState / setState are called early, otherwise
// e.g. menu will not persist theme from initial msg
globalThis.__enhancerApi.setState(_state ?? {});
const { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
for (const mod of await getMods()) {