tab layout & label styles

This commit is contained in:
dragonwocky 2021-12-11 14:19:08 +11:00
parent cb7838350f
commit 3e07f23128
6 changed files with 367 additions and 252 deletions

View File

@ -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);
}
}

View File

@ -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"
}
]
}

View File

@ -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`
<webview class="notion-webview" partition="persist:notion"
preload="file://${fs.notionPath('renderer/preload.js')}"
webpreferences="spellcheck=yes, enableremotemodule=yes"
></webview>
`;
$search = web.html`
<webview class="search-webview" partition="persist:notion"
preload="file://${fs.notionPath('renderer/search.js')}"
src="file:///${fs.notionPath('renderer/search.html')}"
webpreferences="spellcheck=no, enableremotemodule=yes"
></webview>
`;
$tabIcon = web.html`<span class="tab-icon"></span>`;
$tabTitle = web.html`<span class="tab-title"></span>`;
$closeTab = web.html`<span class="tab-close">${xIcon}</span>`;
$tab = web.render(
web.html`<div class="tab" draggable="true" id="${this.id}"></div>`,
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`<header></header>`,
$tabs = web.html`<div id="tabs"></div>`,
$newTab = web.html`<div class="new-tab">${await components.feather('plus')}</div>`,
$root = document.querySelector('#root'),
$windowActions = web.html`<div id="window-actions"></div>`;
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;

230
repo/tabs/tab.cjs Normal file
View File

@ -0,0 +1,230 @@
/**
* notion-enhancer: tabs
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (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`
<webview class="notion-webview" partition="persist:notion"
preload="file://${fs.notionPath('renderer/preload.js')}"
webpreferences="spellcheck=yes, enableremotemodule=yes"
></webview>
`;
$search = web.html`
<webview class="search-webview" partition="persist:notion"
preload="file://${fs.notionPath('renderer/search.js')}"
src="file:///${fs.notionPath('renderer/search.html')}"
webpreferences="spellcheck=no, enableremotemodule=yes"
></webview>
`;
$tabIcon = web.html`<span class="tab-icon"></span>`;
$svgIconPlaceholder = web.html`<svg viewBox="0 0 30 30">
<g><path d="M16,1H4v28h22V11L16,1z M16,3.828L23.172,11H16V3.828z M24,27H6V3h8v10h10V27z
M8,17h14v-2H8V17z M8,21h14v-2H8V21z M8,25h14v-2H8V25z"></path></g>
</svg>
`;
$tabTitle = web.html`<span class="tab-title"></span>`;
$closeTab = web.html`<span class="tab-close">${xIcon}</span>`;
$tab = web.render(
web.html`<div class="tab" draggable="true" id="${this.id}"></div>`,
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;
}
};
};

View File

@ -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;
}

View File

@ -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'),