side panel switcher

This commit is contained in:
dragonwocky 2021-10-03 03:13:10 +11:00
parent bc38f5d972
commit 6656b639d7
4 changed files with 238 additions and 107 deletions

View File

@ -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);
}

View File

@ -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
);

View File

@ -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 });
};

View File

@ -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>`;
});
}