mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-11 15:59: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",
|
"word-counter",
|
||||||
"code-line-numbers",
|
"code-line-numbers",
|
||||||
"calendar-scroll",
|
"calendar-scroll",
|
||||||
"collapsible-headers",
|
|
||||||
"collapsible-properties",
|
"collapsible-properties",
|
||||||
"weekly-view",
|
"weekly-view",
|
||||||
"truncated-titles",
|
"truncated-titles",
|
||||||
|
Loading…
Reference in New Issue
Block a user