/* * notion-icons * (c) 2019 jayhxmo (https://jaymo.io/) * (c) 2020 dragonwocky (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(`
`); overlayContainer.addEventListener('click', closeRestoreOverlay); document.querySelector('.notion-app-inner').appendChild(overlayContainer); const rect = document.querySelector('.notion-icons--restore-button') .getBoundingClientRect(); const div = createElement(`
`); const restoreOverlay = createElement(`
`) 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(`
${iconData.name}
`) 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( `
` ) 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( '
' ) ) } 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( '
' ); iconSets.forEach( set => notionIcons.appendChild(set) ); // Insert icons overlay document.querySelector('.notion-media-menu > .notion-scroller') .appendChild(notionIcons); } } function renderIconSet(iconData) { const iconSet = createElement('
') try { const authorText = iconData.author ? iconData.authorUrl ? ` by ${iconData.author}` : ` by ${iconData.author}` : ''; const iconSetToggle = createElement( `
${iconData.name}${authorText}
` ); const iconSetBody = createElement( '
' ); 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(`
`); if (iconData.enhancerIcons) { // Load sprite sheet icon.innerHTML = `
`; } else { icon.innerHTML = ``; // 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( '
' ); 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, }); } }); }, }, };