mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-06 21:49:03 +00:00
side panel switcher
This commit is contained in:
parent
bc38f5d972
commit
6656b639d7
@ -72,61 +72,81 @@
|
||||
transform: translateX(calc(-1 * var(--component--panel-width)));
|
||||
}
|
||||
|
||||
#enhancer--panel-header {
|
||||
font-size: 1.35rem;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
padding: 0.75rem 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
#enhancer--panel-content {
|
||||
font-size: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
#enhancer--panel-header-title {
|
||||
padding-left: 0.5em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
#enhancer--panel-header-title > p {
|
||||
.enhancer--panel-view-title {
|
||||
margin: 0;
|
||||
height: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
#enhancer--panel-header-title > p svg,
|
||||
#enhancer--panel-header-title > p img {
|
||||
.enhancer--panel-view-title svg,
|
||||
.enhancer--panel-view-title img {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
#enhancer--panel-header-title > p span {
|
||||
.enhancer--panel-view-title span {
|
||||
font-size: 0.9em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
#enhancer--panel-header {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
#enhancer--panel-header .enhancer--panel-view-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
#enhancer--panel-content {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
#enhancer--panel-header-title {
|
||||
padding: 0 0.5em 0.1em 1rem;
|
||||
}
|
||||
#enhancer--panel-header-switcher {
|
||||
padding: 4px;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
#enhancer--panel-header-toggle {
|
||||
margin-left: auto;
|
||||
padding-right: 1rem;
|
||||
height: 100%;
|
||||
width: 2.5em;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
}
|
||||
#enhancer--panel-header-toggle,
|
||||
#enhancer--panel-header-switcher {
|
||||
#enhancer--panel-header-toggle > div {
|
||||
margin: auto 0 auto auto;
|
||||
}
|
||||
#enhancer--panel-header-switcher,
|
||||
#enhancer--panel-header-toggle > div {
|
||||
color: var(--theme--icon_secondary);
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: 300ms ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 3px;
|
||||
transition: 300ms ease-in-out;
|
||||
}
|
||||
#enhancer--panel-header-switcher svg {
|
||||
width: 0.5em;
|
||||
height: 0.5em;
|
||||
display: block;
|
||||
margin: auto;
|
||||
#enhancer--panel-header-switcher:hover,
|
||||
#enhancer--panel-header-switcher:focus,
|
||||
#enhancer--panel-header-toggle > div:hover,
|
||||
#enhancer--panel-header-toggle > div:focus {
|
||||
background: var(--theme--ui_interactive-hover);
|
||||
}
|
||||
|
||||
#enhancer--panel:not([data-enhancer-panel-pinned]) #enhancer--panel-header-toggle {
|
||||
#enhancer--panel #enhancer--panel-header-toggle svg {
|
||||
transition: 300ms ease-in-out;
|
||||
}
|
||||
#enhancer--panel:not([data-enhancer-panel-pinned]) #enhancer--panel-header-toggle svg {
|
||||
transform: rotateZ(-180deg);
|
||||
}
|
||||
#enhancer--panel:hover #enhancer--panel-header-toggle,
|
||||
#enhancer--panel:hover #enhancer--panel-header-switcher {
|
||||
#enhancer--panel:hover #enhancer--panel-header-toggle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -149,3 +169,39 @@
|
||||
#enhancer--panel[data-enhancer-panel-pinned] #enhancer--panel-resize:hover div {
|
||||
background: var(--theme--ui_divider);
|
||||
}
|
||||
|
||||
#enhancer--panel-switcher-overlay-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
overflow: hidden;
|
||||
}
|
||||
#enhancer--panel-switcher {
|
||||
max-width: 320px;
|
||||
position: relative;
|
||||
right: 14px;
|
||||
border-radius: 3px;
|
||||
padding: 8px 0;
|
||||
background: var(--theme--bg_popup);
|
||||
box-shadow: var(--theme--ui_shadow, rgba(15, 15, 15, 0.05)) 0px 0px 0px 1px,
|
||||
var(--theme--ui_shadow, rgba(15, 15, 15, 0.1)) 0px 3px 6px,
|
||||
var(--theme--ui_shadow, rgba(15, 15, 15, 0.2)) 0px 9px 24px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
.enhancer--panel-switcher-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: background 300ms ease;
|
||||
}
|
||||
.enhancer--panel-switcher-item:hover,
|
||||
.enhancer--panel-switcher-item:focus {
|
||||
background: var(--theme--ui_interactive-hover);
|
||||
}
|
||||
|
@ -17,6 +17,15 @@ const db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d');
|
||||
let $panel,
|
||||
_views = [];
|
||||
|
||||
const svgExpand = web.raw`<svg viewBox="-1 -1 9 11">
|
||||
<path d="M 3.5 0L 3.98809 -0.569442L 3.5 -0.987808L 3.01191 -0.569442L 3.5 0ZM 3.5 9L 3.01191
|
||||
9.56944L 3.5 9.98781L 3.98809 9.56944L 3.5 9ZM 0.488094 3.56944L 3.98809 0.569442L 3.01191
|
||||
-0.569442L -0.488094 2.43056L 0.488094 3.56944ZM 3.01191 0.569442L 6.51191 3.56944L 7.48809
|
||||
2.43056L 3.98809 -0.569442L 3.01191 0.569442ZM -0.488094 6.56944L 3.01191 9.56944L 3.98809
|
||||
8.43056L 0.488094 5.43056L -0.488094 6.56944ZM 3.98809 9.56944L 7.48809 6.56944L 6.51191
|
||||
5.43056L 3.01191 8.43056L 3.98809 9.56944Z"></path>
|
||||
</svg>`;
|
||||
|
||||
export const panel = async (icon, title, generator = () => {}) => {
|
||||
_views.push({
|
||||
icon: web.html`${icon}`,
|
||||
@ -31,44 +40,31 @@ export const panel = async (icon, title, generator = () => {}) => {
|
||||
await web.whenReady([notionRightSidebarSelector]);
|
||||
web.loadStylesheet('api/components/panel.css');
|
||||
|
||||
const $title = web.html`<div id="enhancer--panel-header-title"></div>`,
|
||||
$header = web.render(web.html`<div id="enhancer--panel-header"></div>`, $title),
|
||||
$content = web.html`<div id="enhancer--panel-content"></div>`;
|
||||
|
||||
// opening/closing
|
||||
const $notionFrame = document.querySelector('.notion-frame'),
|
||||
$notionRightSidebar = document.querySelector(notionRightSidebarSelector),
|
||||
$pinnedToggle = web.html`<div id="enhancer--panel-header-toggle">
|
||||
$pinnedToggle = web.html`<div id="enhancer--panel-header-toggle"><div>
|
||||
${await components.feather('chevrons-right')}
|
||||
</div>`,
|
||||
</div></div>`,
|
||||
$hoverTrigger = web.html`<div id="enhancer--panel-hover-trigger"></div>`,
|
||||
panelPinnedAttr = 'data-enhancer-panel-pinned',
|
||||
isPinned = () => $panel.hasAttribute(panelPinnedAttr),
|
||||
isRightSidebarOpen = () =>
|
||||
$notionRightSidebar.matches('[style*="border-left: 1px solid rgba(0, 0, 0, 0)"]'),
|
||||
togglePanel = () => {
|
||||
const $elems = [$notionRightSidebar, $hoverTrigger, $panel];
|
||||
const $elems = [$notionRightSidebar, $notionFrame, $hoverTrigger, $panel];
|
||||
if (isPinned()) {
|
||||
if (isRightSidebarOpen()) $elems.push($notionFrame);
|
||||
closeSwitcher();
|
||||
for (const $elem of $elems) $elem.removeAttribute(panelPinnedAttr);
|
||||
} else {
|
||||
$elems.push($notionFrame);
|
||||
for (const $elem of $elems) $elem.setAttribute(panelPinnedAttr, 'true');
|
||||
}
|
||||
db.set(['panel.pinned'], isPinned());
|
||||
};
|
||||
web.addDocumentObserver(() => {
|
||||
if (isPinned()) {
|
||||
if (isRightSidebarOpen()) {
|
||||
$notionFrame.removeAttribute(panelPinnedAttr);
|
||||
} else {
|
||||
$notionFrame.setAttribute(panelPinnedAttr, 'true');
|
||||
}
|
||||
}
|
||||
}, [notionRightSidebarSelector]);
|
||||
if (await db.get(['panel.pinned'])) togglePanel();
|
||||
web.addHotkeyListener(await db.get(['panel.hotkey']), togglePanel);
|
||||
$pinnedToggle.addEventListener('click', togglePanel);
|
||||
$pinnedToggle.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
togglePanel();
|
||||
});
|
||||
|
||||
// resizing
|
||||
let dragStartX,
|
||||
@ -120,19 +116,90 @@ export const panel = async (icon, title, generator = () => {}) => {
|
||||
});
|
||||
|
||||
// view selection
|
||||
const $switcherTrigger = web.html`<div id="enhancer--panel-header-switcher">
|
||||
${await components.feather('chevron-up')}
|
||||
${await components.feather('chevron-down')}
|
||||
const $title = web.html`<div id="enhancer--panel-header-title"></div>`,
|
||||
$header = web.render(web.html`<div id="enhancer--panel-header"></div>`, $title),
|
||||
$content = web.html`<div id="enhancer--panel-content"></div>`,
|
||||
$switcherTrigger = web.html`<div id="enhancer--panel-header-switcher">
|
||||
${svgExpand}
|
||||
</div>`,
|
||||
$notionApp = document.querySelector('.notion-app-inner'),
|
||||
$switcherOverlayContainer = web.html`<div id="enhancer--panel-switcher-overlay-container"></div>`,
|
||||
$switcher = web.html`<div id="enhancer--panel-switcher"></div>`,
|
||||
isSwitcherOpen = () => document.body.contains($switcher),
|
||||
renderView = (view) => {
|
||||
web.render(web.empty($title), web.render(web.html`<p></p>`, view.icon, view.title));
|
||||
web.render(
|
||||
web.empty($title),
|
||||
web.render(
|
||||
web.html`<p class="enhancer--panel-view-title"></p>`,
|
||||
view.icon,
|
||||
view.title
|
||||
)
|
||||
);
|
||||
web.render(web.empty($content), view.$elem);
|
||||
},
|
||||
openSwitcher = () => {
|
||||
if (!isPinned()) return togglePanel();
|
||||
web.render($notionApp, $switcherOverlayContainer);
|
||||
web.empty($switcher);
|
||||
for (const view of _views) {
|
||||
const $item = web.render(
|
||||
web.html`<div class="enhancer--panel-switcher-item" tabindex="0"></div>`,
|
||||
web.render(
|
||||
web.html`<p class="enhancer--panel-view-title"></p>`,
|
||||
view.icon.cloneNode(true),
|
||||
view.title.cloneNode(true)
|
||||
)
|
||||
);
|
||||
$item.addEventListener('click', () => renderView(view));
|
||||
web.render($switcher, $item);
|
||||
}
|
||||
const rect = $header.getBoundingClientRect();
|
||||
web.render(
|
||||
web.empty($switcherOverlayContainer),
|
||||
web.render(
|
||||
web.html`<div style="position: fixed; top: ${rect.top}px; left: ${rect.left}px;
|
||||
width: ${rect.width}px; height: ${rect.height}px;"></div>`,
|
||||
web.render(
|
||||
web.html`<div style="position: relative; top: 100%; pointer-events: auto;"></div>`,
|
||||
$switcher
|
||||
)
|
||||
)
|
||||
);
|
||||
$switcher.firstElementChild.focus();
|
||||
$switcher.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
|
||||
};
|
||||
function closeSwitcher() {
|
||||
$switcher.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200 }).onfinish = () =>
|
||||
$switcherOverlayContainer.remove();
|
||||
}
|
||||
web.addHotkeyListener(['Escape'], () => {
|
||||
if (isSwitcherOpen()) closeSwitcher();
|
||||
});
|
||||
web.addHotkeyListener(['Enter'], () => {
|
||||
if (isSwitcherOpen()) document.activeElement.click();
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (isSwitcherOpen()) {
|
||||
if (event.key === 'ArrowUp') {
|
||||
const $prev = event.target.previousElementSibling;
|
||||
($prev || event.target.parentElement.lastElementChild).focus();
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (event.key === 'ArrowDown') {
|
||||
const $next = event.target.nextElementSibling;
|
||||
($next || event.target.parentElement.firstElementChild).focus();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
$header.addEventListener('click', openSwitcher);
|
||||
$switcherTrigger.addEventListener('click', openSwitcher);
|
||||
$switcherOverlayContainer.addEventListener('click', closeSwitcher);
|
||||
renderView(_views[0]);
|
||||
|
||||
web.render(
|
||||
$panel,
|
||||
web.render($header, $switcherTrigger, $title, $pinnedToggle),
|
||||
web.render($header, $title, $switcherTrigger, $pinnedToggle),
|
||||
$content,
|
||||
$resizeHandle
|
||||
);
|
||||
|
@ -11,7 +11,12 @@
|
||||
* @module notion-enhancer/api/web
|
||||
*/
|
||||
|
||||
import { fs, fmt } from './_.mjs';
|
||||
import { fs } from './_.mjs';
|
||||
|
||||
let _hotkeyEventListeners = [],
|
||||
_documentObserver,
|
||||
_documentObserverListeners = [],
|
||||
_documentObserverEvents = [];
|
||||
|
||||
import '../dep/jscolor.min.js';
|
||||
/** color picker with alpha channel using https://jscolor.com/ */
|
||||
@ -138,27 +143,26 @@ export const loadStylesheet = (path) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const _hotkeyEvent = document.addEventListener('keyup', (event) => {
|
||||
if (document.activeElement.nodeName === 'INPUT') return;
|
||||
for (const hotkey of _hotkeyEventListeners) {
|
||||
const pressed = hotkey.keys.every((key) => {
|
||||
key = key.toLowerCase();
|
||||
const modifiers = {
|
||||
metaKey: ['meta', 'os', 'win', 'cmd', 'command'],
|
||||
ctrlKey: ['ctrl', 'control'],
|
||||
shiftKey: ['shift'],
|
||||
altKey: ['alt'],
|
||||
};
|
||||
for (const modifier in modifiers) {
|
||||
const pressed = modifiers[modifier].includes(key) && event[modifier];
|
||||
if (pressed) return true;
|
||||
}
|
||||
if (key === event.key.toLowerCase()) return true;
|
||||
});
|
||||
if (pressed) hotkey.callback();
|
||||
}
|
||||
}),
|
||||
_hotkeyEventListeners = [];
|
||||
document.addEventListener('keyup', (event) => {
|
||||
if (document.activeElement.nodeName === 'INPUT') return;
|
||||
for (const hotkey of _hotkeyEventListeners) {
|
||||
const pressed = hotkey.keys.every((key) => {
|
||||
key = key.toLowerCase();
|
||||
const modifiers = {
|
||||
metaKey: ['meta', 'os', 'win', 'cmd', 'command'],
|
||||
ctrlKey: ['ctrl', 'control'],
|
||||
shiftKey: ['shift'],
|
||||
altKey: ['alt'],
|
||||
};
|
||||
for (const modifier in modifiers) {
|
||||
const pressed = modifiers[modifier].includes(key) && event[modifier];
|
||||
if (pressed) return true;
|
||||
}
|
||||
if (key === event.key.toLowerCase()) return true;
|
||||
});
|
||||
if (pressed) hotkey.callback(event);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* register a hotkey listener to the page
|
||||
@ -181,39 +185,40 @@ export const removeHotkeyListener = (callback) => {
|
||||
);
|
||||
};
|
||||
|
||||
const _documentObserver = new MutationObserver((list, observer) => {
|
||||
if (!_documentObserverEvents.length)
|
||||
requestIdleCallback(() => (queue) => {
|
||||
while (queue.length) {
|
||||
const event = queue.shift();
|
||||
for (const listener of _documentObserverListeners) {
|
||||
if (
|
||||
!listener.selectors.length ||
|
||||
listener.selectors.some(
|
||||
(selector) =>
|
||||
event.target.matches(selector) || event.target.matches(`${selector} *`)
|
||||
)
|
||||
) {
|
||||
listener.callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
_documentObserverEvents.push(...list);
|
||||
}),
|
||||
_documentObserverListeners = [],
|
||||
_documentObserverEvents = [];
|
||||
_documentObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
});
|
||||
/**
|
||||
* add a listener to watch for changes to the dom
|
||||
* @param {onDocumentObservedCallback} callback
|
||||
* @param {array<string>} [selectors]
|
||||
*/
|
||||
export const addDocumentObserver = (callback, selectors = []) => {
|
||||
if (!_documentObserver) {
|
||||
const handle = (queue) => {
|
||||
while (queue.length) {
|
||||
const event = queue.shift();
|
||||
for (const listener of _documentObserverListeners) {
|
||||
if (
|
||||
!listener.selectors.length ||
|
||||
listener.selectors.some(
|
||||
(selector) =>
|
||||
event.target.matches(selector) || event.target.matches(`${selector} *`)
|
||||
)
|
||||
) {
|
||||
listener.callback(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
_documentObserver = new MutationObserver((list, observer) => {
|
||||
if (!_documentObserverEvents.length)
|
||||
requestIdleCallback(() => handle(_documentObserverEvents));
|
||||
_documentObserverEvents.push(...list);
|
||||
});
|
||||
_documentObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
_documentObserverListeners.push({ callback, selectors });
|
||||
};
|
||||
|
||||
|
@ -34,4 +34,7 @@ export default async function (api, db) {
|
||||
components.panel(await components.feather('sidebar'), 'Test Panel', ($panel) => {
|
||||
return web.html`<p>test</p>`;
|
||||
});
|
||||
components.panel(await components.feather('users'), 'Other Panel', ($panel) => {
|
||||
return web.html`<p>yay</p>`;
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user