canvasframework 0.5.61 → 0.5.63

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.
@@ -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;
@@ -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';
@@ -491,9 +492,10 @@ class CanvasFramework {
491
492
  }
492
493
 
493
494
  this.platform = this.detectPlatform();
494
- setTimeout(() => {
495
+ /*setTimeout(() => {
495
496
  this.initScrollWorker();
496
- }, 100);
497
+ }, 100);*/
498
+ Promise.resolve().then(() => this.initScrollWorker());
497
499
  // État actuel + préférence
498
500
  this.themeMode = options.themeMode || 'system'; // 'light', 'dark', 'system'
499
501
  this.userThemeOverride = null; // null = suit system, sinon 'light' ou 'dark'
@@ -2957,6 +2959,7 @@ class CanvasFramework {
2957
2959
 
2958
2960
  startRenderLoop() {
2959
2961
  let lastScrollOffset = this.scrollOffset;
2962
+ this._needsRender = true; // ← AJOUTER
2960
2963
 
2961
2964
  const render = () => {
2962
2965
  if (!this._splashFinished) {
@@ -3136,84 +3139,34 @@ class CanvasFramework {
3136
3139
  /**
3137
3140
  * Dessine l'effet d'overscroll (overlay gris)
3138
3141
  */
3139
- drawOverscrollEffect() {console.log('dessine');
3142
+ drawOverscrollEffect() {
3140
3143
  if (Math.abs(this.overscrollDistance) < 1) return;
3141
3144
 
3142
3145
  const ctx = this.ctx;
3143
- ctx.save();
3144
-
3145
- // Calculer l'opacité (max 0.4 pour Android)
3146
3146
  const maxOverscroll = 150;
3147
3147
  const opacity = Math.min(Math.abs(this.overscrollDistance) / maxOverscroll, 1) * 0.4;
3148
-
3149
- // Hauteur de l'overlay
3150
3148
  const overlayHeight = Math.min(Math.abs(this.overscrollDistance) * 1.2, 250);
3149
+ const isTop = this.overscrollDistance > 0;
3151
3150
 
3152
- let gradient;
3151
+ // Cache key basé sur les paramètres
3152
+ const cacheKey = `${isTop}-${opacity.toFixed(3)}-${overlayHeight.toFixed(0)}`;
3153
3153
 
3154
- // Overscroll en haut
3155
- if (this.overscrollDistance > 0) {
3156
- gradient = ctx.createLinearGradient(0, 0, 0, overlayHeight);
3157
- gradient.addColorStop(0, `rgba(100, 100, 100, ${opacity})`);
3158
- gradient.addColorStop(1, 'rgba(100, 100, 100, 0)');
3154
+ if (this._overscrollGradientCache?.key !== cacheKey) {
3155
+ const gradient = isTop
3156
+ ? ctx.createLinearGradient(0, 0, 0, overlayHeight)
3157
+ : ctx.createLinearGradient(0, this.height - overlayHeight, 0, this.height);
3159
3158
 
3160
- ctx.fillStyle = gradient;
3161
- ctx.fillRect(0, 0, this.width, overlayHeight);
3162
- }
3163
- // Overscroll en bas
3164
- else if (this.overscrollDistance < 0) {
3165
- gradient = ctx.createLinearGradient(0, this.height - overlayHeight, 0, this.height);
3166
- gradient.addColorStop(0, 'rgba(100, 100, 100, 0)');
3167
- gradient.addColorStop(1, `rgba(100, 100, 100, ${opacity})`);
3159
+ gradient.addColorStop(0, isTop ? `rgba(100,100,100,${opacity})` : 'rgba(100,100,100,0)');
3160
+ gradient.addColorStop(1, isTop ? 'rgba(100,100,100,0)' : `rgba(100,100,100,${opacity})`);
3168
3161
 
3169
- ctx.fillStyle = gradient;
3170
- ctx.fillRect(0, this.height - overlayHeight, this.width, overlayHeight);
3162
+ this._overscrollGradientCache = { key: cacheKey, gradient, overlayHeight, isTop };
3171
3163
  }
3172
3164
 
3165
+ ctx.save();
3166
+ ctx.fillStyle = this._overscrollGradientCache.gradient;
3167
+ ctx.fillRect(0, isTop ? 0 : this.height - overlayHeight, this.width, overlayHeight);
3173
3168
  ctx.restore();
3174
3169
  }
3175
-
3176
- /**
3177
- * Rendu normal (sans transition)
3178
- */
3179
- renderFull() {
3180
- this.ctx.save();
3181
-
3182
- // Séparer les composants fixes et scrollables
3183
- const scrollableComponents = [];
3184
- const fixedComponents = [];
3185
-
3186
- for (let comp of this.components) {
3187
- if (this.isFixedComponent(comp)) {
3188
- fixedComponents.push(comp);
3189
- } else {
3190
- scrollableComponents.push(comp);
3191
- }
3192
- }
3193
-
3194
- // Dessiner les composants scrollables
3195
- if (scrollableComponents.length > 0) {
3196
- this.ctx.save();
3197
- this.ctx.translate(0, this.scrollOffset);
3198
-
3199
- for (let comp of scrollableComponents) {
3200
- if (comp.visible) {
3201
- comp.draw(this.ctx);
3202
- }
3203
- }
3204
-
3205
- this.ctx.restore();
3206
- }
3207
-
3208
- // Dessiner les composants fixes
3209
- for (let comp of fixedComponents) {
3210
- if (comp.visible) {
3211
- comp.draw(this.ctx);
3212
- }
3213
- }
3214
-
3215
- this.ctx.restore();
3216
- }
3217
3170
 
3218
3171
  /**
3219
3172
  * Mettre à jour la progression de la transition
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.61",
3
+ "version": "0.5.63",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"