notion-enhancer/mods/outliner/panel.js
2021-10-30 23:06:10 +11:00

162 lines
4.8 KiB
JavaScript

/*
* outliner
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (c) 2020 CloudHill
* under the MIT license
*/
'use strict';
const { createElement } = require("../../pkg/helpers");
module.exports = (store, __exports) => {
let lastSearch;
// Observe for page changes
const pageObserver = new MutationObserver((list, observer) => {
for ( let { addedNodes } of list) {
if (addedNodes[0]) {
if (addedNodes[0].className === 'notion-presence-container') {
startContentObserver();
}
// Clear outline on database pages
else if (addedNodes[0].className === 'notion-scroller') {
contentObserver.disconnect();
const outline = document.querySelector('.outliner');
if (outline) outline.textContent = '';
}
}
}
});
// Observe for header changes
const contentObserver = new MutationObserver((list, observer) => {
list.forEach(m => {
let header;
if (
(
m.type === 'childList' &&
(
m.target.hasAttribute('placeholder') ||
m.target.className?.includes('header-block')
) &&
(
(header = getHeaderBlock(m.target)) ||
(header = getHeaderBlock(m.addedNodes[0]))
)
) ||
(
m.type === 'characterData' &&
(header = getHeaderBlock(m.target.parentElement))
)
) updateOutlineHeader(header);
else if (
m.type === 'childList' && m.removedNodes[0] &&
(
isHeaderElement(m.removedNodes[0]) ||
m.removedNodes[0].querySelector?.('[class*="header-block"]')
)
) findHeaders();
})
});
function startContentObserver() {
findHeaders();
contentObserver.disconnect();
contentObserver.observe(
document.querySelector('.notion-page-content'),
{
childList: true,
subtree: true,
characterData: true,
}
);
}
function findHeaders() {
// Add cooldown to prevent the function being run twice at the 'same' time
if (lastSearch >= (Date.now() - 10)) return;
lastSearch = Date.now();
const outline = document.querySelector('.outliner');
if (!outline) return;
outline.textContent = '';
const pageContent = document.querySelector('.notion-page-content'),
headerBlocks = pageContent.querySelectorAll('[class*="header-block"]'),
fragment = new DocumentFragment();
headerBlocks.forEach(header => {
const blockId = header.dataset.blockId.replace(/-/g, ''),
headerEl = header.querySelector('[placeholder]'),
placeholder = headerEl.getAttribute('placeholder');
const outlineHeader = createElement(`
<div class="outline-header" header-level="${placeholder.slice(-1)}">
<a href="${window.location.pathname}#${blockId}" class="outline-link"
outline-placeholder="${placeholder}"></a>
</div>
`);
header.outline = outlineHeader;
outlineHeader.firstElementChild.innerHTML = headerEl.innerHTML;
fragment.appendChild(outlineHeader);
})
outline.appendChild(fragment);
}
function updateOutlineHeader(header) {
const headerEl = header.querySelector('[placeholder]');
if (!(
headerEl &&
header.outline?.parentElement
)) return findHeaders();
const outlineHeader = header.outline;
outlineHeader.firstElementChild.innerHTML = headerEl.innerHTML;
setOutlineLevel(outlineHeader, headerEl.getAttribute('placeholder').slice(-1));
}
function setOutlineLevel(outlineHeader, level) {
outlineHeader.setAttribute('header-level', level);
outlineHeader.firstElementChild.setAttribute('outline-placeholder', `Header ${level}`)
}
function getHeaderBlock(el) {
return el?.closest?.('[class*="header-block"]');
}
function isHeaderElement(el) {
let placeholder;
if (el) {
placeholder = el.getAttribute?.('placeholder') ||
el.querySelector?.('[placeholder]')?.getAttribute('placeholder');
}
if (!placeholder) placeholder = '';
return placeholder.includes('Heading');
}
return {
onLoad() {
if (store().lined) {
const outline = document.querySelector('.outliner');
outline?.setAttribute('lined', '');
}
// Find headers when switching panels
if (document.querySelector('.notion-page-content')) {
startContentObserver();
}
pageObserver.observe(document.body, {
childList: true,
subtree: true,
});
},
onSwitch() {
pageObserver.disconnect();
contentObserver.disconnect();
}
}
}