From 38484b7e421c4e594cd50f00c26d715177e201f8 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Thu, 21 Oct 2021 23:27:58 +1100 Subject: [PATCH] extension: collapsible headers --- repo/calendar-scroll/mod.json | 2 +- repo/collapsible-headers/app.css | 86 ++++++ repo/collapsible-headers/client.css | 81 +++++ repo/collapsible-headers/client.mjs | 279 ++++++++++++++++++ repo/collapsible-headers/mod.json | 48 +++ .../client.css | 0 .../client.mjs | 0 .../mod.json | 4 +- repo/global-block-links/client.mjs | 4 +- repo/outliner/client.mjs | 3 + repo/registry.json | 3 +- repo/scroll-to-top/mod.json | 2 +- 12 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 repo/collapsible-headers/app.css create mode 100644 repo/collapsible-headers/client.css create mode 100644 repo/collapsible-headers/client.mjs create mode 100644 repo/collapsible-headers/mod.json rename repo/{collapse-properties => collapsible-properties}/client.css (100%) rename repo/{collapse-properties => collapsible-properties}/client.mjs (100%) rename repo/{collapse-properties => collapsible-properties}/mod.json (73%) diff --git a/repo/calendar-scroll/mod.json b/repo/calendar-scroll/mod.json index 8044b7e..380b95f 100644 --- a/repo/calendar-scroll/mod.json +++ b/repo/calendar-scroll/mod.json @@ -2,7 +2,7 @@ "name": "calendar scroll", "id": "b1c7db33-dfee-489a-a76c-0dd66f7ed29a", "version": "0.2.0", - "description": "add a button to jump down to the current week in fullpage/infinite-scroll calendars.", + "description": "adds a button to jump down to the current week in fullpage/infinite-scroll calendars.", "tags": ["extension", "shortcut"], "authors": [ { diff --git a/repo/collapsible-headers/app.css b/repo/collapsible-headers/app.css new file mode 100644 index 0000000..d96a25f --- /dev/null +++ b/repo/collapsible-headers/app.css @@ -0,0 +1,86 @@ +/* + * collapsible headers + * (c) 2020 dragonwocky (https://dragonwocky.me/) + * (c) 2020 CloudHill + * under the MIT license + */ + + .notion-page-content .notion-selectable[collapsed] { + max-height: 0px; + overflow: hidden; + opacity: 0; +} + +.notion-page-content .notion-selectable[collapsed] .notion-selectable { + pointer-events: none; +} + +.collapse-header { + 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; +} +.collapse-header:hover { + background: var(--theme--interactive_hover); +} +/* position: left */ +.collapse-header:first-child { + margin-left: 2px; +} +/* position: right / inline */ +.collapse-header:last-child { + opacity: 0; +} + +/* show toggle on: collapsed, hover, focus */ +[data-collapsed="true"] .collapse-header:last-child, +[data-collapsed]:hover .collapse-header:last-child, +[data-collapsed] :focus + .collapse-header:last-child { + opacity: 1; +} + +.collapse-header svg { + width: 100%; + height: 100%; + transition: transform 200ms ease-out 0s; +} +/* position: left */ +.collapse-header:first-child svg { + transform: rotateZ(90deg); +} +/* position: right / inline */ +.collapse-header:last-child svg { + transform: rotateZ(270deg); +} + +[data-collapsed="false"] .collapse-header svg { + transform: rotateZ(180deg); +} + +/* position: inline */ +[inline-toggle] { + position: relative; + overflow: hidden; +} +[inline-toggle] [placeholder] { + width: auto !important; +} +[inline-toggle] [placeholder]::after { + content: ''; + position: absolute; + top: 0; + width: 100%; + height: 100%; + cursor: text; +} diff --git a/repo/collapsible-headers/client.css b/repo/collapsible-headers/client.css new file mode 100644 index 0000000..69016ed --- /dev/null +++ b/repo/collapsible-headers/client.css @@ -0,0 +1,81 @@ +/* + * notion-enhancer: collapsible headerrs + * (c) 2020 CloudHill (https://github.com/CloudHill) + * (c) 2021 dragonwocky (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; +} diff --git a/repo/collapsible-headers/client.mjs b/repo/collapsible-headers/client.mjs new file mode 100644 index 0000000..5b04f44 --- /dev/null +++ b/repo/collapsible-headers/client.mjs @@ -0,0 +1,279 @@ +/* + * notion-enhancer: collapsible headerrs + * (c) 2020 CloudHill (https://github.com/CloudHill) + * (c) 2021 dragonwocky (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"]', + 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', + }; + + const 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, + { + height: $block.scrollHeight + 'px', + 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) => { + 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` +
+ +
+ `; + if (togglePosition === 'left') { + $header.firstChild.prepend($toggle); + } else $header.firstChild.append($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, [headerSelector]); + + web.addHotkeyListener(toggleHotkey, (event) => { + const $header = document.activeElement.closest(headerSelector); + if ($header) { + toggleHeaderSection($header, animateToggle); + } else { + getSelectedHeaders().forEach(($header) => toggleHeaderSection($header, animateToggle)); + } + }); +} diff --git a/repo/collapsible-headers/mod.json b/repo/collapsible-headers/mod.json new file mode 100644 index 0000000..eb11cf2 --- /dev/null +++ b/repo/collapsible-headers/mod.json @@ -0,0 +1,48 @@ +{ + "name": "collapsible headers", + "id": "548fe2d7-174a-44dd-88d8-35c7f9a093a7", + "version": "0.2.0", + "description": "adds toggles to collapse header sections of pages.", + "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" + } + ] +} diff --git a/repo/collapse-properties/client.css b/repo/collapsible-properties/client.css similarity index 100% rename from repo/collapse-properties/client.css rename to repo/collapsible-properties/client.css diff --git a/repo/collapse-properties/client.mjs b/repo/collapsible-properties/client.mjs similarity index 100% rename from repo/collapse-properties/client.mjs rename to repo/collapsible-properties/client.mjs diff --git a/repo/collapse-properties/mod.json b/repo/collapsible-properties/mod.json similarity index 73% rename from repo/collapse-properties/mod.json rename to repo/collapsible-properties/mod.json index 90fe55c..1516af5 100644 --- a/repo/collapse-properties/mod.json +++ b/repo/collapsible-properties/mod.json @@ -1,8 +1,8 @@ { - "name": "collapse properties", + "name": "collapsible properties", "id": "4034a578-7dd3-4633-80c6-f47ac5b7b160", "version": "0.3.0", - "description": "add a button to quickly collapse/expand page properties that usually push down page content.", + "description": "adds a button to quickly collapse/expand page properties that usually push down page content.", "tags": ["extension", "layout"], "authors": [ { diff --git a/repo/global-block-links/client.mjs b/repo/global-block-links/client.mjs index dabdcde..bb336fc 100644 --- a/repo/global-block-links/client.mjs +++ b/repo/global-block-links/client.mjs @@ -24,8 +24,8 @@ export default async function ({ web, components, notion }, db) { 8c0,2.98,1.634,5.575,4.051,6.951C4.021,18.638,4,18.321,4,18 c0-0.488,0.046-0.967, 0.115-1.436C2.823,15.462,2,13.827,2,12z M25.953,11.051C25.984,11.363,26,11.68,26,12 c0,0.489-0.047,0.965-0.117,1.434C27.176,14.536,28,16.172,28,18c0,3.309-2.691,6-6,6h-8c-3.309, - 0-6-2.691-6-6s2.691-6,6-6h6 c0-0.731-0.199-1.413-0.545-2H14c-4.418,0-8,3.582-8,8c0,4.418,3.582,8,8, - 8h8c4.418,0,8-3.582,8-8 C30,15.021,28.368,12.428,25.953,11.051z"> + 0-6-2.691-6-6s2.691-6,6-6h6 c0-0.731-0.199-1.413-0.545-2H14c-4.418,0-8,3.582-8,8c0, + 4.418,3.582,8,8,8h8c4.418,0,8-3.582,8-8 C30,15.021,28.368,12.428,25.953,11.051z"> Copy link Link copied! diff --git a/repo/outliner/client.mjs b/repo/outliner/client.mjs index d856992..0f282b1 100644 --- a/repo/outliner/client.mjs +++ b/repo/outliner/client.mjs @@ -58,6 +58,9 @@ export default async function ({ web, components }, db) { style="--outliner--indent:${indent}px;">`, $header.innerText ); + $outlineHeader.addEventListener('click', (event) => { + location.hash = ''; + }); $fragment.append($outlineHeader); } if ($fragment.innerHTML !== $headingList.innerHTML) { diff --git a/repo/registry.json b/repo/registry.json index 1a04c7a..6f83131 100644 --- a/repo/registry.json +++ b/repo/registry.json @@ -28,8 +28,9 @@ "word-counter", "code-line-numbers", "calendar-scroll", + "collapsible-headers", + "collapsible-properties", "weekly-view", - "collapse-properties", "truncated-titles", "focus-mode", "global-block-links" diff --git a/repo/scroll-to-top/mod.json b/repo/scroll-to-top/mod.json index 9ad03e3..6e16702 100644 --- a/repo/scroll-to-top/mod.json +++ b/repo/scroll-to-top/mod.json @@ -2,7 +2,7 @@ "name": "scroll to top", "id": "0a958f5a-17c5-48b5-8713-16190cae1959", "version": "0.3.0", - "description": "add an arrow in the bottom right corner to scroll back to the top of a page.", + "description": "adds an arrow in the bottom right corner to scroll back to the top of a page.", "tags": ["extension", "shortcut"], "authors": [ {