adalo-pdf-viewer 1.2.2
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 +153 -0
- package/adalo.json +19 -0
- package/dist/editor.js +2 -0
- package/dist/editor.js.LICENSE.txt +1 -0
- package/dist/icons/PDFViewer.png +0 -0
- package/dist/input/editor.js +9 -0
- package/dist/input/runtime.js +1 -0
- package/dist/logo.png +0 -0
- package/dist/metadata.json +1 -0
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.LICENSE.txt +1 -0
- package/logo.png +0 -0
- package/package.json +30 -0
- package/scripts/install_android.sh +20 -0
- package/scripts/install_android.ts +22 -0
- package/scripts/install_ios.sh +13 -0
- package/scripts/install_ios.ts +23 -0
- package/src/components/PDFViewer/icon.png +0 -0
- package/src/components/PDFViewer/index.js +506 -0
- package/src/components/PDFViewer/index.native.js +521 -0
- package/src/components/PDFViewer/index.web.js +172 -0
- package/src/components/PDFViewer/manifest.json +110 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDFViewer — Web / Editor / Previewer component
|
|
3
|
+
*
|
|
4
|
+
* Toolbar: liquid-glass floating pill at the bottom (mobile only).
|
|
5
|
+
* - Share: fetches the PDF as a Blob → shares it as a FILE via Web Share
|
|
6
|
+
* API Level 2 (iOS/Android shows "Save to Files", AirDrop, etc.).
|
|
7
|
+
* Falls back to URL share if CORS blocks the fetch.
|
|
8
|
+
* - Search: expands an inline text field; on submit reloads the iframe
|
|
9
|
+
* with #search=term (Chrome PDF viewer highlights matches).
|
|
10
|
+
*
|
|
11
|
+
* Fit-to-width: appends #zoom=FitH on non-iOS browsers (Chrome/Edge).
|
|
12
|
+
* Height: uses Adalo's _height / _width internal props for exact sizing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { Component } from 'react'
|
|
16
|
+
|
|
17
|
+
// ─── Global styles ───────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function ensureGlobalStyles() {
|
|
20
|
+
if (typeof document === 'undefined') return
|
|
21
|
+
const id = 'adalo-pdf-viewer-styles'
|
|
22
|
+
if (document.getElementById(id)) return
|
|
23
|
+
const el = document.createElement('style')
|
|
24
|
+
el.id = id
|
|
25
|
+
el.textContent = `
|
|
26
|
+
@keyframes adalo-pdf-spin {
|
|
27
|
+
to { transform: rotate(360deg); }
|
|
28
|
+
}
|
|
29
|
+
@keyframes adalo-pdf-fadein {
|
|
30
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
31
|
+
to { opacity: 1; transform: translateY(0); }
|
|
32
|
+
}
|
|
33
|
+
@keyframes adalo-pdf-toolbar-in {
|
|
34
|
+
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
|
35
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
document.head.appendChild(el)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Platform helpers ────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const isIOS = () =>
|
|
44
|
+
typeof navigator !== 'undefined' &&
|
|
45
|
+
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
|
|
46
|
+
!window.MSStream
|
|
47
|
+
|
|
48
|
+
const isMobileBrowser = () =>
|
|
49
|
+
typeof navigator !== 'undefined' &&
|
|
50
|
+
/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
|
|
51
|
+
|
|
52
|
+
// ─── SVG icons ───────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// SF-style "square.and.arrow.up"
|
|
55
|
+
const ShareSVG = () => (
|
|
56
|
+
<svg width="17" height="17" viewBox="0 0 17 17" fill="none">
|
|
57
|
+
<path d="M8.5 10.5V1.5" stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
|
58
|
+
<path d="M5.5 4.5l3-3 3 3" stroke="white" strokeWidth="1.6"
|
|
59
|
+
strokeLinecap="round" strokeLinejoin="round"/>
|
|
60
|
+
<path d="M4.5 7.5H3a1 1 0 00-1 1v6a1 1 0 001 1h11a1 1 0 001-1v-6a1 1 0 00-1-1h-1.5"
|
|
61
|
+
stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
|
62
|
+
</svg>
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// SF-style magnifying glass
|
|
66
|
+
const SearchSVG = () => (
|
|
67
|
+
<svg width="17" height="17" viewBox="0 0 17 17" fill="none">
|
|
68
|
+
<circle cx="7.5" cy="7.5" r="5" stroke="white" strokeWidth="1.6"/>
|
|
69
|
+
<path d="M11.5 11.5l3 3" stroke="white" strokeWidth="1.6" strokeLinecap="round"/>
|
|
70
|
+
</svg>
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// X / close
|
|
74
|
+
const CloseSVG = () => (
|
|
75
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
|
76
|
+
<path d="M1.5 1.5l10 10M11.5 1.5l-10 10" stroke="rgba(255,255,255,0.7)"
|
|
77
|
+
strokeWidth="1.6" strokeLinecap="round"/>
|
|
78
|
+
</svg>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
class PDFViewerWeb extends Component {
|
|
84
|
+
constructor(props) {
|
|
85
|
+
super(props)
|
|
86
|
+
this.state = {
|
|
87
|
+
loading: true,
|
|
88
|
+
error: false,
|
|
89
|
+
searchActive: false,
|
|
90
|
+
searchQuery: '',
|
|
91
|
+
sharing: false,
|
|
92
|
+
}
|
|
93
|
+
this.containerRef = null
|
|
94
|
+
this.iframeRef = null
|
|
95
|
+
ensureGlobalStyles()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── iframe events ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
handleLoad = () => {
|
|
101
|
+
const { onLoadComplete } = this.props
|
|
102
|
+
this.setState({ loading: false, error: false })
|
|
103
|
+
if (onLoadComplete) onLoadComplete(0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
handleError = () => {
|
|
107
|
+
const { onError } = this.props
|
|
108
|
+
this.setState({ loading: false, error: true })
|
|
109
|
+
if (onError) onError()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Share (file, not link) ─────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
handleShare = async () => {
|
|
115
|
+
const { pdfUrl } = this.props
|
|
116
|
+
if (!pdfUrl || this.state.sharing) return
|
|
117
|
+
this.setState({ sharing: true })
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Attempt to fetch the PDF as a Blob so we share a FILE, not just a URL.
|
|
121
|
+
// This triggers "Save to Files", AirDrop, etc. on iOS.
|
|
122
|
+
let shared = false
|
|
123
|
+
|
|
124
|
+
if (navigator.share) {
|
|
125
|
+
try {
|
|
126
|
+
const resp = await fetch(pdfUrl)
|
|
127
|
+
const blob = await resp.blob()
|
|
128
|
+
const rawName = decodeURIComponent(pdfUrl.split('/').pop().split('?')[0]) || 'document'
|
|
129
|
+
const fileName = rawName.endsWith('.pdf') ? rawName : rawName + '.pdf'
|
|
130
|
+
const file = new File([blob], fileName, { type: 'application/pdf' })
|
|
131
|
+
|
|
132
|
+
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
|
133
|
+
await navigator.share({ files: [file], title: 'PDF Document' })
|
|
134
|
+
shared = true
|
|
135
|
+
}
|
|
136
|
+
} catch (_) {
|
|
137
|
+
// CORS error or canShare not supported — fall through to URL share
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!shared) {
|
|
141
|
+
try {
|
|
142
|
+
await navigator.share({ url: pdfUrl, title: 'PDF Document' })
|
|
143
|
+
shared = true
|
|
144
|
+
} catch (e) {
|
|
145
|
+
if (e.name === 'AbortError') { shared = true } // user cancelled
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!shared) window.open(pdfUrl, '_blank')
|
|
151
|
+
} finally {
|
|
152
|
+
this.setState({ sharing: false })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Search ─────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
handleSearchToggle = () => {
|
|
159
|
+
this.setState(prev => ({
|
|
160
|
+
searchActive: !prev.searchActive,
|
|
161
|
+
searchQuery: '',
|
|
162
|
+
}), () => {
|
|
163
|
+
// If closing search, reset iframe to non-search URL
|
|
164
|
+
if (!this.state.searchActive && this.iframeRef) {
|
|
165
|
+
this.iframeRef.src = this.buildEmbedUrl(this.props.pdfUrl)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
handleSearchChange = (e) => {
|
|
171
|
+
this.setState({ searchQuery: e.target.value })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
handleSearchSubmit = (e) => {
|
|
175
|
+
e.preventDefault()
|
|
176
|
+
const { pdfUrl } = this.props
|
|
177
|
+
const { searchQuery } = this.state
|
|
178
|
+
if (!pdfUrl || !this.iframeRef) return
|
|
179
|
+
|
|
180
|
+
const base = this.buildEmbedUrl(pdfUrl)
|
|
181
|
+
const sep = base.includes('#') ? '&' : '#'
|
|
182
|
+
// Chrome PDF viewer supports #search=term to highlight first match
|
|
183
|
+
this.iframeRef.src = searchQuery
|
|
184
|
+
? `${base}${sep}search=${encodeURIComponent(searchQuery)}`
|
|
185
|
+
: base
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── URL builder ────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
buildEmbedUrl = (url) => {
|
|
191
|
+
if (!url) return null
|
|
192
|
+
// FitH scales the PDF to fill the iframe width in Chrome / Edge.
|
|
193
|
+
// iOS Safari ignores PDF open parameters — the share button is the
|
|
194
|
+
// better path for iOS users who want to view at full quality.
|
|
195
|
+
if (!isIOS()) {
|
|
196
|
+
const sep = url.includes('#') ? '&' : '#'
|
|
197
|
+
return `${url}${sep}zoom=FitH`
|
|
198
|
+
}
|
|
199
|
+
return url
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
render() {
|
|
205
|
+
const {
|
|
206
|
+
pdfUrl,
|
|
207
|
+
backgroundColor,
|
|
208
|
+
controlColor,
|
|
209
|
+
loadingText,
|
|
210
|
+
loadingTextColor,
|
|
211
|
+
errorText,
|
|
212
|
+
errorTextColor,
|
|
213
|
+
_height,
|
|
214
|
+
_width,
|
|
215
|
+
} = this.props
|
|
216
|
+
|
|
217
|
+
const { loading, error, searchActive, searchQuery, sharing } = this.state
|
|
218
|
+
const accent = controlColor || '#0066CC'
|
|
219
|
+
const bg = backgroundColor || '#FFFFFF'
|
|
220
|
+
const mobile = isMobileBrowser()
|
|
221
|
+
|
|
222
|
+
const explicitW = _width ? `${_width}px` : '100%'
|
|
223
|
+
const explicitH = _height ? `${_height}px` : '100%'
|
|
224
|
+
|
|
225
|
+
// ── Styles ──────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
const root = {
|
|
228
|
+
position: 'relative',
|
|
229
|
+
display: 'flex',
|
|
230
|
+
flexDirection: 'column',
|
|
231
|
+
width: explicitW,
|
|
232
|
+
height: explicitH,
|
|
233
|
+
backgroundColor: bg,
|
|
234
|
+
overflow: 'hidden',
|
|
235
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const overlay = {
|
|
239
|
+
position: 'absolute',
|
|
240
|
+
top: 0, left: 0, right: 0, bottom: 0,
|
|
241
|
+
display: 'flex',
|
|
242
|
+
flexDirection: 'column',
|
|
243
|
+
alignItems: 'center',
|
|
244
|
+
justifyContent: 'center',
|
|
245
|
+
backgroundColor: bg,
|
|
246
|
+
zIndex: 2,
|
|
247
|
+
animation: 'adalo-pdf-fadein 0.2s ease both',
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Liquid-glass pill — dark, blurred, very iOS 26
|
|
251
|
+
const pill = {
|
|
252
|
+
position: 'absolute',
|
|
253
|
+
bottom: 18,
|
|
254
|
+
left: '50%',
|
|
255
|
+
transform: 'translateX(-50%)',
|
|
256
|
+
zIndex: 10,
|
|
257
|
+
display: 'flex',
|
|
258
|
+
flexDirection: 'row',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
gap: 2,
|
|
261
|
+
// Liquid glass: dark translucent, heavy blur, subtle inner rim light
|
|
262
|
+
background: 'rgba(20, 20, 22, 0.72)',
|
|
263
|
+
backdropFilter: 'blur(28px) saturate(1.8)',
|
|
264
|
+
WebkitBackdropFilter: 'blur(28px) saturate(1.8)',
|
|
265
|
+
borderRadius: 40,
|
|
266
|
+
border: '0.5px solid rgba(255,255,255,0.16)',
|
|
267
|
+
boxShadow: [
|
|
268
|
+
'0 8px 32px rgba(0,0,0,0.28)',
|
|
269
|
+
'inset 0 1px 0 rgba(255,255,255,0.10)',
|
|
270
|
+
'inset 0 -1px 0 rgba(0,0,0,0.20)',
|
|
271
|
+
].join(', '),
|
|
272
|
+
padding: '5px 5px',
|
|
273
|
+
animation: 'adalo-pdf-toolbar-in 0.3s cubic-bezier(0.34,1.56,0.64,1) both',
|
|
274
|
+
// Prevent layout from extending beyond component
|
|
275
|
+
maxWidth: 'calc(100% - 32px)',
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const iconBtn = (pressed) => ({
|
|
279
|
+
width: 38,
|
|
280
|
+
height: 38,
|
|
281
|
+
borderRadius: 19,
|
|
282
|
+
border: 'none',
|
|
283
|
+
background: pressed ? 'rgba(255,255,255,0.14)' : 'transparent',
|
|
284
|
+
cursor: 'pointer',
|
|
285
|
+
display: 'flex',
|
|
286
|
+
alignItems: 'center',
|
|
287
|
+
justifyContent: 'center',
|
|
288
|
+
padding: 0,
|
|
289
|
+
flexShrink: 0,
|
|
290
|
+
transition: 'background 0.12s',
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// ── No URL: placeholder ────────────────────────────────────────────────
|
|
294
|
+
if (!pdfUrl) {
|
|
295
|
+
return (
|
|
296
|
+
<div ref={el => { this.containerRef = el }} style={root}>
|
|
297
|
+
<div style={{
|
|
298
|
+
...overlay,
|
|
299
|
+
border: '1.5px dashed',
|
|
300
|
+
borderColor: accent + '55',
|
|
301
|
+
borderRadius: 10,
|
|
302
|
+
top: 4, left: 4, right: 4, bottom: 4,
|
|
303
|
+
}}>
|
|
304
|
+
<svg width="48" height="48" viewBox="0 0 48 48" fill="none"
|
|
305
|
+
style={{ marginBottom: 12, opacity: 0.75 }}>
|
|
306
|
+
<rect x="8" y="4" width="28" height="36" rx="3" fill={accent} opacity="0.12"/>
|
|
307
|
+
<rect x="8" y="4" width="28" height="36" rx="3" stroke={accent} strokeWidth="1.5"/>
|
|
308
|
+
<path d="M28 4v9h9" stroke={accent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
309
|
+
<path d="M16 20h16M16 26h10" stroke={accent} strokeWidth="1.5" strokeLinecap="round"/>
|
|
310
|
+
</svg>
|
|
311
|
+
<span style={{ fontSize: 15, fontWeight: 600, color: '#1A1A1A', marginBottom: 4 }}>
|
|
312
|
+
PDF Viewer
|
|
313
|
+
</span>
|
|
314
|
+
<span style={{ fontSize: 13, color: loadingTextColor || '#8E8E93',
|
|
315
|
+
textAlign: 'center', lineHeight: '18px' }}>
|
|
316
|
+
Set a PDF URL to preview
|
|
317
|
+
</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const embedUrl = this.buildEmbedUrl(pdfUrl)
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div ref={el => { this.containerRef = el }} style={root}>
|
|
327
|
+
|
|
328
|
+
{/* ── Loading overlay ───────────────────────────────────────────── */}
|
|
329
|
+
{loading && (
|
|
330
|
+
<div style={overlay}>
|
|
331
|
+
<div style={{
|
|
332
|
+
width: 36, height: 36, borderRadius: '50%',
|
|
333
|
+
border: `3px solid ${accent}22`,
|
|
334
|
+
borderTopColor: accent,
|
|
335
|
+
animation: 'adalo-pdf-spin 0.75s linear infinite',
|
|
336
|
+
marginBottom: 14,
|
|
337
|
+
}}/>
|
|
338
|
+
<span style={{ fontSize: 13, color: loadingTextColor || '#8E8E93' }}>
|
|
339
|
+
{loadingText || 'Loading PDF…'}
|
|
340
|
+
</span>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{/* ── Error overlay ─────────────────────────────────────────────── */}
|
|
345
|
+
{error && (
|
|
346
|
+
<div style={overlay}>
|
|
347
|
+
<div style={{
|
|
348
|
+
width: 44, height: 44, borderRadius: '50%',
|
|
349
|
+
backgroundColor: (errorTextColor || '#FF3B30') + '15',
|
|
350
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
351
|
+
marginBottom: 12,
|
|
352
|
+
}}>
|
|
353
|
+
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
|
|
354
|
+
<circle cx="11" cy="11" r="10" stroke={errorTextColor || '#FF3B30'} strokeWidth="1.5"/>
|
|
355
|
+
<path d="M11 7v5M11 15v.5" stroke={errorTextColor || '#FF3B30'}
|
|
356
|
+
strokeWidth="1.75" strokeLinecap="round"/>
|
|
357
|
+
</svg>
|
|
358
|
+
</div>
|
|
359
|
+
<span style={{ fontSize: 14, color: errorTextColor || '#FF3B30',
|
|
360
|
+
textAlign: 'center', lineHeight: '20px', maxWidth: 220 }}>
|
|
361
|
+
{errorText || 'Could not load PDF.\nPlease check the URL.'}
|
|
362
|
+
</span>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{/* ── PDF iframe ────────────────────────────────────────────────── */}
|
|
367
|
+
{!error && (() => {
|
|
368
|
+
const iosDevice = isIOS()
|
|
369
|
+
// iOS Safari ignores PDF open parameters like #zoom=FitH.
|
|
370
|
+
// Workaround: render the iframe at natural PDF page width (612pt),
|
|
371
|
+
// then scale it down so it fits our component width exactly.
|
|
372
|
+
// Standard A4/Letter pages are 612pt wide; we scale to component width.
|
|
373
|
+
const compW = parseInt(_width || '400', 10) || 400
|
|
374
|
+
const compH = parseInt(_height || '687', 10) || 687
|
|
375
|
+
const PDF_PAGE_W = 612 // standard letter/A4 PDF width in points
|
|
376
|
+
const iosScale = compW / PDF_PAGE_W // e.g. 400/612 ≈ 0.65
|
|
377
|
+
const scaledW = PDF_PAGE_W // render at natural width
|
|
378
|
+
const scaledH = Math.round(compH / iosScale)
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
// Wrapper clips the scaled iframe to the component bounds
|
|
382
|
+
<div style={{
|
|
383
|
+
position: 'relative',
|
|
384
|
+
flexGrow: 1, flexShrink: 1, flexBasis: '0%',
|
|
385
|
+
overflow: 'hidden',
|
|
386
|
+
minHeight: 0,
|
|
387
|
+
}}>
|
|
388
|
+
<iframe
|
|
389
|
+
ref={el => { this.iframeRef = el }}
|
|
390
|
+
src={embedUrl}
|
|
391
|
+
onLoad={this.handleLoad}
|
|
392
|
+
onError={this.handleError}
|
|
393
|
+
title="PDF Viewer"
|
|
394
|
+
allow="fullscreen"
|
|
395
|
+
style={iosDevice ? {
|
|
396
|
+
// Render at the PDF's natural width, then scale to fit
|
|
397
|
+
display: 'block',
|
|
398
|
+
border: 'none',
|
|
399
|
+
width: `${scaledW}px`,
|
|
400
|
+
height: `${scaledH}px`,
|
|
401
|
+
transform: `scale(${iosScale})`,
|
|
402
|
+
transformOrigin: '0 0',
|
|
403
|
+
} : {
|
|
404
|
+
// Chrome / desktop: FitH handles width; just fill the box
|
|
405
|
+
display: 'block',
|
|
406
|
+
border: 'none',
|
|
407
|
+
width: '100%',
|
|
408
|
+
height: '100%',
|
|
409
|
+
position: 'absolute',
|
|
410
|
+
top: 0, left: 0,
|
|
411
|
+
}}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
)
|
|
415
|
+
})()}
|
|
416
|
+
|
|
417
|
+
{/* ── Liquid-glass toolbar (mobile, after load) ─────────────────── */}
|
|
418
|
+
{mobile && !loading && !error && (
|
|
419
|
+
<div style={pill}>
|
|
420
|
+
|
|
421
|
+
{/* Search toggle / close */}
|
|
422
|
+
<button
|
|
423
|
+
onClick={this.handleSearchToggle}
|
|
424
|
+
style={iconBtn(false)}
|
|
425
|
+
title={searchActive ? 'Close search' : 'Search PDF'}
|
|
426
|
+
aria-label={searchActive ? 'Close search' : 'Search PDF'}
|
|
427
|
+
>
|
|
428
|
+
{searchActive ? <CloseSVG /> : <SearchSVG />}
|
|
429
|
+
</button>
|
|
430
|
+
|
|
431
|
+
{/* Expandable search input */}
|
|
432
|
+
{searchActive && (
|
|
433
|
+
<form
|
|
434
|
+
onSubmit={this.handleSearchSubmit}
|
|
435
|
+
style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}
|
|
436
|
+
>
|
|
437
|
+
<input
|
|
438
|
+
type="text"
|
|
439
|
+
autoFocus
|
|
440
|
+
placeholder="Search…"
|
|
441
|
+
value={searchQuery}
|
|
442
|
+
onChange={this.handleSearchChange}
|
|
443
|
+
style={{
|
|
444
|
+
width: 160,
|
|
445
|
+
height: 30,
|
|
446
|
+
background: 'rgba(255,255,255,0.10)',
|
|
447
|
+
border: 'none',
|
|
448
|
+
borderRadius: 8,
|
|
449
|
+
color: 'white',
|
|
450
|
+
fontSize: 15,
|
|
451
|
+
padding: '0 10px',
|
|
452
|
+
outline: 'none',
|
|
453
|
+
caretColor: 'white',
|
|
454
|
+
// placeholder colour via CSS class isn't possible inline;
|
|
455
|
+
// it'll default to the browser's placeholder style
|
|
456
|
+
}}
|
|
457
|
+
/>
|
|
458
|
+
</form>
|
|
459
|
+
)}
|
|
460
|
+
|
|
461
|
+
{/* Divider */}
|
|
462
|
+
{!searchActive && (
|
|
463
|
+
<div style={{
|
|
464
|
+
width: 0.5,
|
|
465
|
+
height: 22,
|
|
466
|
+
background: 'rgba(255,255,255,0.18)',
|
|
467
|
+
margin: '0 2px',
|
|
468
|
+
flexShrink: 0,
|
|
469
|
+
}}/>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{/* Share */}
|
|
473
|
+
<button
|
|
474
|
+
onClick={this.handleShare}
|
|
475
|
+
disabled={sharing}
|
|
476
|
+
style={{
|
|
477
|
+
...iconBtn(false),
|
|
478
|
+
opacity: sharing ? 0.5 : 1,
|
|
479
|
+
cursor: sharing ? 'default' : 'pointer',
|
|
480
|
+
}}
|
|
481
|
+
title="Share PDF"
|
|
482
|
+
aria-label="Share PDF"
|
|
483
|
+
>
|
|
484
|
+
{sharing
|
|
485
|
+
? (
|
|
486
|
+
<div style={{
|
|
487
|
+
width: 16, height: 16, borderRadius: '50%',
|
|
488
|
+
border: '2px solid rgba(255,255,255,0.3)',
|
|
489
|
+
borderTopColor: 'white',
|
|
490
|
+
animation: 'adalo-pdf-spin 0.7s linear infinite',
|
|
491
|
+
}}/>
|
|
492
|
+
)
|
|
493
|
+
: <ShareSVG />
|
|
494
|
+
}
|
|
495
|
+
</button>
|
|
496
|
+
|
|
497
|
+
</div>
|
|
498
|
+
)}
|
|
499
|
+
|
|
500
|
+
</div>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export { PDFViewerWeb as PDFViewer }
|
|
506
|
+
export default PDFViewerWeb
|