Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for Tree Style Tab 4.0 and later #38

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tst-search",
"version": "0.0.7",
"version": "0.0.7+",
"title": "TST Tab Search",
"description": "Filter Tree Style Tab's sidebar by search terms.",
"author": "Niklas Gollenstede",
Expand Down
95 changes: 60 additions & 35 deletions src/background/index.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,50 @@ import options from '../common/options.esm.js';

let debug = false; options.debug.whenChange(([ value, ]) => { debug = value; });

// constants
const classes = {
matching: [ 'tst-search:matching', ], // applied to all tabs matching the search
hasChild: [ 'tst-search:child-matching', ], // applied to all tabs who have an "offspring" that is `matching` (potentially in addition to `matching` themselves)
hidden: /**@type{string[]}*/([ ]), // applied to `.hidden` tabs
failed: [ 'tst-search:not-matching', ], // applied to tabs that are neither `matching` nor `hasChild`
active: [ 'tst-search:active', ], // applied to a single one of the `matching` tabs
searching: [ 'tst-search:searching', ], // applied to all tabs while the search bar is focused
};
let cache = /**@type{{ tabs: Tab[] & { byId: Map<number, Tab>, all: Tab[], }, windowId: number, } | null}*/(null);
const queueClearCache = debounce(() => { cache = null; }, 30e3);

/// setup of/with TST

const onTstError = console.error.bind(console, 'TST error');
const TST = tstApi({
getManifest() { return {
name: manifest.name,
icons: manifest.icons,
style: [ 'miss', 'child', 'hit', 'active', 'custom', ].map(
name => options.result.children[name].children.styles.children.map(style => { try {
const values = Array.isArray(style.value) ? style.value : [ style.value, ];
return typeof style.model.extra.get === 'function' ? style.model.extra.get(...values) : values[0] ? style.model.extra.value : '';
} catch (error) { console.error(error); return ''; } }).join('\n')
).join('\n')
+'\n'+ options.advanced.children.hideHeader.value
+'\n'+ String.raw`
.tab.${globalThis.CSS.escape(classes.searching[0])} tab-label .label-content::before {
content: attr(data-tab-id) ": ";
}
`,
subPanel: {
title: manifest.name,
url: Runtime.getURL('src/content/embed.html'),
initialHeight: '35px', fixedHeight: '35px',
},
}; },
async getManifest() {
const supportsShrunkenTabs = await TST.supportsShrunkenTabs();
return {
name: manifest.name,
icons: manifest.icons,
style: [ 'miss', 'child', 'hit', 'active', 'custom', ].map(
name => options.result.children[name].children.styles.children.map(style => { try {
if (style.name === 'hide' && !supportsShrunkenTabs) {
// TST 4.0 and later does not support shrinking of tabs due to restrictions of its virtual scrolling implementation.
return '';
}
const values = Array.isArray(style.value) ? style.value : [ style.value, ];
return typeof style.model.extra.get === 'function' ? style.model.extra.get(...values) : values[0] ? style.model.extra.value : '';
} catch (error) { console.error(error); return ''; } }).join('\n')
).join('\n')
+'\n'+ options.advanced.children.hideHeader.value
+'\n'+ String.raw`
.tab.${globalThis.CSS.escape(classes.searching[0])} tab-label .label-content::before {
content: attr(data-tab-id) ": ";
}
`,
subPanel: {
title: manifest.name,
url: Runtime.getURL('src/content/embed.html'),
initialHeight: '35px', fixedHeight: '35px',
},
};
},
methods: [
'get-tree',
'scroll',
Expand All @@ -49,7 +67,7 @@ const TST = tstApi({
Object.defineProperty(TST, 'debug', { get() { return debug; }, });

TST.register().catch(() => null) // may very well not be ready yet
.then(() => TST.isRegistered && TST.methods.removeTabState({ tabs: '*', state: [ ].concat(...Object.values(classes)), }).catch(onTstError));
.then(() => TST.isRegistered && Promise.all([showAll(), TST.methods.removeTabState({ tabs: '*', state: [ ].concat(...Object.values(classes)), }).catch(onTstError),]));
options.result.onAnyChange(() => TST.register().catch(notify.error));

/**
Expand All @@ -69,17 +87,6 @@ options.result.onAnyChange(() => TST.register().catch(notify.error));

/// extension logic

const classes = {
matching: [ 'tst-search:matching', ], // applied to all tabs matching the search
hasChild: [ 'tst-search:child-matching', ], // applied to all tabs who have an "offspring" that is `matching` (potentially in addition to `matching` themselves)
hidden: /**@type{string[]}*/([ ]), // applied to `.hidden` tabs
failed: [ 'tst-search:not-matching', ], // applied to tabs that are neither `matching` nor `hasChild`
active: [ 'tst-search:active', ], // applied to a single one of the `matching` tabs
searching: [ 'tst-search:searching', ], // applied to all tabs while the search bar is focused
};
let cache = /**@type{{ tabs: Tab[] & { byId: Map<number, Tab>, all: Tab[], }, windowId: number, } | null}*/(null);
const queueClearCache = debounce(() => { cache = null; }, 30e3);

/** @typedef {{
tabId: number;
result: SearchResult;
Expand Down Expand Up @@ -159,6 +166,7 @@ async function doSearch({

// clear previous search on empty term
if (!term) {
await showAll();
TST.methods.removeTabState({ tabs: '*', state: [ ].concat(...Object.values(classes).slice(0, -1/*searching*/)), }).catch(onTstError);
return States.set(windowId, { tabId: -1, result: { windowId, inputTerm: '', matches: 0, cleared: true, }, }).result;
} term += ''; const inputTerm = term;
Expand Down Expand Up @@ -256,11 +264,21 @@ async function doSearch({
: -1;

// apply tab states
const shownTabs = [
...(result.matching || []),
...(!options.result.children.miss.children.styles.children.hide.value && result.failed || []),
];
const hiddenTabs = [
...(options.result.children.miss.children.styles.children.hide.value && result.failed || []),
...(result.hidden || []),
];
(await Promise.all([
...Object.keys(classes).map(state => !classes[state].length ? [ ] : [
TST.methods.removeTabState({ tabs: (!result[state]?.size ? tabs.all : tabs.all.filter(tab => !result[state].has(tab))).map(_=>_.id), state: classes[state], }),
result[state]?.size && TST.methods.addTabState({ tabs: Array.from(result[state], _=>_.id), state: classes[state], }),
]).flat(1),
browser.tabs.show(Array.from(shownTabs, _=>_.id)),
hiddenTabs.length > 0 && browser.tabs.hide(Array.from(hiddenTabs, _=>_.id)),
(async () => {
collapsed.length && (await Promise.all(collapsed.map(tab => TST.methods.expandTree({ tab: tab.id, }))));
scrollTo >= 0 && (await TST.methods.scroll({ tab: scrollTo, }));
Expand All @@ -273,9 +291,16 @@ async function doSearch({

} catch (error) { notify.error('Search failed!', error); return { windowId, inputTerm: '', matches: 0, failed: true, }; } }

async function startSearch() { options.search.children.searchByTabIds.value[1] && Promise.all([showAll(), TST.methods.addTabState({ tabs: '*', state: classes.searching, }).catch(onTstError),]); }
async function stopSearch() { options.search.children.searchByTabIds.value[1] && Promise.all([showAll(), TST.methods.removeTabState({ tabs: '*', state: classes.searching, }).catch(onTstError),]); }

async function startSearch() { options.search.children.searchByTabIds.value[1] && TST.methods.addTabState({ tabs: '*', state: classes.searching, }).catch(onTstError); }
async function stopSearch() { options.search.children.searchByTabIds.value[1] && TST.methods.removeTabState({ tabs: '*', state: classes.searching, }).catch(onTstError); }
async function showAll() {
const tabs = await browser.tabs.query({
hidden: true,
windowId: this?.windowId || (await Windows.getCurrent()).id,
});
await browser.tabs.show(Array.from(tabs, _=>_.id));
}


async function focusActiveTab({
Expand Down
11 changes: 8 additions & 3 deletions src/background/tst-api.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function tstAPI({ getManifest, methods = [ ], events = { __proto_
const ownName = browser.runtime.getManifest().name;

async function register() {
const tstManifest = { listeningTypes: [ ...Object.keys(events), 'wait-for-shutdown', ], ...getManifest(), };
const tstManifest = { listeningTypes: [ ...Object.keys(events), 'wait-for-shutdown', ], ...(await getManifest()), };
//API.debug && console.info(ownName +': registering with TST ...', tstManifest);
(await TST.registerSelf(tstManifest));
API.isRegistered = true;
Expand All @@ -42,8 +42,13 @@ export default function tstAPI({ getManifest, methods = [ ], events = { __proto_
API.isRegistered = false;
}

async function supportsShrunkenTabs() {
const version = await TST.getVersion();
return version === null;
}

const TST = Object.fromEntries([
'register-self', 'unregister-self', ...methods,
'register-self', 'unregister-self', 'get-version', ...methods,
].map(name => [
name.replace(/-([a-z])/g, (_, l) => l.toUpperCase()),
(options) => {
Expand All @@ -66,5 +71,5 @@ export default function tstAPI({ getManifest, methods = [ ], events = { __proto_
} }
browser.runtime.onMessageExternal.addListener(onMessageExternal);

const API = { register, unregister, isRegistered: false, methods: TST, debug, }; return API;
const API = { register, unregister, supportsShrunkenTabs, isRegistered: false, methods: TST, debug, }; return API;
}
1 change: 1 addition & 0 deletions web-ext-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ stages:
- storage
- notifications
- tabs
- tabHide
commands:
globalFocusKey:
description: 'Focus the TST Tab Search bar'
Expand Down