mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-06 21:49:03 +00:00
extension: collapsible headers
This commit is contained in:
parent
dbb94bc29b
commit
38484b7e42
@ -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": [
|
||||
{
|
||||
|
86
repo/collapsible-headers/app.css
Normal file
86
repo/collapsible-headers/app.css
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* collapsible headers
|
||||
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (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;
|
||||
}
|
81
repo/collapsible-headers/client.css
Normal file
81
repo/collapsible-headers/client.css
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* notion-enhancer: collapsible headerrs
|
||||
* (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;
|
||||
}
|
279
repo/collapsible-headers/client.mjs
Normal file
279
repo/collapsible-headers/client.mjs
Normal file
@ -0,0 +1,279 @@
|
||||
/*
|
||||
* notion-enhancer: collapsible headerrs
|
||||
* (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"]',
|
||||
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`
|
||||
<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 $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));
|
||||
}
|
||||
});
|
||||
}
|
48
repo/collapsible-headers/mod.json
Normal file
48
repo/collapsible-headers/mod.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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": [
|
||||
{
|
@ -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"></path>
|
||||
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"></path>
|
||||
</svg>
|
||||
<span>Copy link</span>
|
||||
<span class="${hiddenClass}">Link copied!</span>
|
||||
|
@ -58,6 +58,9 @@ export default async function ({ web, components }, db) {
|
||||
style="--outliner--indent:${indent}px;"></a>`,
|
||||
$header.innerText
|
||||
);
|
||||
$outlineHeader.addEventListener('click', (event) => {
|
||||
location.hash = '';
|
||||
});
|
||||
$fragment.append($outlineHeader);
|
||||
}
|
||||
if ($fragment.innerHTML !== $headingList.innerHTML) {
|
||||
|
@ -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"
|
||||
|
@ -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": [
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user