Skip to content

Commit 6697c6c

Browse files
author
Jonathan D.A. Jewell
committed
Wire popup to active-tab block detection via background runtime
1 parent e5b1050 commit 6697c6c

2 files changed

Lines changed: 181 additions & 23 deletions

File tree

public/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
},
1717
"permissions": [
1818
"activeTab",
19+
"tabs",
1920
"storage",
2021
"scripting"
2122
],

src/popup.res

Lines changed: 180 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,190 @@
11
/* SPDX-License-Identifier: AGPL-3.0-or-later WITH Palimpsest */
22

3-
let demoBlocks: array<PdfTool.block> = [
4-
{
5-
label: "Full Name",
6-
x: 100.,
7-
y: 140.,
8-
width: 160.,
9-
height: 20.,
10-
},
11-
{
12-
label: "Date of Birth",
13-
x: 100.,
14-
y: 170.,
15-
width: 160.,
16-
height: 20.,
17-
},
18-
]
3+
type runtime
4+
type tabsApi
5+
type tab = Js.Json.t
196

20-
let mount = () => {
21-
switch Webapi.Dom.document->Webapi.Dom.Document.querySelector("#root") {
22-
| Some(root) =>
23-
ReactDOM.Client.createRoot(root)->ReactDOM.Client.Root.render(
7+
@val @scope("browser")
8+
external runtime: runtime = "runtime"
9+
10+
@val @scope("browser")
11+
external tabsApi: tabsApi = "tabs"
12+
13+
@send
14+
external sendMessage: (runtime, Js.Json.t) => Js.Promise.t<Js.Json.t> = "sendMessage"
15+
16+
@send
17+
external queryTabs: (tabsApi, Js.Json.t) => Js.Promise.t<array<tab>> = "query"
18+
19+
let unsafeJson = (value: 'a): Js.Json.t => Obj.magic(value)
20+
21+
let makeTabQuery = (): Js.Json.t => {
22+
let payload = Js.Dict.empty()
23+
Js.Dict.set(payload, "active", Js.Json.boolean(true))
24+
Js.Dict.set(payload, "currentWindow", Js.Json.boolean(true))
25+
unsafeJson(payload)
26+
}
27+
28+
let makeDetectMessage = (url: string): Js.Json.t => {
29+
let payload = Js.Dict.empty()
30+
Js.Dict.set(payload, "action", Js.Json.string("detectBlocks"))
31+
Js.Dict.set(payload, "url", Js.Json.string(url))
32+
unsafeJson(payload)
33+
}
34+
35+
let looksLikePdfUrl = (url: string): bool => {
36+
let lower = url->Js.String2.toLowerCase
37+
lower->Js.String2.endsWith(".pdf")
38+
|| lower->Js.String2.includes(".pdf?")
39+
|| lower->Js.String2.includes(".pdf#")
40+
}
41+
42+
let tabUrl = (entry: tab): option<string> =>
43+
switch Js.Json.decodeObject(entry) {
44+
| Some(obj) => obj->Js.Dict.get("url")->Belt.Option.flatMap(Js.Json.decodeString)
45+
| None => None
46+
}
47+
48+
type detectResult = {
49+
ok: bool,
50+
blocks: array<PdfTool.block>,
51+
error: option<string>,
52+
}
53+
54+
let decodeDetectResult = (payload: Js.Json.t): detectResult => {
55+
switch Js.Json.decodeObject(payload) {
56+
| Some(obj) =>
57+
let ok = obj->Js.Dict.get("ok")->Belt.Option.flatMap(Js.Json.decodeBoolean)->Belt.Option.getWithDefault(false)
58+
let blocks: array<PdfTool.block> =
59+
switch obj->Js.Dict.get("blocks") {
60+
| Some(rawBlocks) =>
61+
switch Js.Json.decodeArray(rawBlocks) {
62+
| Some(_) => Obj.magic(rawBlocks)
63+
| None => []
64+
}
65+
| None => []
66+
}
67+
let error = obj->Js.Dict.get("error")->Belt.Option.flatMap(Js.Json.decodeString)
68+
{ok, blocks, error}
69+
| None => {ok: false, blocks: [], error: Some("invalid background response")}
70+
}
71+
}
72+
73+
let detectBlocksForActiveTab = (): Js.Promise.t<detectResult> => {
74+
let tabsPromise = queryTabs(tabsApi, makeTabQuery())
75+
let detectPromise =
76+
Js.Promise2.then(tabsPromise, entries => {
77+
if Belt.Array.length(entries) == 0 {
78+
Js.Promise.resolve({ok: false, blocks: [], error: Some("no active tab found")})
79+
} else {
80+
switch tabUrl(entries[0]) {
81+
| None => Js.Promise.resolve({ok: false, blocks: [], error: Some("active tab URL unavailable")})
82+
| Some(url) =>
83+
if !looksLikePdfUrl(url) {
84+
Js.Promise.resolve({ok: false, blocks: [], error: Some("active tab is not a PDF URL")})
85+
} else {
86+
Js.Promise2.then(sendMessage(runtime, makeDetectMessage(url)), response =>
87+
Js.Promise.resolve(decodeDetectResult(response))
88+
)
89+
}
90+
}
91+
}
92+
})
93+
94+
Js.Promise2.catch(detectPromise, err => {
95+
Js.log2("popup detect failed", err)
96+
Js.Promise.resolve({ok: false, blocks: [], error: Some("request failed")})
97+
})
98+
}
99+
100+
module App = {
101+
@react.component
102+
let make = () => {
103+
let (blocks, setBlocks) = React.useState(() => [])
104+
let (status, setStatus) = React.useState(() => "Waiting to detect PDF fields")
105+
let (isLoading, setIsLoading) = React.useState(() => false)
106+
107+
let refresh = () => {
108+
setIsLoading(_ => true)
109+
setStatus(_ => "Detecting blocks from active PDF tab...")
110+
let detectPromise =
111+
Js.Promise2.then(detectBlocksForActiveTab(), result => {
112+
setIsLoading(_ => false)
113+
if result.ok {
114+
let count = Belt.Array.length(result.blocks)
115+
setBlocks(_ => result.blocks)
116+
setStatus(_ =>
117+
if count == 0 {
118+
"No form widgets detected in this PDF"
119+
} else {
120+
"Detected " ++ Belt.Int.toString(count) ++ " blocks"
121+
}
122+
)
123+
} else {
124+
setBlocks(_ => [])
125+
let message = result.error->Belt.Option.getWithDefault("unknown error")
126+
setStatus(_ => "Detection failed: " ++ message)
127+
}
128+
Js.Promise.resolve(())
129+
})
130+
let _ = Js.Promise2.catch(detectPromise, _ => {
131+
setIsLoading(_ => false)
132+
setStatus(_ => "Detection failed: unexpected error")
133+
Js.Promise.resolve(())
134+
})
135+
()
136+
}
137+
138+
React.useEffect0(() => {
139+
refresh()
140+
None
141+
})
142+
143+
<div>
144+
<h1 style={ReactDOM.Style.make(~fontSize="16px", ~margin="0 0 8px 0", ())}>
145+
{React.string("Blocky Writer")}
146+
</h1>
147+
<p style={ReactDOM.Style.make(~margin="0 0 10px 0", ~fontSize="12px", ~color="#475467", ())}>
148+
{React.string(status)}
149+
</p>
150+
{
151+
if isLoading {
152+
<div style={ReactDOM.Style.make(~fontSize="12px", ~marginBottom="10px", ())}>
153+
{React.string("Loading...")}
154+
</div>
155+
} else {
156+
React.null
157+
}
158+
}
159+
<button
160+
onClick={_ => refresh()}
161+
style={
162+
ReactDOM.Style.make(
163+
~width="100%",
164+
~padding="8px",
165+
~marginBottom="10px",
166+
~borderRadius="6px",
167+
~border="1px solid #344054",
168+
~backgroundColor="#fff",
169+
~cursor="pointer",
170+
(),
171+
)
172+
}>
173+
{React.string("Refresh Detection")}
174+
</button>
24175
<FormFiller
25-
blocks={demoBlocks}
176+
blocks={blocks}
26177
onFill={fields => {
27178
Js.log2("Fill requested", fields)
28179
}}
29-
/>,
30-
)
180+
/>
181+
</div>
182+
}
183+
}
184+
185+
let mount = () => {
186+
switch Webapi.Dom.document->Webapi.Dom.Document.querySelector("#root") {
187+
| Some(root) => ReactDOM.Client.createRoot(root)->ReactDOM.Client.Root.render(<App />)
31188
| None => Js.log("popup root node missing")
32189
}
33190
}

0 commit comments

Comments
 (0)