@arraypress/extension-kit 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -19
- package/package.json +1 -1
- package/src/build/vendor.js +1 -1
- package/src/dom/i18n.d.ts +11 -3
- package/src/dom/i18n.js +17 -4
- package/src/dom/setup.d.ts +4 -3
- package/src/dom/setup.js +3 -2
- package/src/index.d.ts +8 -3
- package/src/index.js +8 -3
- package/src/sites/sites.d.ts +33 -1
- package/src/sites/sites.js +42 -1
- package/src/testing/browser.js +1 -1
- package/src/testing/e2e.js +1 -1
- package/src/testing/index.d.ts +0 -1
- package/src/testing/index.js +0 -1
- package/src/{testing → time}/time.d.ts +7 -6
- package/src/{testing → time}/time.js +7 -6
- package/src/ui/site-list.d.ts +23 -6
- package/src/ui/site-list.js +46 -6
- package/src/ui/text-dialog.d.ts +107 -37
- package/src/ui/text-dialog.js +167 -72
package/src/ui/site-list.d.ts
CHANGED
|
@@ -90,6 +90,14 @@ export type SiteListWords = {
|
|
|
90
90
|
*/
|
|
91
91
|
/** @type {Readonly<SiteListWords>} */
|
|
92
92
|
export declare const SITE_LIST_WORDS: Readonly<SiteListWords>;
|
|
93
|
+
/**
|
|
94
|
+
* The message key each word is read from when a page passes its `t`, so
|
|
95
|
+
* every extension's `_locales` names them alike. `removeLabel` and
|
|
96
|
+
* `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
|
|
97
|
+
*
|
|
98
|
+
* @type {Readonly<Record<keyof SiteListWords, string>>}
|
|
99
|
+
*/
|
|
100
|
+
export declare const SITE_LIST_MESSAGES: Readonly<Record<keyof SiteListWords, string>>;
|
|
93
101
|
export type SiteListOptions = {
|
|
94
102
|
/**
|
|
95
103
|
* The `<ul class="list">` to fill.
|
|
@@ -140,7 +148,13 @@ export type SiteListOptions = {
|
|
|
140
148
|
*/
|
|
141
149
|
maxLength?: number;
|
|
142
150
|
/**
|
|
143
|
-
*
|
|
151
|
+
* The page's message lookup
|
|
152
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
153
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
154
|
+
*/
|
|
155
|
+
t?: (key: string, substitutions?: string | string[]) => string;
|
|
156
|
+
/**
|
|
157
|
+
* Words that differ from those, by name.
|
|
144
158
|
*/
|
|
145
159
|
words?: Partial<SiteListWords>;
|
|
146
160
|
};
|
|
@@ -170,7 +184,10 @@ export type SiteList = {
|
|
|
170
184
|
* @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
|
|
171
185
|
* @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
|
|
172
186
|
* @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
173
|
-
* @property {
|
|
187
|
+
* @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
|
|
188
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
189
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
190
|
+
* @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
|
|
174
191
|
*/
|
|
175
192
|
/**
|
|
176
193
|
* A site list.
|
|
@@ -190,10 +207,10 @@ export type SiteList = {
|
|
|
190
207
|
* const never = createSiteList({
|
|
191
208
|
* list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
|
|
192
209
|
* parse: siteHost,
|
|
193
|
-
* add: (host) => save({ neverSites:
|
|
194
|
-
* remove: (host) => save({ neverSites: settings.neverSites
|
|
195
|
-
*
|
|
210
|
+
* add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
|
|
211
|
+
* remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
|
|
212
|
+
* t, // its words from optionsAdd, optionsRemove, …
|
|
196
213
|
* });
|
|
197
214
|
* never.render(settings.neverSites);
|
|
198
215
|
*/
|
|
199
|
-
export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, words, }: SiteListOptions): SiteList;
|
|
216
|
+
export declare function createSiteList({ list, empty, form, addButton, parse, add, remove, replace, invalid, label, maxLength, t, words, }: SiteListOptions): SiteList;
|
package/src/ui/site-list.js
CHANGED
|
@@ -63,6 +63,43 @@ export const SITE_LIST_WORDS = Object.freeze({
|
|
|
63
63
|
moreActions: (/** @type {string} */ label) => `More actions for “${label}”`,
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The message key each word is read from when a page passes its `t`, so
|
|
68
|
+
* every extension's `_locales` names them alike. `removeLabel` and
|
|
69
|
+
* `moreActions` take the site as `$1` (`"More actions for $SITE$"`).
|
|
70
|
+
*
|
|
71
|
+
* @type {Readonly<Record<keyof SiteListWords, string>>}
|
|
72
|
+
*/
|
|
73
|
+
export const SITE_LIST_MESSAGES = Object.freeze({
|
|
74
|
+
add: 'optionsAdd',
|
|
75
|
+
addSite: 'optionsAddSite',
|
|
76
|
+
editSite: 'optionsEditSite',
|
|
77
|
+
edit: 'optionsEdit',
|
|
78
|
+
remove: 'optionsRemove',
|
|
79
|
+
save: 'optionsSave',
|
|
80
|
+
cancel: 'optionsCancel',
|
|
81
|
+
site: 'optionsSiteLabel',
|
|
82
|
+
invalid: 'optionsSiteInvalid',
|
|
83
|
+
removeLabel: 'optionsRemoveLabel',
|
|
84
|
+
moreActions: 'optionsMoreActions',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The list's words from a page's messages, under {@link SITE_LIST_MESSAGES}.
|
|
89
|
+
*
|
|
90
|
+
* @param {(key: string, substitutions?: string | string[]) => string} t - The page's message lookup.
|
|
91
|
+
* @returns {SiteListWords} The words.
|
|
92
|
+
*/
|
|
93
|
+
function translatedWords(t) {
|
|
94
|
+
const m = SITE_LIST_MESSAGES;
|
|
95
|
+
return {
|
|
96
|
+
add: t(m.add), addSite: t(m.addSite), editSite: t(m.editSite), edit: t(m.edit), remove: t(m.remove),
|
|
97
|
+
save: t(m.save), cancel: t(m.cancel), site: t(m.site), invalid: t(m.invalid),
|
|
98
|
+
removeLabel: (label) => t(m.removeLabel, [label]),
|
|
99
|
+
moreActions: (label) => t(m.moreActions, [label]),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
/**
|
|
67
104
|
* What {@link createSiteList} takes: the list, one way to add to it, and
|
|
68
105
|
* what adding, removing and editing do.
|
|
@@ -83,7 +120,10 @@ export const SITE_LIST_WORDS = Object.freeze({
|
|
|
83
120
|
* @property {(text: string) => void} [invalid] The add row: called with what was typed when it isn't a site.
|
|
84
121
|
* @property {(site: string) => string} [label] How a site reads. Default {@link siteLabel}.
|
|
85
122
|
* @property {number} [maxLength] The dialog field's `maxlength`. Default {@link MAX_HOST_LENGTH}.
|
|
86
|
-
* @property {
|
|
123
|
+
* @property {(key: string, substitutions?: string | string[]) => string} [t] The page's message lookup
|
|
124
|
+
* (`setUpPage`'s): the words are then its messages under {@link SITE_LIST_MESSAGES}. Without it,
|
|
125
|
+
* Chrome's English ({@link SITE_LIST_WORDS}).
|
|
126
|
+
* @property {Partial<SiteListWords>} [words] Words that differ from those, by name.
|
|
87
127
|
*/
|
|
88
128
|
|
|
89
129
|
/**
|
|
@@ -105,17 +145,17 @@ export const SITE_LIST_WORDS = Object.freeze({
|
|
|
105
145
|
* const never = createSiteList({
|
|
106
146
|
* list: byId('never-sites'), form: byId('add-never', HTMLFormElement), empty: byId('no-sites'),
|
|
107
147
|
* parse: siteHost,
|
|
108
|
-
* add: (host) => save({ neverSites:
|
|
109
|
-
* remove: (host) => save({ neverSites: settings.neverSites
|
|
110
|
-
*
|
|
148
|
+
* add: (host) => save({ neverSites: withSite(settings.neverSites, host) }),
|
|
149
|
+
* remove: (host) => save({ neverSites: withoutSite(settings.neverSites, host) }),
|
|
150
|
+
* t, // its words from optionsAdd, optionsRemove, …
|
|
111
151
|
* });
|
|
112
152
|
* never.render(settings.neverSites);
|
|
113
153
|
*/
|
|
114
154
|
export function createSiteList({
|
|
115
|
-
list, empty, form, addButton, parse, add, remove, replace, invalid, label = siteLabel, maxLength = MAX_HOST_LENGTH, words,
|
|
155
|
+
list, empty, form, addButton, parse, add, remove, replace, invalid, label = siteLabel, maxLength = MAX_HOST_LENGTH, t, words,
|
|
116
156
|
}) {
|
|
117
157
|
const doc = list.ownerDocument;
|
|
118
|
-
const w = { ...SITE_LIST_WORDS, ...words };
|
|
158
|
+
const w = { ...(t ? translatedWords(t) : SITE_LIST_WORDS), ...words };
|
|
119
159
|
const menu = addButton ? createMenu({ doc }) : null;
|
|
120
160
|
|
|
121
161
|
/**
|
package/src/ui/text-dialog.d.ts
CHANGED
|
@@ -1,43 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chrome's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Chrome's text dialog (cr-dialog with cr-inputs): a title, labelled fields,
|
|
3
|
+
* Cancel and an action button. One field, as chrome://bookmarks asks for a
|
|
4
|
+
* folder's name (`askText`); several, as chrome://settings/searchEngines asks
|
|
5
|
+
* for a site search (`askFields`). The action button stays disabled while a
|
|
6
|
+
* field is blank or isn't valid, with the reason under that field. Enter
|
|
7
|
+
* means the action button, Esc and Cancel cancel, and the focus goes back to
|
|
8
|
+
* what opened it. The page needs no markup: the dialog (`#text-dialog`) is
|
|
9
|
+
* made the first time, its fields each time it's asked.
|
|
8
10
|
*
|
|
9
11
|
* @module ui/text-dialog
|
|
10
12
|
*/
|
|
11
|
-
export type
|
|
13
|
+
export type FieldRequest = {
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
15
|
+
* Its key in the answer ("folder"), and part of its id.
|
|
14
16
|
*/
|
|
15
|
-
|
|
17
|
+
name: string;
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
19
|
+
* Its label ("Folder").
|
|
18
20
|
*/
|
|
19
21
|
label: string;
|
|
20
22
|
/**
|
|
21
|
-
* The text to start with
|
|
23
|
+
* The text to start with. Default ''.
|
|
22
24
|
*/
|
|
23
25
|
value?: string;
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
26
|
-
*/
|
|
27
|
-
action?: string;
|
|
28
|
-
/**
|
|
29
|
-
* The cancel button's text. Default "Cancel".
|
|
27
|
+
* Shown while it's empty. Default none.
|
|
30
28
|
*/
|
|
31
|
-
|
|
29
|
+
placeholder?: string;
|
|
32
30
|
/**
|
|
33
31
|
* Why the (non-blank) text can't be used,
|
|
34
32
|
* shown under the field; null when it can. Default: anything but blank.
|
|
35
33
|
*/
|
|
36
34
|
validate?: (text: string) => string | null;
|
|
37
35
|
/**
|
|
38
|
-
*
|
|
36
|
+
* Its `maxlength`. Default none.
|
|
39
37
|
*/
|
|
40
38
|
maxLength?: number;
|
|
39
|
+
};
|
|
40
|
+
export type DialogRequest = {
|
|
41
|
+
/**
|
|
42
|
+
* The dialog's title ("Rename folder").
|
|
43
|
+
*/
|
|
44
|
+
title: string;
|
|
45
|
+
/**
|
|
46
|
+
* The action button's text. Default "Save".
|
|
47
|
+
*/
|
|
48
|
+
action?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The cancel button's text. Default "Cancel".
|
|
51
|
+
*/
|
|
52
|
+
cancel?: string;
|
|
41
53
|
/**
|
|
42
54
|
* Where the focus
|
|
43
55
|
* goes when it closes, or a function that says (asked then, so it can pick a row drawn since).
|
|
@@ -49,23 +61,44 @@ export type TextRequest = {
|
|
|
49
61
|
*/
|
|
50
62
|
doc?: Document;
|
|
51
63
|
};
|
|
64
|
+
export type TextRequest = DialogRequest & Omit<FieldRequest, 'name'>;
|
|
65
|
+
export type FieldsRequest = DialogRequest & {
|
|
66
|
+
fields: FieldRequest[];
|
|
67
|
+
};
|
|
52
68
|
/**
|
|
53
|
-
*
|
|
69
|
+
* One field of the dialog.
|
|
54
70
|
*
|
|
55
|
-
* @typedef {object}
|
|
71
|
+
* @typedef {object} FieldRequest
|
|
72
|
+
* @property {string} name Its key in the answer ("folder"), and part of its id.
|
|
73
|
+
* @property {string} label Its label ("Folder").
|
|
74
|
+
* @property {string} [value] The text to start with. Default ''.
|
|
75
|
+
* @property {string} [placeholder] Shown while it's empty. Default none.
|
|
76
|
+
* @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
|
|
77
|
+
* shown under the field; null when it can. Default: anything but blank.
|
|
78
|
+
* @property {number} [maxLength] Its `maxlength`. Default none.
|
|
79
|
+
*/
|
|
80
|
+
/**
|
|
81
|
+
* What the dialog asks, whatever its fields.
|
|
82
|
+
*
|
|
83
|
+
* @typedef {object} DialogRequest
|
|
56
84
|
* @property {string} title The dialog's title ("Rename folder").
|
|
57
|
-
* @property {string} label The field's label ("Name").
|
|
58
|
-
* @property {string} [value] The text to start with (selected). Default ''.
|
|
59
85
|
* @property {string} [action] The action button's text. Default "Save".
|
|
60
86
|
* @property {string} [cancel] The cancel button's text. Default "Cancel".
|
|
61
|
-
* @property {(text: string) => string | null} [validate] Why the (non-blank) text can't be used,
|
|
62
|
-
* shown under the field; null when it can. Default: anything but blank.
|
|
63
|
-
* @property {number} [maxLength] The field's `maxlength`. Default none.
|
|
64
87
|
* @property {HTMLElement | (() => HTMLElement | null | undefined) | null} [returnTo] Where the focus
|
|
65
88
|
* goes when it closes, or a function that says (asked then, so it can pick a row drawn since).
|
|
66
89
|
* Default what had the focus when it opened.
|
|
67
90
|
* @property {Document} [doc] The page. Default `document`.
|
|
68
91
|
*/
|
|
92
|
+
/**
|
|
93
|
+
* What the one-field dialog asks.
|
|
94
|
+
*
|
|
95
|
+
* @typedef {DialogRequest & Omit<FieldRequest, 'name'>} TextRequest
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* What the dialog with several fields asks.
|
|
99
|
+
*
|
|
100
|
+
* @typedef {DialogRequest & {fields: FieldRequest[]}} FieldsRequest
|
|
101
|
+
*/
|
|
69
102
|
/**
|
|
70
103
|
* Whether a text can be used, and the reason to show when it can't. Pure.
|
|
71
104
|
*
|
|
@@ -81,6 +114,20 @@ export declare function checkText(text: string, validate?: (text: string) => str
|
|
|
81
114
|
ok: boolean;
|
|
82
115
|
reason: string;
|
|
83
116
|
};
|
|
117
|
+
/**
|
|
118
|
+
* A field's element ids. A one-field dialog's are `text-dialog-input` and `text-dialog-error`;
|
|
119
|
+
* with several, each is named: `text-dialog-<name>` and `text-dialog-<name>-error`. Pure.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} name - The field's name.
|
|
122
|
+
* @param {boolean} only - Whether it's the dialog's only field.
|
|
123
|
+
* @returns {{input: string, error: string}} The ids.
|
|
124
|
+
* @example
|
|
125
|
+
* fieldIds('folder', false); // → {input: 'text-dialog-folder', error: 'text-dialog-folder-error'}
|
|
126
|
+
*/
|
|
127
|
+
export declare function fieldIds(name: string, only: boolean): {
|
|
128
|
+
input: string;
|
|
129
|
+
error: string;
|
|
130
|
+
};
|
|
84
131
|
export type Parts = {
|
|
85
132
|
/**
|
|
86
133
|
* The `<dialog>`.
|
|
@@ -95,17 +142,9 @@ export type Parts = {
|
|
|
95
142
|
*/
|
|
96
143
|
title: HTMLElement;
|
|
97
144
|
/**
|
|
98
|
-
*
|
|
99
|
-
*/
|
|
100
|
-
label: HTMLLabelElement;
|
|
101
|
-
/**
|
|
102
|
-
* The field.
|
|
103
|
-
*/
|
|
104
|
-
input: HTMLInputElement;
|
|
105
|
-
/**
|
|
106
|
-
* The reason under the field.
|
|
145
|
+
* Where the fields go.
|
|
107
146
|
*/
|
|
108
|
-
|
|
147
|
+
body: HTMLElement;
|
|
109
148
|
/**
|
|
110
149
|
* The cancel button.
|
|
111
150
|
*/
|
|
@@ -115,8 +154,39 @@ export type Parts = {
|
|
|
115
154
|
*/
|
|
116
155
|
confirm: HTMLButtonElement;
|
|
117
156
|
};
|
|
157
|
+
export type Field = {
|
|
158
|
+
/**
|
|
159
|
+
* What was asked.
|
|
160
|
+
*/
|
|
161
|
+
request: FieldRequest;
|
|
162
|
+
/**
|
|
163
|
+
* Its input.
|
|
164
|
+
*/
|
|
165
|
+
input: HTMLInputElement;
|
|
166
|
+
/**
|
|
167
|
+
* The reason under it.
|
|
168
|
+
*/
|
|
169
|
+
error: HTMLElement;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Ask for several pieces of text in the page's dialog, one field each.
|
|
173
|
+
*
|
|
174
|
+
* @param {FieldsRequest} request - What to ask.
|
|
175
|
+
* @returns {Promise<Record<string, string> | null>} Each field's text by its name, trimmed; null
|
|
176
|
+
* when cancelled.
|
|
177
|
+
* @example
|
|
178
|
+
* const rule = await askFields({
|
|
179
|
+
* title: 'Add folder rule', action: 'Add',
|
|
180
|
+
* fields: [
|
|
181
|
+
* {name: 'from', label: 'Downloads from', placeholder: 'example.com or .pdf', validate: checkFrom},
|
|
182
|
+
* {name: 'folder', label: 'Folder', placeholder: 'Receipts'},
|
|
183
|
+
* ],
|
|
184
|
+
* });
|
|
185
|
+
* if (rule) addRule(rule.from, rule.folder);
|
|
186
|
+
*/
|
|
187
|
+
export declare function askFields({ title, fields: requests, action, cancel, returnTo, doc, }: FieldsRequest): Promise<Record<string, string> | null>;
|
|
118
188
|
/**
|
|
119
|
-
* Ask for a piece of text in the page's one
|
|
189
|
+
* Ask for a piece of text in the page's dialog, with one field.
|
|
120
190
|
*
|
|
121
191
|
* @param {TextRequest} request - What to ask.
|
|
122
192
|
* @returns {Promise<string | null>} The text, trimmed; null when cancelled.
|
|
@@ -129,4 +199,4 @@ export type Parts = {
|
|
|
129
199
|
* validate: (text) => (siteFromInput(text) ? null : 'Enter a site, like example.com'),
|
|
130
200
|
* });
|
|
131
201
|
*/
|
|
132
|
-
export declare function askText({
|
|
202
|
+
export declare function askText({ label, value, placeholder, validate, maxLength, ...dialog }: TextRequest): Promise<string | null>;
|