mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-04 12:49:03 +00:00
collapsible headers: deprecate in favour of toggle headers
This commit is contained in:
parent
ad248ece6b
commit
ec5af9735d
@ -1,81 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: collapsible headers
|
||||
* (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
|
||||
*/
|
||||
|
||||
.collapsible_headers--toggle {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 6px;
|
||||
margin: 0 6px;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
transition: 200ms ease-in;
|
||||
}
|
||||
.collapsible_headers--toggle:hover {
|
||||
background: var(--theme--ui_interactive-hover);
|
||||
}
|
||||
|
||||
.collapsible_headers--toggle svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 200ms ease-out 0s;
|
||||
}
|
||||
[data-section-collapsed='false'] .collapsible_headers--toggle svg {
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
|
||||
/* position = left */
|
||||
[data-section-collapsed='true'] .collapsible_headers--toggle:first-child svg {
|
||||
transform: rotateZ(90deg);
|
||||
}
|
||||
.collapsible_headers--toggle:first-child {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* position = right / inline */
|
||||
[data-section-collapsed='true'] .collapsible_headers--toggle:last-child svg {
|
||||
transform: rotateZ(270deg);
|
||||
}
|
||||
.collapsible_headers--toggle:last-child {
|
||||
opacity: 0;
|
||||
}
|
||||
[data-section-collapsed='true'] .collapsible_headers--toggle:last-child,
|
||||
[data-section-collapsed]:hover .collapsible_headers--toggle:last-child,
|
||||
[data-section-collapsed] :focus + .collapsible_headers--toggle:last-child {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* position = inline */
|
||||
.collapsible_headers--inline {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.collapsible_headers--inline [placeholder] {
|
||||
width: auto !important;
|
||||
}
|
||||
.collapsible_headers--inline [placeholder]::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.notion-page-content .notion-selectable[data-collapsed] {
|
||||
margin: 0px !important;
|
||||
pointer-events: none;
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
@ -1,286 +0,0 @@
|
||||
/**
|
||||
* notion-enhancer: collapsible headers
|
||||
* (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
|
||||
*/
|
||||
|
||||
export default async function ({ web }, db) {
|
||||
const headerSelector = '.notion-page-content [class*="header-block"]',
|
||||
pageScroller = '.notion-frame > .notion-scroller.vertical.horizontal',
|
||||
haloClass = 'notion-selectable-halo',
|
||||
blockSelector = '.notion-selectable[data-block-id]',
|
||||
dividerClass = 'notion-divider-block',
|
||||
toggleClass = 'collapsible_headers--toggle',
|
||||
inlineToggleClass = 'collapsible_headers--inline';
|
||||
|
||||
const togglePosition = await db.get(['position']),
|
||||
animateToggle = await db.get(['animate']),
|
||||
breakOnDividers = await db.get(['dividers']),
|
||||
toggleHotkey = await db.get(['hotkey']);
|
||||
|
||||
const animationStyle = {
|
||||
duration: 250,
|
||||
easing: 'ease',
|
||||
},
|
||||
animationCollapsed = {
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
let collapseParentsCache = new Map(),
|
||||
collapsedBlocksCache = new Map();
|
||||
|
||||
const getHeaderLevel = ($block) => {
|
||||
if (!$block?.className?.includes?.('header-block')) return 9;
|
||||
return ($block.className.match(/sub_/gi)?.length || 0) + 1;
|
||||
},
|
||||
getSelectedHeaders = () => {
|
||||
return [...document.querySelectorAll(`${headerSelector} ${haloClass}`)]
|
||||
.map(($halo) => $halo.parentElement)
|
||||
.filter(($header) => $header.dataset.sectionCollapsed === 'true');
|
||||
},
|
||||
getHeaderSection = ($header) => {
|
||||
const blockList = [];
|
||||
let $nextBlock = $header?.nextElementSibling;
|
||||
// is this weird? yes
|
||||
// labels were the simplest way to do this tho
|
||||
blockLoop: while (true) {
|
||||
const isSectionEnd =
|
||||
!$nextBlock ||
|
||||
getHeaderLevel($nextBlock) <= getHeaderLevel($header) ||
|
||||
(breakOnDividers && $nextBlock?.classList?.contains(dividerClass));
|
||||
if (isSectionEnd) break;
|
||||
blockList.push($nextBlock);
|
||||
const $childBlock = $nextBlock.querySelector(blockSelector);
|
||||
if ($childBlock) {
|
||||
$nextBlock = $childBlock;
|
||||
} else if ($nextBlock.nextElementSibling) {
|
||||
$nextBlock = $nextBlock.nextElementSibling;
|
||||
} else {
|
||||
let $parentBlock = $nextBlock.parentElement.closest(blockSelector);
|
||||
while (!$parentBlock?.nextElementSibling) {
|
||||
if (!$parentBlock) break blockLoop;
|
||||
if ($parentBlock === $header.parentElement) break blockLoop;
|
||||
$parentBlock = $parentBlock.parentElement.closest(blockSelector);
|
||||
}
|
||||
$nextBlock = $parentBlock.nextElementSibling;
|
||||
}
|
||||
}
|
||||
return blockList;
|
||||
};
|
||||
|
||||
const expandBlock = async ($header, $block, animate) => {
|
||||
const collapseParents = collapseParentsCache.get($block.dataset.blockId),
|
||||
expand = async () => {
|
||||
delete $block.dataset.collapsed;
|
||||
if (animate) {
|
||||
await $block.animate(
|
||||
[
|
||||
animationCollapsed,
|
||||
{
|
||||
maxHeight: '100%',
|
||||
opacity: 1,
|
||||
marginTop: $block.style.marginTop,
|
||||
marginBottom: $block.style.marginBottom,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
],
|
||||
animationStyle
|
||||
).finished;
|
||||
}
|
||||
};
|
||||
if (collapseParents) {
|
||||
collapseParents.delete($header.dataset.blockId);
|
||||
if (!collapseParents.size) await expand();
|
||||
} else await expand();
|
||||
},
|
||||
expandHeaderSection = async ($header, animate) => {
|
||||
const isBusy = $header.dataset.collapseAnimating,
|
||||
isCollapsibleHeader =
|
||||
$header.matches(headerSelector) && $header.dataset.sectionCollapsed === 'true';
|
||||
if (isBusy || !isCollapsibleHeader) return;
|
||||
$header.dataset.collapseAnimating = 'true';
|
||||
$header.dataset.sectionCollapsed = false;
|
||||
await db.set(['collapsed_ids', $header.dataset.blockId], false);
|
||||
|
||||
const sectionContent = getHeaderSection($header),
|
||||
animations = [];
|
||||
for (const $block of sectionContent) {
|
||||
animations.push(expandBlock($header, $block, animate));
|
||||
}
|
||||
if ($header.dataset.collapsed) {
|
||||
const collapseParents = collapseParentsCache.get($header.dataset.blockId) || [];
|
||||
for (const parentId of collapseParents) {
|
||||
animations.push(
|
||||
expandHeaderSection(
|
||||
document.querySelector(`[data-block-id="${parentId}"]`),
|
||||
animate
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
collapsedBlocksCache.set($header.dataset.blockId, undefined);
|
||||
await Promise.all(animations);
|
||||
delete $header.dataset.collapseAnimating;
|
||||
},
|
||||
collapseHeaderSection = async ($header, animate) => {
|
||||
const isBusy = $header.dataset.collapseAnimating,
|
||||
isCollapsibleHeader =
|
||||
$header.matches(headerSelector) && $header.dataset.sectionCollapsed === 'false';
|
||||
if (isBusy || !isCollapsibleHeader) return;
|
||||
$header.dataset.collapseAnimating = 'true';
|
||||
$header.dataset.sectionCollapsed = true;
|
||||
await db.set(['collapsed_ids', $header.dataset.blockId], true);
|
||||
|
||||
const sectionContent = getHeaderSection($header),
|
||||
animations = [];
|
||||
collapsedBlocksCache.set($header.dataset.blockId, sectionContent);
|
||||
for (const $block of sectionContent) {
|
||||
if (!collapseParentsCache.get($block.dataset.blockId)) {
|
||||
collapseParentsCache.set($block.dataset.blockId, new Set());
|
||||
}
|
||||
const collapseParents = collapseParentsCache.get($block.dataset.blockId);
|
||||
collapseParents.add($header.dataset.blockId);
|
||||
|
||||
if (animate) {
|
||||
animations.push(
|
||||
$block.animate(
|
||||
[
|
||||
{
|
||||
maxHeight: $block.offsetHeight + 'px',
|
||||
opacity: 1,
|
||||
marginTop: $block.style.marginTop,
|
||||
marginBottom: $block.style.marginBottom,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
animationCollapsed,
|
||||
],
|
||||
animationStyle
|
||||
).finished
|
||||
);
|
||||
}
|
||||
$block.dataset.collapsed = true;
|
||||
}
|
||||
await Promise.all(animations);
|
||||
|
||||
delete $header.dataset.collapseAnimating;
|
||||
},
|
||||
toggleHeaderSection = async ($header, animate) => {
|
||||
if ($header.dataset.collapseAnimating) return;
|
||||
if ($header.dataset.sectionCollapsed === 'true') {
|
||||
const collapseParents = collapseParentsCache.get($header.dataset.blockId) ?? [];
|
||||
for (const $parent of collapseParents) {
|
||||
await expandHeaderSection($parent, animateToggle);
|
||||
}
|
||||
await expandHeaderSection($header, animate);
|
||||
} else await collapseHeaderSection($header, animate);
|
||||
};
|
||||
|
||||
const insertToggles = async (event) => {
|
||||
if ([...event.addedNodes].some(($node) => $node?.matches?.(pageScroller))) {
|
||||
collapseParentsCache = new Map();
|
||||
collapsedBlocksCache = new Map();
|
||||
return;
|
||||
}
|
||||
|
||||
const childNodeEvent =
|
||||
event.target.matches(blockSelector) && !event.target.matches(headerSelector);
|
||||
if (childNodeEvent) return;
|
||||
|
||||
const removeHeaderEvent = [...event.removedNodes].filter(($node) =>
|
||||
$node?.className?.includes?.('header-blocks')
|
||||
);
|
||||
if (removeHeaderEvent.length) {
|
||||
return removeHeaderEvent.forEach(($header) => expandHeaderSection($header, false));
|
||||
}
|
||||
|
||||
const toggleEvent =
|
||||
[...event.addedNodes, ...event.removedNodes].some(($node) =>
|
||||
$node?.classList?.contains(toggleClass)
|
||||
) ||
|
||||
event.target.classList.contains(toggleClass) ||
|
||||
event.attributeName === 'data-collapsed' ||
|
||||
(event.target.classList.contains(inlineToggleClass) && event.attributeName === 'class');
|
||||
if (toggleEvent) return;
|
||||
|
||||
const haloRemoveEvent =
|
||||
event.target.classList.contains(haloClass) ||
|
||||
[...event.removedNodes].some(($node) => $node?.classList?.contains(haloClass));
|
||||
if (haloRemoveEvent) return;
|
||||
|
||||
for (const $header of document.querySelectorAll(headerSelector)) {
|
||||
const $nextBlock = $header.nextElementSibling,
|
||||
sectionContent = getHeaderSection($header),
|
||||
prevCollapseCache = collapsedBlocksCache.get($header.dataset.blockId) ?? [];
|
||||
|
||||
let hasMoved =
|
||||
prevCollapseCache.length && prevCollapseCache.length !== sectionContent.length;
|
||||
for (const $collapsedBlock of prevCollapseCache) {
|
||||
if (hasMoved) break;
|
||||
if (!sectionContent.includes($collapsedBlock)) hasMoved = true;
|
||||
}
|
||||
if (hasMoved) {
|
||||
for (const $collapsedBlock of prevCollapseCache)
|
||||
expandBlock($header, $collapsedBlock, animateToggle);
|
||||
await db.set(['collapsed_ids', $header.dataset.blockId], false);
|
||||
}
|
||||
|
||||
const isEmpty =
|
||||
!$nextBlock ||
|
||||
getHeaderLevel($nextBlock) <= getHeaderLevel($header) ||
|
||||
(breakOnDividers && $nextBlock.classList.contains(dividerClass));
|
||||
if (isEmpty) {
|
||||
delete $header.dataset.sectionCollapsed;
|
||||
$header.querySelector(`.${toggleClass}`)?.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($header.querySelector(`.${toggleClass}`)) continue;
|
||||
const $toggle = web.html`
|
||||
<div class="${toggleClass}">
|
||||
<svg viewBox="0 0 100 100"><polygon points="5.9,88.2 50,11.8 94.1,88.2"></polygon></svg>
|
||||
</div>
|
||||
`;
|
||||
if (togglePosition === 'left') {
|
||||
$header.firstChild.prepend($toggle);
|
||||
} else web.render($header.firstChild, $toggle);
|
||||
if (togglePosition === 'inline') $header.firstChild.classList.add(inlineToggleClass);
|
||||
|
||||
$toggle.header = $header;
|
||||
$toggle.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
$header.querySelector('[contenteditable="true"]').click();
|
||||
toggleHeaderSection($header, animateToggle);
|
||||
});
|
||||
|
||||
$header.dataset.sectionCollapsed = false;
|
||||
if (await db.get(['collapsed_ids', $header.dataset.blockId], false)) {
|
||||
await collapseHeaderSection($header, false);
|
||||
}
|
||||
}
|
||||
|
||||
const haloAddedEvent =
|
||||
[...event.addedNodes].some(($node) => $node?.classList?.contains(haloClass)) &&
|
||||
event.target.matches(headerSelector),
|
||||
$selectedHeaders = new Set(getSelectedHeaders());
|
||||
if (haloAddedEvent) $selectedHeaders.add(event.target);
|
||||
for (const $header of $selectedHeaders) {
|
||||
expandHeaderSection($header, animateToggle);
|
||||
}
|
||||
};
|
||||
web.addDocumentObserver(insertToggles, ['.notion-page-content', headerSelector]);
|
||||
|
||||
web.addHotkeyListener(toggleHotkey, (event) => {
|
||||
const $header = document.activeElement.closest(headerSelector);
|
||||
if ($header) {
|
||||
toggleHeaderSection($header, animateToggle);
|
||||
} else {
|
||||
getSelectedHeaders().forEach(($header) => toggleHeaderSection($header, animateToggle));
|
||||
}
|
||||
});
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 122 KiB |
@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "collapsible headers",
|
||||
"id": "548fe2d7-174a-44dd-88d8-35c7f9a093a7",
|
||||
"version": "0.2.0",
|
||||
"description": "adds toggles to collapse header sections of pages.",
|
||||
"preview": "collapsible-headers.gif",
|
||||
"tags": ["extension", "layout"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "CloudHill",
|
||||
"email": "rh.cloudhill@gmail.com",
|
||||
"homepage": "https://github.com/CloudHill",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/54142180"
|
||||
}
|
||||
],
|
||||
"js": {
|
||||
"client": ["client.mjs"]
|
||||
},
|
||||
"css": {
|
||||
"client": ["client.css"]
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"type": "select",
|
||||
"key": "position",
|
||||
"label": "toggle icon position",
|
||||
"values": ["left", "right", "inline"]
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "animate",
|
||||
"label": "animate opening/closing",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "dividers",
|
||||
"label": "use divider blocks to break header sections",
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"type": "hotkey",
|
||||
"key": "hotkey",
|
||||
"label": "toggle header collapse hotkey",
|
||||
"tooltip": "**opens/closes the currently focused header section**",
|
||||
"value": "Ctrl+Enter"
|
||||
}
|
||||
]
|
||||
}
|
@ -23,7 +23,6 @@
|
||||
"word-counter",
|
||||
"code-line-numbers",
|
||||
"calendar-scroll",
|
||||
"collapsible-headers",
|
||||
"collapsible-properties",
|
||||
"weekly-view",
|
||||
"truncated-titles",
|
||||
|
Loading…
Reference in New Issue
Block a user