canvasframework 0.5.61 → 0.5.62
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/components/PDFViewer.js +1069 -0
- package/core/CanvasFramework.js +1 -0
- package/core/UIBuilder.js +2 -0
- package/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lecteur PDF embarqué avec styles Material You et Cupertino.
|
|
5
|
+
* Supporte : affichage multi-pages, zoom, navigation, miniature, recherche,
|
|
6
|
+
* téléchargement, impression, plein-écran, rotation.
|
|
7
|
+
*
|
|
8
|
+
* Utilise PDF.js (CDN) pour le rendu. La lib est chargée automatiquement.
|
|
9
|
+
*
|
|
10
|
+
* @class
|
|
11
|
+
* @extends Component
|
|
12
|
+
* @property {string} platform - 'material' ou 'cupertino'
|
|
13
|
+
* @property {string|Uint8Array|null} src - URL ou données binaires du PDF
|
|
14
|
+
* @property {number} currentPage - Page courante (1-based)
|
|
15
|
+
* @property {number} totalPages - Nombre total de pages
|
|
16
|
+
* @property {number} scale - Niveau de zoom (1 = 100%)
|
|
17
|
+
* @property {number} rotation - Rotation en degrés (0, 90, 180, 270)
|
|
18
|
+
* @property {boolean} loading - Chargement en cours
|
|
19
|
+
* @property {string|null} error - Message d'erreur
|
|
20
|
+
*/
|
|
21
|
+
class PDFViewer extends Component {
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {CanvasFramework} framework
|
|
25
|
+
* @param {Object} [options={}]
|
|
26
|
+
* @param {string|Uint8Array} [options.src] - URL ou données binaires du PDF
|
|
27
|
+
* @param {number} [options.initialPage=1] - Page initiale
|
|
28
|
+
* @param {number} [options.initialScale=1.0] - Zoom initial
|
|
29
|
+
* @param {boolean} [options.showToolbar=true] - Afficher la toolbar
|
|
30
|
+
* @param {boolean} [options.showThumbnails=false] - Afficher le panneau miniatures
|
|
31
|
+
* @param {boolean} [options.allowDownload=true] - Bouton de téléchargement
|
|
32
|
+
* @param {boolean} [options.allowPrint=true] - Bouton d'impression
|
|
33
|
+
* @param {boolean} [options.allowFullscreen=true] - Bouton plein écran
|
|
34
|
+
* @param {boolean} [options.allowRotate=true] - Bouton rotation
|
|
35
|
+
* @param {boolean} [options.allowSearch=true] - Bouton recherche
|
|
36
|
+
* @param {number} [options.minScale=0.25] - Zoom minimum
|
|
37
|
+
* @param {number} [options.maxScale=5.0] - Zoom maximum
|
|
38
|
+
* @param {string} [options.backgroundColor] - Fond global
|
|
39
|
+
* @param {string} [options.primaryColor] - Couleur primaire (toolbar)
|
|
40
|
+
* @param {string} [options.pageBackground='#FFFFFF'] - Fond page
|
|
41
|
+
* @param {Function} [options.onPageChange] - Callback(page, total)
|
|
42
|
+
* @param {Function} [options.onScaleChange] - Callback(scale)
|
|
43
|
+
* @param {Function} [options.onLoad] - Callback(totalPages) après chargement
|
|
44
|
+
* @param {Function} [options.onError] - Callback(errorMessage)
|
|
45
|
+
*/
|
|
46
|
+
constructor(framework, options = {}) {
|
|
47
|
+
super(framework, options);
|
|
48
|
+
|
|
49
|
+
this.platform = framework.platform;
|
|
50
|
+
this.src = options.src || null;
|
|
51
|
+
this.currentPage = options.initialPage || 1;
|
|
52
|
+
this.totalPages = 0;
|
|
53
|
+
this.scale = options.initialScale || 1.0;
|
|
54
|
+
this.rotation = 0;
|
|
55
|
+
this.loading = false;
|
|
56
|
+
this.error = null;
|
|
57
|
+
this.showToolbar = options.showToolbar !== false;
|
|
58
|
+
this.showThumbnails = options.showThumbnails || false;
|
|
59
|
+
this.allowDownload = options.allowDownload !== false;
|
|
60
|
+
this.allowPrint = options.allowPrint !== false;
|
|
61
|
+
this.allowFullscreen= options.allowFullscreen !== false;
|
|
62
|
+
this.allowRotate = options.allowRotate !== false;
|
|
63
|
+
this.allowSearch = options.allowSearch !== false;
|
|
64
|
+
this.minScale = options.minScale || 0.25;
|
|
65
|
+
this.maxScale = options.maxScale || 5.0;
|
|
66
|
+
this.backgroundColor= options.backgroundColor || null;
|
|
67
|
+
this.primaryColor = options.primaryColor || null;
|
|
68
|
+
this.pageBackground = options.pageBackground || '#FFFFFF';
|
|
69
|
+
|
|
70
|
+
// Callbacks
|
|
71
|
+
this.onPageChange = options.onPageChange || (() => {});
|
|
72
|
+
this.onScaleChange = options.onScaleChange || (() => {});
|
|
73
|
+
this.onLoad = options.onLoad || (() => {});
|
|
74
|
+
this.onErrorCb = options.onError || (() => {});
|
|
75
|
+
|
|
76
|
+
// État interne DOM
|
|
77
|
+
this._containerEl = null;
|
|
78
|
+
this._toolbarEl = null;
|
|
79
|
+
this._viewerEl = null;
|
|
80
|
+
this._thumbsEl = null;
|
|
81
|
+
this._searchBarEl = null;
|
|
82
|
+
this._mounted = false;
|
|
83
|
+
this._pdfDoc = null;
|
|
84
|
+
this._pageCanvases = {};
|
|
85
|
+
this._thumbCanvases = {};
|
|
86
|
+
this._renderTask = null;
|
|
87
|
+
this._searchOpen = false;
|
|
88
|
+
this._searchQuery = '';
|
|
89
|
+
this._fullscreen = false;
|
|
90
|
+
|
|
91
|
+
this._toolbarHeight = this.showToolbar ? 52 : 0;
|
|
92
|
+
this._thumbsWidth = this.showThumbnails ? 120 : 0;
|
|
93
|
+
|
|
94
|
+
// Couleurs Material You 3
|
|
95
|
+
this.m3Colors = {
|
|
96
|
+
primary: '#6750A4',
|
|
97
|
+
onPrimary: '#FFFFFF',
|
|
98
|
+
surface: '#FFFBFE',
|
|
99
|
+
surfaceVariant: '#E7E0EC',
|
|
100
|
+
onSurface: '#1C1B1F',
|
|
101
|
+
onSurfaceVariant: '#49454F',
|
|
102
|
+
outline: '#79747E',
|
|
103
|
+
outlineVariant: '#CAC4D0',
|
|
104
|
+
error: '#BA1A1A',
|
|
105
|
+
shadow: '#00000040',
|
|
106
|
+
toolbarBg: '#6750A4',
|
|
107
|
+
toolbarText: '#FFFFFF',
|
|
108
|
+
viewerBg: '#F7F2FA',
|
|
109
|
+
thumbsBg: '#EFE9F4',
|
|
110
|
+
thumbBorder: '#CAC4D0',
|
|
111
|
+
thumbActive: '#6750A4',
|
|
112
|
+
pageBox: '#FFFFFF',
|
|
113
|
+
pageShadow: '#00000026',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Couleurs Cupertino
|
|
117
|
+
this.cupertinoColors = {
|
|
118
|
+
primary: '#007AFF',
|
|
119
|
+
onPrimary: '#FFFFFF',
|
|
120
|
+
surface: '#FFFFFF',
|
|
121
|
+
error: '#FF3B30',
|
|
122
|
+
toolbarBg: '#F2F2F7',
|
|
123
|
+
toolbarText: '#000000',
|
|
124
|
+
toolbarBorder:'#C6C6C8',
|
|
125
|
+
viewerBg: '#D1D1D6',
|
|
126
|
+
thumbsBg: '#F2F2F7',
|
|
127
|
+
thumbBorder: '#C6C6C8',
|
|
128
|
+
thumbActive: '#007AFF',
|
|
129
|
+
pageBox: '#FFFFFF',
|
|
130
|
+
pageShadow: '#00000026',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get _colors() {
|
|
135
|
+
return this.platform === 'material' ? this.m3Colors : this.cupertinoColors;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get _primary() {
|
|
139
|
+
return this.primaryColor || this._colors.primary;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Chargement PDF.js ────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Charge PDF.js depuis CDN si non disponible
|
|
146
|
+
* @returns {Promise<Object>} pdfjsLib
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
async _loadPDFJS() {
|
|
150
|
+
if (window.pdfjsLib) return window.pdfjsLib;
|
|
151
|
+
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const script = document.createElement('script');
|
|
154
|
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
|
|
155
|
+
script.onload = () => {
|
|
156
|
+
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
|
|
157
|
+
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
|
158
|
+
resolve(window.pdfjsLib);
|
|
159
|
+
};
|
|
160
|
+
script.onerror = () => reject(new Error('Impossible de charger PDF.js'));
|
|
161
|
+
document.head.appendChild(script);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Montage DOM ─────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Monte le composant DOM complet sur le canvas
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
_mount() {
|
|
172
|
+
if (this._mounted) return;
|
|
173
|
+
this._mounted = true;
|
|
174
|
+
|
|
175
|
+
const canvas = this.framework.canvas;
|
|
176
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
177
|
+
const isMat = this.platform === 'material';
|
|
178
|
+
const colors = this._colors;
|
|
179
|
+
|
|
180
|
+
// Conteneur principal
|
|
181
|
+
this._containerEl = document.createElement('div');
|
|
182
|
+
this._containerEl.style.cssText = `
|
|
183
|
+
position: fixed;
|
|
184
|
+
left: ${canvasRect.left + this.x}px;
|
|
185
|
+
top: ${canvasRect.top + this.y}px;
|
|
186
|
+
width: ${this.width}px;
|
|
187
|
+
height: ${this.height}px;
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
z-index: 1000;
|
|
191
|
+
overflow: hidden;
|
|
192
|
+
border-radius: ${isMat ? '4px' : '12px'};
|
|
193
|
+
background: ${this.backgroundColor || colors.viewerBg};
|
|
194
|
+
box-shadow: ${isMat ? '0 4px 16px ' + colors.shadow : '0 2px 12px ' + colors.pageShadow};
|
|
195
|
+
box-sizing: border-box;
|
|
196
|
+
font-family: ${isMat ? "'Roboto', sans-serif" : "-apple-system, BlinkMacSystemFont, sans-serif"};
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
// Toolbar
|
|
200
|
+
if (this.showToolbar) {
|
|
201
|
+
this._toolbarEl = this._buildToolbar();
|
|
202
|
+
this._containerEl.appendChild(this._toolbarEl);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Barre de recherche
|
|
206
|
+
this._searchBarEl = this._buildSearchBar();
|
|
207
|
+
this._containerEl.appendChild(this._searchBarEl);
|
|
208
|
+
|
|
209
|
+
// Corps principal (miniatures + visionneuse)
|
|
210
|
+
const body = document.createElement('div');
|
|
211
|
+
body.style.cssText = `
|
|
212
|
+
display: flex;
|
|
213
|
+
flex: 1;
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
// Panneau miniatures
|
|
218
|
+
if (this.showThumbnails) {
|
|
219
|
+
this._thumbsEl = this._buildThumbnailsPanel();
|
|
220
|
+
body.appendChild(this._thumbsEl);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Zone de visualisation
|
|
224
|
+
this._viewerEl = document.createElement('div');
|
|
225
|
+
this._viewerEl.style.cssText = `
|
|
226
|
+
flex: 1;
|
|
227
|
+
overflow: auto;
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
align-items: center;
|
|
231
|
+
padding: 16px;
|
|
232
|
+
gap: 12px;
|
|
233
|
+
background: ${colors.viewerBg};
|
|
234
|
+
box-sizing: border-box;
|
|
235
|
+
scroll-behavior: smooth;
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
// Événements scroll → sync page courante
|
|
239
|
+
this._viewerEl.addEventListener('scroll', () => this._onViewerScroll());
|
|
240
|
+
|
|
241
|
+
// Zoom à la molette
|
|
242
|
+
this._viewerEl.addEventListener('wheel', (e) => {
|
|
243
|
+
if (e.ctrlKey || e.metaKey) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
246
|
+
this._setScale(this.scale + delta);
|
|
247
|
+
}
|
|
248
|
+
}, { passive: false });
|
|
249
|
+
|
|
250
|
+
body.appendChild(this._viewerEl);
|
|
251
|
+
this._containerEl.appendChild(body);
|
|
252
|
+
document.body.appendChild(this._containerEl);
|
|
253
|
+
|
|
254
|
+
// Charger le PDF si src fournie
|
|
255
|
+
if (this.src) this._loadPDF(this.src);
|
|
256
|
+
else this._renderEmptyState();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Toolbar ──────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
_buildToolbar() {
|
|
262
|
+
const isMat = this.platform === 'material';
|
|
263
|
+
const colors = this._colors;
|
|
264
|
+
|
|
265
|
+
const toolbar = document.createElement('div');
|
|
266
|
+
toolbar.style.cssText = `
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
gap: ${isMat ? 4 : 2}px;
|
|
270
|
+
padding: 0 ${isMat ? 12 : 8}px;
|
|
271
|
+
height: ${this._toolbarHeight}px;
|
|
272
|
+
background: ${this._primary};
|
|
273
|
+
color: ${colors.onPrimary};
|
|
274
|
+
flex-shrink: 0;
|
|
275
|
+
box-shadow: ${isMat ? '0 2px 4px rgba(0,0,0,0.2)' : 'none'};
|
|
276
|
+
border-bottom: ${isMat ? 'none' : `1px solid ${colors.toolbarBorder || '#C6C6C8'}`};
|
|
277
|
+
overflow: hidden;
|
|
278
|
+
user-select: none;
|
|
279
|
+
-webkit-user-select: none;
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
// Titre / Nom de fichier
|
|
283
|
+
const title = document.createElement('span');
|
|
284
|
+
title.id = 'pdf_title';
|
|
285
|
+
title.style.cssText = `
|
|
286
|
+
flex: 1;
|
|
287
|
+
font-size: ${isMat ? 16 : 17}px;
|
|
288
|
+
font-weight: ${isMat ? '500' : '600'};
|
|
289
|
+
color: ${colors.onPrimary};
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
text-overflow: ellipsis;
|
|
292
|
+
white-space: nowrap;
|
|
293
|
+
`;
|
|
294
|
+
title.textContent = typeof this.src === 'string'
|
|
295
|
+
? this.src.split('/').pop() || 'Document.pdf'
|
|
296
|
+
: 'Document.pdf';
|
|
297
|
+
toolbar.appendChild(title);
|
|
298
|
+
|
|
299
|
+
// Groupe navigation
|
|
300
|
+
const navGroup = document.createElement('div');
|
|
301
|
+
navGroup.style.cssText = `display: flex; align-items: center; gap: 4px;`;
|
|
302
|
+
|
|
303
|
+
// Bouton page précédente
|
|
304
|
+
const btnPrev = this._makeToolbarBtn(this._svgChevron('left'), 'Page précédente');
|
|
305
|
+
btnPrev.id = 'pdf_btn_prev';
|
|
306
|
+
btnPrev.addEventListener('click', () => this.goToPreviousPage());
|
|
307
|
+
|
|
308
|
+
// Input page
|
|
309
|
+
this._pageInput = document.createElement('input');
|
|
310
|
+
this._pageInput.type = 'text';
|
|
311
|
+
this._pageInput.value = '1';
|
|
312
|
+
this._pageInput.style.cssText = `
|
|
313
|
+
width: 40px;
|
|
314
|
+
height: 28px;
|
|
315
|
+
text-align: center;
|
|
316
|
+
border: none;
|
|
317
|
+
border-radius: ${isMat ? 4 : 6}px;
|
|
318
|
+
background: rgba(255,255,255,0.2);
|
|
319
|
+
color: white;
|
|
320
|
+
font-size: 14px;
|
|
321
|
+
outline: none;
|
|
322
|
+
`;
|
|
323
|
+
this._pageInput.addEventListener('change', () => {
|
|
324
|
+
const p = parseInt(this._pageInput.value);
|
|
325
|
+
if (p >= 1 && p <= this.totalPages) this.goToPage(p);
|
|
326
|
+
else this._pageInput.value = this.currentPage;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
this._pageTotalLabel = document.createElement('span');
|
|
330
|
+
this._pageTotalLabel.style.cssText = `font-size: 14px; color: rgba(255,255,255,0.8);`;
|
|
331
|
+
this._pageTotalLabel.textContent = '/ -';
|
|
332
|
+
|
|
333
|
+
// Bouton page suivante
|
|
334
|
+
const btnNext = this._makeToolbarBtn(this._svgChevron('right'), 'Page suivante');
|
|
335
|
+
btnNext.id = 'pdf_btn_next';
|
|
336
|
+
btnNext.addEventListener('click', () => this.goToNextPage());
|
|
337
|
+
|
|
338
|
+
navGroup.appendChild(btnPrev);
|
|
339
|
+
navGroup.appendChild(this._pageInput);
|
|
340
|
+
navGroup.appendChild(this._pageTotalLabel);
|
|
341
|
+
navGroup.appendChild(btnNext);
|
|
342
|
+
toolbar.appendChild(navGroup);
|
|
343
|
+
|
|
344
|
+
// Séparateur
|
|
345
|
+
toolbar.appendChild(this._makeSep());
|
|
346
|
+
|
|
347
|
+
// Zoom
|
|
348
|
+
const zoomGroup = document.createElement('div');
|
|
349
|
+
zoomGroup.style.cssText = `display: flex; align-items: center; gap: 4px;`;
|
|
350
|
+
|
|
351
|
+
const btnZoomOut = this._makeToolbarBtn('−', 'Dézoomer');
|
|
352
|
+
btnZoomOut.style.fontSize = '20px';
|
|
353
|
+
btnZoomOut.addEventListener('click', () => this._setScale(this.scale - 0.25));
|
|
354
|
+
|
|
355
|
+
this._scaleLabel = document.createElement('span');
|
|
356
|
+
this._scaleLabel.style.cssText = `
|
|
357
|
+
font-size: 13px; color: rgba(255,255,255,0.9);
|
|
358
|
+
min-width: 40px; text-align: center;
|
|
359
|
+
`;
|
|
360
|
+
this._scaleLabel.textContent = '100%';
|
|
361
|
+
|
|
362
|
+
const btnZoomIn = this._makeToolbarBtn('+', 'Zoomer');
|
|
363
|
+
btnZoomIn.style.fontSize = '20px';
|
|
364
|
+
btnZoomIn.addEventListener('click', () => this._setScale(this.scale + 0.25));
|
|
365
|
+
|
|
366
|
+
// Bouton zoom automatique
|
|
367
|
+
const btnZoomFit = this._makeToolbarBtn(this._svgFit(), 'Ajuster à la page');
|
|
368
|
+
btnZoomFit.addEventListener('click', () => this._fitToWidth());
|
|
369
|
+
|
|
370
|
+
zoomGroup.appendChild(btnZoomOut);
|
|
371
|
+
zoomGroup.appendChild(this._scaleLabel);
|
|
372
|
+
zoomGroup.appendChild(btnZoomIn);
|
|
373
|
+
zoomGroup.appendChild(btnZoomFit);
|
|
374
|
+
toolbar.appendChild(zoomGroup);
|
|
375
|
+
|
|
376
|
+
// Outils supplémentaires
|
|
377
|
+
if (this.allowRotate || this.allowSearch || this.allowDownload || this.allowPrint || this.allowFullscreen) {
|
|
378
|
+
toolbar.appendChild(this._makeSep());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.allowRotate) {
|
|
382
|
+
const btnRotate = this._makeToolbarBtn(this._svgRotate(), 'Rotation 90°');
|
|
383
|
+
btnRotate.addEventListener('click', () => this._rotate());
|
|
384
|
+
toolbar.appendChild(btnRotate);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (this.allowSearch) {
|
|
388
|
+
const btnSearch = this._makeToolbarBtn(this._svgSearch(), 'Rechercher');
|
|
389
|
+
btnSearch.addEventListener('click', () => this._toggleSearch());
|
|
390
|
+
toolbar.appendChild(btnSearch);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (this.allowDownload) {
|
|
394
|
+
const btnDl = this._makeToolbarBtn(this._svgDownload(), 'Télécharger');
|
|
395
|
+
btnDl.addEventListener('click', () => this._download());
|
|
396
|
+
toolbar.appendChild(btnDl);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (this.allowPrint) {
|
|
400
|
+
const btnPrint = this._makeToolbarBtn(this._svgPrint(), 'Imprimer');
|
|
401
|
+
btnPrint.addEventListener('click', () => this._print());
|
|
402
|
+
toolbar.appendChild(btnPrint);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (this.allowFullscreen) {
|
|
406
|
+
const btnFs = this._makeToolbarBtn(this._svgFullscreen(), 'Plein écran');
|
|
407
|
+
btnFs.addEventListener('click', () => this._toggleFullscreen());
|
|
408
|
+
toolbar.appendChild(btnFs);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return toolbar;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
_makeToolbarBtn(content, title) {
|
|
415
|
+
const btn = document.createElement('button');
|
|
416
|
+
btn.innerHTML = content;
|
|
417
|
+
btn.title = title;
|
|
418
|
+
btn.style.cssText = `
|
|
419
|
+
display: inline-flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
justify-content: center;
|
|
422
|
+
width: 34px;
|
|
423
|
+
height: 34px;
|
|
424
|
+
border: none;
|
|
425
|
+
border-radius: ${this.platform === 'material' ? 4 : 6}px;
|
|
426
|
+
background: transparent;
|
|
427
|
+
color: white;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
padding: 0;
|
|
430
|
+
transition: background 0.15s;
|
|
431
|
+
flex-shrink: 0;
|
|
432
|
+
`;
|
|
433
|
+
btn.addEventListener('mouseenter', () => btn.style.background = 'rgba(255,255,255,0.2)');
|
|
434
|
+
btn.addEventListener('mouseleave', () => btn.style.background = 'transparent');
|
|
435
|
+
return btn;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
_makeSep() {
|
|
439
|
+
const sep = document.createElement('div');
|
|
440
|
+
sep.style.cssText = `width:1px; height:24px; background:rgba(255,255,255,0.3); margin:0 4px; flex-shrink:0;`;
|
|
441
|
+
return sep;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ─── Barre de recherche ───────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
_buildSearchBar() {
|
|
447
|
+
const isMat = this.platform === 'material';
|
|
448
|
+
const bar = document.createElement('div');
|
|
449
|
+
bar.style.cssText = `
|
|
450
|
+
display: none;
|
|
451
|
+
align-items: center;
|
|
452
|
+
gap: 8px;
|
|
453
|
+
padding: 8px 12px;
|
|
454
|
+
background: ${isMat ? this.m3Colors.surfaceVariant : '#F2F2F7'};
|
|
455
|
+
border-bottom: 1px solid ${isMat ? this.m3Colors.outlineVariant : '#C6C6C8'};
|
|
456
|
+
flex-shrink: 0;
|
|
457
|
+
`;
|
|
458
|
+
|
|
459
|
+
const input = document.createElement('input');
|
|
460
|
+
input.type = 'text';
|
|
461
|
+
input.placeholder = 'Rechercher dans le document...';
|
|
462
|
+
input.style.cssText = `
|
|
463
|
+
flex: 1;
|
|
464
|
+
border: 1px solid ${isMat ? this.m3Colors.outline : '#C6C6C8'};
|
|
465
|
+
border-radius: ${isMat ? 4 : 8}px;
|
|
466
|
+
padding: 6px 10px;
|
|
467
|
+
font-size: 14px;
|
|
468
|
+
outline: none;
|
|
469
|
+
background: white;
|
|
470
|
+
`;
|
|
471
|
+
|
|
472
|
+
const btnClose = document.createElement('button');
|
|
473
|
+
btnClose.innerHTML = '✕';
|
|
474
|
+
btnClose.style.cssText = `
|
|
475
|
+
border: none; background: transparent; cursor: pointer;
|
|
476
|
+
font-size: 16px; color: ${isMat ? this.m3Colors.onSurfaceVariant : '#8E8E93'};
|
|
477
|
+
`;
|
|
478
|
+
btnClose.addEventListener('click', () => this._toggleSearch());
|
|
479
|
+
|
|
480
|
+
bar.appendChild(input);
|
|
481
|
+
bar.appendChild(btnClose);
|
|
482
|
+
this._searchInput = input;
|
|
483
|
+
return bar;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
_toggleSearch() {
|
|
487
|
+
this._searchOpen = !this._searchOpen;
|
|
488
|
+
this._searchBarEl.style.display = this._searchOpen ? 'flex' : 'none';
|
|
489
|
+
if (this._searchOpen) this._searchInput.focus();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ─── Panneau miniatures ───────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
_buildThumbnailsPanel() {
|
|
495
|
+
const isMat = this.platform === 'material';
|
|
496
|
+
const panel = document.createElement('div');
|
|
497
|
+
panel.style.cssText = `
|
|
498
|
+
width: ${this._thumbsWidth}px;
|
|
499
|
+
height: 100%;
|
|
500
|
+
overflow-y: auto;
|
|
501
|
+
background: ${this._colors.thumbsBg};
|
|
502
|
+
border-right: 1px solid ${this._colors.thumbBorder};
|
|
503
|
+
padding: 8px;
|
|
504
|
+
box-sizing: border-box;
|
|
505
|
+
flex-shrink: 0;
|
|
506
|
+
display: flex;
|
|
507
|
+
flex-direction: column;
|
|
508
|
+
gap: 8px;
|
|
509
|
+
align-items: center;
|
|
510
|
+
scrollbar-width: thin;
|
|
511
|
+
`;
|
|
512
|
+
return panel;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─── Chargement PDF ───────────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Charge et affiche un PDF
|
|
519
|
+
* @param {string|Uint8Array} src
|
|
520
|
+
*/
|
|
521
|
+
async _loadPDF(src) {
|
|
522
|
+
this.loading = true;
|
|
523
|
+
this.error = null;
|
|
524
|
+
this._renderLoadingState();
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const pdfjsLib = await this._loadPDFJS();
|
|
528
|
+
const loadingTask = pdfjsLib.getDocument(
|
|
529
|
+
typeof src === 'string' ? src : { data: src }
|
|
530
|
+
);
|
|
531
|
+
this._pdfDoc = await loadingTask.promise;
|
|
532
|
+
this.totalPages = this._pdfDoc.numPages;
|
|
533
|
+
this.loading = false;
|
|
534
|
+
|
|
535
|
+
// Met à jour toolbar
|
|
536
|
+
if (this._pageTotalLabel) this._pageTotalLabel.textContent = `/ ${this.totalPages}`;
|
|
537
|
+
|
|
538
|
+
// Met à jour le titre si URL
|
|
539
|
+
if (typeof src === 'string' && this._containerEl) {
|
|
540
|
+
const t = this._containerEl.querySelector('#pdf_title');
|
|
541
|
+
if (t) t.textContent = src.split('/').pop();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Vide la visionneuse
|
|
545
|
+
this._viewerEl.innerHTML = '';
|
|
546
|
+
|
|
547
|
+
// Rend toutes les pages
|
|
548
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
549
|
+
await this._renderPage(p);
|
|
550
|
+
if (this.showThumbnails) await this._renderThumbnail(p);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Aller à la page initiale
|
|
554
|
+
this.scrollToPage(this.currentPage);
|
|
555
|
+
this.onLoad(this.totalPages);
|
|
556
|
+
|
|
557
|
+
} catch (err) {
|
|
558
|
+
this.loading = false;
|
|
559
|
+
this.error = err.message || 'Erreur lors du chargement du PDF';
|
|
560
|
+
this._renderErrorState();
|
|
561
|
+
this.onErrorCb(this.error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Rend une page PDF dans la zone de visualisation
|
|
567
|
+
* @param {number} pageNum
|
|
568
|
+
* @private
|
|
569
|
+
*/
|
|
570
|
+
async _renderPage(pageNum) {
|
|
571
|
+
const page = await this._pdfDoc.getPage(pageNum);
|
|
572
|
+
const viewport = page.getViewport({ scale: this.scale, rotation: this.rotation });
|
|
573
|
+
|
|
574
|
+
// Wrapper de page
|
|
575
|
+
const pageWrapper = document.createElement('div');
|
|
576
|
+
pageWrapper.id = `pdf_page_${pageNum}`;
|
|
577
|
+
pageWrapper.dataset.page = pageNum;
|
|
578
|
+
pageWrapper.style.cssText = `
|
|
579
|
+
position: relative;
|
|
580
|
+
box-shadow: 0 2px 8px ${this._colors.pageShadow};
|
|
581
|
+
flex-shrink: 0;
|
|
582
|
+
background: ${this.pageBackground};
|
|
583
|
+
line-height: 0;
|
|
584
|
+
`;
|
|
585
|
+
|
|
586
|
+
const pageCanvas = document.createElement('canvas');
|
|
587
|
+
pageCanvas.width = viewport.width;
|
|
588
|
+
pageCanvas.height = viewport.height;
|
|
589
|
+
pageCanvas.style.cssText = `display: block; max-width: 100%;`;
|
|
590
|
+
|
|
591
|
+
pageWrapper.appendChild(pageCanvas);
|
|
592
|
+
|
|
593
|
+
// Numéro de page (en bas)
|
|
594
|
+
const pageLabel = document.createElement('div');
|
|
595
|
+
pageLabel.style.cssText = `
|
|
596
|
+
position: absolute;
|
|
597
|
+
bottom: 6px;
|
|
598
|
+
right: 10px;
|
|
599
|
+
font-size: 11px;
|
|
600
|
+
color: #888;
|
|
601
|
+
line-height: 1;
|
|
602
|
+
pointer-events: none;
|
|
603
|
+
`;
|
|
604
|
+
pageLabel.textContent = `${pageNum} / ${this.totalPages}`;
|
|
605
|
+
pageWrapper.appendChild(pageLabel);
|
|
606
|
+
|
|
607
|
+
this._viewerEl.appendChild(pageWrapper);
|
|
608
|
+
this._pageCanvases[pageNum] = pageCanvas;
|
|
609
|
+
|
|
610
|
+
const ctx = pageCanvas.getContext('2d');
|
|
611
|
+
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Rend une miniature de page
|
|
616
|
+
* @param {number} pageNum
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
async _renderThumbnail(pageNum) {
|
|
620
|
+
if (!this._thumbsEl) return;
|
|
621
|
+
const page = await this._pdfDoc.getPage(pageNum);
|
|
622
|
+
const viewport = page.getViewport({ scale: 0.2 });
|
|
623
|
+
|
|
624
|
+
const thumbWrapper = document.createElement('div');
|
|
625
|
+
thumbWrapper.id = `pdf_thumb_${pageNum}`;
|
|
626
|
+
thumbWrapper.dataset.page = pageNum;
|
|
627
|
+
thumbWrapper.style.cssText = `
|
|
628
|
+
cursor: pointer;
|
|
629
|
+
border: 2px solid ${pageNum === this.currentPage ? this._colors.thumbActive : this._colors.thumbBorder};
|
|
630
|
+
border-radius: 4px;
|
|
631
|
+
overflow: hidden;
|
|
632
|
+
transition: border-color 0.2s;
|
|
633
|
+
`;
|
|
634
|
+
thumbWrapper.addEventListener('click', () => {
|
|
635
|
+
this.goToPage(pageNum);
|
|
636
|
+
this._highlightThumbnail(pageNum);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const thumbCanvas = document.createElement('canvas');
|
|
640
|
+
thumbCanvas.width = viewport.width;
|
|
641
|
+
thumbCanvas.height = viewport.height;
|
|
642
|
+
thumbCanvas.style.cssText = `display: block; width: 100%;`;
|
|
643
|
+
thumbWrapper.appendChild(thumbCanvas);
|
|
644
|
+
|
|
645
|
+
const numLabel = document.createElement('div');
|
|
646
|
+
numLabel.style.cssText = `
|
|
647
|
+
text-align: center;
|
|
648
|
+
font-size: 10px;
|
|
649
|
+
color: #666;
|
|
650
|
+
padding: 2px;
|
|
651
|
+
background: white;
|
|
652
|
+
`;
|
|
653
|
+
numLabel.textContent = pageNum;
|
|
654
|
+
thumbWrapper.appendChild(numLabel);
|
|
655
|
+
|
|
656
|
+
this._thumbsEl.appendChild(thumbWrapper);
|
|
657
|
+
this._thumbCanvases[pageNum] = thumbWrapper;
|
|
658
|
+
|
|
659
|
+
const ctx = thumbCanvas.getContext('2d');
|
|
660
|
+
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Met en surbrillance la miniature de la page active
|
|
665
|
+
* @param {number} pageNum
|
|
666
|
+
* @private
|
|
667
|
+
*/
|
|
668
|
+
_highlightThumbnail(pageNum) {
|
|
669
|
+
if (!this._thumbsEl) return;
|
|
670
|
+
this._thumbsEl.querySelectorAll('[data-page]').forEach(el => {
|
|
671
|
+
el.style.borderColor = this._colors.thumbBorder;
|
|
672
|
+
});
|
|
673
|
+
const active = this._thumbsEl.querySelector(`#pdf_thumb_${pageNum}`);
|
|
674
|
+
if (active) active.style.borderColor = this._colors.thumbActive;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Détecte la page visible lors du scroll
|
|
679
|
+
* @private
|
|
680
|
+
*/
|
|
681
|
+
_onViewerScroll() {
|
|
682
|
+
if (!this._viewerEl) return;
|
|
683
|
+
const scrollTop = this._viewerEl.scrollTop + this._viewerEl.clientHeight / 2;
|
|
684
|
+
let nearest = 1;
|
|
685
|
+
let nearestDist = Infinity;
|
|
686
|
+
|
|
687
|
+
this._viewerEl.querySelectorAll('[data-page]').forEach(el => {
|
|
688
|
+
const p = parseInt(el.dataset.page);
|
|
689
|
+
const dist = Math.abs(el.offsetTop - scrollTop);
|
|
690
|
+
if (dist < nearestDist) { nearestDist = dist; nearest = p; }
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (nearest !== this.currentPage) {
|
|
694
|
+
this.currentPage = nearest;
|
|
695
|
+
if (this._pageInput) this._pageInput.value = nearest;
|
|
696
|
+
this.onPageChange(this.currentPage, this.totalPages);
|
|
697
|
+
this._highlightThumbnail(nearest);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ─── États visuels (vide, chargement, erreur) ─────────────────────────────────
|
|
702
|
+
|
|
703
|
+
_renderEmptyState() {
|
|
704
|
+
this._viewerEl.innerHTML = '';
|
|
705
|
+
const wrap = document.createElement('div');
|
|
706
|
+
wrap.style.cssText = `
|
|
707
|
+
display: flex; flex-direction: column; align-items: center;
|
|
708
|
+
justify-content: center; height: 100%; gap: 16px; opacity: 0.5;
|
|
709
|
+
`;
|
|
710
|
+
wrap.innerHTML = `
|
|
711
|
+
<svg width="64" height="64" viewBox="0 0 24 24" fill="${this._colors.onSurfaceVariant || '#49454F'}">
|
|
712
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/>
|
|
713
|
+
<polyline points="14,2 14,8 20,8" fill="none" stroke="white" stroke-width="1.5"/>
|
|
714
|
+
<text x="7" y="19" font-size="5" fill="white" font-weight="bold">PDF</text>
|
|
715
|
+
</svg>
|
|
716
|
+
<p style="margin:0; font-size:15px; color:${this._colors.onSurfaceVariant || '#49454F'}">
|
|
717
|
+
Aucun document chargé
|
|
718
|
+
</p>
|
|
719
|
+
`;
|
|
720
|
+
this._viewerEl.appendChild(wrap);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
_renderLoadingState() {
|
|
724
|
+
this._viewerEl.innerHTML = '';
|
|
725
|
+
const wrap = document.createElement('div');
|
|
726
|
+
wrap.style.cssText = `
|
|
727
|
+
display: flex; flex-direction: column; align-items: center;
|
|
728
|
+
justify-content: center; height: 100%; gap: 16px;
|
|
729
|
+
`;
|
|
730
|
+
|
|
731
|
+
const spinner = document.createElement('div');
|
|
732
|
+
spinner.style.cssText = `
|
|
733
|
+
width: 40px; height: 40px;
|
|
734
|
+
border: 4px solid ${this._colors.thumbBorder};
|
|
735
|
+
border-top-color: ${this._primary};
|
|
736
|
+
border-radius: 50%;
|
|
737
|
+
animation: pdf_spin 0.8s linear infinite;
|
|
738
|
+
`;
|
|
739
|
+
|
|
740
|
+
if (!document.querySelector('#pdf_spinner_style')) {
|
|
741
|
+
const s = document.createElement('style');
|
|
742
|
+
s.id = 'pdf_spinner_style';
|
|
743
|
+
s.textContent = `@keyframes pdf_spin { to { transform: rotate(360deg); } }`;
|
|
744
|
+
document.head.appendChild(s);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const label = document.createElement('p');
|
|
748
|
+
label.style.cssText = `margin:0; font-size:15px; color:${this._colors.onSurfaceVariant || '#49454F'};`;
|
|
749
|
+
label.textContent = 'Chargement du document...';
|
|
750
|
+
|
|
751
|
+
wrap.appendChild(spinner);
|
|
752
|
+
wrap.appendChild(label);
|
|
753
|
+
this._viewerEl.appendChild(wrap);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
_renderErrorState() {
|
|
757
|
+
this._viewerEl.innerHTML = '';
|
|
758
|
+
const wrap = document.createElement('div');
|
|
759
|
+
wrap.style.cssText = `
|
|
760
|
+
display: flex; flex-direction: column; align-items: center;
|
|
761
|
+
justify-content: center; height: 100%; gap: 16px; padding: 24px;
|
|
762
|
+
`;
|
|
763
|
+
wrap.innerHTML = `
|
|
764
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="${this._colors.error || '#BA1A1A'}">
|
|
765
|
+
<circle cx="12" cy="12" r="10"/>
|
|
766
|
+
<line x1="12" y1="8" x2="12" y2="12" stroke="white" stroke-width="2"/>
|
|
767
|
+
<circle cx="12" cy="16" r="1" fill="white"/>
|
|
768
|
+
</svg>
|
|
769
|
+
<p style="margin:0; font-size:15px; color:${this._colors.error || '#BA1A1A'}; text-align:center;">
|
|
770
|
+
${this.error}
|
|
771
|
+
</p>
|
|
772
|
+
`;
|
|
773
|
+
|
|
774
|
+
// Bouton réessayer
|
|
775
|
+
const retryBtn = document.createElement('button');
|
|
776
|
+
retryBtn.textContent = 'Réessayer';
|
|
777
|
+
retryBtn.style.cssText = `
|
|
778
|
+
padding: 8px 20px;
|
|
779
|
+
background: ${this._primary};
|
|
780
|
+
color: white;
|
|
781
|
+
border: none;
|
|
782
|
+
border-radius: ${this.platform === 'material' ? 20 : 8}px;
|
|
783
|
+
cursor: pointer;
|
|
784
|
+
font-size: 14px;
|
|
785
|
+
`;
|
|
786
|
+
retryBtn.addEventListener('click', () => {
|
|
787
|
+
if (this.src) this._loadPDF(this.src);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
wrap.appendChild(retryBtn);
|
|
791
|
+
this._viewerEl.appendChild(wrap);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ─── Actions ──────────────────────────────────────────────────────────────────
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Change le niveau de zoom
|
|
798
|
+
* @param {number} newScale
|
|
799
|
+
* @private
|
|
800
|
+
*/
|
|
801
|
+
_setScale(newScale) {
|
|
802
|
+
this.scale = Math.max(this.minScale, Math.min(this.maxScale, newScale));
|
|
803
|
+
this.scale = Math.round(this.scale * 100) / 100;
|
|
804
|
+
if (this._scaleLabel) this._scaleLabel.textContent = `${Math.round(this.scale * 100)}%`;
|
|
805
|
+
this.onScaleChange(this.scale);
|
|
806
|
+
if (this._pdfDoc) this._reRenderAll();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Ajuste le zoom à la largeur du viewer
|
|
811
|
+
* @private
|
|
812
|
+
*/
|
|
813
|
+
async _fitToWidth() {
|
|
814
|
+
if (!this._pdfDoc) return;
|
|
815
|
+
const page = await this._pdfDoc.getPage(1);
|
|
816
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
817
|
+
const availW = this._viewerEl.clientWidth - 32;
|
|
818
|
+
const ratio = availW / viewport.width;
|
|
819
|
+
this._setScale(ratio);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Re-rend toutes les pages (après zoom ou rotation)
|
|
824
|
+
* @private
|
|
825
|
+
*/
|
|
826
|
+
async _reRenderAll() {
|
|
827
|
+
this._viewerEl.innerHTML = '';
|
|
828
|
+
this._pageCanvases = {};
|
|
829
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
830
|
+
await this._renderPage(p);
|
|
831
|
+
}
|
|
832
|
+
this.scrollToPage(this.currentPage);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Effectue une rotation de 90°
|
|
837
|
+
* @private
|
|
838
|
+
*/
|
|
839
|
+
_rotate() {
|
|
840
|
+
this.rotation = (this.rotation + 90) % 360;
|
|
841
|
+
if (this._pdfDoc) this._reRenderAll();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Télécharge le PDF
|
|
846
|
+
* @private
|
|
847
|
+
*/
|
|
848
|
+
_download() {
|
|
849
|
+
if (!this.src || typeof this.src !== 'string') return;
|
|
850
|
+
const a = document.createElement('a');
|
|
851
|
+
a.href = this.src;
|
|
852
|
+
a.download = this.src.split('/').pop() || 'document.pdf';
|
|
853
|
+
a.click();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Imprime le PDF
|
|
858
|
+
* @private
|
|
859
|
+
*/
|
|
860
|
+
_print() {
|
|
861
|
+
if (!this.src || typeof this.src !== 'string') return;
|
|
862
|
+
const w = window.open(this.src);
|
|
863
|
+
if (w) w.addEventListener('load', () => w.print());
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Bascule en plein écran
|
|
868
|
+
* @private
|
|
869
|
+
*/
|
|
870
|
+
_toggleFullscreen() {
|
|
871
|
+
if (!this._containerEl) return;
|
|
872
|
+
this._fullscreen = !this._fullscreen;
|
|
873
|
+
if (this._fullscreen) {
|
|
874
|
+
this._savedStyle = {
|
|
875
|
+
left: this._containerEl.style.left,
|
|
876
|
+
top: this._containerEl.style.top,
|
|
877
|
+
width:this._containerEl.style.width,
|
|
878
|
+
height:this._containerEl.style.height,
|
|
879
|
+
zIndex:this._containerEl.style.zIndex,
|
|
880
|
+
borderRadius: this._containerEl.style.borderRadius,
|
|
881
|
+
};
|
|
882
|
+
this._containerEl.style.cssText += `
|
|
883
|
+
left: 0 !important;
|
|
884
|
+
top: 0 !important;
|
|
885
|
+
width: 100vw !important;
|
|
886
|
+
height: 100vh !important;
|
|
887
|
+
z-index: 99999 !important;
|
|
888
|
+
border-radius: 0 !important;
|
|
889
|
+
`;
|
|
890
|
+
} else {
|
|
891
|
+
if (this._savedStyle) {
|
|
892
|
+
Object.assign(this._containerEl.style, this._savedStyle);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Navigue vers une page spécifique
|
|
901
|
+
* @param {number} page
|
|
902
|
+
*/
|
|
903
|
+
goToPage(page) {
|
|
904
|
+
if (!this._pdfDoc || page < 1 || page > this.totalPages) return;
|
|
905
|
+
this.currentPage = page;
|
|
906
|
+
if (this._pageInput) this._pageInput.value = page;
|
|
907
|
+
this.scrollToPage(page);
|
|
908
|
+
this._highlightThumbnail(page);
|
|
909
|
+
this.onPageChange(this.currentPage, this.totalPages);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Fait défiler jusqu'à une page
|
|
914
|
+
* @param {number} page
|
|
915
|
+
*/
|
|
916
|
+
scrollToPage(page) {
|
|
917
|
+
const el = this._viewerEl && this._viewerEl.querySelector(`#pdf_page_${page}`);
|
|
918
|
+
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Page précédente
|
|
923
|
+
*/
|
|
924
|
+
goToPreviousPage() {
|
|
925
|
+
if (this.currentPage > 1) this.goToPage(this.currentPage - 1);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Page suivante
|
|
930
|
+
*/
|
|
931
|
+
goToNextPage() {
|
|
932
|
+
if (this.currentPage < this.totalPages) this.goToPage(this.currentPage + 1);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// ─── API publique ─────────────────────────────────────────────────────────────
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Charge un nouveau document PDF
|
|
939
|
+
* @param {string|Uint8Array} src - URL ou données binaires
|
|
940
|
+
*/
|
|
941
|
+
load(src) {
|
|
942
|
+
this.src = src;
|
|
943
|
+
if (this._viewerEl) {
|
|
944
|
+
this._pdfDoc = null;
|
|
945
|
+
this._pageCanvases = {};
|
|
946
|
+
this._thumbCanvases = {};
|
|
947
|
+
if (this._thumbsEl) this._thumbsEl.innerHTML = '';
|
|
948
|
+
this._loadPDF(src);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Zoom à un niveau précis
|
|
954
|
+
* @param {number} scale - Facteur de zoom (ex: 1.5 = 150%)
|
|
955
|
+
*/
|
|
956
|
+
setScale(scale) {
|
|
957
|
+
this._setScale(scale);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Retourne les métadonnées du document
|
|
962
|
+
* @returns {Promise<Object>}
|
|
963
|
+
*/
|
|
964
|
+
async getMetadata() {
|
|
965
|
+
if (!this._pdfDoc) return null;
|
|
966
|
+
return this._pdfDoc.getMetadata();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ─── SVG Icons ────────────────────────────────────────────────────────────────
|
|
970
|
+
|
|
971
|
+
_svgChevron(dir) {
|
|
972
|
+
const pts = dir === 'left' ? '15 18 9 12 15 6' : '9 18 15 12 9 6';
|
|
973
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round">
|
|
974
|
+
<polyline points="${pts}"/>
|
|
975
|
+
</svg>`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
_svgFit() {
|
|
979
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round">
|
|
980
|
+
<polyline points="15,3 21,3 21,9"/><polyline points="9,21 3,21 3,15"/>
|
|
981
|
+
<line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>
|
|
982
|
+
</svg>`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
_svgRotate() {
|
|
986
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round">
|
|
987
|
+
<path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 .49-4.46"/>
|
|
988
|
+
</svg>`;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
_svgSearch() {
|
|
992
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round">
|
|
993
|
+
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
994
|
+
</svg>`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
_svgDownload() {
|
|
998
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round">
|
|
999
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
1000
|
+
<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>
|
|
1001
|
+
</svg>`;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
_svgPrint() {
|
|
1005
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round">
|
|
1006
|
+
<polyline points="6 9 6 2 18 2 18 9"/>
|
|
1007
|
+
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/>
|
|
1008
|
+
<rect x="6" y="14" width="12" height="8"/>
|
|
1009
|
+
</svg>`;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
_svgFullscreen() {
|
|
1013
|
+
return `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round">
|
|
1014
|
+
<polyline points="15 3 21 3 21 9"/><polyline points="9 3 3 3 3 9"/>
|
|
1015
|
+
<polyline points="21 15 21 21 15 21"/><polyline points="3 15 3 21 9 21"/>
|
|
1016
|
+
</svg>`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ─── Rendu Canvas (shell) ─────────────────────────────────────────────────────
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Rendu Canvas : monte l'overlay DOM et synchronise la position
|
|
1023
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
1024
|
+
*/
|
|
1025
|
+
draw(ctx) {
|
|
1026
|
+
ctx.save();
|
|
1027
|
+
|
|
1028
|
+
if (!this._mounted) this._mount();
|
|
1029
|
+
|
|
1030
|
+
// Synchronise la position
|
|
1031
|
+
if (this._containerEl) {
|
|
1032
|
+
const canvas = this.framework.canvas;
|
|
1033
|
+
const rect = canvas.getBoundingClientRect();
|
|
1034
|
+
this._containerEl.style.left = `${rect.left + this.x}px`;
|
|
1035
|
+
this._containerEl.style.top = `${rect.top + this.y - (this.framework.scrollOffset || 0)}px`;
|
|
1036
|
+
this._containerEl.style.width = `${this.width}px`;
|
|
1037
|
+
this._containerEl.style.height = `${this.height}px`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Fond Canvas (zone réservée) — invisible car l'overlay DOM le couvre
|
|
1041
|
+
ctx.fillStyle = this.backgroundColor || this._colors.viewerBg;
|
|
1042
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
1043
|
+
|
|
1044
|
+
ctx.restore();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Vérifie si un point est dans le composant
|
|
1049
|
+
*/
|
|
1050
|
+
isPointInside(x, y) {
|
|
1051
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
1052
|
+
y >= this.y && y <= this.y + this.height;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Nettoie les ressources
|
|
1057
|
+
*/
|
|
1058
|
+
destroy() {
|
|
1059
|
+
if (this._containerEl && this._containerEl.parentNode) {
|
|
1060
|
+
this._containerEl.parentNode.removeChild(this._containerEl);
|
|
1061
|
+
}
|
|
1062
|
+
this._pdfDoc = null;
|
|
1063
|
+
this._mounted = false;
|
|
1064
|
+
this._containerEl = null;
|
|
1065
|
+
super.destroy && super.destroy();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export default PDFViewer;
|
package/core/CanvasFramework.js
CHANGED
|
@@ -65,6 +65,7 @@ import ColorPicker from '../components/ColorPicker.js';
|
|
|
65
65
|
import Rating from '../components/Rating.js';
|
|
66
66
|
import Breadcrumb from '../components/Breadcrumb.js';
|
|
67
67
|
import Popover from '../components/Popover.js';
|
|
68
|
+
import PDFViewer from '../components/PDFViewer.js';
|
|
68
69
|
|
|
69
70
|
// Utils
|
|
70
71
|
import SafeArea from '../utils/SafeArea.js';
|
package/core/UIBuilder.js
CHANGED
|
@@ -65,6 +65,7 @@ import ColorPicker from '../components/ColorPicker.js';
|
|
|
65
65
|
import Rating from '../components/Rating.js';
|
|
66
66
|
import Breadcrumb from '../components/Breadcrumb.js';
|
|
67
67
|
import Popover from '../components/Popover.js';
|
|
68
|
+
import PDFViewer from '../components/PDFViewer.js';
|
|
68
69
|
|
|
69
70
|
// Features
|
|
70
71
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
@@ -140,6 +141,7 @@ const Components = {
|
|
|
140
141
|
PasswordInput,
|
|
141
142
|
InputTags,
|
|
142
143
|
InputDatalist,
|
|
144
|
+
PDFViewer,
|
|
143
145
|
PullToRefresh,
|
|
144
146
|
Skeleton,
|
|
145
147
|
SignaturePad,
|
package/index.js
CHANGED
|
@@ -72,6 +72,7 @@ export { default as ColorPicker } from './components/ColorPicker.js';
|
|
|
72
72
|
export { default as Rating } from './components/Rating.js';
|
|
73
73
|
export { default as Breadcrumb } from './components/Breadcrumb.js';
|
|
74
74
|
export { default as Popover } from './components/Popover.js';
|
|
75
|
+
export { default as PDFViewer } from './components/PDFViewer.js';
|
|
75
76
|
|
|
76
77
|
// Utils
|
|
77
78
|
export { default as SafeArea } from './utils/SafeArea.js';
|