Skip to content

Commit 81c51f4

Browse files
authored
Add ability to customize custom selectors from settings page (#292)
* Add ability to customize custom hints from the settings page * Transform customSelectors to array for easier displaying in settings page
1 parent 6339022 commit 81c51f4

17 files changed

+510
-121
lines changed

src/background/messaging/handleRequestFromContent.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,11 @@ export async function handleRequestFromContent(
108108
return getTabMarker(tabId);
109109

110110
case "storeCustomSelectors":
111-
await storeCustomSelectors(request.pattern, request.selectors);
111+
await storeCustomSelectors(request.url, request.selectors);
112112
break;
113113

114114
case "resetCustomSelectors":
115-
return resetCustomSelectors(request.pattern);
115+
return resetCustomSelectors(request.url);
116116

117117
case "removeReference":
118118
return removeReference(request.hostPattern, request.name);

src/background/utils/arrayUtils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Function to filter an array in place
2+
export function filterInPlace<T>(
3+
array: T[],
4+
filter: (item: T, index: number) => boolean
5+
) {
6+
for (let i = array.length - 1; i >= 0; i--) {
7+
if (!filter(array[i]!, i)) {
8+
array.splice(i, 1);
9+
}
10+
}
11+
}

src/background/utils/storeCustomSelectors.ts

+30-36
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import assertNever from "assert-never";
22
import { debounce } from "lodash";
3-
import { CustomSelectorsForPattern } from "../../typings/StorageSchema";
3+
import { CustomSelector } from "../../typings/StorageSchema";
44
import { notify } from "./notify";
55
import { withLockedStorageAccess } from "./withLockedStorageValue";
6+
import { filterInPlace } from "./arrayUtils";
67

78
let notifySuccess = false;
8-
const modifiedSelectors = new Set<string>();
99
let batchUpdatePromise: Promise<void> | undefined;
1010
let batchUpdatePromiseResolve: (() => void) | undefined;
1111

@@ -37,9 +37,8 @@ const debouncedNotifyAndReset = debounce(async (action: ActionType) => {
3737

3838
await notify(message, { type });
3939

40-
// Reset the success flag and clear the set after the debounce period.
40+
// Reset the success flag after the debounce period.
4141
notifySuccess = false;
42-
modifiedSelectors.clear();
4342

4443
if (batchUpdatePromiseResolve) {
4544
batchUpdatePromiseResolve();
@@ -50,35 +49,34 @@ const debouncedNotifyAndReset = debounce(async (action: ActionType) => {
5049

5150
async function updateCustomSelectors(
5251
action: ActionType,
53-
pattern: string,
54-
selectors?: CustomSelectorsForPattern
52+
url: string,
53+
selectors?: CustomSelector[]
5554
) {
5655
const selectorsAffected = await withLockedStorageAccess(
5756
"customSelectors",
5857
async (customSelectors) => {
59-
const selectorsForPattern = customSelectors.get(pattern) ?? {
60-
include: [],
61-
exclude: [],
62-
};
63-
6458
if (action === "store") {
6559
if (!selectors) throw new Error("No selectors provided to store");
60+
customSelectors.push(...selectors);
6661

67-
selectorsForPattern.include = Array.from(
68-
new Set([...selectorsForPattern.include, ...selectors.include])
69-
);
70-
selectorsForPattern.exclude = Array.from(
71-
new Set([...selectorsForPattern.exclude, ...selectors.exclude])
72-
);
73-
customSelectors.set(pattern, selectorsForPattern);
74-
return [...selectors.include, ...selectors.exclude];
62+
return selectors;
7563
}
7664

7765
if (action === "reset") {
78-
customSelectors.delete(pattern);
79-
return selectorsForPattern
80-
? [...selectorsForPattern.include, ...selectorsForPattern.exclude]
81-
: [];
66+
const selectorsForPattern =
67+
customSelectors.filter(({ pattern }) => {
68+
const patternRe = new RegExp(pattern);
69+
return patternRe.test(url);
70+
}) ?? [];
71+
72+
// We need to filter the array in place because assigning would just
73+
// modify the argument.
74+
filterInPlace(customSelectors, ({ pattern }) => {
75+
const patternRe = new RegExp(pattern);
76+
return !patternRe.test(url);
77+
});
78+
79+
return selectorsForPattern ?? [];
8280
}
8381

8482
return assertNever(action);
@@ -88,10 +86,6 @@ async function updateCustomSelectors(
8886
// Update notifySuccess to true if any of the calls within the debounce period is successful.
8987
if (selectorsAffected.length > 0) notifySuccess = true;
9088

91-
for (const selector of selectorsAffected) {
92-
modifiedSelectors.add(selector);
93-
}
94-
9589
await debouncedNotifyAndReset(action);
9690

9791
if (batchUpdatePromise) {
@@ -105,21 +99,21 @@ async function updateCustomSelectors(
10599
}
106100

107101
/**
108-
* Stores the custom selectors for the given URL pattern. It handles being
102+
* Stores the custom selectors for the given URL. It handles being
109103
* called multiple times to handle multiple frames wanting to change the custom
110104
* selectors. It waits for a sequence of calls to finish before returning. Once
111105
* calls have stopped it notifies if storing the custom selectors was
112106
* successful, that is if any of the calls in the sequence resulted in custom
113107
* selectors being added.
114108
*
115-
* @param pattern The URL pattern where selectors apply
116-
* @param selectors An object with `include` and `exclude` CSS selectors for the given pattern
109+
* @param url The URL where selectors apply
110+
* @param selectors The selectors to store for the URL
117111
*/
118112
export async function storeCustomSelectors(
119-
pattern: string,
120-
selectors: CustomSelectorsForPattern
113+
url: string,
114+
selectors: CustomSelector[]
121115
) {
122-
await updateCustomSelectors("store", pattern, selectors);
116+
await updateCustomSelectors("store", url, selectors);
123117
}
124118

125119
/**
@@ -130,8 +124,8 @@ export async function storeCustomSelectors(
130124
* successful, that is if any of the calls in the sequence resulted in custom
131125
* selectors being removed.
132126
*
133-
* @param pattern The URL pattern where selectors apply
127+
* @param url The URL pattern where selectors apply
134128
*/
135-
export async function resetCustomSelectors(pattern: string) {
136-
await updateCustomSelectors("reset", pattern);
129+
export async function resetCustomSelectors(url: string) {
130+
await updateCustomSelectors("reset", url);
137131
}

src/common/settings.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import Color from "color";
2-
import {
3-
CustomSelectorsForPattern,
4-
StorageSchema,
5-
} from "../typings/StorageSchema";
2+
import { CustomSelector, StorageSchema } from "../typings/StorageSchema";
63

74
export const defaultSettings = {
85
hintUppercaseLetters: false,
@@ -36,7 +33,7 @@ export const defaultSettings = {
3633
uppercaseTabMarkers: true,
3734
keyboardClicking: false,
3835
keysToExclude: new Array<[string, string]>(),
39-
customSelectors: new Map<string, CustomSelectorsForPattern>(),
36+
customSelectors: new Array<CustomSelector>(),
4037
customScrollPositions: new Map<string, Map<string, number>>(),
4138
references: new Map<string, Map<string, string>>(),
4239
showWhatsNewPageOnUpdate: true,

src/common/storage.ts

+11-15
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/* eslint-disable no-await-in-loop */
22
import { Mutex } from "async-mutex";
33
import browser from "webextension-polyfill";
4-
import {
5-
CustomSelectorsForPattern,
6-
StorageSchema,
7-
zStorageSchema,
8-
} from "../typings/StorageSchema";
4+
import { StorageSchema, zStorageSchema } from "../typings/StorageSchema";
95
import { defaultStorage } from "./defaultStorage";
106
import {
117
Settings,
128
defaultSettings,
139
isSetting,
1410
isValidSetting,
1511
} from "./settings";
12+
import {
13+
prepareSettingForStoring,
14+
upgradeCustomSelectors,
15+
} from "./transformSettings";
1616

1717
const useLocalStorage = new Set<keyof StorageSchema>([
1818
"hintsToggleTabs",
@@ -56,16 +56,17 @@ export async function store<T extends keyof StorageSchema>(
5656
): Promise<StorageSchema[T]> {
5757
if (isSetting(key) && !isValidSetting(key, value)) return retrieve(key);
5858

59+
const prepared = prepareSettingForStoring(key, value);
5960
const stringified = JSON.stringify(
60-
zStorageSchema.shape[key].parse(value),
61+
zStorageSchema.shape[key].parse(prepared),
6162
replacer
6263
);
6364

6465
await (useLocalStorage.has(key)
6566
? browser.storage.local.set({ [key]: stringified })
6667
: browser.storage.sync.set({ [key]: stringified }));
6768

68-
return value;
69+
return prepared;
6970
}
7071

7172
async function parseStorageItem(key: keyof StorageSchema) {
@@ -98,17 +99,12 @@ async function parseStorageItem(key: keyof StorageSchema) {
9899
async function initStorageItem<T extends keyof StorageSchema>(key: T) {
99100
const item = await parseStorageItem(key);
100101

101-
// Handle customSelectors type conversion from an object to a Map. This is
102-
// only necessary temporarily in order not to lose user's customizations.
103-
// Introduced in v0.5.0.
104-
if (item && key === "customSelectors") {
102+
if (key === "customSelectors") {
105103
try {
106-
const normalized = new Map<string, CustomSelectorsForPattern>(
107-
Object.entries(item as Record<string, CustomSelectorsForPattern>)
108-
) as StorageSchema[T];
104+
const upgraded = upgradeCustomSelectors(item);
109105

110106
const parsed = zStorageSchema.shape[key].parse(
111-
normalized
107+
upgraded
112108
) as StorageSchema[T];
113109
return await store(key, parsed);
114110
} catch {

src/common/transformSettings.test.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { upgradeCustomSelectors } from "./transformSettings";
2+
3+
describe("upgradeCustomSelectors", () => {
4+
test("upgrades custom selectors from plain object format", () => {
5+
const value = {
6+
pattern1: { include: ["selector1", "selector2"], exclude: ["selector3"] },
7+
pattern2: { include: ["selector4"], exclude: ["selector5", "selector6"] },
8+
};
9+
10+
const upgraded = upgradeCustomSelectors(value);
11+
12+
expect(upgraded).toEqual([
13+
{ pattern: "pattern1", type: "include", selector: "selector1" },
14+
{ pattern: "pattern1", type: "include", selector: "selector2" },
15+
{ pattern: "pattern1", type: "exclude", selector: "selector3" },
16+
{ pattern: "pattern2", type: "include", selector: "selector4" },
17+
{ pattern: "pattern2", type: "exclude", selector: "selector5" },
18+
{ pattern: "pattern2", type: "exclude", selector: "selector6" },
19+
]);
20+
});
21+
22+
test("upgrades custom selectors from Map format", () => {
23+
const value = new Map([
24+
[
25+
"pattern1",
26+
{ include: ["selector1", "selector2"], exclude: ["selector3"] },
27+
],
28+
[
29+
"pattern2",
30+
{ include: ["selector4"], exclude: ["selector5", "selector6"] },
31+
],
32+
]);
33+
34+
const upgraded = upgradeCustomSelectors(value);
35+
36+
expect(upgraded).toEqual([
37+
{ pattern: "pattern1", type: "include", selector: "selector1" },
38+
{ pattern: "pattern1", type: "include", selector: "selector2" },
39+
{ pattern: "pattern1", type: "exclude", selector: "selector3" },
40+
{ pattern: "pattern2", type: "include", selector: "selector4" },
41+
{ pattern: "pattern2", type: "exclude", selector: "selector5" },
42+
{ pattern: "pattern2", type: "exclude", selector: "selector6" },
43+
]);
44+
});
45+
46+
test("returns original value if not upgradable", () => {
47+
const value = "invalid value";
48+
49+
const upgraded = upgradeCustomSelectors(value);
50+
51+
expect(upgraded).toBe(value);
52+
});
53+
});

0 commit comments

Comments
 (0)