@openpicker/sdk 0.1.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/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/index.cjs +179 -0
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +175 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Usertour
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @openpicker/sdk
|
|
2
|
+
|
|
3
|
+
SDK for invoking the [openpicker](https://github.com/usertour/openpicker) browser extension to
|
|
4
|
+
pick a CSS selector on any page.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @openpicker/sdk
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createOpenpicker, OpenpickerError } from "@openpicker/sdk"
|
|
12
|
+
|
|
13
|
+
const op = createOpenpicker({ appName: "My App" })
|
|
14
|
+
|
|
15
|
+
if (await op.isAvailable()) {
|
|
16
|
+
// `url` is required: the extension opens it in a tab, the user picks there,
|
|
17
|
+
// and the selector is routed back here.
|
|
18
|
+
const { selector, matchCount, element } = await op.pick({
|
|
19
|
+
url: "https://app.example.com",
|
|
20
|
+
screenshot: "element", // optional: "none" | "element" | "viewport"
|
|
21
|
+
})
|
|
22
|
+
console.log(selector, matchCount, element)
|
|
23
|
+
} else {
|
|
24
|
+
// Prompt the user to install the openpicker extension.
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See the [protocol spec](https://github.com/usertour/openpicker/blob/main/PROTOCOL.md) for the
|
|
29
|
+
wire format, and the [main README](https://github.com/usertour/openpicker) for the full API.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ../protocol/src/constants.ts
|
|
4
|
+
var CHANNEL = "openpicker";
|
|
5
|
+
var PROTOCOL_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
// ../protocol/src/envelope.ts
|
|
8
|
+
function isEnvelope(value) {
|
|
9
|
+
if (typeof value !== "object" || value === null) return false;
|
|
10
|
+
const v = value;
|
|
11
|
+
return v.channel === CHANNEL && typeof v.kind === "string";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/index.ts
|
|
15
|
+
var OpenpickerError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
data;
|
|
18
|
+
constructor(error) {
|
|
19
|
+
super(error.message);
|
|
20
|
+
this.name = "OpenpickerError";
|
|
21
|
+
this.code = error.code;
|
|
22
|
+
this.data = error.data;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
26
|
+
function randomId(length) {
|
|
27
|
+
const bytes = new Uint8Array(length);
|
|
28
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
29
|
+
let out = "";
|
|
30
|
+
for (const byte of bytes) {
|
|
31
|
+
out += ID_ALPHABET[byte % ID_ALPHABET.length];
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
var Openpicker = class {
|
|
36
|
+
options;
|
|
37
|
+
win;
|
|
38
|
+
instanceId = randomId(6);
|
|
39
|
+
pending = /* @__PURE__ */ new Map();
|
|
40
|
+
seq = 0;
|
|
41
|
+
listening = false;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.win = options.targetWindow ?? window;
|
|
45
|
+
}
|
|
46
|
+
onMessage = (event) => {
|
|
47
|
+
if (event.source !== this.win) return;
|
|
48
|
+
if (event.origin !== this.win.origin) return;
|
|
49
|
+
const data = event.data;
|
|
50
|
+
if (!isEnvelope(data) || data.kind !== "res") return;
|
|
51
|
+
const entry = this.pending.get(data.id);
|
|
52
|
+
if (!entry) return;
|
|
53
|
+
this.pending.delete(data.id);
|
|
54
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
55
|
+
if (data.ok) {
|
|
56
|
+
entry.resolve(data.result);
|
|
57
|
+
} else {
|
|
58
|
+
entry.reject(
|
|
59
|
+
new OpenpickerError(
|
|
60
|
+
data.error ?? { code: "internal_error", message: "openpicker: malformed error response" }
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
ensureListening() {
|
|
66
|
+
if (this.listening) return;
|
|
67
|
+
this.win.addEventListener("message", this.onMessage);
|
|
68
|
+
this.listening = true;
|
|
69
|
+
}
|
|
70
|
+
request(method, params, timeout) {
|
|
71
|
+
this.ensureListening();
|
|
72
|
+
const id = `op:${this.instanceId}:${++this.seq}`;
|
|
73
|
+
const envelope = {
|
|
74
|
+
channel: CHANNEL,
|
|
75
|
+
v: PROTOCOL_VERSION,
|
|
76
|
+
kind: "req",
|
|
77
|
+
id,
|
|
78
|
+
method,
|
|
79
|
+
params
|
|
80
|
+
};
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
let timer;
|
|
83
|
+
if (timeout > 0) {
|
|
84
|
+
timer = setTimeout(() => {
|
|
85
|
+
this.pending.delete(id);
|
|
86
|
+
reject(
|
|
87
|
+
new OpenpickerError({
|
|
88
|
+
code: "timeout",
|
|
89
|
+
message: `openpicker: "${method}" timed out after ${timeout}ms`
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
}, timeout);
|
|
93
|
+
}
|
|
94
|
+
this.pending.set(id, {
|
|
95
|
+
resolve,
|
|
96
|
+
reject,
|
|
97
|
+
timer
|
|
98
|
+
});
|
|
99
|
+
this.win.postMessage(envelope, this.win.origin);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Probe the extension and negotiate version/capabilities. */
|
|
103
|
+
async ping() {
|
|
104
|
+
try {
|
|
105
|
+
return await this.request(
|
|
106
|
+
"ping",
|
|
107
|
+
{ appName: this.options.appName },
|
|
108
|
+
this.options.pingTimeout ?? 1500
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof OpenpickerError && error.code === "timeout") {
|
|
112
|
+
throw new OpenpickerError({
|
|
113
|
+
code: "extension_not_installed",
|
|
114
|
+
message: "openpicker: the extension is not installed or did not respond"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Convenience: resolve `true` if the extension responds to a ping. */
|
|
121
|
+
async isAvailable() {
|
|
122
|
+
try {
|
|
123
|
+
await this.ping();
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Open `params.url` in a tab and start element selection there; resolves when the
|
|
131
|
+
* user confirms (OK) in the sidebar. `url` is required — the extension picks
|
|
132
|
+
* across the tab/origin boundary, which is the thing a page can't do for itself.
|
|
133
|
+
*/
|
|
134
|
+
pick(params) {
|
|
135
|
+
return this.request("pick", { appName: this.options.appName, ...params }, 0);
|
|
136
|
+
}
|
|
137
|
+
/** Cancel an in-flight pick. */
|
|
138
|
+
async cancel() {
|
|
139
|
+
await this.request("cancel", {}, this.options.defaultTimeout ?? 3e3);
|
|
140
|
+
}
|
|
141
|
+
/** Highlight element(s) matching a selector without entering pick mode. */
|
|
142
|
+
highlight(selector) {
|
|
143
|
+
return this.request("highlight", { selector }, this.options.defaultTimeout ?? 3e3);
|
|
144
|
+
}
|
|
145
|
+
/** Remove any active highlight. */
|
|
146
|
+
async clearHighlight() {
|
|
147
|
+
await this.request("clearHighlight", {}, this.options.defaultTimeout ?? 3e3);
|
|
148
|
+
}
|
|
149
|
+
/** Bring the calling tab to the foreground (a tab can only focus itself). */
|
|
150
|
+
async activateSelf() {
|
|
151
|
+
await this.request("activateSelf", {}, this.options.defaultTimeout ?? 3e3);
|
|
152
|
+
}
|
|
153
|
+
/** Whether the cross-tab target tab opened by this tab is still open. */
|
|
154
|
+
async isTargetOpen() {
|
|
155
|
+
const { open } = await this.request("isTargetOpen", {}, this.options.defaultTimeout ?? 3e3);
|
|
156
|
+
return open;
|
|
157
|
+
}
|
|
158
|
+
/** Stop listening and reject any in-flight requests. */
|
|
159
|
+
destroy() {
|
|
160
|
+
if (this.listening) {
|
|
161
|
+
this.win.removeEventListener("message", this.onMessage);
|
|
162
|
+
this.listening = false;
|
|
163
|
+
}
|
|
164
|
+
for (const entry of this.pending.values()) {
|
|
165
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
166
|
+
entry.reject(
|
|
167
|
+
new OpenpickerError({ code: "internal_error", message: "openpicker: instance destroyed" })
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
this.pending.clear();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function createOpenpicker(options) {
|
|
174
|
+
return new Openpicker(options);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
exports.Openpicker = Openpicker;
|
|
178
|
+
exports.OpenpickerError = OpenpickerError;
|
|
179
|
+
exports.createOpenpicker = createOpenpicker;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PingResult, PickParams, PickResult, HighlightResult, ProtocolError } from '@openpicker/protocol';
|
|
2
|
+
export { HighlightResult, PickParams, PickResult, PickedElement, PingResult, ProtocolError, ScreenshotMode } from '@openpicker/protocol';
|
|
3
|
+
|
|
4
|
+
/** Error thrown by the SDK; carries a stable {@link ProtocolError.code}. */
|
|
5
|
+
declare class OpenpickerError extends Error {
|
|
6
|
+
readonly code: ProtocolError["code"];
|
|
7
|
+
readonly data?: unknown;
|
|
8
|
+
constructor(error: ProtocolError);
|
|
9
|
+
}
|
|
10
|
+
interface OpenpickerOptions {
|
|
11
|
+
/** Display name shown in the extension's consent prompt (never trusted for auth). */
|
|
12
|
+
appName?: string;
|
|
13
|
+
/** Timeout for `ping` before assuming the extension is not installed. Default 1500ms. */
|
|
14
|
+
pingTimeout?: number;
|
|
15
|
+
/** Timeout for quick operations (cancel/highlight/clearHighlight). Default 3000ms. */
|
|
16
|
+
defaultTimeout?: number;
|
|
17
|
+
/** Window to communicate over. Defaults to the global `window`. */
|
|
18
|
+
targetWindow?: Window;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A handle to the openpicker browser extension. Construct one per integration via
|
|
22
|
+
* {@link createOpenpicker}; call {@link Openpicker.destroy} when done.
|
|
23
|
+
*/
|
|
24
|
+
declare class Openpicker {
|
|
25
|
+
private readonly options;
|
|
26
|
+
private readonly win;
|
|
27
|
+
private readonly instanceId;
|
|
28
|
+
private readonly pending;
|
|
29
|
+
private seq;
|
|
30
|
+
private listening;
|
|
31
|
+
constructor(options?: OpenpickerOptions);
|
|
32
|
+
private onMessage;
|
|
33
|
+
private ensureListening;
|
|
34
|
+
private request;
|
|
35
|
+
/** Probe the extension and negotiate version/capabilities. */
|
|
36
|
+
ping(): Promise<PingResult>;
|
|
37
|
+
/** Convenience: resolve `true` if the extension responds to a ping. */
|
|
38
|
+
isAvailable(): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Open `params.url` in a tab and start element selection there; resolves when the
|
|
41
|
+
* user confirms (OK) in the sidebar. `url` is required — the extension picks
|
|
42
|
+
* across the tab/origin boundary, which is the thing a page can't do for itself.
|
|
43
|
+
*/
|
|
44
|
+
pick(params: PickParams): Promise<PickResult>;
|
|
45
|
+
/** Cancel an in-flight pick. */
|
|
46
|
+
cancel(): Promise<void>;
|
|
47
|
+
/** Highlight element(s) matching a selector without entering pick mode. */
|
|
48
|
+
highlight(selector: string): Promise<HighlightResult>;
|
|
49
|
+
/** Remove any active highlight. */
|
|
50
|
+
clearHighlight(): Promise<void>;
|
|
51
|
+
/** Bring the calling tab to the foreground (a tab can only focus itself). */
|
|
52
|
+
activateSelf(): Promise<void>;
|
|
53
|
+
/** Whether the cross-tab target tab opened by this tab is still open. */
|
|
54
|
+
isTargetOpen(): Promise<boolean>;
|
|
55
|
+
/** Stop listening and reject any in-flight requests. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Create an {@link Openpicker} handle. */
|
|
59
|
+
declare function createOpenpicker(options?: OpenpickerOptions): Openpicker;
|
|
60
|
+
|
|
61
|
+
export { Openpicker, OpenpickerError, type OpenpickerOptions, createOpenpicker };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PingResult, PickParams, PickResult, HighlightResult, ProtocolError } from '@openpicker/protocol';
|
|
2
|
+
export { HighlightResult, PickParams, PickResult, PickedElement, PingResult, ProtocolError, ScreenshotMode } from '@openpicker/protocol';
|
|
3
|
+
|
|
4
|
+
/** Error thrown by the SDK; carries a stable {@link ProtocolError.code}. */
|
|
5
|
+
declare class OpenpickerError extends Error {
|
|
6
|
+
readonly code: ProtocolError["code"];
|
|
7
|
+
readonly data?: unknown;
|
|
8
|
+
constructor(error: ProtocolError);
|
|
9
|
+
}
|
|
10
|
+
interface OpenpickerOptions {
|
|
11
|
+
/** Display name shown in the extension's consent prompt (never trusted for auth). */
|
|
12
|
+
appName?: string;
|
|
13
|
+
/** Timeout for `ping` before assuming the extension is not installed. Default 1500ms. */
|
|
14
|
+
pingTimeout?: number;
|
|
15
|
+
/** Timeout for quick operations (cancel/highlight/clearHighlight). Default 3000ms. */
|
|
16
|
+
defaultTimeout?: number;
|
|
17
|
+
/** Window to communicate over. Defaults to the global `window`. */
|
|
18
|
+
targetWindow?: Window;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A handle to the openpicker browser extension. Construct one per integration via
|
|
22
|
+
* {@link createOpenpicker}; call {@link Openpicker.destroy} when done.
|
|
23
|
+
*/
|
|
24
|
+
declare class Openpicker {
|
|
25
|
+
private readonly options;
|
|
26
|
+
private readonly win;
|
|
27
|
+
private readonly instanceId;
|
|
28
|
+
private readonly pending;
|
|
29
|
+
private seq;
|
|
30
|
+
private listening;
|
|
31
|
+
constructor(options?: OpenpickerOptions);
|
|
32
|
+
private onMessage;
|
|
33
|
+
private ensureListening;
|
|
34
|
+
private request;
|
|
35
|
+
/** Probe the extension and negotiate version/capabilities. */
|
|
36
|
+
ping(): Promise<PingResult>;
|
|
37
|
+
/** Convenience: resolve `true` if the extension responds to a ping. */
|
|
38
|
+
isAvailable(): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Open `params.url` in a tab and start element selection there; resolves when the
|
|
41
|
+
* user confirms (OK) in the sidebar. `url` is required — the extension picks
|
|
42
|
+
* across the tab/origin boundary, which is the thing a page can't do for itself.
|
|
43
|
+
*/
|
|
44
|
+
pick(params: PickParams): Promise<PickResult>;
|
|
45
|
+
/** Cancel an in-flight pick. */
|
|
46
|
+
cancel(): Promise<void>;
|
|
47
|
+
/** Highlight element(s) matching a selector without entering pick mode. */
|
|
48
|
+
highlight(selector: string): Promise<HighlightResult>;
|
|
49
|
+
/** Remove any active highlight. */
|
|
50
|
+
clearHighlight(): Promise<void>;
|
|
51
|
+
/** Bring the calling tab to the foreground (a tab can only focus itself). */
|
|
52
|
+
activateSelf(): Promise<void>;
|
|
53
|
+
/** Whether the cross-tab target tab opened by this tab is still open. */
|
|
54
|
+
isTargetOpen(): Promise<boolean>;
|
|
55
|
+
/** Stop listening and reject any in-flight requests. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
58
|
+
/** Create an {@link Openpicker} handle. */
|
|
59
|
+
declare function createOpenpicker(options?: OpenpickerOptions): Openpicker;
|
|
60
|
+
|
|
61
|
+
export { Openpicker, OpenpickerError, type OpenpickerOptions, createOpenpicker };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// ../protocol/src/constants.ts
|
|
2
|
+
var CHANNEL = "openpicker";
|
|
3
|
+
var PROTOCOL_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
// ../protocol/src/envelope.ts
|
|
6
|
+
function isEnvelope(value) {
|
|
7
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8
|
+
const v = value;
|
|
9
|
+
return v.channel === CHANNEL && typeof v.kind === "string";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
var OpenpickerError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
data;
|
|
16
|
+
constructor(error) {
|
|
17
|
+
super(error.message);
|
|
18
|
+
this.name = "OpenpickerError";
|
|
19
|
+
this.code = error.code;
|
|
20
|
+
this.data = error.data;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
24
|
+
function randomId(length) {
|
|
25
|
+
const bytes = new Uint8Array(length);
|
|
26
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
27
|
+
let out = "";
|
|
28
|
+
for (const byte of bytes) {
|
|
29
|
+
out += ID_ALPHABET[byte % ID_ALPHABET.length];
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
var Openpicker = class {
|
|
34
|
+
options;
|
|
35
|
+
win;
|
|
36
|
+
instanceId = randomId(6);
|
|
37
|
+
pending = /* @__PURE__ */ new Map();
|
|
38
|
+
seq = 0;
|
|
39
|
+
listening = false;
|
|
40
|
+
constructor(options = {}) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.win = options.targetWindow ?? window;
|
|
43
|
+
}
|
|
44
|
+
onMessage = (event) => {
|
|
45
|
+
if (event.source !== this.win) return;
|
|
46
|
+
if (event.origin !== this.win.origin) return;
|
|
47
|
+
const data = event.data;
|
|
48
|
+
if (!isEnvelope(data) || data.kind !== "res") return;
|
|
49
|
+
const entry = this.pending.get(data.id);
|
|
50
|
+
if (!entry) return;
|
|
51
|
+
this.pending.delete(data.id);
|
|
52
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
53
|
+
if (data.ok) {
|
|
54
|
+
entry.resolve(data.result);
|
|
55
|
+
} else {
|
|
56
|
+
entry.reject(
|
|
57
|
+
new OpenpickerError(
|
|
58
|
+
data.error ?? { code: "internal_error", message: "openpicker: malformed error response" }
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
ensureListening() {
|
|
64
|
+
if (this.listening) return;
|
|
65
|
+
this.win.addEventListener("message", this.onMessage);
|
|
66
|
+
this.listening = true;
|
|
67
|
+
}
|
|
68
|
+
request(method, params, timeout) {
|
|
69
|
+
this.ensureListening();
|
|
70
|
+
const id = `op:${this.instanceId}:${++this.seq}`;
|
|
71
|
+
const envelope = {
|
|
72
|
+
channel: CHANNEL,
|
|
73
|
+
v: PROTOCOL_VERSION,
|
|
74
|
+
kind: "req",
|
|
75
|
+
id,
|
|
76
|
+
method,
|
|
77
|
+
params
|
|
78
|
+
};
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
let timer;
|
|
81
|
+
if (timeout > 0) {
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
this.pending.delete(id);
|
|
84
|
+
reject(
|
|
85
|
+
new OpenpickerError({
|
|
86
|
+
code: "timeout",
|
|
87
|
+
message: `openpicker: "${method}" timed out after ${timeout}ms`
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}, timeout);
|
|
91
|
+
}
|
|
92
|
+
this.pending.set(id, {
|
|
93
|
+
resolve,
|
|
94
|
+
reject,
|
|
95
|
+
timer
|
|
96
|
+
});
|
|
97
|
+
this.win.postMessage(envelope, this.win.origin);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Probe the extension and negotiate version/capabilities. */
|
|
101
|
+
async ping() {
|
|
102
|
+
try {
|
|
103
|
+
return await this.request(
|
|
104
|
+
"ping",
|
|
105
|
+
{ appName: this.options.appName },
|
|
106
|
+
this.options.pingTimeout ?? 1500
|
|
107
|
+
);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof OpenpickerError && error.code === "timeout") {
|
|
110
|
+
throw new OpenpickerError({
|
|
111
|
+
code: "extension_not_installed",
|
|
112
|
+
message: "openpicker: the extension is not installed or did not respond"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Convenience: resolve `true` if the extension responds to a ping. */
|
|
119
|
+
async isAvailable() {
|
|
120
|
+
try {
|
|
121
|
+
await this.ping();
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Open `params.url` in a tab and start element selection there; resolves when the
|
|
129
|
+
* user confirms (OK) in the sidebar. `url` is required — the extension picks
|
|
130
|
+
* across the tab/origin boundary, which is the thing a page can't do for itself.
|
|
131
|
+
*/
|
|
132
|
+
pick(params) {
|
|
133
|
+
return this.request("pick", { appName: this.options.appName, ...params }, 0);
|
|
134
|
+
}
|
|
135
|
+
/** Cancel an in-flight pick. */
|
|
136
|
+
async cancel() {
|
|
137
|
+
await this.request("cancel", {}, this.options.defaultTimeout ?? 3e3);
|
|
138
|
+
}
|
|
139
|
+
/** Highlight element(s) matching a selector without entering pick mode. */
|
|
140
|
+
highlight(selector) {
|
|
141
|
+
return this.request("highlight", { selector }, this.options.defaultTimeout ?? 3e3);
|
|
142
|
+
}
|
|
143
|
+
/** Remove any active highlight. */
|
|
144
|
+
async clearHighlight() {
|
|
145
|
+
await this.request("clearHighlight", {}, this.options.defaultTimeout ?? 3e3);
|
|
146
|
+
}
|
|
147
|
+
/** Bring the calling tab to the foreground (a tab can only focus itself). */
|
|
148
|
+
async activateSelf() {
|
|
149
|
+
await this.request("activateSelf", {}, this.options.defaultTimeout ?? 3e3);
|
|
150
|
+
}
|
|
151
|
+
/** Whether the cross-tab target tab opened by this tab is still open. */
|
|
152
|
+
async isTargetOpen() {
|
|
153
|
+
const { open } = await this.request("isTargetOpen", {}, this.options.defaultTimeout ?? 3e3);
|
|
154
|
+
return open;
|
|
155
|
+
}
|
|
156
|
+
/** Stop listening and reject any in-flight requests. */
|
|
157
|
+
destroy() {
|
|
158
|
+
if (this.listening) {
|
|
159
|
+
this.win.removeEventListener("message", this.onMessage);
|
|
160
|
+
this.listening = false;
|
|
161
|
+
}
|
|
162
|
+
for (const entry of this.pending.values()) {
|
|
163
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
164
|
+
entry.reject(
|
|
165
|
+
new OpenpickerError({ code: "internal_error", message: "openpicker: instance destroyed" })
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
this.pending.clear();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function createOpenpicker(options) {
|
|
172
|
+
return new Openpicker(options);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { Openpicker, OpenpickerError, createOpenpicker };
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openpicker/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open-source CSS element picker — SDK for invoking the openpicker browser extension.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Usertour (https://www.usertour.io)",
|
|
7
|
+
"homepage": "https://github.com/usertour/openpicker#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/usertour/openpicker.git",
|
|
11
|
+
"directory": "packages/sdk"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/usertour/openpicker/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openpicker",
|
|
18
|
+
"element-picker",
|
|
19
|
+
"css-selector",
|
|
20
|
+
"selector",
|
|
21
|
+
"picker",
|
|
22
|
+
"browser-extension",
|
|
23
|
+
"devtools",
|
|
24
|
+
"dom"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.cjs",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"tsup": "^8.5.0",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"@openpicker/protocol": "0.0.0"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
}
|
|
55
|
+
}
|