notion-enhancer/mods/notion-icons/mod.js
2020-11-24 13:53:06 +07:00

490 lines
16 KiB
JavaScript

/*
* notion-icons
* (c) 2019 jayhxmo (https://jaymo.io/)
* (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (c) 2020 CloudHill
* under the MIT license
*/
'use strict';
const { createElement } = require('../../pkg/helpers.js'),
fs = require('fs-extra'),
path = require('path');
module.exports = {
id: '2d1f4809-9581-40dd-9bf3-4239db406483',
tags: ['extension'],
name: 'notion icons',
desc:
'use custom icon sets directly in notion.',
version: '1.0.0',
author: 'jayhxmo',
options: [
{
key: 'hide',
label: 'hide icon sets by default.',
type: 'toggle',
value: false,
},
{
key: 'json',
label: 'insert custom json',
type: 'file',
extensions: ['json'],
},
],
hacks: {
'renderer/preload.js'(store, __exports) {
let garbageCollector = [];
const iconsUrl = 'https://raw.githubusercontent.com/notion-enhancer/icons/main/';
function getAsync(urlString, callback) {
let httpReq = new XMLHttpRequest();
httpReq.onreadystatechange = function() {
if (httpReq.readyState == 4 && httpReq.status == 200) callback(httpReq.responseText);
};
httpReq.open('GET', urlString, true);
httpReq.send(null);
}
let modalIcons;
(async () => {
modalIcons = {
remove: await fs.readFile( path.resolve(__dirname, 'icons/remove.svg') ),
restore: await fs.readFile( path.resolve(__dirname, 'icons/restore.svg') ),
}
})();
// Retrieve icons data
let notionIconsData;
getAsync(iconsUrl + 'icons.json', iconsData => {
notionIconsData = JSON.parse(iconsData);
});
// Retrieve custom icons data
let customIconsData;
if (store().json) {
customIconsData = JSON.parse(
fs.readFileSync(store().json)
)
}
function getTab(n, button = false) {
return document.querySelector(
`.notion-media-menu > :first-child > :first-child > :nth-child(${n}) ${button ? 'div' : ''}`
);
}
function isCurrentTab(n) {
return getTab(n).childNodes.length > 1;
}
// Submits the icon's url as an image link
function setPageIcon(iconUrl) {
const input = document.querySelector('input[type=url]');
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
nativeInputValueSetter.call(input, iconUrl);
input.dispatchEvent(
new Event('input', { bubbles: true })
);
input.dispatchEvent(
new KeyboardEvent('keydown', { bubbles: true, cancelable: true, keyCode: 13 })
);
removeIcons();
}
function addIconsTab() {
// Prevent icons tab duplication
if (getTab(5)) {
removeIcons();
return;
}
// Change 'Upload an image' to 'Upload'
getTab(2, true).innerText = 'Upload';
// Initialize icons tab
const iconsTab = getTab(3).cloneNode(true);
iconsTab.className = 'notion-icons--tab';
iconsTab.firstChild.innerText = 'Icons';
iconsTab.firstChild.addEventListener('click', renderIconsOverlay);
// Insert icons tab
const tabStrip = getTab(1).parentElement;
tabStrip.insertBefore(iconsTab, tabStrip.lastChild);
// Remove the icons overlay when clicking...
const closeTriggers = [
// The fog layer
document.querySelector('.notion-overlay-container [style*="width: 100vw; height: 100vh;"]'),
// The first three buttons
...Array.from( Array(3), (e, i) => getTab(i + 1, true) ),
// The remove button
getTab(5).lastChild,
];
closeTriggers.forEach(trigger => {
trigger.addEventListener('click', removeIcons);
garbageCollector.push(trigger);
})
// Remove the icons overlay when pressing the Escape key
document.querySelector('.notion-media-menu')
.addEventListener('keydown', e => {
if (e.keyCode === 27) removeIcons();
});
}
function addRestoreButton() {
const buttons = getTab(5) ? getTab(5) : getTab(4);
const restoreButton = buttons.lastChild.cloneNode(true);
restoreButton.className = 'notion-icons--restore-button';
restoreButton.innerHTML = modalIcons.restore;
buttons.prepend(restoreButton);
restoreButton.addEventListener('click', renderRestoreOverlay);
}
function renderRestoreOverlay() {
if (!store().removedSets) return;
store().removedSets.sort((a, b) => {
const setA = a.name.toLowerCase(),
setB = b.name.toLowerCase();
if (setA < setB) return -1;
if (setA > setB) return 1;
return 0;
});
const overlayContainer = createElement(`
<div class="notion-icons--overlay-container"></div>
`);
overlayContainer.addEventListener('click', closeRestoreOverlay);
document.querySelector('.notion-app-inner').appendChild(overlayContainer);
const rect = document.querySelector('.notion-icons--restore-button')
.getBoundingClientRect();
const div = createElement(`
<div style="position: fixed; top: ${rect.top}px; left: ${rect.left}px; height: ${rect.height}px;">
<div style="position: relative; top: 100%; pointer-events: auto;"></div>
</div>
`);
const restoreOverlay = createElement(`
<div class="notion-icons--restore"></div>
`)
overlayContainer.appendChild(div);
div.firstElementChild.appendChild(restoreOverlay);
// Fade in
restoreOverlay.animate(
[ {opacity: 0}, {opacity: 1} ],
{ duration: 200 }
);
store().removedSets.forEach(iconData => {
const restoreItem = renderRestoreItem(iconData);
restoreOverlay.appendChild(restoreItem);
})
}
function renderRestoreItem(iconData) {
const iconUrl = `${iconData.sourceUrl}/${iconData.source}_${0}.${iconData.extension}`;
const restoreItem = createElement(`
<div class="notion-icons--removed-set">
<div style="flex-grow: 0; flex-shrink: 0; width: 32px; height: 32px;">
<img style="width: 100%; height: 100%" src="${iconUrl}" />
</div>
<span style="margin: 0 8px;">${iconData.name}</span>
</div>
`)
restoreItem.addEventListener('click', () => restoreIconSet(iconData));
return restoreItem;
}
function closeRestoreOverlay() {
const overlayContainer = document.querySelector('.notion-icons--overlay-container');
overlayContainer.removeEventListener('click', closeRestoreOverlay);
// Fade out
document.querySelector('.notion-icons--restore').animate(
[ {opacity: 1}, {opacity: 0} ],
{ duration: 200 }
).onfinish = () => overlayContainer.remove();
}
function renderIconsOverlay() {
if (!isCurrentTab(4)) {
// Switch to 3rd tab so that the link can be inputed in the underlay
if (!isCurrentTab(3)) getTab(3, true).click();
if (
store().removedSets &&
store().removedSets.length > 0
) {
addRestoreButton();
}
// Set active bar on icons tab
const iconsTab = getTab(4);
const activeBar = createElement(
`<div id="notion-icons--active-bar"></div>`
)
activeBar.style = 'border-bottom: 2px solid var(--theme--text); position: absolute; bottom: -1px; left: 8px; right: 8px;';
iconsTab.appendChild(activeBar);
getTab(4).style.position = 'relative';
getTab(3).className = 'hide-active-bar';
// Convert icons data into renderable
const iconSets = [];
if (customIconsData && customIconsData.icons) {
customIconsData.icons.forEach(i => {
iconSets.push( renderIconSet(i) );
});
// Divider
iconSets.push(
createElement(
'<div style="height: 1px; margin-bottom: 9px; border-bottom: 1px solid var(--theme--table-border);"></div>'
)
)
}
if (notionIconsData && notionIconsData.icons) {
notionIconsData.icons.forEach(i => {
i.sourceUrl = i.sourceUrl || (iconsUrl + i.source);
if ( store().removedSets ) {
for (let iconData of store().removedSets) {
if (iconData.source === i.source) return;
}
}
i.enhancerIcons = true;
iconSets.push( renderIconSet(i) );
});
}
// Create icons overlay
const notionIcons = createElement(
'<div id="notion-icons"></div>'
);
iconSets.forEach( set => notionIcons.appendChild(set) );
// Insert icons overlay
document.querySelector('.notion-media-menu > .notion-scroller')
.appendChild(notionIcons);
}
}
function renderIconSet(iconData) {
const iconSet = createElement('<div class="notion-icons--icon-set"></div>')
try {
const authorText = iconData.author
? iconData.authorUrl
? ` by <a target="_blank" href="${iconData.authorUrl}" style="opacity: 0.6;">${iconData.author}</a>`
: ` by <span style="opacity: 0.6;">${iconData.author}</span>`
: '';
const iconSetToggle = createElement(
`<div class="notion-icons--toggle">
<svg viewBox="0 0 100 100" class="triangle"><polygon points="5.9,88.2 50,11.8 94.1,88.2"></polygon></svg>
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${iconData.name}${authorText}</div>
<div class="notion-icons--extra">
<div class="notion-icons--spinner">
<img src="/images/loading-spinner.4dc19970.svg" />
</div>
</div>
</div>`
);
const iconSetBody = createElement(
'<div class="notion-icons--body"></div>'
);
iconSet.appendChild(iconSetToggle);
iconSet.appendChild(iconSetBody);
const promiseArray = [];
// Render icons
for (let i = 0; i < (iconData.count || iconData.source.length); i++) {
const iconUrl = iconData.sourceUrl
? Array.isArray(iconData.source)
? `${iconData.sourceUrl}/${iconData.source[i]}.${iconData.extension}`
: `${iconData.sourceUrl}/${iconData.source}_${i}.${iconData.extension}`
: iconData.source[i];
const icon = createElement(`<div class="notion-icons--icon"></div>`);
if (iconData.enhancerIcons) {
// Load sprite sheet
icon.innerHTML = `
<div style="width: 32px; height: 32px; background: url(${iconsUrl}${iconData.source}/sprite.png) 0 -${i * 32}px no-repeat; background-size: 32px;">
</div>
`;
} else {
icon.innerHTML = `<img src="${iconUrl}" />`;
// Make sure icons load
promiseArray.push(
new Promise((resolve, reject) => {
icon.firstChild.onload = resolve;
icon.firstChild.onerror = () => {
reject();
icon.classList.add('error');
icon.innerHTML = '!';
};
})
);
}
iconSetBody.appendChild(icon);
garbageCollector.push(icon);
icon.addEventListener('click', () => setPageIcon(iconUrl));
}
// Hide spinner after all icons finish loading
(async () => {
const spinner = iconSetToggle.querySelector('.notion-icons--spinner'),
loadPromise = Promise.all(promiseArray);
loadPromise.then(
() => spinner.remove(),
() => {
iconSet.classList.add('alert')
spinner.remove();
}
)
})();
// Add hide icon set button
if (iconData.enhancerIcons) {
const removeButton = createElement(
'<div class="notion-icons--remove-button"></div>'
);
removeButton.innerHTML = modalIcons.remove;
removeButton.addEventListener('click', e => {
e.stopPropagation();
removeIconSet(iconData)
});
iconSet.querySelector('.notion-icons--extra')
.appendChild(removeButton);
}
// Set up Toggle
requestAnimationFrame(() => {
iconSetBody.style.height = iconSetBody.style.maxHeight = `${iconSetBody.offsetHeight}px`;
if (store().removed) iconSetToggle.click();
});
iconSetToggle.addEventListener('click', e => {
if (e.target.nodeName === 'A') return;
toggleIconSet(iconSet);
});
} catch (err) {
iconSet.classList.add('error');
iconSet.innerHTML = `Invalid Icon Set: ${iconData.name}`;
}
return iconSet;
}
function toggleIconSet(iconSet) {
iconSet.classList.toggle('hidden-set');
const iconSetBody = iconSet.lastChild;
if (iconSetBody) {
iconSetBody.style.height = iconSet.classList.contains('hidden-set')
? 0
: iconSetBody.style.maxHeight;
}
}
function removeIconSet(iconData) {
if (!store().removedSets) store().removedSets = [];
for (const hiddenIconData of store().removedSets) {
if (hiddenIconData.source === iconData.source) return;
}
store().removedSets.push(iconData);
removeIcons();
renderIconsOverlay();
}
function restoreIconSet(iconData) {
if (!store().removedSets) return;
for (let i = 0; i < store().removedSets.length; i++) {
if (store().removedSets[i].source === iconData.source)
store().removedSets.splice(i, 1);
}
removeIcons();
renderIconsOverlay();
}
function removeIcons() {
const notionIcons = document.getElementById('notion-icons'),
activeBar = document.getElementById('notion-icons--active-bar'),
restoreButton = document.querySelector('.notion-icons--restore-button'),
overlayContainer = document.querySelector('.notion-icons--overlay-container');
if (notionIcons)
notionIcons.remove();
if (activeBar) {
activeBar.remove();
getTab(4).style.position = '';
}
if (getTab(3)) getTab(3).className = '';
if (restoreButton)
restoreButton.remove();
if (overlayContainer)
closeRestoreOverlay();
if (garbageCollector.length) {
for (let i = 0; i < garbageCollector.length; i++) {
garbageCollector[i] = null;
}
garbageCollector = [];
}
}
document.addEventListener('readystatechange', () => {
if (document.readyState !== 'complete') return false;
const attempt_interval = setInterval(enhance, 500);
function enhance() {
const overlay = document.querySelector('.notion-overlay-container');
if (!overlay) return;
clearInterval(attempt_interval);
const observer = new MutationObserver((list, observer) => {
for ( let { addedNodes } of list) {
if (
addedNodes[0] &&
addedNodes[0].style &&
document.querySelector('.notion-media-menu')
) {
for (let i = 0; i <= 3; i++) {
if (addedNodes[0].style.cssText === `pointer-events: auto; position: relative; z-index: ${i};`) {
addIconsTab();
return;
}
}
}
}
});
observer.observe(overlay, {
childList: true,
subtree: true,
});
}
});
},
},
};