notion-enhancer/mods/side-panel/mod.js
Ryo Hilmawan 4a9d6477b7
New extensions: side panel and outliner (#284)
* upload side panel core

* upload outliner extension

* add onLoad, onSwitch, and onResize functions to panel extensions

* Update panel.js

* Add onLock and onUnlock functions to panel extensions

* Reordered side panel content loading

* change panel switcher font color

* fix loading last opened panel

* use svg icon files instead of defining them inline

* outliner: add full height option
2020-12-02 23:05:33 +11:00

390 lines
14 KiB
JavaScript

/*
* side panel
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (c) 2020 CloudHill
* under the MIT license
*/
'use strict';
const { createElement, getEnhancements } = require('../../pkg/helpers.js'),
path = require('path'),
fs = require('fs-extra');
module.exports = {
id: 'c8b1db83-ee37-45b4-bdb3-a7f3d36113db',
tags: ['extension', 'panel'],
name: 'side panel',
desc: 'adds a side panel to notion.',
version: '1.0.0',
author: 'CloudHill',
hacks: {
'renderer/preload.js'(store, __exports) {
// Load icons
let icons = {};
(async () => {
icons.doubleChevron = await fs.readFile( path.resolve(__dirname, 'icons/double-chevron.svg') );
icons.switcher = await fs.readFile( path.resolve(__dirname, 'icons/switcher.svg') );
icons.reload = await fs.readFile( path.resolve(__dirname, 'icons/reload.svg') );
})();
// Load panel mods
let panelMods = getEnhancements().loaded.filter(mod =>
(mod.panel && (store('mods')[mod.id] || {}).enabled)
);
// Get panel info
panelMods.forEach(mod => initMod(mod));
async function initMod(mod) {
try {
if (typeof mod.panel === 'object') {
// html
mod.panel.html = await fs.readFile(
path.resolve(__dirname, `../${mod.dir}/${mod.panel.html}`)
);
// name
if (!mod.panel.name) mod.panel.name = mod.name;
// icon
if (mod.panel.icon) {
const iconPath = path.resolve(__dirname, `../${mod.dir}/${mod.panel.icon}`);
if (await fs.pathExists(iconPath))
mod.panel.icon = await fs.readFile(iconPath);
} else {
mod.panel.icon = mod.panel.name[0];
}
// js
if (mod.panel.js) {
const jsPath = `../${mod.dir}/${mod.panel.js}`;
if (await fs.pathExists(path.resolve(__dirname, jsPath)))
mod.panel.js = require(jsPath)(store(mod.id));
}
} else if (typeof mod.panel === 'string') {
mod.panel.icon = mod.name[0];
mod.panel.html = await fs.readFile(
path.resolve(__dirname, `../${mod.dir}/${mod.panel}`)
);
} else throw Error;
} catch (err) {
console.log('invalid panel mod: ' + mod.name);
panelMods = panelMods.filter(panelMod => panelMod !== mod);
}
}
document.addEventListener('readystatechange', (event) => {
if (document.readyState !== 'complete') return false;
if (panelMods.length < 1) return;
const attempt_interval = setInterval(enhance, 500);
function enhance() {
let curPanel = {};
const frame = document.querySelector('.notion-frame');
if (!frame) return;
clearInterval(attempt_interval);
// Initialize panel
const container = createElement(
'<div class="enhancer-panel--container"></div>'
);
const panel = createElement(
`<div id="enhancer-panel"></div>`
);
frame.after(container);
container.appendChild(panel);
// Panel contents
const header = createElement(`
<div class="enhancer-panel--header">
<div class="enhancer-panel--icon"></div>
<div class="enhancer-panel--title"></div>
</div>
`);
const toggle = createElement(
`<div class="enhancer-panel--toggle">${icons.doubleChevron}</div>`
);
const content = createElement(
'<div id="enhancer-panel--content"></div>'
);
const resize = createElement(`
<div class="enhancer-panel--resize">
<div style="cursor: col-resize;"></div>
</div>
`);
panel.append(header, content, resize);
// Add switcher if there is more than one panel mods
if (panelMods.length > 1) {
header.addEventListener('click', renderSwitcher);
const switcherIcon = createElement(
`<div class="enhancer-panel--switcher-icon">${icons.switcher}</div>`
)
header.appendChild(switcherIcon);
} else header.addEventListener('click', togglePanel);
header.appendChild(toggle);
toggle.addEventListener('click', togglePanel);
// Restore lock state
if (store().locked === 'true') lockPanel();
else unlockPanel(false);
enableResize();
// Attempt to load last opened mod
let loaded = false;
if (store().last_open) {
panelMods.forEach(mod => {
if (mod.id === store().last_open) {
loadContent(mod);
loaded = true;
}
})
}
if (!loaded) {
loadContent(panelMods[0]);
}
function loadContent(mod) {
if (curPanel.js && curPanel.js.onSwitch) curPanel.js.onSwitch();
curPanel = mod.panel;
store().last_open = mod.id;
panel.querySelector('.enhancer-panel--title').innerHTML = mod.panel.name || mod.name;
// reload button
let reloadButton = document.querySelector('.enhancer-panel--reload-button');
if (reloadButton) reloadButton.remove();
if (mod.panel.reload) {
reloadButton = createElement(
`<div class="enhancer-panel--reload-button">${icons.reload}</div>`
)
reloadButton.addEventListener('click', e => {
e.stopPropagation();
loadContent(mod);
})
panel.querySelector('.enhancer-panel--title').after(reloadButton);
}
panel.querySelector('.enhancer-panel--icon').innerHTML = mod.panel.icon;
document.getElementById('enhancer-panel--content').innerHTML = mod.panel.html;
panel.dataset.fullHeight = mod.panel.fullHeight || false;
if (curPanel.js && curPanel.js.onLoad)
curPanel.js.onLoad();
}
function unlockPanel(animate) {
panel.dataset.locked = 'false';
setPanelWidth(store().width);
if (animate) {
panel.animate(
[
{ opacity: 1, transform: 'none' },
{ opacity: 1, transform: 'translateY(60px)', offset: 0.4},
{ opacity: 0, transform: `translateX(${store().width - 30}px) translateY(60px)`},
],
{ duration: 600, easing: 'ease-out' }
).onfinish = () => {
panel.addEventListener('mouseover', showPanel);
panel.addEventListener('mouseleave', hidePanel);
}
} else {
panel.addEventListener('mouseover', showPanel);
panel.addEventListener('mouseleave', hidePanel);
}
hidePanel();
if (curPanel.js && curPanel.js.onUnlock) {
curPanel.js.onUnlock();
}
}
function lockPanel() {
panel.dataset.locked = 'true';
setPanelWidth(store().width);
// Reset animation styles
panel.style.opacity = '';
panel.style.transform = '';
// Hover event listeners
panel.removeEventListener('mouseover', showPanel);
panel.removeEventListener('mouseleave', hidePanel);
if (curPanel.js && curPanel.js.onLock) {
curPanel.js.onLock();
}
}
function togglePanel(e) {
e.stopPropagation();
if (isLocked()) unlockPanel(true);
else lockPanel();
store().locked = panel.dataset.locked;
}
function showPanel() {
if (!isLocked()) {
panel.style.opacity = 1;
panel.style.transform = 'translateY(60px)';
}
}
function hidePanel() {
if (!isLocked()) {
panel.style.opacity = 0;
panel.style.transform = `translateX(${store().width - 30}px) translateY(60px)`;
}
}
function renderSwitcherItem(mod) {
if (mod.panel) {
const item = createElement(
`<div class="enhancer-panel--switcher-item">
<div class="enhancer-panel--icon">${mod.panel.icon}</div>
<div class="enhancer-panel--title">${mod.panel.name || mod.name}</div>
</div>`
);
item.addEventListener('click', () => loadContent(mod));
return item;
}
}
function renderSwitcher() {
if (document.querySelector('.enhancer-panel--overlay-container')) return;
// Layer to close switcher
const overlayContainer = createElement(
'<div class="enhancer-panel--overlay-container"></div>'
);
overlayContainer.addEventListener('click', hideSwitcher)
document.querySelector('.notion-app-inner').appendChild(overlayContainer);
// Position switcher below header
const rect = header.getBoundingClientRect();
const div = createElement(`
<div style="position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px ">
<div style="position: relative; top: 100%; pointer-events: auto;"></div>
</div>
`);
// Render switcher
const switcher = createElement(
'<div class="enhancer-panel--switcher"></div>'
);
panelMods.forEach(mod =>
switcher.append(renderSwitcherItem(mod))
);
overlayContainer.appendChild(div);
div.firstElementChild.appendChild(switcher);
// Fade in
switcher.animate(
[ {opacity: 0}, {opacity: 1} ],
{ duration: 200 }
);
// Prevent panel from closing if unlocked
panel.removeEventListener('mouseleave', hidePanel);
}
function hideSwitcher() {
const overlayContainer = document.querySelector('.enhancer-panel--overlay-container');
overlayContainer.removeEventListener('click', hideSwitcher);
// Fade out
document.querySelector('.enhancer-panel--switcher').animate(
[ {opacity: 1}, {opacity: 0} ],
{ duration: 200 }
).onfinish = () => overlayContainer.remove();
if (!isLocked()) panel.addEventListener('mouseleave', hidePanel);
}
function setPanelWidth(width) {
store().width = width;
panel.style.width = width + 'px';
if (isLocked()) {
container.style.width = width + 'px';
frame.style.paddingRight = width + 'px';
panel.style.right = 0;
} else {
container.style.width = 0;
frame.style.paddingRight = 0;
panel.style.right = width + 'px';
}
}
function enableResize() {
const handle = resize.firstElementChild;
handle.addEventListener('mousedown', initDrag);
let startX, startWidth;
const div = createElement(
'<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 99;"></div>'
);
function initDrag(e) {
startX = e.clientX;
startWidth = store().width;
panel.appendChild(div);
// Set transitions
container.style.transition = 'width 50ms ease-in';
panel.style.transition = 'width 50ms ease-in, right 50ms ease-in';
frame.style.transition = 'padding-right 50ms ease-in';
handle.style.cursor = '';
// Prevent panel from closing if unlocked
panel.removeEventListener('mouseleave', hidePanel);
document.body.addEventListener('mousemove', drag);
document.body.addEventListener('mouseup', stopDrag);
}
function drag(e) {
e.preventDefault();
let width = startWidth + (startX - e.clientX);
if (width < 190) width = 190;
if (width > 480) width = 480;
setPanelWidth(width);
if (curPanel.js && curPanel.js.onResize) {
curPanel.js.onResize();
}
}
function stopDrag() {
handle.style.cursor = 'col-resize';
panel.removeChild(div);
// Reset transitions
container.style.transition =
panel.style.transition =
frame.style.transition = '';
if (!isLocked()) panel.addEventListener('mouseleave', hidePanel);
document.body.removeEventListener('mousemove', drag);
document.body.removeEventListener('mouseup', stopDrag);
}
}
function isLocked() {
if (panel.dataset.locked === 'true') return true;
else return false;
}
}
});
},
},
};