/* * simpler databases * (c) 2020 dragonwocky (https://dragonwocky.me/) * (c) 2020 CloudHill * under the MIT license */ 'use strict'; const { createElement } = require('../../pkg/helpers.js'); module.exports = { id: '752933b5-1258-44e3-b49a-61b4885f8bda', tags: ['extension'], name: 'simpler databases', desc: 'adds a menu to inline databases to toggle ui elements.', version: '1.0.2', author: 'CloudHill', hacks: { 'renderer/preload.js'(store, __exports) { if (!store().blocks) store().blocks = {}; const menuItems = [ { key: 'replace_title', name: 'Replace title...', type: 'input', linkedOnly: true, default: '', action: replaceTitle, }, { key: 'title', name: 'Title', type: 'toggle', default: true, }, { key: 'toggle', name: 'Toggle', type: 'toggle', default: false, action: toggle, }, { key: 'link', name: 'Link arrow', type: 'toggle', default: true, linkedOnly: true, }, { key: 'views', name: 'Views', type: 'toggle', default: true, }, { key: 'toolbar', name: 'Toolbar', type: 'toggle', default: true, }, { key: 'divider', views: ['table', 'board', 'timeline', 'list', 'gallery'], }, { key: 'header_row', name: 'Header row', type: 'toggle', default: true, views: ['table'], }, { key: 'new_item', name: 'New row', type: 'toggle', default: true, views: ['table', 'timeline'], }, { key: 'new_item', name: 'New item', type: 'toggle', default: true, views: ['board', 'list', 'gallery'], }, { key: 'calc_row', name: 'Calculation row', type: 'toggle', default: true, views: ['table', 'timeline'], }, { key: 'divider', views: ['table', 'board'], }, { key: 'hidden_column', name: 'Hidden columns', type: 'toggle', default: true, views: ['board'], }, { key: 'add_group', name: 'Add group', type: 'toggle', default: true, views: ['board'], }, { key: 'new_column', name: 'New column', type: 'toggle', default: true, views: ['table'], }, { key: 'full_width', name: 'Full width', type: 'toggle', default: true, views: ['table'], }, ] document.addEventListener('readystatechange', (event) => { if (document.readyState !== 'complete') return false; // observe for new or moved collection blocks const contentObserver = new MutationObserver((list, observer) => { for (let { addedNodes } of list) { if ( addedNodes[0] && addedNodes[0].querySelector && addedNodes[0].querySelector('.notion-collection_view-block') ) findInlineCollections(); } }); // observe for page changes let queue = []; const pageObserver = new MutationObserver((list, observer) => { if (!queue.length) requestAnimationFrame(() => process(queue)); queue.push(...list); }); pageObserver.observe(document.body, { childList: true, subtree: true, }); function process(list) { queue = []; for (let { addedNodes } of list) { if ( addedNodes[0] && addedNodes[0].className === 'notion-presence-container' ) { findInlineCollections(); contentObserver.disconnect(); contentObserver.observe(addedNodes[0], { childList: true, subtree: true, } ); } } } }); function findInlineCollections() { const collections = document.querySelectorAll('.notion-collection_view-block[style*="width"][style*="max-width"]'); collections.forEach(collection => { if (collection.querySelector('.simpler-databases--config-button')) return; const blockId = collection.dataset.blockId; // config button const add = collection.querySelector('.notion-collection-view-item-add'); if (!add) return; const configButton = add.previousElementSibling.cloneNode(); configButton.className = 'simpler-databases--config-button'; configButton.innerHTML = ``; configButton.collectionBlock = collection; configButton.addEventListener('click', renderConfig); add.parentElement.prepend(configButton); // initialize store variable if (!store().blocks[blockId]) store().blocks[blockId] = {}; // restore stored states menuItems.forEach( item => { if (item.key === 'divider') return; let storedValue = store().blocks[blockId][item.key]; if (storedValue == null) // set defaults storedValue = store().blocks[blockId][item.key] = item.default; if (item.action) item.action(storedValue, collection); if ( item.type !== 'input' && !item.linkedOnly || isLinked(collection) ) { toggleDataTweaks(collection, item.key, storedValue); } } ); }); } // config function renderConfig(e) { if (document.querySelector('.simpler-databases--overlay-container')) return; const button = e.currentTarget; const collection = button.collectionBlock; if (!collection) return; const collectionView = getView(collection); if (!collectionView) return; // layer to close config const overlayContainer = createElement( '
' ); overlayContainer.addEventListener('click', hideConfig) document .querySelector('.notion-app-inner') .appendChild(overlayContainer); const div = createElement(`
`); // render config toggleDataTweaks(collection, 'config-open', true); const config = createElement( '
' ); // get menu items relevant to current view const viewMenu = menuItems.filter( item => (!item.views || item.views.includes(collectionView)) && (!item.linkedOnly || isLinked(collection)) ); config.append(...viewMenu.map( item => renderConfigItem(item, collection) )); overlayContainer.appendChild(div); div.firstElementChild.appendChild(config); focusConfigItem(config.firstElementChild); // config positioning const rect = button.getBoundingClientRect(); div.style.left = Math.min( rect.left + rect.width / 2, window.innerWidth - (config.offsetWidth + 14) ) + 'px'; div.style.top = Math.min( rect.top + rect.height / 2, window.innerHeight - (config.offsetHeight + 14) ) + 'px'; // fade in config.animate( [ {opacity: 0}, {opacity: 1} ], { duration: 200 } ); // key bindings document.addEventListener('keydown', configKeyEvent); } function hideConfig() { const overlayContainer = document.querySelector('.simpler-databases--overlay-container'); if (!overlayContainer) return; overlayContainer.removeEventListener('click', hideConfig); document.removeEventListener('keydown', configKeyEvent); toggleDataTweaks( document.querySelector('[data-tweaks*="config-open"]'), 'config-open', false ); // fade out document.querySelector('.simpler-databases--config-menu').animate( [ {opacity: 1}, {opacity: 0} ], { duration: 200 } ).onfinish = () => overlayContainer.remove(); } function renderConfigItem(menuItem, collection) { if (menuItem.key === 'divider') return createElement('
`); const storedValue = store().blocks[blockId][menuItem.key]; switch (menuItem.type) { case 'toggle': const toggleLabel = createElement(`
${menuItem.name}
`) const toggle = createElement(`
`); item.append(toggleLabel, toggle) item.setAttribute('tabindex', 0); item.addEventListener('click', e => { e.stopPropagation(); const newState = !(toggle.dataset.toggled === 'true'); toggle.dataset.toggled = newState; store().blocks[blockId][menuItem.key] = newState; toggleDataTweaks(collection, menuItem.key, newState); if (menuItem.action) menuItem.action(newState, collection); }); break; case 'input': const input = createElement(`
`); item.appendChild(input) item.addEventListener('click', e => e.stopPropagation()); if (menuItem.action) { input.firstElementChild.addEventListener('input', e => { e.stopPropagation(); const newValue = e.target.value; store().blocks[blockId][menuItem.key] = newValue; menuItem.action(newValue, collection); }); } break; } return item; } function focusConfigItem(item) { ( item.getElementsByTagName('input')[0] || item ).focus(); } function configKeyEvent(e) { e.stopPropagation(); if (e.key === 'Escape') return hideConfig(); let currentFocus = document.activeElement .closest('[class^="simpler-databases--config-item"]'); const parentEl = currentFocus.parentElement; if ( [' ', 'Enter'].includes(e.key) ) return currentFocus.click(); const focusNext = () => { let nextEl = currentFocus.nextElementSibling; if (nextEl) { if (nextEl.className.includes('divider')) nextEl = nextEl.nextElementSibling; focusConfigItem(nextEl); } else focusConfigItem(parentEl.firstElementChild); } const focusPrevious = () => { let prevEl = currentFocus.previousElementSibling; if (prevEl) { if (prevEl.className.includes('divider')) prevEl = prevEl.previousElementSibling; if (prevEl.className.includes('input')) prevEl.getElementsByTagName('input')[0].focus(); focusConfigItem(prevEl); } else focusConfigItem(parentEl.lastElementChild); } if (e.key === 'ArrowUp') focusPrevious(); else if (e.key === 'ArrowDown') focusNext(); else if (e.key === 'Tab') { if (e.shiftKey) { if (currentFocus === parentEl.firstElementChild) { focusConfigItem(parentEl.lastElementChild); e.preventDefault(); } } else if (currentFocus === parentEl.lastElementChild) { focusConfigItem(parentEl.firstElementChild); e.preventDefault(); } } } // get collection info function isLinked(collection) { return collection.querySelector('[style*=" height: 42px;"] .alias'); } function getView(collection) { return collection.querySelector('.notion-scroller [class$="view"]') ?.className.split('-')[1] } // add/remove keys to data-tweaks function toggleDataTweaks(collection, key, state) { if (!collection.dataset.tweaks) collection.dataset.tweaks = ''; const isActive = collection.dataset.tweaks.includes(`[${key}]`); if (state == null) state = !isActive; if (state && !isActive) { collection.dataset.tweaks += `[${key}]`; } else if (!state && isActive) { collection.dataset.tweaks = collection.dataset.tweaks .replace(`[${key}]`, ''); } } // menu actions // replace and add observer to linked database titles function replaceTitle(value, collection) { const titleDiv = collection.querySelector('[style*="height: 42px;"] a [placeholder]'); if (!titleDiv) return; if (!titleDiv.originalTitle && value) titleDiv.originalTitle = titleDiv.innerText; if (!titleDiv.titleObserver) { if (!value) return; // store reference to observer to disconnect() in future calls titleDiv.titleObserver = new MutationObserver(() => { const title = store().blocks[collection.dataset.blockId]['replace_title']; if (title && titleDiv.innerText !== title) titleDiv.innerText = title; }); } else { titleDiv.titleObserver.disconnect(); } if (value) { // observe titleDiv.innerText = value titleDiv.titleObserver.observe(titleDiv, {characterData: true, childList: true}) } else { // reset titleDiv.titleObserver.disconnect(); titleDiv.innerText = titleDiv.originalTitle; delete titleDiv.originalTitle; } } // show or hide toggle function toggle(state, collection) { const header = collection.querySelector('[style*=" height: 42px"]'); if (!header) return; // define functions const collectionView = collection.querySelector('.notion-scroller'); const hideCollection = () => { collectionView.style.height = collectionView.offsetHeight + 'px'; requestAnimationFrame(() => { collection.dataset.toggledHidden = true; setTimeout(() => collectionView.dataset.hideItems = 'true', 200); // hide drag handles }); } const showCollection = () => { // get height collection.dataset.toggledHidden = false; collectionView.style.height = ''; collectionView.style.height = collectionView.offsetHeight + 'px'; collection.dataset.toggledHidden = true; delete collectionView.dataset.hideItems; requestAnimationFrame(() =>{ collection.dataset.toggledHidden = false; setTimeout(() => collectionView.style.height = '', 200); }); } // restore previous state if (!collection.dataset.toggledHidden) { const storedState = store().blocks[collection.dataset.blockId].toggledHidden || false; if (storedState) hideCollection(); } let toggle = header.querySelector('.simpler-databases--toggle'); if (toggle) { // return if toggle is already there if (!state) toggle.remove(); return; } else if (state) { // add toggle toggle = createElement(`
`); toggle.addEventListener('click', () => { const hide = !(collection.dataset.toggledHidden === 'true'); store().blocks[collection.dataset.blockId].toggledHidden = hide; if (hide) hideCollection(); else showCollection(); }); header.prepend(toggle); } } }, }, };