canvasframework 0.5.65 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/components/PDFViewer.js +1026 -722
  2. package/package.json +1 -1
@@ -9,745 +9,1049 @@ import Component from '../core/Component.js';
9
9
  */
10
10
  class PDFViewer extends Component {
11
11
 
12
- constructor(framework, options = {}) {
13
- super(framework, options);
14
-
15
- this.platform = framework.platform;
16
- this.src = options.src || null;
17
- this.currentPage = options.initialPage || 1;
18
- this.totalPages = 0;
19
- this.scale = options.initialScale || 1.0;
20
- this.rotation = 0;
21
- this.loading = false;
22
- this.error = null;
23
-
24
- this.showToolbar = options.showToolbar !== false;
25
- this.allowZoom = options.allowZoom !== false;
26
- this.allowNavigation = options.allowNavigation !== false;
27
- this.allowDownload = options.allowDownload !== false;
28
- this.allowPrint = options.allowPrint !== false;
29
- this.allowRotate = options.allowRotate !== false;
30
- this.allowSearch = options.allowSearch !== false;
31
- this.minScale = options.minScale || 0.25;
32
- this.maxScale = options.maxScale || 5.0;
33
- this.backgroundColor = options.backgroundColor || null;
34
- this.primaryColor = options.primaryColor || null;
35
- this.pageBackground = options.pageBackground || '#FFFFFF';
36
-
37
- this.onPageChange = options.onPageChange || (() => {});
38
- this.onScaleChange = options.onScaleChange || (() => {});
39
- this.onLoad = options.onLoad || (() => {});
40
- this.onErrorCb = options.onError || (() => {});
41
-
42
- // Layout
43
- this._toolbarHeight = this.showToolbar ? 52 : 0;
44
- this._searchOpen = false;
45
- this._searchQuery = '';
46
- this._thumbsWidth = 110;
47
-
48
- // PDF state
49
- this._pdfDoc = null;
50
- this._pageImages = {};
51
- this._thumbImages = {};
52
-
53
- // Scroll interne du viewer
54
- this._scrollY = 0;
55
- this._maxScrollY = 0;
56
-
57
- // Drag interne (scroll des pages)
58
- this._dragging = false;
59
- this._dragStartY = 0;
60
- this._dragStartScroll = 0;
61
- this._pressX = undefined;
62
- this._pressY = undefined;
63
-
64
- // Hit areas — coordonnées MONDE (this.x + offset local)
65
- this._tbButtons = [];
66
- this._pageInputRect = null;
67
- this._retryBtn = null;
68
- this._searchCloseRect = null;
69
- this._hoveredBtn = null;
70
-
71
- // Spinner
72
- this._spinAngle = 0;
73
- this._spinRAF = null;
74
-
75
- // Couleurs Material You 3
76
- this.m3Colors = {
77
- primary: '#6750A4',
78
- onPrimary: '#FFFFFF',
79
- surfaceVariant: '#E7E0EC',
80
- onSurface: '#1C1B1F',
81
- onSurfaceVariant: '#49454F',
82
- outline: '#79747E',
83
- outlineVariant: '#CAC4D0',
84
- error: '#BA1A1A',
85
- viewerBg: '#F7F2FA',
86
- thumbsBg: '#EFE9F4',
87
- thumbBorder: '#CAC4D0',
88
- thumbActive: '#6750A4',
89
- pageShadow: 'rgba(0,0,0,0.15)',
90
- };
91
-
92
- // Couleurs Cupertino
93
- this.cupertinoColors = {
94
- primary: '#007AFF',
95
- onPrimary: '#FFFFFF',
96
- error: '#FF3B30',
97
- toolbarBorder: '#C6C6C8',
98
- viewerBg: '#D1D1D6',
99
- thumbsBg: '#F2F2F7',
100
- thumbBorder: '#C6C6C8',
101
- thumbActive: '#007AFF',
102
- pageShadow: 'rgba(0,0,0,0.15)',
103
- onSurfaceVariant: '#8E8E93',
104
- outlineVariant: '#C6C6C8',
105
- outline: '#C6C6C8',
106
- };
107
-
108
- this._startSpinner();
109
- if (this.src) this._loadPDF(this.src);
110
- }
111
-
112
- // ─── Couleurs ────────────────────────────────────────────────────────────────
113
-
114
- get _colors() { return this.platform === 'material' ? this.m3Colors : this.cupertinoColors; }
115
- get _primary() { return this.primaryColor || this._colors.primary; }
116
- get _isMat() { return this.platform === 'material'; }
117
-
118
- // ─── Géométrie en coordonnées MONDE ──────────────────────────────────────────
119
-
120
- get _tbRect() {
121
- return { x: this.x, y: this.y, w: this.width, h: this._toolbarHeight };
122
- }
123
- get _sbRect() {
124
- return { x: this.x, y: this.y + this._toolbarHeight, w: this.width, h: this._searchOpen ? 44 : 0 };
125
- }
126
- get _activeThumbsWidth() { return this.totalPages > 1 ? this._thumbsWidth : 0; }
127
- get _viewerRect() {
128
- const top = this._toolbarHeight + this._sbRect.h;
129
- return {
130
- x: this.x + this._activeThumbsWidth,
131
- y: this.y + top,
132
- w: this.width - this._activeThumbsWidth,
133
- h: this.height - top,
134
- };
135
- }
136
- get _thumbRect() {
137
- const top = this._toolbarHeight + this._sbRect.h;
138
- return { x: this.x, y: this.y + top, w: this._thumbsWidth, h: this.height - top };
139
- }
140
-
141
- // ─── Événements framework ────────────────────────────────────────────────────
142
- // Le framework (checkComponentsAtPosition) appelle :
143
- // onPress(x, y) sur 'start' avec comp.pressed = true
144
- // onPress(x, y) sur 'end' avec comp.pressed = false (via onClick fallback)
145
- // onMove(x, y) sur 'move'
146
- //
147
- // ATTENTION : le framework appelle onPress sur 'start' ET onClick sur 'end'.
148
- // On surcharge onClick pour le clic final, et onPress pour le press initial.
149
-
150
- onPress(x, y) {
151
- // Appelé sur 'start' — enregistrer la position initiale
152
- this._pressX = x;
153
- this._pressY = y;
154
- this._dragging = false;
155
- this._dragStartScroll = this._scrollY;
156
- }
157
-
158
- // onClick est appelé par le framework sur 'end' quand comp.pressed était true
159
- // On le remplace ici pour recevoir les coordonnées via onPress qu'on a mémorisé.
160
- // Mais le framework appelle onClick() sans args... On utilise donc une autre approche :
161
- // on surcharge handleClick via _lastPressX/Y mémorisés dans onPress.
162
-
163
- onMount() {
164
- // Surcharger onClick pour intercepter le clic framework
165
- // Le framework appelle comp.onClick() sans coordonnées sur 'end'.
166
- // On utilise _pressX/_pressY mémorisés dans onPress.
167
- this.onClick = () => {
168
- if (!this._dragging && this._pressX !== undefined) {
169
- this._handleClick(this._pressX, this._pressY);
170
- }
171
- this._pressX = undefined;
172
- this._pressY = undefined;
173
- this._dragging = false;
174
- };
175
- }
176
-
177
- onMove(x, y) {
178
- this._checkHover(x, y);
179
-
180
- // Drag interne du viewer si le press initial était dans le viewer
181
- if (this._pressX !== undefined) {
182
- const vr = this._viewerRect;
183
- const inViewer = this._pressX >= vr.x && this._pressX <= vr.x + vr.w &&
184
- this._pressY >= vr.y && this._pressY <= vr.y + vr.h;
185
- if (inViewer) {
186
- const dy = Math.abs(y - this._pressY);
187
- if (dy > 5 || this._dragging) {
188
- if (!this._dragging) {
189
- this._dragging = true;
190
- this._dragStartY = this._pressY;
191
- this._dragStartScroll = this._scrollY;
192
- }
193
- this._scrollY = Math.max(0, Math.min(this._maxScrollY,
194
- this._dragStartScroll + (this._dragStartY - y)));
195
- this._syncPage();
196
- this.markDirty();
12
+ constructor(framework, options = {}) {
13
+ super(framework, options);
14
+
15
+ this.platform = framework.platform;
16
+ this.src = options.src || null;
17
+ this.currentPage = options.initialPage || 1;
18
+ this.totalPages = 0;
19
+ this.scale = options.initialScale || 0.4;
20
+ this.rotation = 0;
21
+ this.loading = false;
22
+ this.error = null;
23
+
24
+ this.showToolbar = options.showToolbar !== false;
25
+ this.allowZoom = options.allowZoom !== false;
26
+ this.allowNavigation = options.allowNavigation !== false;
27
+ this.allowDownload = options.allowDownload !== false;
28
+ this.allowPrint = options.allowPrint !== false;
29
+ this.allowRotate = options.allowRotate !== false;
30
+ this.toolbarColor = options.toolbarColor || null;
31
+ this.toolbarTextColor = options.toolbarTextColor || '#FFFFFF';
32
+ this.toolbarIconColor = options.toolbarIconColor || '#FFFFFF';
33
+ this.toolbarColor = options.toolbarColor || null;
34
+ this.allowSearch = options.allowSearch !== false;
35
+ this.minScale = options.minScale || 0.25;
36
+ this.maxScale = options.maxScale || 5.0;
37
+ this.backgroundColor = options.backgroundColor || null;
38
+ this.primaryColor = options.primaryColor || null;
39
+ this.pageBackground = options.pageBackground || '#FFFFFF';
40
+
41
+ this.onPageChange = options.onPageChange || (() => {});
42
+ this.onScaleChange = options.onScaleChange || (() => {});
43
+ this.onLoad = options.onLoad || (() => {});
44
+ this.onErrorCb = options.onError || (() => {});
45
+
46
+ // Layout
47
+ this._toolbarHeight = this.showToolbar ? 52 : 0;
48
+ this._searchOpen = false;
49
+ this._searchQuery = '';
50
+ this._thumbsWidth = 110;
51
+
52
+ // PDF state
53
+ this._pdfDoc = null;
54
+ this._pageImages = {};
55
+ this._thumbImages = {};
56
+
57
+ // Scroll interne du viewer
58
+ this._scrollY = 0;
59
+ this._maxScrollY = 0;
60
+ this._scrollX = 0;
61
+ this._maxScrollX = 0;
62
+
63
+ // Drag interne (scroll des pages)
64
+ this._dragging = false;
65
+ this._dragStartY = 0;
66
+ this._dragStartScroll = 0;
67
+ this._dragStartX = 0;
68
+ this._dragStartScrollX = 0;
69
+ this._pressX = undefined;
70
+ this._pressY = undefined;
71
+
72
+ // Hit areas — coordonnées MONDE (this.x + offset local)
73
+ this._tbButtons = [];
74
+ this._pageInputRect = null;
75
+ this._retryBtn = null;
76
+ this._searchCloseRect = null;
77
+ this._hoveredBtn = null;
78
+
79
+ // Spinner
80
+ this._spinAngle = 0;
81
+ this._spinRAF = null;
82
+
83
+ // Couleurs Material You 3
84
+ this.m3Colors = {
85
+ primary: '#6750A4',
86
+ onPrimary: '#FFFFFF',
87
+ surfaceVariant: '#E7E0EC',
88
+ onSurface: '#1C1B1F',
89
+ onSurfaceVariant: '#49454F',
90
+ outline: '#79747E',
91
+ outlineVariant: '#CAC4D0',
92
+ error: '#BA1A1A',
93
+ viewerBg: '#F7F2FA',
94
+ thumbsBg: '#EFE9F4',
95
+ thumbBorder: '#CAC4D0',
96
+ thumbActive: '#6750A4',
97
+ pageShadow: 'rgba(0,0,0,0.15)',
98
+ };
99
+
100
+ // Couleurs Cupertino
101
+ this.cupertinoColors = {
102
+ primary: '#007AFF',
103
+ onPrimary: '#FFFFFF',
104
+ error: '#FF3B30',
105
+ toolbarBorder: '#C6C6C8',
106
+ viewerBg: '#D1D1D6',
107
+ thumbsBg: '#F2F2F7',
108
+ thumbBorder: '#C6C6C8',
109
+ thumbActive: '#007AFF',
110
+ pageShadow: 'rgba(0,0,0,0.15)',
111
+ onSurfaceVariant: '#8E8E93',
112
+ outlineVariant: '#C6C6C8',
113
+ outline: '#C6C6C8',
114
+ };
115
+
116
+ this._startSpinner();
117
+ if (this.src) this._loadPDF(this.src);
118
+
119
+ // Gestion directe des events sur le canvas du framework
120
+ this._canvas = framework.canvas;
121
+ this._boundMouseDown = this._onMouseDown.bind(this);
122
+ this._boundMouseMove = this._onMouseMove.bind(this);
123
+ this._boundMouseUp = this._onMouseUp.bind(this);
124
+ this._canvas.addEventListener('mousedown', this._boundMouseDown);
125
+ this._canvas.addEventListener('mousemove', this._boundMouseMove);
126
+ this._canvas.addEventListener('mouseup', this._boundMouseUp);
127
+ this._canvas.addEventListener('touchstart', this._boundMouseDown, {
128
+ passive: false
129
+ });
130
+ this._canvas.addEventListener('touchmove', this._boundMouseMove, {
131
+ passive: false
132
+ });
133
+ this._canvas.addEventListener('touchend', this._boundMouseUp, {
134
+ passive: false
135
+ });
136
+ }
137
+
138
+
139
+ _getPos(e) {
140
+ const rect = this._canvas.getBoundingClientRect();
141
+ let src;
142
+ if (e.changedTouches && e.changedTouches.length > 0) {
143
+ src = e.changedTouches[0];
144
+ } else if (e.touches && e.touches.length > 0) {
145
+ src = e.touches[0];
146
+ } else {
147
+ src = e;
197
148
  }
198
- }
149
+ return {
150
+ x: src.clientX - rect.left,
151
+ y: src.clientY - rect.top
152
+ };
199
153
  }
200
- }
201
154
 
202
- // ─── Logique de clic ─────────────────────────────────────────────────────────
155
+ _isInMe(x, y) {
156
+ return x >= this.x && x <= this.x + this.width &&
157
+ y >= this.y && y <= this.y + this.height;
158
+ }
203
159
 
204
- _handleClick(x, y) {
205
- // Retry
206
- if (this._retryBtn) {
207
- const r = this._retryBtn;
208
- if (x >= r.x && x <= r.x+r.w && y >= r.y && y <= r.y+r.h) {
209
- if (this.src) this._loadPDF(this.src); return;
210
- }
160
+ _onMouseDown(e) {
161
+ const {
162
+ x,
163
+ y
164
+ } = this._getPos(e);
165
+ if (!this._isInMe(x, y)) return;
166
+ this._pressX = x;
167
+ this._pressY = y;
168
+ this._dragging = false;
169
+ this._dragStartScroll = this._scrollY;
211
170
  }
212
171
 
213
- // Fermer recherche
214
- if (this._searchCloseRect) {
215
- const r = this._searchCloseRect;
216
- if (x >= r.x && x <= r.x+r.w && y >= r.y && y <= r.y+r.h) {
217
- this._toggleSearch(); return;
218
- }
172
+ _onMouseMove(e) {
173
+ const {
174
+ x,
175
+ y
176
+ } = this._getPos(e);
177
+ if (!this._isInMe(x, y)) return;
178
+ this._checkHover(x, y);
179
+
180
+ if (this._pressX !== undefined) {
181
+ const vr = this._viewerRect;
182
+ const inViewer = this._pressX >= vr.x && this._pressX <= vr.x + vr.w &&
183
+ this._pressY >= vr.y && this._pressY <= vr.y + vr.h;
184
+ if (inViewer) {
185
+ const dx = Math.abs(x - this._pressX);
186
+ const dy = Math.abs(y - this._pressY);
187
+ if (dx > 5 || dy > 5 || this._dragging) {
188
+ if (!this._dragging) {
189
+ this._dragging = true;
190
+ this._dragStartY = this._pressY;
191
+ this._dragStartX = this._pressX;
192
+ this._dragStartScroll = this._scrollY;
193
+ this._dragStartScrollX = this._scrollX;
194
+ }
195
+ this._scrollY = Math.max(0, Math.min(this._maxScrollY,
196
+ this._dragStartScroll + (this._dragStartY - y)));
197
+ this._scrollX = Math.max(0, Math.min(this._maxScrollX,
198
+ this._dragStartScrollX + (this._dragStartX - x)));
199
+ this._syncPage();
200
+ this.markDirty();
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ _onMouseUp(e) {
207
+ const {
208
+ x,
209
+ y
210
+ } = this._getPos(e);
211
+ if (!this._isInMe(x, y)) {
212
+ this._pressX = undefined;
213
+ this._pressY = undefined;
214
+ return;
215
+ }
216
+ if (!this._dragging && this._pressX !== undefined) {
217
+ this._handleClick(x, y); // ← x,y directs, pas de surprise
218
+ }
219
+ this._pressX = undefined;
220
+ this._pressY = undefined;
221
+ this._dragging = false;
219
222
  }
220
223
 
221
- // Boutons toolbar
222
- for (const b of this._tbButtons) {
223
- if (!b.disabled && x >= b.x && x <= b.x+b.w && y >= b.y && y <= b.y+b.h) {
224
- this._handleBtn(b.id); return;
225
- }
224
+ destroy() {
225
+ this._canvas.removeEventListener('mousedown', this._boundMouseDown);
226
+ this._canvas.removeEventListener('mousemove', this._boundMouseMove);
227
+ this._canvas.removeEventListener('mouseup', this._boundMouseUp);
228
+ this._canvas.removeEventListener('touchstart', this._boundMouseDown);
229
+ this._canvas.removeEventListener('touchmove', this._boundMouseMove);
230
+ this._canvas.removeEventListener('touchend', this._boundMouseUp);
231
+ if (this._spinRAF) cancelAnimationFrame(this._spinRAF);
232
+ this._pdfDoc = null;
233
+ super.destroy?.();
226
234
  }
235
+ // ─── Couleurs ────────────────────────────────────────────────────────────────
227
236
 
228
- // Indicateur de page
229
- if (this._pageInputRect) {
230
- const r = this._pageInputRect;
231
- if (x >= r.x && x <= r.x+r.w && y >= r.y && y <= r.y+r.h) {
232
- this._promptPage(); return;
233
- }
237
+ get _colors() {
238
+ return this.platform === 'material' ? this.m3Colors : this.cupertinoColors;
239
+ }
240
+ get _primary() {
241
+ return this.primaryColor || this._colors.primary;
242
+ }
243
+ get _isMat() {
244
+ return this.platform === 'material';
234
245
  }
235
246
 
236
- // Miniatures
237
- if (this.totalPages > 1) {
238
- const tr = this._thumbRect;
239
- if (x >= tr.x && x <= tr.x+tr.w && y >= tr.y && y <= tr.y+tr.h) {
247
+ // ─── Géométrie en coordonnées MONDE ──────────────────────────────────────────
248
+
249
+ get _tbRect() {
250
+ return {
251
+ x: this.x,
252
+ y: this.y,
253
+ w: this.width,
254
+ h: this._toolbarHeight
255
+ };
256
+ }
257
+ get _sbRect() {
258
+ return {
259
+ x: this.x,
260
+ y: this.y + this._toolbarHeight,
261
+ w: this.width,
262
+ h: this._searchOpen ? 44 : 0
263
+ };
264
+ }
265
+ get _activeThumbsWidth() {
266
+ return this.totalPages > 1 ? this._thumbsWidth : 0;
267
+ }
268
+ get _viewerRect() {
269
+ const top = this._toolbarHeight + this._sbRect.h;
270
+ return {
271
+ x: this.x + this._activeThumbsWidth,
272
+ y: this.y + top,
273
+ w: this.width - this._activeThumbsWidth,
274
+ h: this.height - top,
275
+ };
276
+ }
277
+ get _thumbRect() {
278
+ const top = this._toolbarHeight + this._sbRect.h;
279
+ return {
280
+ x: this.x,
281
+ y: this.y + top,
282
+ w: this._thumbsWidth,
283
+ h: this.height - top
284
+ };
285
+ }
286
+
287
+ // ─── Événements framework ────────────────────────────────────────────────────
288
+ // Le framework (checkComponentsAtPosition) appelle :
289
+ // onPress(x, y) sur 'start' avec comp.pressed = true
290
+ // onPress(x, y) sur 'end' avec comp.pressed = false (via onClick fallback)
291
+ // onMove(x, y) sur 'move'
292
+ //
293
+ // ATTENTION : le framework appelle onPress sur 'start' ET onClick sur 'end'.
294
+ // On surcharge onClick pour le clic final, et onPress pour le press initial.
295
+
296
+
297
+
298
+ // onClick est appelé par le framework sur 'end' quand comp.pressed était true
299
+ // On le remplace ici pour recevoir les coordonnées via onPress qu'on a mémorisé.
300
+ // Mais le framework appelle onClick() sans args... On utilise donc une autre approche :
301
+ // on surcharge handleClick via _lastPressX/Y mémorisés dans onPress.
302
+
303
+
304
+
305
+ // ─── Logique de clic ─────────────────────────────────────────────────────────
306
+
307
+ _handleClick(x, y) {
308
+ console.log('_handleClick', x, y, 'thumbRect', this._thumbRect);
309
+ // Retry
310
+ if (this._retryBtn) {
311
+ const r = this._retryBtn;
312
+ if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
313
+ if (this.src) this._loadPDF(this.src);
314
+ return;
315
+ }
316
+ }
317
+
318
+ // Fermer recherche
319
+ if (this._searchCloseRect) {
320
+ const r = this._searchCloseRect;
321
+ if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
322
+ this._toggleSearch();
323
+ return;
324
+ }
325
+ }
326
+
327
+ // Boutons toolbar
328
+ for (const b of this._tbButtons) {
329
+ if (!b.disabled && x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
330
+ this._handleBtn(b.id);
331
+ return;
332
+ }
333
+ }
334
+
335
+ // Indicateur de page
336
+ if (this._pageInputRect) {
337
+ const r = this._pageInputRect;
338
+ if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
339
+ this._promptPage();
340
+ return;
341
+ }
342
+ }
343
+
344
+ // Miniatures
345
+ if (this.totalPages > 1) {
346
+ const tr = this._thumbRect;
347
+ if (x >= tr.x && x <= tr.x + tr.w && y >= tr.y && y <= tr.y + tr.h) {
348
+ let ty = tr.y + 8;
349
+ for (let p = 1; p <= this.totalPages; p++) {
350
+ const img = this._thumbImages[p];
351
+ const tw = tr.w - 16;
352
+ const th = img ? Math.round(img.height * tw / img.width) : 70;
353
+ if (y >= ty && y <= ty + th) {
354
+ this.goToPage(p);
355
+ return;
356
+ }
357
+ ty += th + 18;
358
+ }
359
+ }
360
+ }
361
+ }
362
+
363
+ _checkHover(x, y) {
364
+ let hover = null;
365
+ for (const b of this._tbButtons) {
366
+ if (!b.disabled && x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
367
+ hover = b.id;
368
+ break;
369
+ }
370
+ }
371
+ if (!hover && this._searchCloseRect) {
372
+ const r = this._searchCloseRect;
373
+ if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) hover = 'searchClose';
374
+ }
375
+ if (!hover && this._retryBtn) {
376
+ const r = this._retryBtn;
377
+ if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) hover = 'retry';
378
+ }
379
+ if (hover !== this._hoveredBtn) {
380
+ this._hoveredBtn = hover;
381
+ this.markDirty();
382
+ }
383
+ }
384
+
385
+ // ─── Spinner ─────────────────────────────────────────────────────────────────
386
+
387
+ _startSpinner() {
388
+ const tick = () => {
389
+ if (this.loading) {
390
+ this._spinAngle = (this._spinAngle + 0.08) % (Math.PI * 2);
391
+ this.markDirty();
392
+ }
393
+ this._spinRAF = requestAnimationFrame(tick);
394
+ };
395
+ this._spinRAF = requestAnimationFrame(tick);
396
+ }
397
+
398
+ // ─── PDF.js ──────────────────────────────────────────────────────────────────
399
+
400
+ async _loadPDFJS() {
401
+ if (window.pdfjsLib) return window.pdfjsLib;
402
+ return new Promise((resolve, reject) => {
403
+ const s = document.createElement('script');
404
+ s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
405
+ s.onload = () => {
406
+ window.pdfjsLib.GlobalWorkerOptions.workerSrc =
407
+ 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
408
+ resolve(window.pdfjsLib);
409
+ };
410
+ s.onerror = () => reject(new Error('Impossible de charger PDF.js'));
411
+ document.head.appendChild(s);
412
+ });
413
+ }
414
+
415
+ async _loadPDF(src) {
416
+ this.loading = true;
417
+ this.error = null;
418
+ this._pageImages = {};
419
+ this._thumbImages = {};
420
+ this._scrollY = 0;
421
+ this.markDirty();
422
+ try {
423
+ const lib = await this._loadPDFJS();
424
+ const task = lib.getDocument(typeof src === 'string' ? src : {
425
+ data: src
426
+ });
427
+ this._pdfDoc = await task.promise;
428
+ this.totalPages = this._pdfDoc.numPages;
429
+ const promises = [];
430
+ for (let p = 1; p <= this.totalPages; p++) {
431
+ promises.push(this._renderPage(p), this._renderThumb(p));
432
+ }
433
+ await Promise.all(promises);
434
+ this.loading = false;
435
+ this._computeMaxScroll();
436
+ this.onLoad(this.totalPages);
437
+ this.markDirty();
438
+ } catch (err) {
439
+ this.loading = false;
440
+ this.error = err.message || 'Erreur de chargement';
441
+ this.onErrorCb(this.error);
442
+ this.markDirty();
443
+ }
444
+ }
445
+
446
+ async _renderPage(p) {
447
+ try {
448
+ const page = await this._pdfDoc.getPage(p);
449
+ const vp = page.getViewport({
450
+ scale: this.scale,
451
+ rotation: this.rotation
452
+ });
453
+ const off = new OffscreenCanvas(Math.ceil(vp.width), Math.ceil(vp.height));
454
+ await page.render({
455
+ canvasContext: off.getContext('2d'),
456
+ viewport: vp
457
+ }).promise;
458
+ this._pageImages[p] = await createImageBitmap(off);
459
+ } catch (err) {
460
+ console.error(`Erreur page ${p}:`, err);
461
+ }
462
+ }
463
+
464
+ async _renderThumb(p) {
465
+ try {
466
+ const page = await this._pdfDoc.getPage(p);
467
+ const vp = page.getViewport({
468
+ scale: 0.18,
469
+ rotation: this.rotation
470
+ });
471
+ const off = new OffscreenCanvas(Math.ceil(vp.width), Math.ceil(vp.height));
472
+ await page.render({
473
+ canvasContext: off.getContext('2d'),
474
+ viewport: vp
475
+ }).promise;
476
+ this._thumbImages[p] = await createImageBitmap(off);
477
+ } catch (err) {
478
+ console.error(`Erreur miniature ${p}:`, err);
479
+ }
480
+ }
481
+
482
+ async _reRenderAll() {
483
+ if (!this._pdfDoc) return;
484
+ this._pageImages = {};
485
+ this._thumbImages = {};
486
+ this._scrollX = 0; // ← ajouter
487
+ this.loading = true;
488
+ this.markDirty();
489
+ const promises = [];
490
+ for (let p = 1; p <= this.totalPages; p++) {
491
+ promises.push(this._renderPage(p), this._renderThumb(p));
492
+ }
493
+ await Promise.all(promises);
494
+ this.loading = false;
495
+ this._computeMaxScroll();
496
+ this._scrollY = Math.min(this._scrollY, this._maxScrollY);
497
+ this.markDirty();
498
+ }
499
+
500
+ // ─── Scroll interne ───────────────────────────────────────────────────────────
501
+
502
+ _computeMaxScroll() {
503
+ const vr = this._viewerRect;
504
+ let totalH = 16;
505
+ let maxW = 0;
506
+ for (let p = 1; p <= this.totalPages; p++) {
507
+ totalH += (this._pageImages[p] ? this._pageImages[p].height : 200) + 16;
508
+ const w = this._pageImages[p] ? this._pageImages[p].width : 0;
509
+ if (w > maxW) maxW = w;
510
+ }
511
+ this._maxScrollY = Math.max(0, totalH - vr.h);
512
+ this._maxScrollX = Math.max(0, maxW + 32 - vr.w);
513
+ }
514
+
515
+ _pageScrollOffset(pageNum) {
516
+ let y = 16;
517
+ for (let p = 1; p < pageNum; p++) {
518
+ y += (this._pageImages[p] ? this._pageImages[p].height : 200) + 16;
519
+ }
520
+ return y;
521
+ }
522
+
523
+ _syncPage() {
524
+ if (!this.totalPages) return;
525
+ const mid = this._scrollY + this._viewerRect.h / 2;
526
+ let y = 16;
527
+ for (let p = 1; p <= this.totalPages; p++) {
528
+ const h = this._pageImages[p] ? this._pageImages[p].height : 200;
529
+ if (mid <= y + h || p === this.totalPages) {
530
+ if (p !== this.currentPage) {
531
+ this.currentPage = p;
532
+ this.onPageChange(p, this.totalPages);
533
+ }
534
+ return;
535
+ }
536
+ y += h + 16;
537
+ }
538
+ }
539
+
540
+ // ─── Dessin ───────────────────────────────────────────────────────────────────
541
+
542
+ draw(ctx) {
543
+ ctx.save();
544
+ ctx.beginPath();
545
+ this._rr(ctx, this.x, this.y, this.width, this.height, this._isMat ? 4 : 12);
546
+ ctx.clip();
547
+
548
+ ctx.fillStyle = this.backgroundColor || this._colors.viewerBg;
549
+ ctx.fillRect(this.x, this.y, this.width, this.height);
550
+
551
+ if (this.totalPages > 1) this._drawThumbs(ctx);
552
+ this._drawViewer(ctx);
553
+ if (this.showToolbar) this._drawToolbar(ctx);
554
+ if (this._searchOpen) this._drawSearchBar(ctx);
555
+
556
+ ctx.strokeStyle = this._isMat ? 'rgba(0,0,0,0.12)' : (this._colors.toolbarBorder || '#C6C6C8');
557
+ ctx.lineWidth = 1;
558
+ ctx.beginPath();
559
+ this._rr(ctx, this.x, this.y, this.width, this.height, this._isMat ? 4 : 12);
560
+ ctx.stroke();
561
+
562
+ ctx.restore();
563
+ }
564
+
565
+ _drawViewer(ctx) {
566
+ const vr = this._viewerRect;
567
+ ctx.save();
568
+ ctx.beginPath();
569
+ ctx.rect(vr.x, vr.y, vr.w, vr.h);
570
+ ctx.clip();
571
+ ctx.fillStyle = this._colors.viewerBg;
572
+ ctx.fillRect(vr.x, vr.y, vr.w, vr.h);
573
+
574
+ const hasImages = Object.keys(this._pageImages).length > 0;
575
+ if (this.loading && !hasImages) this._drawLoading(ctx, vr);
576
+ else if (this.error && !hasImages) this._drawError(ctx, vr);
577
+ else if (!this.totalPages && !this.loading) this._drawEmpty(ctx, vr);
578
+ else this._drawPages(ctx, vr);
579
+
580
+ ctx.restore();
581
+ }
582
+
583
+ _drawPages(ctx, vr) {
584
+ const pad = 16;
585
+ let y = vr.y + pad - this._scrollY;
586
+
587
+ for (let p = 1; p <= this.totalPages; p++) {
588
+ const img = this._pageImages[p];
589
+ const ph = img ? img.height : 200;
590
+ const pw = img ? img.width : vr.w - pad * 2;
591
+
592
+ // Centrer si plus petit que le viewer, sinon scroll horizontal
593
+ const baseX = this._maxScrollX > 0 ?
594
+ vr.x + pad - this._scrollX :
595
+ vr.x + (vr.w - pw) / 2;
596
+
597
+ if (y + ph >= vr.y && y <= vr.y + vr.h) {
598
+ ctx.shadowColor = this._colors.pageShadow;
599
+ ctx.shadowBlur = 8;
600
+ ctx.shadowOffsetY = 2;
601
+ ctx.fillStyle = this.pageBackground;
602
+ ctx.fillRect(baseX, y, pw, ph);
603
+ ctx.shadowBlur = 0;
604
+ ctx.shadowOffsetY = 0;
605
+
606
+ if (img) {
607
+ ctx.drawImage(img, 0, 0, img.width, img.height, baseX, y, pw, ph);
608
+ } else {
609
+ ctx.fillStyle = '#ccc';
610
+ ctx.font = '13px sans-serif';
611
+ ctx.textAlign = 'center';
612
+ ctx.textBaseline = 'middle';
613
+ ctx.fillText(`Page ${p}…`, baseX + pw / 2, y + ph / 2);
614
+ }
615
+
616
+ ctx.fillStyle = 'rgba(0,0,0,0.28)';
617
+ ctx.font = '11px sans-serif';
618
+ ctx.textAlign = 'right';
619
+ ctx.textBaseline = 'bottom';
620
+ ctx.fillText(`${p} / ${this.totalPages}`, baseX + pw - 6, y + ph - 4);
621
+ }
622
+ y += ph + pad;
623
+ }
624
+ this._drawScrollbar(ctx, vr);
625
+ this._drawScrollbarH(ctx, vr);
626
+ }
627
+
628
+ _drawScrollbarH(ctx, vr) {
629
+ if (this._maxScrollX <= 0) return;
630
+ const th = 5,
631
+ tr = 3;
632
+ const ty = vr.y + vr.h - th - 3;
633
+ const tx = vr.x + 4,
634
+ tw = vr.w - 8;
635
+ const ratio = vr.w / (vr.w + this._maxScrollX);
636
+ const thumbW = Math.max(24, tw * ratio);
637
+ const thumbX = tx + (this._scrollX / this._maxScrollX) * (tw - thumbW);
638
+ ctx.fillStyle = 'rgba(0,0,0,0.08)';
639
+ this._rr(ctx, tx, ty, tw, th, tr);
640
+ ctx.fill();
641
+ ctx.fillStyle = 'rgba(0,0,0,0.32)';
642
+ this._rr(ctx, thumbX, ty, thumbW, th, tr);
643
+ ctx.fill();
644
+ }
645
+
646
+ _drawScrollbar(ctx, vr) {
647
+ if (this._maxScrollY <= 0) return;
648
+ const tw = 5,
649
+ tr = 3;
650
+ const tx = vr.x + vr.w - tw - 3;
651
+ const ty = vr.y + 4,
652
+ th = vr.h - 8;
653
+ const ratio = vr.h / (vr.h + this._maxScrollY);
654
+ const thumbH = Math.max(24, th * ratio);
655
+ const thumbY = ty + (this._scrollY / this._maxScrollY) * (th - thumbH);
656
+ ctx.fillStyle = 'rgba(0,0,0,0.08)';
657
+ this._rr(ctx, tx, ty, tw, th, tr);
658
+ ctx.fill();
659
+ ctx.fillStyle = 'rgba(0,0,0,0.32)';
660
+ this._rr(ctx, tx, thumbY, tw, thumbH, tr);
661
+ ctx.fill();
662
+ }
663
+
664
+ _drawLoading(ctx, vr) {
665
+ const cx = vr.x + vr.w / 2,
666
+ cy = vr.y + vr.h / 2 - 20;
667
+ ctx.strokeStyle = 'rgba(0,0,0,0.1)';
668
+ ctx.lineWidth = 4;
669
+ ctx.beginPath();
670
+ ctx.arc(cx, cy, 22, 0, Math.PI * 2);
671
+ ctx.stroke();
672
+ ctx.strokeStyle = this._primary;
673
+ ctx.lineWidth = 4;
674
+ ctx.lineCap = 'round';
675
+ ctx.beginPath();
676
+ ctx.arc(cx, cy, 22, this._spinAngle, this._spinAngle + 1.3);
677
+ ctx.stroke();
678
+ ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
679
+ ctx.font = '13px sans-serif';
680
+ ctx.textAlign = 'center';
681
+ ctx.textBaseline = 'top';
682
+ ctx.fillText('Chargement...', cx, cy + 32);
683
+ }
684
+
685
+ _drawEmpty(ctx, vr) {
686
+ const cx = vr.x + vr.w / 2,
687
+ cy = vr.y + vr.h / 2 - 20;
688
+ ctx.globalAlpha = 0.35;
689
+ ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
690
+ ctx.beginPath();
691
+ const fx = cx - 20,
692
+ fy = cy - 26;
693
+ ctx.moveTo(fx + 6, fy);
694
+ ctx.lineTo(fx + 28, fy);
695
+ ctx.lineTo(fx + 40, fy + 14);
696
+ ctx.lineTo(fx + 40, fy + 54);
697
+ ctx.quadraticCurveTo(fx + 40, fy + 58, fx + 36, fy + 58);
698
+ ctx.lineTo(fx + 4, fy + 58);
699
+ ctx.quadraticCurveTo(fx, fy + 58, fx, fy + 54);
700
+ ctx.lineTo(fx, fy + 6);
701
+ ctx.quadraticCurveTo(fx, fy, fx + 6, fy);
702
+ ctx.closePath();
703
+ ctx.fill();
704
+ ctx.globalAlpha = 1;
705
+ ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
706
+ ctx.font = '13px sans-serif';
707
+ ctx.textAlign = 'center';
708
+ ctx.textBaseline = 'top';
709
+ ctx.fillText('Aucun document chargé', cx, cy + 42);
710
+ }
711
+
712
+ _drawError(ctx, vr) {
713
+ const cx = vr.x + vr.w / 2,
714
+ cy = vr.y + vr.h / 2 - 30;
715
+ const err = this._colors.error || '#BA1A1A';
716
+ ctx.fillStyle = err;
717
+ ctx.beginPath();
718
+ ctx.arc(cx, cy, 24, 0, Math.PI * 2);
719
+ ctx.fill();
720
+ ctx.fillStyle = '#fff';
721
+ ctx.font = 'bold 26px sans-serif';
722
+ ctx.textAlign = 'center';
723
+ ctx.textBaseline = 'middle';
724
+ ctx.fillText('!', cx, cy);
725
+ ctx.fillStyle = err;
726
+ ctx.font = '13px sans-serif';
727
+ ctx.textBaseline = 'top';
728
+ ctx.fillText((this.error || 'Erreur').substring(0, 60), cx, cy + 34);
729
+ const bx = cx - 48,
730
+ by = cy + 62;
731
+ this._retryBtn = {
732
+ x: bx,
733
+ y: by,
734
+ w: 96,
735
+ h: 30
736
+ };
737
+ ctx.fillStyle = this._hoveredBtn === 'retry' ? this._primary + 'dd' : this._primary;
738
+ this._rr(ctx, bx, by, 96, 30, this._isMat ? 15 : 8);
739
+ ctx.fill();
740
+ ctx.fillStyle = '#fff';
741
+ ctx.font = '13px sans-serif';
742
+ ctx.textBaseline = 'middle';
743
+ ctx.fillText('Réessayer', cx, by + 15);
744
+ }
745
+
746
+ _drawThumbs(ctx) {
747
+ const tr = this._thumbRect;
748
+ ctx.fillStyle = this._colors.thumbsBg;
749
+ ctx.fillRect(tr.x, tr.y, tr.w, tr.h);
750
+ ctx.strokeStyle = this._colors.thumbBorder;
751
+ ctx.lineWidth = 1;
752
+ ctx.beginPath();
753
+ ctx.moveTo(tr.x + tr.w, tr.y);
754
+ ctx.lineTo(tr.x + tr.w, tr.y + tr.h);
755
+ ctx.stroke();
756
+
757
+ ctx.save();
758
+ ctx.beginPath();
759
+ ctx.rect(tr.x, tr.y, tr.w, tr.h);
760
+ ctx.clip();
240
761
  let ty = tr.y + 8;
241
762
  for (let p = 1; p <= this.totalPages; p++) {
242
- const img = this._thumbImages[p];
243
- const tw = tr.w - 16;
244
- const th = img ? Math.round(img.height * tw / img.width) : 70;
245
- if (y >= ty && y <= ty + th) { this.goToPage(p); return; }
246
- ty += th + 18;
763
+ const img = this._thumbImages[p];
764
+ const tw = tr.w - 16;
765
+ const th = img ? Math.round(img.height * tw / img.width) : 70;
766
+ const tx = tr.x + 8;
767
+ if (ty + th > tr.y && ty < tr.y + tr.h) {
768
+ const active = p === this.currentPage;
769
+ ctx.strokeStyle = active ? this._colors.thumbActive : this._colors.thumbBorder;
770
+ ctx.lineWidth = active ? 2 : 1;
771
+ this._rr(ctx, tx, ty, tw, th, 3);
772
+ if (img) {
773
+ ctx.drawImage(img, tx, ty, tw, th);
774
+ ctx.stroke();
775
+ } else {
776
+ ctx.fillStyle = '#fff';
777
+ ctx.fill();
778
+ ctx.stroke();
779
+ }
780
+ ctx.fillStyle = '#666';
781
+ ctx.font = '10px sans-serif';
782
+ ctx.textAlign = 'center';
783
+ ctx.textBaseline = 'top';
784
+ ctx.fillText(p, tx + tw / 2, ty + th + 2);
785
+ }
786
+ ty += th + 18;
247
787
  }
248
- }
249
- }
250
- }
251
-
252
- _checkHover(x, y) {
253
- let hover = null;
254
- for (const b of this._tbButtons) {
255
- if (!b.disabled && x >= b.x && x <= b.x+b.w && y >= b.y && y <= b.y+b.h) {
256
- hover = b.id; break;
257
- }
258
- }
259
- if (!hover && this._searchCloseRect) {
260
- const r = this._searchCloseRect;
261
- if (x >= r.x && x <= r.x+r.w && y >= r.y && y <= r.y+r.h) hover = 'searchClose';
262
- }
263
- if (!hover && this._retryBtn) {
264
- const r = this._retryBtn;
265
- if (x >= r.x && x <= r.x+r.w && y >= r.y && y <= r.y+r.h) hover = 'retry';
266
- }
267
- if (hover !== this._hoveredBtn) { this._hoveredBtn = hover; this.markDirty(); }
268
- }
269
-
270
- // ─── Spinner ─────────────────────────────────────────────────────────────────
271
-
272
- _startSpinner() {
273
- const tick = () => {
274
- if (this.loading) { this._spinAngle = (this._spinAngle + 0.08) % (Math.PI * 2); this.markDirty(); }
275
- this._spinRAF = requestAnimationFrame(tick);
276
- };
277
- this._spinRAF = requestAnimationFrame(tick);
278
- }
279
-
280
- // ─── PDF.js ──────────────────────────────────────────────────────────────────
281
-
282
- async _loadPDFJS() {
283
- if (window.pdfjsLib) return window.pdfjsLib;
284
- return new Promise((resolve, reject) => {
285
- const s = document.createElement('script');
286
- s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
287
- s.onload = () => {
288
- window.pdfjsLib.GlobalWorkerOptions.workerSrc =
289
- 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
290
- resolve(window.pdfjsLib);
291
- };
292
- s.onerror = () => reject(new Error('Impossible de charger PDF.js'));
293
- document.head.appendChild(s);
294
- });
295
- }
296
-
297
- async _loadPDF(src) {
298
- this.loading = true; this.error = null;
299
- this._pageImages = {}; this._thumbImages = {}; this._scrollY = 0;
300
- this.markDirty();
301
- try {
302
- const lib = await this._loadPDFJS();
303
- const task = lib.getDocument(typeof src === 'string' ? src : { data: src });
304
- this._pdfDoc = await task.promise;
305
- this.totalPages = this._pdfDoc.numPages;
306
- const promises = [];
307
- for (let p = 1; p <= this.totalPages; p++) {
308
- promises.push(this._renderPage(p), this._renderThumb(p));
309
- }
310
- await Promise.all(promises);
311
- this.loading = false;
312
- this._computeMaxScroll();
313
- this.onLoad(this.totalPages);
314
- this.markDirty();
315
- } catch (err) {
316
- this.loading = false;
317
- this.error = err.message || 'Erreur de chargement';
318
- this.onErrorCb(this.error);
319
- this.markDirty();
320
- }
321
- }
322
-
323
- async _renderPage(p) {
324
- try {
325
- const page = await this._pdfDoc.getPage(p);
326
- const vp = page.getViewport({ scale: this.scale, rotation: this.rotation });
327
- const off = new OffscreenCanvas(Math.ceil(vp.width), Math.ceil(vp.height));
328
- await page.render({ canvasContext: off.getContext('2d'), viewport: vp }).promise;
329
- this._pageImages[p] = await createImageBitmap(off);
330
- } catch (err) { console.error(`Erreur page ${p}:`, err); }
331
- }
332
-
333
- async _renderThumb(p) {
334
- try {
335
- const page = await this._pdfDoc.getPage(p);
336
- const vp = page.getViewport({ scale: 0.18, rotation: this.rotation });
337
- const off = new OffscreenCanvas(Math.ceil(vp.width), Math.ceil(vp.height));
338
- await page.render({ canvasContext: off.getContext('2d'), viewport: vp }).promise;
339
- this._thumbImages[p] = await createImageBitmap(off);
340
- } catch (err) { console.error(`Erreur miniature ${p}:`, err); }
341
- }
342
-
343
- async _reRenderAll() {
344
- if (!this._pdfDoc) return;
345
- this._pageImages = {}; this._thumbImages = {};
346
- this.loading = true; this.markDirty();
347
- const promises = [];
348
- for (let p = 1; p <= this.totalPages; p++) {
349
- promises.push(this._renderPage(p), this._renderThumb(p));
350
- }
351
- await Promise.all(promises);
352
- this.loading = false;
353
- this._computeMaxScroll();
354
- this._scrollY = Math.min(this._scrollY, this._maxScrollY);
355
- this.markDirty();
356
- }
357
-
358
- // ─── Scroll interne ───────────────────────────────────────────────────────────
359
-
360
- _computeMaxScroll() {
361
- const vr = this._viewerRect;
362
- let total = 16;
363
- for (let p = 1; p <= this.totalPages; p++) {
364
- total += (this._pageImages[p] ? this._pageImages[p].height : 200) + 16;
365
- }
366
- this._maxScrollY = Math.max(0, total - vr.h);
367
- }
368
-
369
- _pageScrollOffset(pageNum) {
370
- let y = 16;
371
- for (let p = 1; p < pageNum; p++) {
372
- y += (this._pageImages[p] ? this._pageImages[p].height : 200) + 16;
373
- }
374
- return y;
375
- }
376
-
377
- _syncPage() {
378
- if (!this.totalPages) return;
379
- const mid = this._scrollY + this._viewerRect.h / 2;
380
- let y = 16;
381
- for (let p = 1; p <= this.totalPages; p++) {
382
- const h = this._pageImages[p] ? this._pageImages[p].height : 200;
383
- if (mid <= y + h || p === this.totalPages) {
384
- if (p !== this.currentPage) { this.currentPage = p; this.onPageChange(p, this.totalPages); }
385
- return;
386
- }
387
- y += h + 16;
388
- }
389
- }
390
-
391
- // ─── Dessin ───────────────────────────────────────────────────────────────────
392
-
393
- draw(ctx) {
394
- ctx.save();
395
- ctx.beginPath();
396
- this._rr(ctx, this.x, this.y, this.width, this.height, this._isMat ? 4 : 12);
397
- ctx.clip();
398
-
399
- ctx.fillStyle = this.backgroundColor || this._colors.viewerBg;
400
- ctx.fillRect(this.x, this.y, this.width, this.height);
401
-
402
- if (this.totalPages > 1) this._drawThumbs(ctx);
403
- this._drawViewer(ctx);
404
- if (this.showToolbar) this._drawToolbar(ctx);
405
- if (this._searchOpen) this._drawSearchBar(ctx);
406
-
407
- ctx.strokeStyle = this._isMat ? 'rgba(0,0,0,0.12)' : (this._colors.toolbarBorder || '#C6C6C8');
408
- ctx.lineWidth = 1;
409
- ctx.beginPath();
410
- this._rr(ctx, this.x, this.y, this.width, this.height, this._isMat ? 4 : 12);
411
- ctx.stroke();
412
-
413
- ctx.restore();
414
- }
415
-
416
- _drawViewer(ctx) {
417
- const vr = this._viewerRect;
418
- ctx.save();
419
- ctx.beginPath(); ctx.rect(vr.x, vr.y, vr.w, vr.h); ctx.clip();
420
- ctx.fillStyle = this._colors.viewerBg;
421
- ctx.fillRect(vr.x, vr.y, vr.w, vr.h);
422
-
423
- const hasImages = Object.keys(this._pageImages).length > 0;
424
- if (this.loading && !hasImages) this._drawLoading(ctx, vr);
425
- else if (this.error && !hasImages) this._drawError(ctx, vr);
426
- else if (!this.totalPages && !this.loading) this._drawEmpty(ctx, vr);
427
- else this._drawPages(ctx, vr);
428
-
429
- ctx.restore();
430
- }
431
-
432
- _drawPages(ctx, vr) {
433
- const pad = 16;
434
- let y = vr.y + pad - this._scrollY;
435
-
436
- for (let p = 1; p <= this.totalPages; p++) {
437
- const img = this._pageImages[p];
438
- const ph = img ? img.height : 200;
439
- const pw = img ? Math.min(img.width, vr.w - pad * 2) : vr.w - pad * 2;
440
- const px = vr.x + (vr.w - pw) / 2;
441
-
442
- if (y + ph >= vr.y && y <= vr.y + vr.h) {
443
- ctx.shadowColor = this._colors.pageShadow;
444
- ctx.shadowBlur = 8; ctx.shadowOffsetY = 2;
445
- ctx.fillStyle = this.pageBackground;
446
- ctx.fillRect(px, y, pw, ph);
447
- ctx.shadowBlur = 0; ctx.shadowOffsetY = 0;
448
-
449
- if (img) {
450
- ctx.drawImage(img, 0, 0, img.width, img.height, px, y, pw, ph);
451
- } else {
452
- ctx.fillStyle = '#ccc'; ctx.font = '13px sans-serif';
453
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
454
- ctx.fillText(`Page ${p}…`, px + pw/2, y + ph/2);
788
+ ctx.restore();
789
+ }
790
+
791
+ _drawToolbar(ctx) {
792
+ const tb = this._tbRect;
793
+ ctx.fillStyle = this.toolbarColor || this._primary;
794
+ ctx.fillRect(tb.x, tb.y, tb.w, tb.h);
795
+ if (!this._isMat) {
796
+ ctx.strokeStyle = this._colors.toolbarBorder || '#C6C6C8'; ctx.lineWidth = 1;
797
+ ctx.beginPath(); ctx.moveTo(tb.x, tb.y+tb.h); ctx.lineTo(tb.x+tb.w, tb.y+tb.h); ctx.stroke();
798
+ }
799
+
800
+ ctx.save(); ctx.beginPath(); ctx.rect(tb.x, tb.y, tb.w, tb.h); ctx.clip();
801
+
802
+ this._tbButtons = [];
803
+ this._pageInputRect = null;
804
+
805
+ let cx = tb.x + 12;
806
+ const my = tb.y + tb.h / 2;
807
+
808
+ // Titre
809
+ const title = typeof this.src === 'string' ? (this.src.split('/').pop() || 'Document.pdf') : 'Document.pdf';
810
+ ctx.fillStyle = this.toolbarTextColor;
811
+ ctx.font = `${this._isMat ? '500' : '600'} ${this._isMat ? 15 : 16}px sans-serif`;
812
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
813
+ let t = title.length > 8 ? title.slice(0, 8) + '…' : title;
814
+ ctx.fillText(t, cx, my);
815
+
816
+ // Navigation
817
+ if (this.totalPages > 1 && this.allowNavigation) {
818
+ cx = tb.x + tb.w - 240;
819
+ cx = this._btn(ctx, cx, my, 'prev', this._icChevron('left'), 0, this.currentPage <= 1);
820
+ ctx.fillStyle = this.toolbarTextColor;
821
+ ctx.font = '13px sans-serif';
822
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
823
+ ctx.fillText(`${this.currentPage}/${this.totalPages}`, cx + 20, my);
824
+ this._pageInputRect = { x: cx, y: my - 16, w: 44, h: 32 };
825
+ cx += 44;
826
+ cx = this._btn(ctx, cx, my, 'next', this._icChevron('right'), 0, this.currentPage >= this.totalPages);
827
+ cx += 8;
828
+ }
829
+
830
+ // Zoom
831
+ cx = tb.x + tb.w - 120;
832
+ if (this.allowZoom) {
833
+ cx = this._btn(ctx, cx, my, 'zoomOut', '−', 20, this.scale <= this.minScale);
834
+ ctx.fillStyle = this.toolbarTextColor;
835
+ ctx.font = '12px sans-serif';
836
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
837
+ ctx.fillText(`${Math.round(this.scale * 100)}%`, cx + 20, my);
838
+ cx += 44;
839
+ cx = this._btn(ctx, cx, my, 'zoomIn', '+', 20, this.scale >= this.maxScale);
840
+ }
841
+
842
+ ctx.restore();
843
+ }
844
+
845
+ _btn(ctx, x, y, id, content, fontSize = 0, disabled = false) {
846
+ const bw = 32, bh = 32, bx = x, by = y - 16;
847
+ if (this._hoveredBtn === id && !disabled) {
848
+ ctx.fillStyle = this.toolbarIconColor + '38';
849
+ this._rr(ctx, bx, by, bw, bh, this._isMat ? 4 : 6); ctx.fill();
850
+ }
851
+ this._tbButtons.push({ id, x: bx, y: by, w: bw, h: bh, disabled });
852
+ ctx.globalAlpha = disabled ? 0.35 : 1;
853
+ if (typeof content === 'string' && content.length <= 2) {
854
+ ctx.fillStyle = this.toolbarIconColor;
855
+ ctx.font = `${fontSize || 18}px sans-serif`;
856
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
857
+ ctx.fillText(content, bx + bw/2, y);
858
+ } else {
859
+ ctx.save(); ctx.translate(bx + 7, y - 9);
860
+ ctx.strokeStyle = this.toolbarIconColor;
861
+ ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
862
+ ctx.stroke(new Path2D(content));
863
+ ctx.restore();
864
+ }
865
+ ctx.globalAlpha = 1;
866
+ return x + bw + 4;
867
+ }
868
+
869
+ _drawSearchBar(ctx) {
870
+ const sb = this._sbRect;
871
+ if (sb.h === 0) return;
872
+ ctx.fillStyle = this._isMat ? this.m3Colors.surfaceVariant : '#F2F2F7';
873
+ ctx.fillRect(sb.x, sb.y, sb.w, sb.h);
874
+ ctx.strokeStyle = this._colors.outlineVariant || '#C6C6C8';
875
+ ctx.lineWidth = 1;
876
+ ctx.beginPath();
877
+ ctx.moveTo(sb.x, sb.y + sb.h);
878
+ ctx.lineTo(sb.x + sb.w, sb.y + sb.h);
879
+ ctx.stroke();
880
+
881
+ const fw = sb.w - 24 - 38,
882
+ fh = sb.h - 16;
883
+ ctx.fillStyle = '#fff';
884
+ this._rr(ctx, sb.x + 12, sb.y + 8, fw, fh, this._isMat ? 4 : 8);
885
+ ctx.fill();
886
+ ctx.strokeStyle = this._colors.outline || '#C6C6C8';
887
+ this._rr(ctx, sb.x + 12, sb.y + 8, fw, fh, this._isMat ? 4 : 8);
888
+ ctx.stroke();
889
+ ctx.fillStyle = this._searchQuery ? '#000' : '#999';
890
+ ctx.font = '13px sans-serif';
891
+ ctx.textAlign = 'left';
892
+ ctx.textBaseline = 'middle';
893
+ ctx.fillText(this._searchQuery || 'Rechercher...', sb.x + 20, sb.y + sb.h / 2);
894
+
895
+ const clx = sb.x + sb.w - 34;
896
+ this._searchCloseRect = {
897
+ x: clx,
898
+ y: sb.y,
899
+ w: 34,
900
+ h: sb.h
901
+ };
902
+ ctx.fillStyle = this._hoveredBtn === 'searchClose' ? '#444' : '#666';
903
+ ctx.font = '15px sans-serif';
904
+ ctx.textAlign = 'center';
905
+ ctx.textBaseline = 'middle';
906
+ ctx.fillText('✕', clx + 17, sb.y + sb.h / 2);
907
+ }
908
+
909
+ // ─── Actions ─────────────────────────────────────────────────────────────────
910
+
911
+ _handleBtn(id) {
912
+ switch (id) {
913
+ case 'prev':
914
+ this.goToPreviousPage();
915
+ break;
916
+ case 'next':
917
+ this.goToNextPage();
918
+ break;
919
+ case 'zoomIn':
920
+ this._setScale(this.scale + 0.25);
921
+ break;
922
+ case 'zoomOut':
923
+ this._setScale(this.scale - 0.25);
924
+ break;
925
+ case 'zoomFit':
926
+ this._fitToWidth();
927
+ break;
928
+ case 'rotate':
929
+ this._rotate();
930
+ break;
931
+ case 'search':
932
+ this._toggleSearch();
933
+ break;
934
+ case 'download':
935
+ this._download();
936
+ break;
937
+ case 'print':
938
+ this._print();
939
+ break;
940
+ }
941
+ this.markDirty();
942
+ }
943
+
944
+ _setScale(s) {
945
+ this.scale = Math.max(this.minScale, Math.min(this.maxScale, Math.round(s * 100) / 100));
946
+ this.onScaleChange(this.scale);
947
+ this._reRenderAll();
948
+ }
949
+
950
+ async _fitToWidth() {
951
+ if (!this._pdfDoc) return;
952
+ const vp = (await this._pdfDoc.getPage(1)).getViewport({
953
+ scale: 1
954
+ });
955
+ this._setScale((this._viewerRect.w - 32) / vp.width);
956
+ }
957
+
958
+ _rotate() {
959
+ this.rotation = (this.rotation + 90) % 360;
960
+ this._reRenderAll();
961
+ }
962
+
963
+ _download() {
964
+ if (typeof this.src !== 'string') return;
965
+ const a = document.createElement('a');
966
+ a.href = this.src;
967
+ a.download = this.src.split('/').pop() || 'document.pdf';
968
+ a.click();
969
+ }
970
+
971
+ _print() {
972
+ if (typeof this.src !== 'string') return;
973
+ const w = window.open(this.src);
974
+ if (w) w.addEventListener('load', () => w.print());
975
+ }
976
+
977
+ _toggleSearch() {
978
+ this._searchOpen = !this._searchOpen;
979
+ this._computeMaxScroll();
980
+ this.markDirty();
981
+ }
982
+
983
+ _promptPage() {
984
+ const v = prompt(`Aller à la page (1–${this.totalPages}) :`, this.currentPage);
985
+ if (v !== null) {
986
+ const p = parseInt(v);
987
+ if (p >= 1 && p <= this.totalPages) this.goToPage(p);
455
988
  }
989
+ }
990
+
991
+ // ─── Navigation publique ─────────────────────────────────────────────────────
992
+
993
+ goToPage(page) {
994
+ if (!this._pdfDoc || page < 1 || page > this.totalPages) return;
995
+ this.currentPage = page;
996
+ this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._pageScrollOffset(page) - 8));
997
+ this.onPageChange(page, this.totalPages);
998
+ this.markDirty();
999
+ }
1000
+
1001
+ goToPreviousPage() {
1002
+ if (this.currentPage > 1) this.goToPage(this.currentPage - 1);
1003
+ }
1004
+ goToNextPage() {
1005
+ if (this.currentPage < this.totalPages) this.goToPage(this.currentPage + 1);
1006
+ }
1007
+
1008
+ // ─── API publique ─────────────────────────────────────────────────────────────
1009
+
1010
+ load(src) {
1011
+ this.src = src;
1012
+ this._pdfDoc = null;
1013
+ this._pageImages = {};
1014
+ this._thumbImages = {};
1015
+ this._scrollY = 0;
1016
+ this._loadPDF(src);
1017
+ }
1018
+
1019
+ setScale(s) {
1020
+ this._setScale(s);
1021
+ }
1022
+ async getMetadata() {
1023
+ return this._pdfDoc ? this._pdfDoc.getMetadata() : null;
1024
+ }
1025
+
1026
+ // ─── Icônes ──────────────────────────────────────────────────────────────────
1027
+
1028
+ _icChevron(d) {
1029
+ return d === 'left' ? 'M12 3 L5 9 L12 15' : 'M6 3 L13 9 L6 15';
1030
+ }
1031
+
1032
+ // ─── roundRect ───────────────────────────────────────────────────────────────
1033
+
1034
+ _rr(ctx, x, y, w, h, r) {
1035
+ r = Math.min(r, w / 2, h / 2);
1036
+ ctx.beginPath();
1037
+ ctx.moveTo(x + r, y);
1038
+ ctx.lineTo(x + w - r, y);
1039
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1040
+ ctx.lineTo(x + w, y + h - r);
1041
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1042
+ ctx.lineTo(x + r, y + h);
1043
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1044
+ ctx.lineTo(x, y + r);
1045
+ ctx.quadraticCurveTo(x, y, x + r, y);
1046
+ ctx.closePath();
1047
+ }
1048
+
1049
+ isPointInside(x, y) {
1050
+ return x >= this.x && x <= this.x + this.width &&
1051
+ y >= this.y && y <= this.y + this.height;
1052
+ }
1053
+
456
1054
 
457
- ctx.fillStyle = 'rgba(0,0,0,0.28)'; ctx.font = '11px sans-serif';
458
- ctx.textAlign = 'right'; ctx.textBaseline = 'bottom';
459
- ctx.fillText(`${p} / ${this.totalPages}`, px + pw - 6, y + ph - 4);
460
- }
461
- y += ph + pad;
462
- }
463
- this._drawScrollbar(ctx, vr);
464
- }
465
-
466
- _drawScrollbar(ctx, vr) {
467
- if (this._maxScrollY <= 0) return;
468
- const tw = 5, tr = 3;
469
- const tx = vr.x + vr.w - tw - 3;
470
- const ty = vr.y + 4, th = vr.h - 8;
471
- const ratio = vr.h / (vr.h + this._maxScrollY);
472
- const thumbH = Math.max(24, th * ratio);
473
- const thumbY = ty + (this._scrollY / this._maxScrollY) * (th - thumbH);
474
- ctx.fillStyle = 'rgba(0,0,0,0.08)'; this._rr(ctx, tx, ty, tw, th, tr); ctx.fill();
475
- ctx.fillStyle = 'rgba(0,0,0,0.32)'; this._rr(ctx, tx, thumbY, tw, thumbH, tr); ctx.fill();
476
- }
477
-
478
- _drawLoading(ctx, vr) {
479
- const cx = vr.x + vr.w/2, cy = vr.y + vr.h/2 - 20;
480
- ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.lineWidth = 4;
481
- ctx.beginPath(); ctx.arc(cx, cy, 22, 0, Math.PI*2); ctx.stroke();
482
- ctx.strokeStyle = this._primary; ctx.lineWidth = 4; ctx.lineCap = 'round';
483
- ctx.beginPath(); ctx.arc(cx, cy, 22, this._spinAngle, this._spinAngle + 1.3); ctx.stroke();
484
- ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
485
- ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
486
- ctx.fillText('Chargement...', cx, cy + 32);
487
- }
488
-
489
- _drawEmpty(ctx, vr) {
490
- const cx = vr.x + vr.w/2, cy = vr.y + vr.h/2 - 20;
491
- ctx.globalAlpha = 0.35; ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
492
- ctx.beginPath();
493
- const fx = cx-20, fy = cy-26;
494
- ctx.moveTo(fx+6,fy); ctx.lineTo(fx+28,fy); ctx.lineTo(fx+40,fy+14);
495
- ctx.lineTo(fx+40,fy+54); ctx.quadraticCurveTo(fx+40,fy+58,fx+36,fy+58);
496
- ctx.lineTo(fx+4,fy+58); ctx.quadraticCurveTo(fx,fy+58,fx,fy+54);
497
- ctx.lineTo(fx,fy+6); ctx.quadraticCurveTo(fx,fy,fx+6,fy);
498
- ctx.closePath(); ctx.fill();
499
- ctx.globalAlpha = 1; ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
500
- ctx.font = '13px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
501
- ctx.fillText('Aucun document chargé', cx, cy + 42);
502
- }
503
-
504
- _drawError(ctx, vr) {
505
- const cx = vr.x + vr.w/2, cy = vr.y + vr.h/2 - 30;
506
- const err = this._colors.error || '#BA1A1A';
507
- ctx.fillStyle = err; ctx.beginPath(); ctx.arc(cx, cy, 24, 0, Math.PI*2); ctx.fill();
508
- ctx.fillStyle = '#fff'; ctx.font = 'bold 26px sans-serif';
509
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('!', cx, cy);
510
- ctx.fillStyle = err; ctx.font = '13px sans-serif'; ctx.textBaseline = 'top';
511
- ctx.fillText((this.error || 'Erreur').substring(0, 60), cx, cy + 34);
512
- const bx = cx - 48, by = cy + 62;
513
- this._retryBtn = { x: bx, y: by, w: 96, h: 30 };
514
- ctx.fillStyle = this._hoveredBtn === 'retry' ? this._primary + 'dd' : this._primary;
515
- this._rr(ctx, bx, by, 96, 30, this._isMat ? 15 : 8); ctx.fill();
516
- ctx.fillStyle = '#fff'; ctx.font = '13px sans-serif'; ctx.textBaseline = 'middle';
517
- ctx.fillText('Réessayer', cx, by + 15);
518
- }
519
-
520
- _drawThumbs(ctx) {
521
- const tr = this._thumbRect;
522
- ctx.fillStyle = this._colors.thumbsBg; ctx.fillRect(tr.x, tr.y, tr.w, tr.h);
523
- ctx.strokeStyle = this._colors.thumbBorder; ctx.lineWidth = 1;
524
- ctx.beginPath(); ctx.moveTo(tr.x+tr.w, tr.y); ctx.lineTo(tr.x+tr.w, tr.y+tr.h); ctx.stroke();
525
-
526
- ctx.save(); ctx.beginPath(); ctx.rect(tr.x, tr.y, tr.w, tr.h); ctx.clip();
527
- let ty = tr.y + 8;
528
- for (let p = 1; p <= this.totalPages; p++) {
529
- const img = this._thumbImages[p];
530
- const tw = tr.w - 16;
531
- const th = img ? Math.round(img.height * tw / img.width) : 70;
532
- const tx = tr.x + 8;
533
- if (ty + th > tr.y && ty < tr.y + tr.h) {
534
- const active = p === this.currentPage;
535
- ctx.strokeStyle = active ? this._colors.thumbActive : this._colors.thumbBorder;
536
- ctx.lineWidth = active ? 2 : 1;
537
- this._rr(ctx, tx, ty, tw, th, 3);
538
- if (img) { ctx.drawImage(img, tx, ty, tw, th); ctx.stroke(); }
539
- else { ctx.fillStyle = '#fff'; ctx.fill(); ctx.stroke(); }
540
- ctx.fillStyle = '#666'; ctx.font = '10px sans-serif';
541
- ctx.textAlign = 'center'; ctx.textBaseline = 'top';
542
- ctx.fillText(p, tx + tw/2, ty + th + 2);
543
- }
544
- ty += th + 18;
545
- }
546
- ctx.restore();
547
- }
548
-
549
- _drawToolbar(ctx) {
550
- const tb = this._tbRect;
551
- ctx.fillStyle = this._primary; ctx.fillRect(tb.x, tb.y, tb.w, tb.h);
552
- if (!this._isMat) {
553
- ctx.strokeStyle = this._colors.toolbarBorder || '#C6C6C8'; ctx.lineWidth = 1;
554
- ctx.beginPath(); ctx.moveTo(tb.x, tb.y+tb.h); ctx.lineTo(tb.x+tb.w, tb.y+tb.h); ctx.stroke();
555
- }
556
-
557
- ctx.save(); ctx.beginPath(); ctx.rect(tb.x, tb.y, tb.w, tb.h); ctx.clip();
558
-
559
- this._tbButtons = [];
560
- this._pageInputRect = null;
561
-
562
- let cx = tb.x + 12;
563
- const my = tb.y + tb.h / 2;
564
-
565
- // Titre
566
- const title = typeof this.src === 'string' ? (this.src.split('/').pop() || 'Document.pdf') : 'Document.pdf';
567
- ctx.fillStyle = 'rgba(255,255,255,0.92)';
568
- ctx.font = `${this._isMat ? '500' : '600'} ${this._isMat ? 15 : 16}px sans-serif`;
569
- ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
570
- let t = title;
571
- const availW = tb.w - 200;
572
- while (ctx.measureText(t).width > availW && t.length > 2) t = t.slice(0, -1);
573
- if (t !== title) t += '…';
574
- ctx.fillText(t, cx, my);
575
-
576
- // Navigation
577
- if (this.totalPages > 1 && this.allowNavigation) {
578
- cx = tb.x + tb.w - 240;
579
- cx = this._btn(ctx, cx, my, 'prev', this._icChevron('left'), 0, this.currentPage <= 1);
580
- ctx.fillStyle = 'rgba(255,255,255,0.88)'; ctx.font = '13px sans-serif';
581
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
582
- ctx.fillText(`${this.currentPage}/${this.totalPages}`, cx + 20, my);
583
- this._pageInputRect = { x: cx, y: my - 16, w: 44, h: 32 };
584
- cx += 44;
585
- cx = this._btn(ctx, cx, my, 'next', this._icChevron('right'), 0, this.currentPage >= this.totalPages);
586
- cx += 8;
587
- }
588
-
589
- // Zoom
590
- cx = tb.x + tb.w - 120;
591
- if (this.allowZoom) {
592
- cx = this._btn(ctx, cx, my, 'zoomOut', '−', 20, this.scale <= this.minScale);
593
- ctx.fillStyle = 'rgba(255,255,255,0.88)'; ctx.font = '12px sans-serif';
594
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
595
- ctx.fillText(`${Math.round(this.scale * 100)}%`, cx + 20, my);
596
- cx += 44;
597
- cx = this._btn(ctx, cx, my, 'zoomIn', '+', 20, this.scale >= this.maxScale);
598
- }
599
-
600
- ctx.restore();
601
- }
602
-
603
- _btn(ctx, x, y, id, content, fontSize = 0, disabled = false) {
604
- const bw = 32, bh = 32, bx = x, by = y - 16;
605
- if (this._hoveredBtn === id && !disabled) {
606
- ctx.fillStyle = 'rgba(255,255,255,0.22)';
607
- this._rr(ctx, bx, by, bw, bh, this._isMat ? 4 : 6); ctx.fill();
608
- }
609
- this._tbButtons.push({ id, x: bx, y: by, w: bw, h: bh, disabled });
610
- ctx.globalAlpha = disabled ? 0.35 : 1;
611
- if (typeof content === 'string' && content.length <= 2) {
612
- ctx.fillStyle = '#fff'; ctx.font = `${fontSize || 18}px sans-serif`;
613
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
614
- ctx.fillText(content, bx + bw/2, y);
615
- } else {
616
- ctx.save(); ctx.translate(bx + 7, y - 9);
617
- ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
618
- ctx.stroke(new Path2D(content));
619
- ctx.restore();
620
- }
621
- ctx.globalAlpha = 1;
622
- return x + bw + 4;
623
- }
624
-
625
- _drawSearchBar(ctx) {
626
- const sb = this._sbRect; if (sb.h === 0) return;
627
- ctx.fillStyle = this._isMat ? this.m3Colors.surfaceVariant : '#F2F2F7';
628
- ctx.fillRect(sb.x, sb.y, sb.w, sb.h);
629
- ctx.strokeStyle = this._colors.outlineVariant || '#C6C6C8'; ctx.lineWidth = 1;
630
- ctx.beginPath(); ctx.moveTo(sb.x, sb.y+sb.h); ctx.lineTo(sb.x+sb.w, sb.y+sb.h); ctx.stroke();
631
-
632
- const fw = sb.w - 24 - 38, fh = sb.h - 16;
633
- ctx.fillStyle = '#fff'; this._rr(ctx, sb.x+12, sb.y+8, fw, fh, this._isMat ? 4 : 8); ctx.fill();
634
- ctx.strokeStyle = this._colors.outline || '#C6C6C8';
635
- this._rr(ctx, sb.x+12, sb.y+8, fw, fh, this._isMat ? 4 : 8); ctx.stroke();
636
- ctx.fillStyle = this._searchQuery ? '#000' : '#999';
637
- ctx.font = '13px sans-serif'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
638
- ctx.fillText(this._searchQuery || 'Rechercher...', sb.x+20, sb.y+sb.h/2);
639
-
640
- const clx = sb.x + sb.w - 34;
641
- this._searchCloseRect = { x: clx, y: sb.y, w: 34, h: sb.h };
642
- ctx.fillStyle = this._hoveredBtn === 'searchClose' ? '#444' : '#666';
643
- ctx.font = '15px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
644
- ctx.fillText('✕', clx + 17, sb.y + sb.h/2);
645
- }
646
-
647
- // ─── Actions ─────────────────────────────────────────────────────────────────
648
-
649
- _handleBtn(id) {
650
- switch (id) {
651
- case 'prev': this.goToPreviousPage(); break;
652
- case 'next': this.goToNextPage(); break;
653
- case 'zoomIn': this._setScale(this.scale + 0.25); break;
654
- case 'zoomOut': this._setScale(this.scale - 0.25); break;
655
- case 'zoomFit': this._fitToWidth(); break;
656
- case 'rotate': this._rotate(); break;
657
- case 'search': this._toggleSearch(); break;
658
- case 'download': this._download(); break;
659
- case 'print': this._print(); break;
660
- }
661
- this.markDirty();
662
- }
663
-
664
- _setScale(s) {
665
- this.scale = Math.max(this.minScale, Math.min(this.maxScale, Math.round(s * 100) / 100));
666
- this.onScaleChange(this.scale);
667
- this._reRenderAll();
668
- }
669
-
670
- async _fitToWidth() {
671
- if (!this._pdfDoc) return;
672
- const vp = (await this._pdfDoc.getPage(1)).getViewport({ scale: 1 });
673
- this._setScale((this._viewerRect.w - 32) / vp.width);
674
- }
675
-
676
- _rotate() { this.rotation = (this.rotation + 90) % 360; this._reRenderAll(); }
677
-
678
- _download() {
679
- if (typeof this.src !== 'string') return;
680
- const a = document.createElement('a');
681
- a.href = this.src; a.download = this.src.split('/').pop() || 'document.pdf'; a.click();
682
- }
683
-
684
- _print() {
685
- if (typeof this.src !== 'string') return;
686
- const w = window.open(this.src);
687
- if (w) w.addEventListener('load', () => w.print());
688
- }
689
-
690
- _toggleSearch() { this._searchOpen = !this._searchOpen; this._computeMaxScroll(); this.markDirty(); }
691
-
692
- _promptPage() {
693
- const v = prompt(`Aller à la page (1–${this.totalPages}) :`, this.currentPage);
694
- if (v !== null) { const p = parseInt(v); if (p >= 1 && p <= this.totalPages) this.goToPage(p); }
695
- }
696
-
697
- // ─── Navigation publique ─────────────────────────────────────────────────────
698
-
699
- goToPage(page) {
700
- if (!this._pdfDoc || page < 1 || page > this.totalPages) return;
701
- this.currentPage = page;
702
- this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._pageScrollOffset(page) - 8));
703
- this.onPageChange(page, this.totalPages);
704
- this.markDirty();
705
- }
706
-
707
- goToPreviousPage() { if (this.currentPage > 1) this.goToPage(this.currentPage - 1); }
708
- goToNextPage() { if (this.currentPage < this.totalPages) this.goToPage(this.currentPage + 1); }
709
-
710
- // ─── API publique ─────────────────────────────────────────────────────────────
711
-
712
- load(src) {
713
- this.src = src; this._pdfDoc = null;
714
- this._pageImages = {}; this._thumbImages = {}; this._scrollY = 0;
715
- this._loadPDF(src);
716
- }
717
-
718
- setScale(s) { this._setScale(s); }
719
- async getMetadata() { return this._pdfDoc ? this._pdfDoc.getMetadata() : null; }
720
-
721
- // ─── Icônes ──────────────────────────────────────────────────────────────────
722
-
723
- _icChevron(d) { return d === 'left' ? 'M12 3 L5 9 L12 15' : 'M6 3 L13 9 L6 15'; }
724
-
725
- // ─── roundRect ───────────────────────────────────────────────────────────────
726
-
727
- _rr(ctx, x, y, w, h, r) {
728
- r = Math.min(r, w/2, h/2);
729
- ctx.beginPath();
730
- ctx.moveTo(x+r, y); ctx.lineTo(x+w-r, y);
731
- ctx.quadraticCurveTo(x+w, y, x+w, y+r);
732
- ctx.lineTo(x+w, y+h-r);
733
- ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
734
- ctx.lineTo(x+r, y+h);
735
- ctx.quadraticCurveTo(x, y+h, x, y+h-r);
736
- ctx.lineTo(x, y+r);
737
- ctx.quadraticCurveTo(x, y, x+r, y);
738
- ctx.closePath();
739
- }
740
-
741
- isPointInside(x, y) {
742
- return x >= this.x && x <= this.x + this.width &&
743
- y >= this.y && y <= this.y + this.height;
744
- }
745
-
746
- destroy() {
747
- if (this._spinRAF) cancelAnimationFrame(this._spinRAF);
748
- this._pdfDoc = null;
749
- super.destroy?.();
750
- }
751
1055
  }
752
1056
 
753
1057
  export default PDFViewer;