mirror of
				https://github.com/notion-enhancer/notion-enhancer.git
				synced 2025-10-25 03:08:08 +11: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