extension: outliner

This commit is contained in:
dragonwocky 2021-10-19 16:40:50 +11:00
parent a9427f4520
commit 382451156c
10 changed files with 261 additions and 66 deletions

View File

@ -19,6 +19,7 @@ export default async function ({ web, env }, db) {
const unsupportedEmojis = [],
emojiReqs = new Map(),
getEmoji = async (emoji) => {
emoji = encodeURIComponent(emoji);
if (unsupportedEmojis.includes(emoji)) return undefined;
try {
if (!emojiReqs.get(emoji)) {
@ -37,16 +38,14 @@ export default async function ({ web, env }, db) {
const updateEmojis = async () => {
const $emojis = document.querySelectorAll(nativeEmojiSelector);
for (const $emoji of $emojis) {
const emojiBg = await getEmoji($emoji.ariaLabel);
if (emojiBg) {
$emoji.style.background = emojiBg;
const emojiSrc = await getEmoji($emoji.ariaLabel);
if (emojiSrc) {
$emoji.style.background = emojiSrc;
$emoji.style.width = '1em';
$emoji.style.height = '1em';
$emoji.style.display = 'inline-block';
$emoji.innerText = '';
} else {
$emoji.dataset.emojiSetsUnsupported = true;
}
} else $emoji.dataset.emojiSetsUnsupported = true;
}
};
web.addDocumentObserver(updateEmojis, [nativeEmojiSelector]);
@ -56,16 +55,14 @@ export default async function ({ web, env }, db) {
const updateEmojis = async () => {
const $emojis = document.querySelectorAll(imgEmojiSelector);
for (const $emoji of $emojis) {
const emojiBg = await getEmoji($emoji.ariaLabel);
if (emojiBg) {
$emoji.style.background = emojiBg;
const emojiSrc = await getEmoji($emoji.ariaLabel);
if (emojiSrc) {
$emoji.style.background = emojiSrc;
$emoji.style.opacity = 1;
if ($emoji.nextElementSibling?.matches?.(imgEmojiOverlaySelector)) {
$emoji.nextElementSibling.style.opacity = 0;
}
} else {
$emoji.dataset.emojiSetsUnsupported = true;
}
} else $emoji.dataset.emojiSetsUnsupported = true;
}
};
updateEmojis();

View File

@ -5,6 +5,41 @@
* (https://notion-enhancer.github.io/) under the MIT license
*/
.notion-page-content .notion-bulleted_list-block > div > div:last-child,
.notion-page-content .notion-numbered_list-block > div > div:last-child,
.notion-page-content .notion-to_do-block > div > div:last-child,
.notion-page-content .notion-toggle-block > div > div:last-child,
.notion-page-content .notion-table_of_contents-block > div > div > a > div > div {
position: relative;
}
.notion-page-content .notion-bulleted_list-block > div > div:last-child::before,
.notion-page-content .notion-numbered_list-block > div > div:last-child::before,
.notion-page-content .notion-to_do-block > div > div:last-child::before,
.notion-page-content .notion-toggle-block > div > div:last-child::before {
content: '';
position: absolute;
height: calc(100% - 2em);
top: 2em;
left: -14.48px;
}
.notion-page-content
.notion-table_of_contents-block
> div
> div
> a
> div
> div:not([style*='margin-left: 0px'])
> div::before {
content: '';
position: absolute;
height: 100%;
top: 0;
left: -14.48px;
}
/* add background to block dragger */
.notion-frame
> [style*='position: absolute; top: 0px; left: 0px;']
[style*='position: absolute; top: 3px; left: -20px; width: 18px; height: 24px; pointer-events: auto; cursor: -webkit-grab;']
@ -57,37 +92,3 @@
> .plus:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.notion-page-content .notion-bulleted_list-block > div > div:last-child,
.notion-page-content .notion-numbered_list-block > div > div:last-child,
.notion-page-content .notion-to_do-block > div > div:last-child,
.notion-page-content .notion-toggle-block > div > div:last-child,
.notion-page-content .notion-table_of_contents-block > div > div > a > div > div {
position: relative;
}
.notion-page-content .notion-bulleted_list-block > div > div:last-child::before,
.notion-page-content .notion-numbered_list-block > div > div:last-child::before,
.notion-page-content .notion-to_do-block > div > div:last-child::before,
.notion-page-content .notion-toggle-block > div > div:last-child::before {
content: '';
position: absolute;
height: calc(100% - 2em);
top: 2em;
left: -14.48px;
}
.notion-page-content
.notion-table_of_contents-block
> div
> div
> a
> div
> div:not([style*='margin-left: 0px'])
> div::before {
content: '';
position: absolute;
height: 100%;
top: 0;
left: -14.48px;
}

View File

@ -58,13 +58,32 @@ export default async function ({ web }, db) {
}`;
if (rainbow) {
for (let i = 0; i < colors.length; i++) {
css += `
.notion-page-content ${`.notion-table_of_contents-block `.repeat(i + 1)}
> div > div > a > div > div:not([style*='margin-left: 0px']) > div::before {
--indentation_lines--color: var(--theme--text_${colors[i]});
}`;
}
css += `
.notion-page-content .notion-table_of_contents-block > div > div > a > div
> div[style*='margin-left: 24px'] > div::before {
--indentation_lines--color: var(--theme--text_${colors[0]});
}
.notion-page-content .notion-table_of_contents-block > div > div > a > div
> div[style*='margin-left: 48px'] > div::before {
--indentation_lines--color: var(--theme--text_${colors[1]});
}`;
}
}
if (db.get(['outliner'])) {
css += `
.outliner--header:not([style='--outliner--indent:0px;'])::before {
border-left: 1px ${style} var(--indentation_lines--color, currentColor);
opacity: ${opacity};
}`;
if (rainbow) {
css += `
.outliner--header[style='--outliner--indent:18px;']::before {
--indentation_lines--color: var(--theme--text_${colors[0]});
}
.outliner--header[style='--outliner--indent:36px;']::before {
--indentation_lines--color: var(--theme--text_${colors[1]});
}`;
}
}

View File

@ -54,6 +54,12 @@
"key": "table_of_contents",
"label": "tables of contents",
"value": true
},
{
"type": "toggle",
"key": "outliner",
"label": "outliner (panel extension)",
"value": true
}
]
}

47
repo/outliner/client.css Normal file
View File

@ -0,0 +1,47 @@
/*
* notion-enhancer: outliner
* (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
*/
#outliner--notice {
color: var(--theme--text_secondary);
font-size: 14px;
margin-top: 0;
margin-bottom: 1rem;
}
.outliner--header {
position: relative;
margin: 0 -1rem;
padding: 0 1rem;
display: block;
font-size: 14px;
line-height: 2.2;
white-space: nowrap;
overflow: hidden;
user-select: none;
text-overflow: ellipsis;
text-decoration: none;
text-indent: var(--outliner--indent);
color: inherit;
cursor: pointer !important;
transition: background 20ms ease-in;
}
.outliner--header:hover {
background: var(--theme--ui_interactive-hover);
}
.outliner--header:empty::after {
color: var(--theme--text_secondary);
content: attr(placeholder);
}
/* indentation lines */
.outliner--header:not([style='--outliner--indent:0px;'])::before {
content: '';
height: 100%;
position: absolute;
left: calc((1rem + var(--outliner--indent)) - 11px);
}

91
repo/outliner/client.mjs Normal file
View File

@ -0,0 +1,91 @@
/*
* notion-enhancer: outliner
* (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, components }, db) {
const dbNoticeText = 'Open a page to see its table of contents.',
pageNoticeText = 'Click on a heading to jump to it.',
$notice = web.html`<p id="outliner--notice">${dbNoticeText}</p>`;
const $headingList = web.html`<div></div>`;
let viewFocused = false;
await components.addPanelView({
id: '87e077cc-5402-451c-ac70-27cc4ae65546',
icon: web.html`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<circle cx="5" cy="7" r="2.8"/>
<circle cx="5" cy="17" r="2.79"/>
<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
<path d="M17,10.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,9.6,17.55,10.05,17,10.05z"/>
<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
</svg>`,
title: 'Outliner',
$content: web.render(web.html`<div></div>`, $notice, $headingList),
onFocus: () => {
viewFocused = true;
},
onBlur: () => {
viewFocused = false;
},
});
await web.whenReady();
let $page;
const updateHeadings = () => {
if (!$page) return;
const $headerBlocks = $page.querySelectorAll(
'[class^="notion-"][class*="header-block"]'
),
$fragment = web.html`<div></div>`;
let depth = 0,
indent = 0;
for (const $header of $headerBlocks) {
const id = $header.dataset.blockId.replace(/-/g, ''),
placeholder = $header.querySelector('[placeholder]').getAttribute('placeholder'),
headerDepth = +placeholder.at(-1);
if (depth && depth < headerDepth) {
indent += 18;
} else if (depth > headerDepth) {
indent = Math.max(indent - 18, 0);
}
depth = headerDepth;
const $outlineHeader = web.render(
web.html`<a href="#${id}" class="outliner--header"
placeholder="${web.escape(placeholder)}"
style="--outliner--indent:${indent}px;"></a>`,
$header.innerText
);
$fragment.append($outlineHeader);
}
if ($fragment.innerHTML !== $headingList.innerHTML) {
web.render(web.empty($headingList), ...$fragment.children);
}
},
pageObserver = () => {
if (!viewFocused) return;
if (document.contains($page)) {
updateHeadings();
} else {
$page = document.getElementsByClassName('notion-page-content')[0];
if ($page) {
$notice.innerText = pageNoticeText;
$headingList.style.display = '';
updateHeadings();
} else {
$notice.innerText = dbNoticeText;
$headingList.style.display = 'none';
}
}
};
web.addDocumentObserver(pageObserver, [
'.notion-header-block',
'.notion-sub_header-block',
'.notion-sub_sub_header-block',
'.notion-collection_view_page-block',
]);
pageObserver();
}

22
repo/outliner/mod.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "outliner",
"id": "87e077cc-5402-451c-ac70-27cc4ae65546",
"version": "0.4.0",
"description": "adds a table of contents to the side panel.",
"tags": ["extension", "panel"],
"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": []
}

View File

@ -17,14 +17,15 @@
"playful-purple",
"pinky-boom",
"bypass-preview",
"font-chooser",
"outliner",
"scroll-to-top",
"indentation-lines",
"emoji-sets",
"bypass-preview",
"topbar-icons",
"word-counter",
"calendar-scroll",
"collapse-properties",
"indentation-lines",
"focus-mode",
"word-counter",
"emoji-sets",
"topbar-icons"
"focus-mode"
]

View File

@ -7,6 +7,7 @@
#word-counter--notice {
color: var(--theme--text_secondary);
font-size: 14px;
margin-top: 0;
}
.word-counter--stat {

View File

@ -35,9 +35,9 @@ const copyToClipboard = async (str) => {
};
export default async function ({ web, components }, db) {
const dbNoticeText = 'Open a normal page to see word count.',
const dbNoticeText = 'Open a page to see its word count.',
pageNoticeText = 'Click a stat to copy it.',
$notice = web.html`<span id="word-counter--notice">${dbNoticeText}</span>`;
$notice = web.html`<p id="word-counter--notice">${dbNoticeText}</p>`;
const $wordCount = web.html`<b>12</b>`,
$characterCount = web.html`<b>12</b>`,
@ -47,7 +47,7 @@ export default async function ({ web, components }, db) {
$readingTooltip = web.html`${await components.feather('info')}`,
$speakingTime = web.html`<b>18 secs</b>`,
$speakingTooltip = web.html`${await components.feather('info')}`,
$statContainer = web.render(
$statList = web.render(
web.html`<div></div>`,
web.render(web.html`<p class="word-counter--stat"></p>`, $wordCount, ' words'),
web.render(web.html`<p class="word-counter--stat"></p>`, $characterCount, ' characters'),
@ -66,15 +66,24 @@ export default async function ({ web, components }, db) {
' speaking time'
)
);
$statContainer.querySelectorAll('.word-counter--stat').forEach(($stat) => {
$statList.querySelectorAll('.word-counter--stat').forEach(($stat) => {
$stat.addEventListener('click', () => copyToClipboard($stat.innerText));
});
components.setTooltip($readingTooltip, '**~ 275 wpm**');
components.setTooltip($speakingTooltip, '**~ 180 wpm**');
let viewFocused = false;
await components.addPanelView({
id: 'b99deb52-6955-43d2-a53b-a31540cd19a5',
icon: await components.feather('type'),
title: 'Word Counter',
$content: web.render(web.html`<div></div>`, $notice, $statContainer),
$content: web.render(web.html`<div></div>`, $notice, $statList),
onFocus: () => {
viewFocused = true;
},
onBlur: () => {
viewFocused = false;
},
});
let $page;
@ -89,17 +98,18 @@ export default async function ({ web, components }, db) {
$speakingTime.innerText = humanTime(words / 180);
},
pageObserver = () => {
if (!viewFocused) return;
if (document.contains($page)) {
updateStats();
} else {
$page = document.getElementsByClassName('notion-page-content')[0];
if ($page) {
$notice.innerText = pageNoticeText;
$statContainer.style.display = '';
$statList.style.display = '';
updateStats();
} else {
$notice.innerText = dbNoticeText;
$statContainer.style.display = 'none';
$statList.style.display = 'none';
}
}
};