From 3e07f231281ec7cc90821e36f320b728bc83a68f Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Sat, 11 Dec 2021 14:19:08 +1100 Subject: [PATCH] tab layout & label styles --- repo/tabs/client.mjs | 22 +++- repo/tabs/mod.json | 20 +++- repo/tabs/rendererIndex.cjs | 229 ++--------------------------------- repo/tabs/tab.cjs | 230 ++++++++++++++++++++++++++++++++++++ repo/tabs/tabs.css | 117 +++++++++++++----- repo/theming/client.mjs | 1 + 6 files changed, 367 insertions(+), 252 deletions(-) create mode 100644 repo/tabs/tab.cjs diff --git a/repo/tabs/client.mjs b/repo/tabs/client.mjs index 961f0d4..934ab44 100644 --- a/repo/tabs/client.mjs +++ b/repo/tabs/client.mjs @@ -6,14 +6,17 @@ 'use strict'; -export default async function ({ electron }, db) { +export default async function ({ web, electron }, db) { + const breadcrumbSelector = + '.notion-topbar > div > :nth-child(2) > .notion-focusable:last-child', + imgIconSelector = `${breadcrumbSelector} .notion-record-icon img`, + nativeIconSelector = `${breadcrumbSelector} .notion-record-icon [role="image"]`, + titleSelector = `${breadcrumbSelector} > :not(.notion-record-icon)`, + viewSelector = '.notion-collection-view-select'; + let title = '', icon = ''; const notionSetWindowTitle = __electronApi.setWindowTitle, - imgIconSelector = - '.notion-topbar > div > :nth-child(2) > .notion-focusable:last-child .notion-record-icon img', - nativeIconSelector = - '.notion-topbar > div > :nth-child(2) > .notion-focusable:last-child .notion-record-icon [role="image"]', getIcon = () => { const $imgIcon = document.querySelector(imgIconSelector), $nativeIcon = document.querySelector(nativeIconSelector); @@ -37,4 +40,13 @@ export default async function ({ electron }, db) { __electronApi.setWindowTitle = (newTitle) => updateTitle(newTitle); document.addEventListener('focus', updateTitle); electron.onMessage('trigger-title-update', () => updateTitle()); + + await web.whenReady([titleSelector]); + const $title = document.querySelector(titleSelector), + $view = document.querySelector(viewSelector); + if (!title && $title) { + if ($view) { + updateTitle(`${$title.innerText} | ${$view.innerText}`); + } else updateTitle($title.innerText); + } } diff --git a/repo/tabs/mod.json b/repo/tabs/mod.json index 794ac28..e299d9f 100644 --- a/repo/tabs/mod.json +++ b/repo/tabs/mod.json @@ -25,10 +25,22 @@ }, "options": [ { + "type": "select", + "key": "label_type", + "label": "tab labels", + "values": ["page icon & title", "page icon only", "page title only"] + }, + { + "type": "select", + "key": "layout_style", + "label": "tab layout", + "values": ["traditional tabbed", "rectangular", "bubble", "compact"] + }, + { + "type": "select", "key": "select_modifier", "label": "tab select modifier", "tooltip": "**usage: Modifier+1 to Modifier+9, Modifier+ArrowLeft, Modifier+ArrowRight and Modifier+ link click**", - "type": "select", "values": [ "Alt", "Command", @@ -51,6 +63,12 @@ "key": "close_tab", "label": "close tab hotkey", "value": "Control+W" + }, + { + "type": "hotkey", + "key": "restore_tab", + "label": "restore previously opened tab hotkey", + "value": "Control+Shift+T" } ] } diff --git a/repo/tabs/rendererIndex.cjs b/repo/tabs/rendererIndex.cjs index 439da97..b5f4691 100644 --- a/repo/tabs/rendererIndex.cjs +++ b/repo/tabs/rendererIndex.cjs @@ -6,231 +6,29 @@ 'use strict'; -module.exports = async function ({ components, env, web, fmt, fs }, db, __exports, __eval) { +module.exports = async function (api, db, __exports, __eval) { const url = require('url'), electron = require('electron'), electronWindow = electron.remote.getCurrentWindow(), - notionIpc = env.notionRequire('helpers/notionIpc'); - - let focusedTab, xIcon; - const tabCache = new Map(); - class Tab { - id = fmt.uuidv4(); - - $notion = web.html` - - `; - $search = web.html` - - `; - - $tabIcon = web.html``; - $tabTitle = web.html``; - $closeTab = web.html`${xIcon}`; - $tab = web.render( - web.html`
`, - this.$tabIcon, - this.$tabTitle, - this.$closeTab - ); - - constructor( - $tabs, - $root, - { - notionUrl = 'notion://www.notion.so/', - cancelAnimation = false, - icon = '', - title = 'notion.so', - } = {} - ) { - this.$notion.src = notionUrl; - this.$tabTitle.innerText = title; - this.setIcon(icon); - tabCache.set(this.$tab.id, this); - - web.render($tabs, this.$tab); - web.render($root, this.$search); - web.render($root, this.$notion); - electronWindow.on('focus', () => { - if (focusedTab === this) this.$notion.focus(); - }); - - this.$tab.addEventListener('click', (event) => { - if (event.target !== this.$closeTab && !this.$closeTab.contains(event.target)) { - this.focus(); - } - }); - this.$closeTab.addEventListener('click', () => this.close()); - - if (!cancelAnimation) { - this.$tab.animate([{ width: '0px' }, { width: `${this.$tab.clientWidth}px` }], { - duration: 100, - easing: 'ease-in', - }).finished; - } - this.focus(); - this.addNotionListeners(); - return this; - } - - async focus() { - document.querySelectorAll('.notion-webview, .search-webview').forEach(($webview) => { - if (![this.$notion, this.$search].includes($webview)) $webview.style.display = ''; - }); - document.querySelectorAll('.tab.current').forEach(($tab) => { - if ($tab !== this.$tab) $tab.classList.remove('current'); - }); - this.$tab.classList.add('current'); - this.$notion.style.display = 'flex'; - this.$search.style.display = 'flex'; - this.focusNotion(); - focusedTab = this; - } - async close() { - const $sibling = this.$tab.nextElementSibling || this.$tab.previousElementSibling; - if ($sibling) { - if (!focusedTab || focusedTab === this) $sibling.click(); - const width = `${this.$tab.clientWidth}px`; - this.$tab.style.width = 0; - this.$tab.style.pointerEvents = 'none'; - await this.$tab.animate([{ width }, { width: '0px' }], { - duration: 100, - easing: 'ease-out', - }).finished; - this.$tab.remove(); - this.$notion.remove(); - this.$search.remove(); - } else electronWindow.close(); - } - - setIcon(icon) { - if (icon.startsWith('url(')) { - // img - this.$tabIcon.style.background = icon; - this.$tabIcon.innerText = ''; - } else { - // unicode (native) - this.$tabIcon.innerText = icon; - this.$tabIcon.style.background = ''; - } - } - - webContents() { - return electron.remote.webContents.fromId(this.$notion.getWebContentsId()); - } - focusNotion() { - document.activeElement?.blur?.(); - this.$notion.blur(); - this.$notion.focus(); - requestAnimationFrame(() => { - notionIpc.sendIndexToNotion(this.$notion, 'notion-enhancer:trigger-title-update'); - }); - } - focusSearch() { - document.activeElement?.blur?.(); - this.$search.blur(); - this.$search.focus(); - } - - addNotionListeners() { - const fromNotion = (channel, listener) => - notionIpc.receiveIndexFromNotion.addListener(this.$notion, channel, listener), - fromSearch = (channel, listener) => - notionIpc.receiveIndexFromSearch.addListener(this.$search, channel, listener), - toSearch = (channel, data) => notionIpc.sendIndexToSearch(this.$search, channel, data); - - this.$notion.addEventListener('dom-ready', () => { - this.focusNotion(); - - const navigateHistory = (event, cmd) => { - const swipe = event === 'swipe', - back = swipe ? cmd === 'left' : cmd === 'browser-backward', - fwd = swipe ? cmd === 'right' : cmd === 'browser-forward'; - if (back && this.$notion.canGoBack()) this.$notion.goBack(); - if (fwd && this.$notion.canGoForward()) this.$notion.goForward(); - }; - electronWindow.addListener('app-command', (e, cmd) => navigateHistory('app-cmd', cmd)); - electronWindow.addListener('swipe', (e, dir) => navigateHistory('swipe', dir)); - - this.webContents().addListener('found-in-page', (event, result) => { - const matches = result - ? { count: result.matches, index: result.activeMatchOrdinal } - : { count: 0, index: 0 }; - toSearch('search:result', matches); - }); - }); - - notionIpc.proxyAllMainToNotion(this.$notion); - - fromNotion('search:start', () => this.startSearch()); - fromNotion('search:stop', () => this.stopSearch()); - fromNotion('search:set-theme', (theme) => toSearch('search:set-theme', theme)); - fromSearch('search:clear', () => this.clearSearch()); - fromSearch('search:stop', () => this.stopSearch()); - fromSearch('search:next', (query) => this.searchNext(query)); - fromSearch('search:prev', (query) => this.searchPrev(query)); - - fromNotion('zoom', (zoomFactor) => { - this.webContents().setZoomFactor(zoomFactor); - }); - - fromNotion('notion-enhancer:set-tab-title', (title) => { - this.$tabTitle.innerText = title; - }); - fromNotion('notion-enhancer:set-tab-icon', (icon) => this.setIcon(icon)); - } - - #firstQuery = true; - startSearch() { - this.$search.classList.add('search-active'); - this.focusSearch(); - notionIpc.sendIndexToSearch(this.$search, 'search:start'); - notionIpc.sendIndexToNotion(this.$search, 'search:started'); - } - clearSearch() { - this.#firstQuery = true; - this.webContents().stopFindInPage('clearSelection'); - } - stopSearch() { - this.$search.classList.remove('search-active'); - this.focusNotion(); - this.clearSearch(); - notionIpc.sendIndexToSearch(this.$search, 'search:reset'); - notionIpc.sendIndexToNotion(this.$notion, 'search:stopped'); - } - searchNext(query) { - this.webContents().findInPage(query, { - forward: true, - findNext: !this.#firstQuery, - }); - this.#firstQuery = false; - } - searchPrev(query) { - this.webContents().findInPage(query, { - forward: false, - findNext: !this.#firstQuery, - }); - this.#firstQuery = false; - } - } + { components, web } = api; window['__start'] = async () => { + const tabCache = new Map(), + Tab = await require('./tab.cjs')(api, db, tabCache); + document.body.dataset.tabLabels = await db.get(['label_type']); + document.body.dataset.tabStyle = await db.get(['layout_style']); + const $header = web.html`
`, $tabs = web.html`
`, $newTab = web.html`
${await components.feather('plus')}
`, $root = document.querySelector('#root'), $windowActions = web.html`
`; document.body.prepend(web.render($header, $tabs, $newTab, $windowActions)); - xIcon = await components.feather('x'); + new Tab($tabs, $root, { + notionUrl: url.parse(window.location.href, true).query.path, + cancelAnimation: true, + }); $newTab.addEventListener('click', () => { new Tab($tabs, $root); }); @@ -239,11 +37,6 @@ module.exports = async function ({ components, env, web, fmt, fs }, db, __export if (tab) tab.close(); }); - new Tab($tabs, $root, { - notionUrl: url.parse(window.location.href, true).query.path, - cancelAnimation: true, - }); - let $draggedTab; const getDragTarget = ($el) => { while (!$el.matches('.tab, header, body')) $el = $el.parentElement; diff --git a/repo/tabs/tab.cjs b/repo/tabs/tab.cjs new file mode 100644 index 0000000..801aeaa --- /dev/null +++ b/repo/tabs/tab.cjs @@ -0,0 +1,230 @@ +/** + * notion-enhancer: tabs + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +let focusedTab; + +module.exports = async function ({ components, env, web, fmt, fs }, db, tabCache) { + const electron = require('electron'), + electronWindow = electron.remote.getCurrentWindow(), + notionIpc = env.notionRequire('helpers/notionIpc'), + xIcon = await components.feather('x'); + + return class Tab { + id = fmt.uuidv4(); + + $notion = web.html` + + `; + $search = web.html` + + `; + + $tabIcon = web.html``; + $svgIconPlaceholder = web.html` + + + `; + $tabTitle = web.html``; + $closeTab = web.html`${xIcon}`; + $tab = web.render( + web.html`
`, + this.$tabIcon, + this.$svgIconPlaceholder, + this.$tabTitle, + this.$closeTab + ); + + constructor( + $tabs, + $root, + { + notionUrl = 'notion://www.notion.so/', + cancelAnimation = false, + icon = '', + title = 'notion.so', + } = {} + ) { + this.$notion.src = notionUrl; + this.$tabTitle.innerText = title; + this.setIcon(icon); + tabCache.set(this.$tab.id, this); + + web.render($tabs, this.$tab); + web.render($root, this.$search); + web.render($root, this.$notion); + electronWindow.on('focus', () => { + if (focusedTab === this) this.$notion.focus(); + }); + + this.$tab.addEventListener('click', (event) => { + if (event.target !== this.$closeTab && !this.$closeTab.contains(event.target)) { + this.focus(); + } + }); + this.$closeTab.addEventListener('click', () => this.close()); + + if (!cancelAnimation) { + this.$tab.animate([{ width: '0px' }, { width: `${this.$tab.clientWidth}px` }], { + duration: 100, + easing: 'ease-in', + }).finished; + } + this.focus(); + this.addNotionListeners(); + return this; + } + + async focus() { + document.querySelectorAll('.notion-webview, .search-webview').forEach(($webview) => { + if (![this.$notion, this.$search].includes($webview)) $webview.style.display = ''; + }); + document.querySelectorAll('.tab.current').forEach(($tab) => { + if ($tab !== this.$tab) $tab.classList.remove('current'); + }); + this.$tab.classList.add('current'); + this.$notion.style.display = 'flex'; + this.$search.style.display = 'flex'; + this.focusNotion(); + focusedTab = this; + } + async close() { + const $sibling = this.$tab.nextElementSibling || this.$tab.previousElementSibling; + if ($sibling) { + if (!focusedTab || focusedTab === this) $sibling.click(); + const width = `${this.$tab.clientWidth}px`; + this.$tab.style.width = 0; + this.$tab.style.pointerEvents = 'none'; + await this.$tab.animate([{ width }, { width: '0px' }], { + duration: 100, + easing: 'ease-out', + }).finished; + this.$tab.remove(); + this.$notion.remove(); + this.$search.remove(); + } else electronWindow.close(); + } + + setIcon(icon) { + if (icon.startsWith('url(')) { + // img + this.$tabIcon.style.background = icon; + this.$tabIcon.innerText = ''; + } else { + // unicode (native) + this.$tabIcon.innerText = icon; + this.$tabIcon.style.background = ''; + } + } + + webContents() { + return electron.remote.webContents.fromId(this.$notion.getWebContentsId()); + } + focusNotion() { + document.activeElement?.blur?.(); + this.$notion.blur(); + this.$notion.focus(); + requestAnimationFrame(() => { + notionIpc.sendIndexToNotion(this.$notion, 'notion-enhancer:trigger-title-update'); + }); + } + focusSearch() { + document.activeElement?.blur?.(); + this.$search.blur(); + this.$search.focus(); + } + + addNotionListeners() { + const fromNotion = (channel, listener) => + notionIpc.receiveIndexFromNotion.addListener(this.$notion, channel, listener), + fromSearch = (channel, listener) => + notionIpc.receiveIndexFromSearch.addListener(this.$search, channel, listener), + toSearch = (channel, data) => notionIpc.sendIndexToSearch(this.$search, channel, data); + + this.$notion.addEventListener('dom-ready', () => { + this.focusNotion(); + + const navigateHistory = (event, cmd) => { + const swipe = event === 'swipe', + back = swipe ? cmd === 'left' : cmd === 'browser-backward', + fwd = swipe ? cmd === 'right' : cmd === 'browser-forward'; + if (back && this.$notion.canGoBack()) this.$notion.goBack(); + if (fwd && this.$notion.canGoForward()) this.$notion.goForward(); + }; + electronWindow.addListener('app-command', (e, cmd) => navigateHistory('app-cmd', cmd)); + electronWindow.addListener('swipe', (e, dir) => navigateHistory('swipe', dir)); + + this.webContents().addListener('found-in-page', (event, result) => { + const matches = result + ? { count: result.matches, index: result.activeMatchOrdinal } + : { count: 0, index: 0 }; + toSearch('search:result', matches); + }); + }); + + notionIpc.proxyAllMainToNotion(this.$notion); + + fromNotion('search:start', () => this.startSearch()); + fromNotion('search:stop', () => this.stopSearch()); + fromNotion('search:set-theme', (theme) => toSearch('search:set-theme', theme)); + fromSearch('search:clear', () => this.clearSearch()); + fromSearch('search:stop', () => this.stopSearch()); + fromSearch('search:next', (query) => this.searchNext(query)); + fromSearch('search:prev', (query) => this.searchPrev(query)); + + fromNotion('zoom', (zoomFactor) => { + this.webContents().setZoomFactor(zoomFactor); + }); + + fromNotion('notion-enhancer:set-tab-title', (title) => { + this.$tabTitle.innerText = title; + }); + fromNotion('notion-enhancer:set-tab-icon', (icon) => this.setIcon(icon)); + } + + #firstQuery = true; + startSearch() { + this.$search.classList.add('search-active'); + this.focusSearch(); + notionIpc.sendIndexToSearch(this.$search, 'search:start'); + notionIpc.sendIndexToNotion(this.$search, 'search:started'); + } + clearSearch() { + this.#firstQuery = true; + this.webContents().stopFindInPage('clearSelection'); + } + stopSearch() { + this.$search.classList.remove('search-active'); + this.focusNotion(); + this.clearSearch(); + notionIpc.sendIndexToSearch(this.$search, 'search:reset'); + notionIpc.sendIndexToNotion(this.$notion, 'search:stopped'); + } + searchNext(query) { + this.webContents().findInPage(query, { + forward: true, + findNext: !this.#firstQuery, + }); + this.#firstQuery = false; + } + searchPrev(query) { + this.webContents().findInPage(query, { + forward: false, + findNext: !this.#firstQuery, + }); + this.#firstQuery = false; + } + }; +}; diff --git a/repo/tabs/tabs.css b/repo/tabs/tabs.css index 7f57848..bc85ee3 100644 --- a/repo/tabs/tabs.css +++ b/repo/tabs/tabs.css @@ -26,16 +26,17 @@ body { header { display: flex; background: var(--theme--bg_secondary); + border-bottom: 1px solid var(--theme--ui_divider); width: 100%; - padding: 8px; + padding: 0.5em; user-select: none; -webkit-app-region: drag; z-index: 3; + font-size: 16px; } #tabs { display: flex; - margin-bottom: -8px; overflow: hidden; } .tab { @@ -45,39 +46,21 @@ header { width: 14em; max-width: 14em; overflow: hidden; - padding: 6.4px 9.6px 6.4px 9.6px; + padding: 0.4em 0.6em 0.4em 0.6em; color: var(--theme--text_secondary); background: var(--theme--bg); font-family: var(--theme--font_sans); - font-size: 16px; font-weight: 500; border: none; - border-bottom: 3px solid var(--theme--ui_divider); -webkit-app-region: no-drag; } -.tab .tab-icon { - font-size: 14px; - margin-right: 6px; -} -.tab .tab-icon[style*='background'] { - width: 14px; - height: 14px; - align-self: center; - margin-right: 8px; -} -.tab .tab-title { - white-space: nowrap; - overflow: hidden; -} - .tab:hover { background: var(--theme--ui_interactive-hover); } .tab.current { background: var(--theme--ui_interactive-active); } - #tabs.dragged-over { box-shadow: 2px 0 0 0 var(--theme--accent_blue-selection); } @@ -85,6 +68,35 @@ header { box-shadow: inset 2px 0 0 0 var(--theme--accent_blue-selection); } +.tab-title { + white-space: nowrap; + overflow: hidden; +} +.tab-icon { + font-size: 0.875em; + margin-right: 0.375em; +} +.tab-icon[style*='background'] { + width: 0.875em; + height: 0.875em; + align-self: center; + margin: 0 0.5em 2px 0; +} +.tab-icon:not([style*='background']):empty, +.tab-icon + svg { + display: none; +} +.tab-icon:not([style*='background']):empty + svg { + /* placeholder icon */ + display: inline-block; + margin: 1px 0.5em 0 0; + width: 1.125em; + height: 1.125em; + display: block; + backface-visibility: hidden; + fill: var(--theme--icon_secondary); +} + .new-tab, .tab-close { transition: background 20ms ease-in 0s; @@ -93,20 +105,20 @@ header { align-items: center; justify-content: center; flex-shrink: 0; - border-radius: 3px; - height: 20px; - width: 20px; + border-radius: 0.1875em; + height: 1.25em; + width: 1.25em; padding: 0 0.25px 0 0; border: none; background: transparent; - font-size: 14px; + font-size: 0.875em; -webkit-app-region: no-drag; } .new-tab svg, .tab-close svg { - width: 14px; - height: 14px; + width: 0.875em; + height: 0.875em; fill: var(--theme--icon_secondary); color: var(--theme--icon_secondary); } @@ -123,19 +135,68 @@ header { .new-tab { align-self: center; - margin: 0 48px -4px 6px; + margin: 0 3em 0 0.375em; } .tab-close { margin-left: auto; } #window-actions { + display: flex; + align-items: center; margin-left: auto; } #window-actions > * { -webkit-app-region: no-drag; } +[data-tab-labels='page title only'] .tab-icon { + display: none; +} +[data-tab-labels='page icon only'] .tab { + width: 4em; + max-width: 4em; +} +[data-tab-labels='page icon only'] .tab-title { + display: none; +} + +[data-tab-style='rectangular'] .new-tab, +[data-tab-style='traditional tabbed'] .new-tab { + margin-bottom: -0.25em; +} +[data-tab-style='rectangular'] #tabs { + margin-bottom: -0.5em; +} +[data-tab-style='traditional tabbed'] header { + padding-top: 0.6875em; +} +[data-tab-style='traditional tabbed'] #tabs { + margin: -0.1875em 0 -0.5em 0; +} +[data-tab-style='traditional tabbed'] .tab { + border-top-left-radius: 0.875em; + border-top-right-radius: 0.875em; + padding: 0.6em 0.6em 0.4em 0.6em; +} +[data-tab-style='bubble'] .tab { + border-radius: 0.375em; +} +[data-tab-style='bubble'] .tab:not(:first-child) { + margin-left: 0.5em; +} +[data-tab-style='compact'] header { + padding: 0; + font-size: 14px; +} +[data-tab-style='compact'] .tab-close { + align-self: center; +} +[data-tab-style='compact'] #window-actions { + transform: scale(0.8); + margin-right: -0.35em; +} + #root { flex-grow: 1; } diff --git a/repo/theming/client.mjs b/repo/theming/client.mjs index c6b6988..88060be 100644 --- a/repo/theming/client.mjs +++ b/repo/theming/client.mjs @@ -17,6 +17,7 @@ export default async function ({ web, registry, storage, electron }, db) { } const updateTheme = async () => { + if (document.visibilityState !== 'visible' && !document.hasFocus()) return; const isDark = document.querySelector('.notion-dark-theme') || document.querySelector('.notion-body.dark'),