canvasframework 0.5.63 → 0.5.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/PDFViewer.js +1039 -890
- package/package.json +1 -1
package/components/PDFViewer.js
CHANGED
|
@@ -1,1068 +1,1217 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Lecteur PDF
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Utilise PDF.js (CDN) pour le rendu. La lib est chargée automatiquement.
|
|
4
|
+
* Lecteur PDF entièrement dessiné sur Canvas.
|
|
5
|
+
* Gère ses propres événements natifs (click, wheel, mouse, touch)
|
|
6
|
+
* directement sur le canvas — aucune dépendance au framework pour les interactions.
|
|
9
7
|
*
|
|
10
8
|
* @class
|
|
11
9
|
* @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
10
|
*/
|
|
21
11
|
class PDFViewer extends Component {
|
|
22
12
|
|
|
23
13
|
/**
|
|
24
14
|
* @param {CanvasFramework} framework
|
|
25
15
|
* @param {Object} [options={}]
|
|
26
|
-
* @param {string|Uint8Array} [options.src]
|
|
27
|
-
* @param {number} [options.initialPage=1]
|
|
28
|
-
* @param {number} [options.initialScale=1.0]
|
|
29
|
-
* @param {boolean} [options.showToolbar=true]
|
|
30
|
-
* @param {boolean} [options.showThumbnails=false]
|
|
31
|
-
* @param {boolean} [options.
|
|
32
|
-
* @param {boolean} [options.
|
|
33
|
-
* @param {boolean} [options.
|
|
34
|
-
* @param {boolean} [options.
|
|
35
|
-
* @param {boolean} [options.
|
|
36
|
-
* @param {
|
|
37
|
-
* @param {number} [options.
|
|
38
|
-
* @param {
|
|
39
|
-
* @param {string} [options.
|
|
40
|
-
* @param {string} [options.
|
|
41
|
-
* @param {
|
|
42
|
-
* @param {Function} [options.
|
|
43
|
-
* @param {Function} [options.
|
|
44
|
-
* @param {Function} [options.
|
|
16
|
+
* @param {string|Uint8Array} [options.src]
|
|
17
|
+
* @param {number} [options.initialPage=1]
|
|
18
|
+
* @param {number} [options.initialScale=1.0]
|
|
19
|
+
* @param {boolean} [options.showToolbar=true]
|
|
20
|
+
* @param {boolean} [options.showThumbnails=false]
|
|
21
|
+
* @param {boolean} [options.allowZoom=true] - Boutons zoom
|
|
22
|
+
* @param {boolean} [options.allowNavigation=true] - Prev/next (masqué si 1 page)
|
|
23
|
+
* @param {boolean} [options.allowDownload=true]
|
|
24
|
+
* @param {boolean} [options.allowPrint=true]
|
|
25
|
+
* @param {boolean} [options.allowRotate=true]
|
|
26
|
+
* @param {boolean} [options.allowSearch=true]
|
|
27
|
+
* @param {number} [options.minScale=0.25]
|
|
28
|
+
* @param {number} [options.maxScale=5.0]
|
|
29
|
+
* @param {string} [options.backgroundColor]
|
|
30
|
+
* @param {string} [options.primaryColor]
|
|
31
|
+
* @param {string} [options.pageBackground='#FFFFFF']
|
|
32
|
+
* @param {Function} [options.onPageChange]
|
|
33
|
+
* @param {Function} [options.onScaleChange]
|
|
34
|
+
* @param {Function} [options.onLoad]
|
|
35
|
+
* @param {Function} [options.onError]
|
|
45
36
|
*/
|
|
46
37
|
constructor(framework, options = {}) {
|
|
47
38
|
super(framework, options);
|
|
48
39
|
|
|
49
|
-
this.platform
|
|
50
|
-
this.src
|
|
51
|
-
this.currentPage
|
|
52
|
-
this.totalPages
|
|
53
|
-
this.scale
|
|
54
|
-
this.rotation
|
|
55
|
-
this.loading
|
|
56
|
-
this.error
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
|
|
40
|
+
this.platform = framework.platform;
|
|
41
|
+
this.src = options.src || null;
|
|
42
|
+
this.currentPage = options.initialPage || 1;
|
|
43
|
+
this.totalPages = 0;
|
|
44
|
+
this.scale = options.initialScale || 1.0;
|
|
45
|
+
this.rotation = 0;
|
|
46
|
+
this.loading = false;
|
|
47
|
+
this.error = null;
|
|
48
|
+
|
|
49
|
+
this.showToolbar = options.showToolbar !== false;
|
|
50
|
+
this.showThumbnails = options.showThumbnails || false;
|
|
51
|
+
this.allowZoom = options.allowZoom !== false;
|
|
52
|
+
this.allowNavigation = options.allowNavigation !== false;
|
|
53
|
+
this.allowDownload = options.allowDownload !== false;
|
|
54
|
+
this.allowPrint = options.allowPrint !== false;
|
|
55
|
+
this.allowRotate = options.allowRotate !== false;
|
|
56
|
+
this.allowSearch = options.allowSearch !== false;
|
|
57
|
+
this.minScale = options.minScale || 0.25;
|
|
58
|
+
this.maxScale = options.maxScale || 5.0;
|
|
59
|
+
this.backgroundColor = options.backgroundColor || null;
|
|
60
|
+
this.primaryColor = options.primaryColor || null;
|
|
61
|
+
this.pageBackground = options.pageBackground || '#FFFFFF';
|
|
62
|
+
|
|
71
63
|
this.onPageChange = options.onPageChange || (() => {});
|
|
72
64
|
this.onScaleChange = options.onScaleChange || (() => {});
|
|
73
65
|
this.onLoad = options.onLoad || (() => {});
|
|
74
66
|
this.onErrorCb = options.onError || (() => {});
|
|
75
67
|
|
|
76
|
-
//
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
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;
|
|
68
|
+
// Layout
|
|
69
|
+
this._toolbarHeight = this.showToolbar ? 52 : 0;
|
|
70
|
+
this._thumbsWidth = 110; // toujours réservé, affiché si totalPages > 1
|
|
87
71
|
this._searchOpen = false;
|
|
88
72
|
this._searchQuery = '';
|
|
89
|
-
this._fullscreen = false;
|
|
90
73
|
|
|
91
|
-
|
|
92
|
-
this.
|
|
74
|
+
// PDF state
|
|
75
|
+
this._pdfDoc = null;
|
|
76
|
+
this._pageImages = {};
|
|
77
|
+
this._thumbImages = {};
|
|
78
|
+
|
|
79
|
+
// Viewer scroll
|
|
80
|
+
this._scrollY = 0;
|
|
81
|
+
this._maxScrollY = 0;
|
|
82
|
+
|
|
83
|
+
// Drag scroll (mouse)
|
|
84
|
+
this._dragging = false;
|
|
85
|
+
this._dragStartY = 0;
|
|
86
|
+
this._dragStartScroll = 0;
|
|
87
|
+
|
|
88
|
+
// Touch scroll
|
|
89
|
+
this._touchStartY = 0;
|
|
90
|
+
this._touchLastY = 0;
|
|
91
|
+
this._touchVelocity = 0;
|
|
92
|
+
this._momentumRAF = null;
|
|
93
|
+
|
|
94
|
+
// Toolbar hit areas (rebuilt each draw)
|
|
95
|
+
this._tbButtons = [];
|
|
96
|
+
this._pageInputRect = null;
|
|
97
|
+
this._retryBtn = null;
|
|
98
|
+
this._searchCloseRect= null;
|
|
99
|
+
this._hoveredBtn = null;
|
|
100
|
+
|
|
101
|
+
// Spinner
|
|
102
|
+
this._spinAngle = 0;
|
|
103
|
+
this._spinRAF = null;
|
|
104
|
+
|
|
105
|
+
// Events
|
|
106
|
+
this._eventsRegistered = false;
|
|
107
|
+
this._boundHandlers = {};
|
|
108
|
+
|
|
109
|
+
// Mouse position tracking
|
|
110
|
+
this._lastMouseX = 0;
|
|
111
|
+
this._lastMouseY = 0;
|
|
93
112
|
|
|
94
113
|
// Couleurs Material You 3
|
|
95
114
|
this.m3Colors = {
|
|
96
115
|
primary: '#6750A4',
|
|
97
116
|
onPrimary: '#FFFFFF',
|
|
98
|
-
surface: '#FFFBFE',
|
|
99
117
|
surfaceVariant: '#E7E0EC',
|
|
100
118
|
onSurface: '#1C1B1F',
|
|
101
119
|
onSurfaceVariant: '#49454F',
|
|
102
120
|
outline: '#79747E',
|
|
103
121
|
outlineVariant: '#CAC4D0',
|
|
104
122
|
error: '#BA1A1A',
|
|
105
|
-
shadow: '#00000040',
|
|
106
|
-
toolbarBg: '#6750A4',
|
|
107
|
-
toolbarText: '#FFFFFF',
|
|
108
123
|
viewerBg: '#F7F2FA',
|
|
109
124
|
thumbsBg: '#EFE9F4',
|
|
110
125
|
thumbBorder: '#CAC4D0',
|
|
111
126
|
thumbActive: '#6750A4',
|
|
112
|
-
|
|
113
|
-
pageShadow: '#00000026',
|
|
127
|
+
pageShadow: 'rgba(0,0,0,0.15)',
|
|
114
128
|
};
|
|
115
129
|
|
|
116
130
|
// Couleurs Cupertino
|
|
117
131
|
this.cupertinoColors = {
|
|
118
|
-
primary:
|
|
119
|
-
onPrimary:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
pageShadow: '#00000026',
|
|
132
|
+
primary: '#007AFF',
|
|
133
|
+
onPrimary: '#FFFFFF',
|
|
134
|
+
error: '#FF3B30',
|
|
135
|
+
toolbarBorder: '#C6C6C8',
|
|
136
|
+
viewerBg: '#D1D1D6',
|
|
137
|
+
thumbsBg: '#F2F2F7',
|
|
138
|
+
thumbBorder: '#C6C6C8',
|
|
139
|
+
thumbActive: '#007AFF',
|
|
140
|
+
pageShadow: 'rgba(0,0,0,0.15)',
|
|
141
|
+
onSurfaceVariant: '#8E8E93',
|
|
142
|
+
outlineVariant: '#C6C6C8',
|
|
143
|
+
outline: '#C6C6C8',
|
|
131
144
|
};
|
|
132
|
-
}
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
if (this.src) this._loadPDF(this.src);
|
|
147
|
+
this._startSpinner();
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
|
|
139
|
-
|
|
150
|
+
// ─── Couleurs & géométrie ────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
get _colors() { return this.platform === 'material' ? this.m3Colors : this.cupertinoColors; }
|
|
153
|
+
get _primary() { return this.primaryColor || this._colors.primary; }
|
|
154
|
+
get _isMat() { return this.platform === 'material'; }
|
|
155
|
+
|
|
156
|
+
get _tbRect() {
|
|
157
|
+
return { x: this.x, y: this.y, w: this.width, h: this._toolbarHeight };
|
|
158
|
+
}
|
|
159
|
+
get _sbRect() {
|
|
160
|
+
return { x: this.x, y: this.y + this._toolbarHeight, w: this.width, h: this._searchOpen ? 44 : 0 };
|
|
161
|
+
}
|
|
162
|
+
get _activeThumbsWidth() { return this.totalPages > 1 ? this._thumbsWidth : 0; }
|
|
163
|
+
get _viewerRect() {
|
|
164
|
+
const top = this._toolbarHeight + this._sbRect.h;
|
|
165
|
+
return { x: this.x + this._activeThumbsWidth, y: this.y + top, w: this.width - this._activeThumbsWidth, h: this.height - top };
|
|
166
|
+
}
|
|
167
|
+
get _thumbRect() {
|
|
168
|
+
const top = this._toolbarHeight + this._sbRect.h;
|
|
169
|
+
return { x: this.x, y: this.y + top, w: this._thumbsWidth, h: this.height - top };
|
|
140
170
|
}
|
|
141
171
|
|
|
142
|
-
// ───
|
|
172
|
+
// ─── Conversion événements → coordonnées canvas ──────────────────────────────
|
|
173
|
+
|
|
174
|
+
_evtToXY(e) {
|
|
175
|
+
const canvas = this.framework.canvas;
|
|
176
|
+
const rect = canvas.getBoundingClientRect();
|
|
177
|
+
// Ratio CSS vs canvas réel
|
|
178
|
+
const sx = canvas.width / rect.width;
|
|
179
|
+
const sy = canvas.height / rect.height;
|
|
180
|
+
let cx, cy;
|
|
181
|
+
if (e.touches && e.touches.length > 0) {
|
|
182
|
+
cx = e.touches[0].clientX; cy = e.touches[0].clientY;
|
|
183
|
+
} else if (e.changedTouches && e.changedTouches.length > 0) {
|
|
184
|
+
cx = e.changedTouches[0].clientX; cy = e.changedTouches[0].clientY;
|
|
185
|
+
} else {
|
|
186
|
+
cx = e.clientX; cy = e.clientY;
|
|
187
|
+
}
|
|
188
|
+
// On ajoute le scrollOffset du framework pour être en coordonnées "monde"
|
|
189
|
+
const worldY = (cy - rect.top) * sy + (this.framework.scrollOffset || 0);
|
|
190
|
+
return { x: (cx - rect.left) * sx, y: worldY };
|
|
191
|
+
}
|
|
143
192
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*/
|
|
149
|
-
async _loadPDFJS() {
|
|
150
|
-
if (window.pdfjsLib) return window.pdfjsLib;
|
|
193
|
+
_inSelf(x, y) {
|
|
194
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
195
|
+
y >= this.y && y <= this.y + this.height;
|
|
196
|
+
}
|
|
151
197
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
});
|
|
198
|
+
_inViewer(x, y) {
|
|
199
|
+
const v = this._viewerRect;
|
|
200
|
+
return x >= v.x && x <= v.x + v.w && y >= v.y && y <= v.y + v.h;
|
|
163
201
|
}
|
|
164
202
|
|
|
165
|
-
// ───
|
|
203
|
+
// ─── Enregistrement des événements natifs (one-shot) ─────────────────────────
|
|
204
|
+
|
|
205
|
+
_registerEvents() {
|
|
206
|
+
if (this._eventsRegistered) return;
|
|
207
|
+
this._eventsRegistered = true;
|
|
208
|
+
const canvas = this.framework.canvas;
|
|
209
|
+
|
|
210
|
+
// ── Mouse ──
|
|
211
|
+
const onMouseDown = (e) => {
|
|
212
|
+
const { x, y } = this._evtToXY(e);
|
|
213
|
+
if (!this._inSelf(x, y)) return;
|
|
214
|
+
|
|
215
|
+
// Vérifier si on clique sur la toolbar ou les miniatures
|
|
216
|
+
const inToolbar = this.showToolbar && y >= this._tbRect.y && y <= this._tbRect.y + this._tbRect.h;
|
|
217
|
+
const inThumbs = this.totalPages > 1 && x >= this._thumbRect.x && x <= this._thumbRect.x + this._thumbRect.w;
|
|
218
|
+
|
|
219
|
+
if (inToolbar || inThumbs) {
|
|
220
|
+
// Ne pas démarrer le drag pour les interactions toolbar/miniatures
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (this._inViewer(x, y)) {
|
|
225
|
+
this._dragging = true;
|
|
226
|
+
this._dragStartY = y;
|
|
227
|
+
this._dragStartScroll = this._scrollY;
|
|
228
|
+
if (this._momentumRAF) {
|
|
229
|
+
cancelAnimationFrame(this._momentumRAF);
|
|
230
|
+
this._momentumRAF = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
166
234
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
`;
|
|
235
|
+
const onMouseMove = (e) => {
|
|
236
|
+
const { x, y } = this._evtToXY(e);
|
|
237
|
+
this._lastMouseX = x;
|
|
238
|
+
this._lastMouseY = y;
|
|
239
|
+
|
|
240
|
+
// Hover - toujours vérifier le survol
|
|
241
|
+
if (this._inSelf(x, y)) {
|
|
242
|
+
this._checkHover(x, y);
|
|
243
|
+
} else if (this._hoveredBtn !== null) {
|
|
244
|
+
this._hoveredBtn = null;
|
|
245
|
+
this._redraw();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Drag
|
|
249
|
+
if (this._dragging) {
|
|
250
|
+
this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._dragStartScroll + (this._dragStartY - y)));
|
|
251
|
+
this._syncPage();
|
|
252
|
+
this._redraw();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
198
255
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this._containerEl.appendChild(this._toolbarEl);
|
|
203
|
-
}
|
|
256
|
+
const onMouseUp = () => {
|
|
257
|
+
this._dragging = false;
|
|
258
|
+
};
|
|
204
259
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
}
|
|
260
|
+
const onMouseLeave = () => {
|
|
261
|
+
this._dragging = false;
|
|
262
|
+
this._hoveredBtn = null;
|
|
263
|
+
this._redraw();
|
|
264
|
+
};
|
|
222
265
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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) => {
|
|
266
|
+
// ── Click ──
|
|
267
|
+
const onClick = (e) => {
|
|
268
|
+
const { x, y } = this._evtToXY(e);
|
|
269
|
+
if (!this._inSelf(x, y)) return;
|
|
270
|
+
this._handleClick(x, y);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// ── Wheel ──
|
|
274
|
+
const onWheel = (e) => {
|
|
275
|
+
const { x, y } = this._evtToXY(e);
|
|
276
|
+
if (!this._inSelf(x, y)) return;
|
|
277
|
+
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
|
|
243
281
|
if (e.ctrlKey || e.metaKey) {
|
|
244
|
-
|
|
282
|
+
// Zoom avec Ctrl+molette
|
|
245
283
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
246
284
|
this._setScale(this.scale + delta);
|
|
285
|
+
} else {
|
|
286
|
+
// Scroll normal
|
|
287
|
+
if (this._inViewer(x, y)) {
|
|
288
|
+
this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._scrollY + e.deltaY));
|
|
289
|
+
this._syncPage();
|
|
290
|
+
this._redraw();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// ── Touch ──
|
|
296
|
+
const onTouchStart = (e) => {
|
|
297
|
+
const { x, y } = this._evtToXY(e);
|
|
298
|
+
if (!this._inSelf(x, y)) return;
|
|
299
|
+
|
|
300
|
+
// Vérifier si on touche la toolbar ou les miniatures
|
|
301
|
+
const inToolbar = this.showToolbar && y >= this._tbRect.y && y <= this._tbRect.y + this._tbRect.h;
|
|
302
|
+
const inThumbs = this.totalPages > 1 && x >= this._thumbRect.x && x <= this._thumbRect.x + this._thumbRect.w;
|
|
303
|
+
|
|
304
|
+
if (inToolbar || inThumbs) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this._inViewer(x, y)) {
|
|
309
|
+
this._dragging = true;
|
|
310
|
+
this._touchStartY = y;
|
|
311
|
+
this._touchLastY = y;
|
|
312
|
+
this._touchVelocity = 0;
|
|
313
|
+
this._dragStartScroll = this._scrollY;
|
|
314
|
+
if (this._momentumRAF) {
|
|
315
|
+
cancelAnimationFrame(this._momentumRAF);
|
|
316
|
+
this._momentumRAF = null;
|
|
317
|
+
}
|
|
318
|
+
e.preventDefault();
|
|
247
319
|
}
|
|
248
|
-
}
|
|
320
|
+
};
|
|
249
321
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
322
|
+
const onTouchMove = (e) => {
|
|
323
|
+
if (!this._dragging) return;
|
|
324
|
+
const { y } = this._evtToXY(e);
|
|
325
|
+
this._touchVelocity = this._touchLastY - y;
|
|
326
|
+
this._touchLastY = y;
|
|
327
|
+
this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._dragStartScroll + (this._touchStartY - y)));
|
|
328
|
+
this._syncPage();
|
|
329
|
+
this._redraw();
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
};
|
|
253
332
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
333
|
+
const onTouchEnd = (e) => {
|
|
334
|
+
if (!this._dragging) return;
|
|
335
|
+
this._dragging = false;
|
|
336
|
+
|
|
337
|
+
// Vérifier si c'était un tap (pas de mouvement)
|
|
338
|
+
if (Math.abs(this._touchVelocity) < 0.5) {
|
|
339
|
+
const { x, y } = this._evtToXY(e);
|
|
340
|
+
this._handleClick(x, y);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Momentum
|
|
345
|
+
let v = this._touchVelocity;
|
|
346
|
+
const momentum = () => {
|
|
347
|
+
if (Math.abs(v) < 0.5) return;
|
|
348
|
+
this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._scrollY + v));
|
|
349
|
+
v *= 0.92;
|
|
350
|
+
this._syncPage();
|
|
351
|
+
this._redraw();
|
|
352
|
+
this._momentumRAF = requestAnimationFrame(momentum);
|
|
353
|
+
};
|
|
354
|
+
this._momentumRAF = requestAnimationFrame(momentum);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Ajout des écouteurs
|
|
358
|
+
canvas.addEventListener('mousedown', onMouseDown);
|
|
359
|
+
canvas.addEventListener('mousemove', onMouseMove);
|
|
360
|
+
canvas.addEventListener('mouseup', onMouseUp);
|
|
361
|
+
canvas.addEventListener('mouseleave', onMouseLeave);
|
|
362
|
+
canvas.addEventListener('click', onClick);
|
|
363
|
+
canvas.addEventListener('wheel', onWheel, { passive: false });
|
|
364
|
+
canvas.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
365
|
+
canvas.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
366
|
+
canvas.addEventListener('touchend', onTouchEnd);
|
|
367
|
+
canvas.addEventListener('touchcancel', onTouchEnd);
|
|
368
|
+
|
|
369
|
+
this._boundHandlers = { onMouseDown, onMouseMove, onMouseUp, onMouseLeave, onClick, onWheel, onTouchStart, onTouchMove, onTouchEnd };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_checkHover(x, y) {
|
|
373
|
+
let hover = null;
|
|
374
|
+
|
|
375
|
+
// Vérifier les boutons de la toolbar
|
|
376
|
+
for (const b of this._tbButtons) {
|
|
377
|
+
if (!b.disabled && x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
|
|
378
|
+
hover = b.id;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Vérifier le bouton de fermeture de recherche
|
|
384
|
+
if (this._searchCloseRect && !hover) {
|
|
385
|
+
const r = this._searchCloseRect;
|
|
386
|
+
if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
|
|
387
|
+
hover = 'searchClose';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Vérifier le bouton de réessai
|
|
392
|
+
if (this._retryBtn && !hover) {
|
|
393
|
+
const r = this._retryBtn;
|
|
394
|
+
if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
|
|
395
|
+
hover = 'retry';
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (hover !== this._hoveredBtn) {
|
|
400
|
+
this._hoveredBtn = hover;
|
|
401
|
+
this._redraw();
|
|
402
|
+
}
|
|
257
403
|
}
|
|
258
404
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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;
|
|
405
|
+
_redraw() {
|
|
406
|
+
if (this.framework && this.framework.redraw) this.framework.redraw();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Spinner ─────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
_startSpinner() {
|
|
412
|
+
const tick = () => {
|
|
413
|
+
if (this.loading) {
|
|
414
|
+
this._spinAngle = (this._spinAngle + 0.08) % (Math.PI * 2);
|
|
415
|
+
this._redraw();
|
|
416
|
+
}
|
|
417
|
+
this._spinRAF = requestAnimationFrame(tick);
|
|
418
|
+
};
|
|
419
|
+
this._spinRAF = requestAnimationFrame(tick);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ─── Chargement PDF.js ───────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
async _loadPDFJS() {
|
|
425
|
+
if (window.pdfjsLib) return window.pdfjsLib;
|
|
426
|
+
return new Promise((resolve, reject) => {
|
|
427
|
+
const s = document.createElement('script');
|
|
428
|
+
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
|
|
429
|
+
s.onload = () => {
|
|
430
|
+
window.pdfjsLib.GlobalWorkerOptions.workerSrc =
|
|
431
|
+
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
|
432
|
+
resolve(window.pdfjsLib);
|
|
433
|
+
};
|
|
434
|
+
s.onerror = () => reject(new Error('Impossible de charger PDF.js'));
|
|
435
|
+
document.head.appendChild(s);
|
|
327
436
|
});
|
|
437
|
+
}
|
|
328
438
|
|
|
329
|
-
|
|
330
|
-
this.
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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());
|
|
439
|
+
async _loadPDF(src) {
|
|
440
|
+
this.loading = true;
|
|
441
|
+
this.error = null;
|
|
442
|
+
this._pageImages = {};
|
|
443
|
+
this._thumbImages = {};
|
|
444
|
+
this._scrollY = 0;
|
|
445
|
+
this._redraw();
|
|
446
|
+
try {
|
|
447
|
+
const lib = await this._loadPDFJS();
|
|
448
|
+
const task = lib.getDocument(typeof src === 'string' ? src : { data: src });
|
|
449
|
+
this._pdfDoc = await task.promise;
|
|
450
|
+
this.totalPages = this._pdfDoc.numPages;
|
|
451
|
+
|
|
452
|
+
// Charger les pages de manière asynchrone sans bloquer
|
|
453
|
+
const loadPromises = [];
|
|
454
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
455
|
+
loadPromises.push(this._renderPage(p));
|
|
456
|
+
loadPromises.push(this._renderThumb(p));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await Promise.all(loadPromises);
|
|
460
|
+
|
|
461
|
+
this.loading = false;
|
|
462
|
+
this._computeMaxScroll();
|
|
463
|
+
this.onLoad(this.totalPages);
|
|
464
|
+
this._redraw();
|
|
465
|
+
} catch (err) {
|
|
466
|
+
this.loading = false;
|
|
467
|
+
this.error = err.message || 'Erreur de chargement';
|
|
468
|
+
this.onErrorCb(this.error);
|
|
469
|
+
this._redraw();
|
|
379
470
|
}
|
|
471
|
+
}
|
|
380
472
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
473
|
+
async _renderPage(p) {
|
|
474
|
+
try {
|
|
475
|
+
const page = await this._pdfDoc.getPage(p);
|
|
476
|
+
const vp = page.getViewport({ scale: this.scale, rotation: this.rotation });
|
|
477
|
+
const off = new OffscreenCanvas(Math.ceil(vp.width), Math.ceil(vp.height));
|
|
478
|
+
await page.render({ canvasContext: off.getContext('2d'), viewport: vp }).promise;
|
|
479
|
+
this._pageImages[p] = await createImageBitmap(off);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
console.error(`Erreur rendu page ${p}:`, err);
|
|
385
482
|
}
|
|
483
|
+
}
|
|
386
484
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
485
|
+
async _renderThumb(p) {
|
|
486
|
+
try {
|
|
487
|
+
const page = await this._pdfDoc.getPage(p);
|
|
488
|
+
const vp = page.getViewport({ scale: 0.18, rotation: this.rotation });
|
|
489
|
+
const off = new OffscreenCanvas(Math.ceil(vp.width), Math.ceil(vp.height));
|
|
490
|
+
await page.render({ canvasContext: off.getContext('2d'), viewport: vp }).promise;
|
|
491
|
+
this._thumbImages[p] = await createImageBitmap(off);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
console.error(`Erreur rendu miniature ${p}:`, err);
|
|
391
494
|
}
|
|
495
|
+
}
|
|
392
496
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
497
|
+
async _reRenderAll() {
|
|
498
|
+
if (!this._pdfDoc) return;
|
|
499
|
+
this._pageImages = {};
|
|
500
|
+
this._thumbImages = {};
|
|
501
|
+
this.loading = true;
|
|
502
|
+
this._redraw();
|
|
503
|
+
|
|
504
|
+
const loadPromises = [];
|
|
505
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
506
|
+
loadPromises.push(this._renderPage(p));
|
|
507
|
+
loadPromises.push(this._renderThumb(p));
|
|
397
508
|
}
|
|
509
|
+
|
|
510
|
+
await Promise.all(loadPromises);
|
|
511
|
+
|
|
512
|
+
this.loading = false;
|
|
513
|
+
this._computeMaxScroll();
|
|
514
|
+
this._scrollY = Math.min(this._scrollY, this._maxScrollY);
|
|
515
|
+
this._redraw();
|
|
516
|
+
}
|
|
398
517
|
|
|
399
|
-
|
|
400
|
-
const btnPrint = this._makeToolbarBtn(this._svgPrint(), 'Imprimer');
|
|
401
|
-
btnPrint.addEventListener('click', () => this._print());
|
|
402
|
-
toolbar.appendChild(btnPrint);
|
|
403
|
-
}
|
|
518
|
+
// ─── Layout & scroll ─────────────────────────────────────────────────────────
|
|
404
519
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
520
|
+
_computeMaxScroll() {
|
|
521
|
+
const vr = this._viewerRect;
|
|
522
|
+
let total = 16;
|
|
523
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
524
|
+
total += (this._pageImages[p] ? this._pageImages[p].height : 200) + 16;
|
|
409
525
|
}
|
|
410
|
-
|
|
411
|
-
return toolbar;
|
|
526
|
+
this._maxScrollY = Math.max(0, total - vr.h);
|
|
412
527
|
}
|
|
413
528
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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;
|
|
529
|
+
_pageScrollOffset(pageNum) {
|
|
530
|
+
let y = 16;
|
|
531
|
+
for (let p = 1; p < pageNum; p++) {
|
|
532
|
+
y += (this._pageImages[p] ? this._pageImages[p].height : 200) + 16;
|
|
533
|
+
}
|
|
534
|
+
return y;
|
|
436
535
|
}
|
|
437
536
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
537
|
+
_syncPage() {
|
|
538
|
+
if (!this.totalPages) return;
|
|
539
|
+
const mid = this._scrollY + this._viewerRect.h / 2;
|
|
540
|
+
let y = 16;
|
|
541
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
542
|
+
const h = this._pageImages[p] ? this._pageImages[p].height : 200;
|
|
543
|
+
if (mid <= y + h || p === this.totalPages) {
|
|
544
|
+
if (p !== this.currentPage) {
|
|
545
|
+
this.currentPage = p;
|
|
546
|
+
this.onPageChange(p, this.totalPages);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
y += h + 16;
|
|
551
|
+
}
|
|
442
552
|
}
|
|
443
553
|
|
|
444
|
-
// ───
|
|
554
|
+
// ─── Dessin principal ────────────────────────────────────────────────────────
|
|
445
555
|
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
}
|
|
556
|
+
draw(ctx) {
|
|
557
|
+
this._registerEvents(); // one-shot
|
|
485
558
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
559
|
+
ctx.save();
|
|
560
|
+
// Clip global
|
|
561
|
+
ctx.beginPath();
|
|
562
|
+
this._rr(ctx, this.x, this.y, this.width, this.height, this._isMat ? 4 : 12);
|
|
563
|
+
ctx.clip();
|
|
564
|
+
|
|
565
|
+
// Fond
|
|
566
|
+
ctx.fillStyle = this.backgroundColor || this._colors.viewerBg;
|
|
567
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
568
|
+
|
|
569
|
+
if (this.totalPages > 1) this._drawThumbs(ctx);
|
|
570
|
+
this._drawViewer(ctx);
|
|
571
|
+
if (this.showToolbar) this._drawToolbar(ctx);
|
|
572
|
+
if (this._searchOpen) this._drawSearchBar(ctx);
|
|
573
|
+
|
|
574
|
+
// Bordure
|
|
575
|
+
ctx.strokeStyle = this._isMat ? 'rgba(0,0,0,0.12)' : (this._colors.toolbarBorder || '#C6C6C8');
|
|
576
|
+
ctx.lineWidth = 1;
|
|
577
|
+
ctx.beginPath();
|
|
578
|
+
this._rr(ctx, this.x, this.y, this.width, this.height, this._isMat ? 4 : 12);
|
|
579
|
+
ctx.stroke();
|
|
491
580
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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;
|
|
581
|
+
ctx.restore();
|
|
582
|
+
|
|
583
|
+
// Restaurer le hover pour le prochain frame
|
|
584
|
+
if (this._hoveredBtn && this._inSelf(this._lastMouseX, this._lastMouseY)) {
|
|
585
|
+
// Le hover sera redessiné au prochain move
|
|
586
|
+
}
|
|
513
587
|
}
|
|
514
588
|
|
|
515
|
-
// ───
|
|
589
|
+
// ─── Zone viewer ─────────────────────────────────────────────────────────────
|
|
516
590
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
this.error = null;
|
|
524
|
-
this._renderLoadingState();
|
|
591
|
+
_drawViewer(ctx) {
|
|
592
|
+
const vr = this._viewerRect;
|
|
593
|
+
ctx.save();
|
|
594
|
+
ctx.beginPath(); ctx.rect(vr.x, vr.y, vr.w, vr.h); ctx.clip();
|
|
595
|
+
ctx.fillStyle = this._colors.viewerBg;
|
|
596
|
+
ctx.fillRect(vr.x, vr.y, vr.w, vr.h);
|
|
525
597
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
}
|
|
598
|
+
const hasImages = Object.keys(this._pageImages).length > 0;
|
|
599
|
+
if (this.loading && !hasImages) this._drawLoading(ctx, vr);
|
|
600
|
+
else if (this.error && !hasImages) this._drawError(ctx, vr);
|
|
601
|
+
else if (!this.totalPages && !this.loading) this._drawEmpty(ctx, vr);
|
|
602
|
+
else this._drawPages(ctx, vr);
|
|
543
603
|
|
|
544
|
-
|
|
545
|
-
|
|
604
|
+
ctx.restore();
|
|
605
|
+
}
|
|
546
606
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (this.showThumbnails) await this._renderThumbnail(p);
|
|
551
|
-
}
|
|
607
|
+
_drawPages(ctx, vr) {
|
|
608
|
+
const pad = 16;
|
|
609
|
+
let y = vr.y + pad - this._scrollY;
|
|
552
610
|
|
|
553
|
-
|
|
554
|
-
this.
|
|
555
|
-
|
|
611
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
612
|
+
const img = this._pageImages[p];
|
|
613
|
+
const ph = img ? img.height : 200;
|
|
614
|
+
// Largeur : on respecte la vraie largeur de la page rendue, mais on ne dépasse pas le viewer
|
|
615
|
+
const pw = img ? Math.min(img.width, vr.w - pad * 2) : vr.w - pad * 2;
|
|
616
|
+
const px = vr.x + (vr.w - pw) / 2;
|
|
617
|
+
|
|
618
|
+
// Dessin uniquement si la page est dans la zone visible
|
|
619
|
+
if (y + ph >= vr.y && y <= vr.y + vr.h) {
|
|
620
|
+
ctx.shadowColor = this._colors.pageShadow;
|
|
621
|
+
ctx.shadowBlur = 8;
|
|
622
|
+
ctx.shadowOffsetY = 2;
|
|
623
|
+
ctx.fillStyle = this.pageBackground;
|
|
624
|
+
ctx.fillRect(px, y, pw, ph);
|
|
625
|
+
ctx.shadowBlur = 0;
|
|
626
|
+
ctx.shadowOffsetY = 0;
|
|
627
|
+
|
|
628
|
+
if (img) {
|
|
629
|
+
ctx.drawImage(img, 0, 0, img.width, img.height, px, y, pw, ph);
|
|
630
|
+
} else {
|
|
631
|
+
ctx.fillStyle = '#ccc';
|
|
632
|
+
ctx.font = '13px sans-serif';
|
|
633
|
+
ctx.textAlign = 'center';
|
|
634
|
+
ctx.textBaseline = 'middle';
|
|
635
|
+
ctx.fillText(`Page ${p}…`, px + pw / 2, y + ph / 2);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
ctx.fillStyle = 'rgba(0,0,0,0.28)';
|
|
639
|
+
ctx.font = '11px sans-serif';
|
|
640
|
+
ctx.textAlign = 'right';
|
|
641
|
+
ctx.textBaseline = 'bottom';
|
|
642
|
+
ctx.fillText(`${p} / ${this.totalPages}`, px + pw - 6, y + ph - 4);
|
|
643
|
+
}
|
|
556
644
|
|
|
557
|
-
|
|
558
|
-
this.loading = false;
|
|
559
|
-
this.error = err.message || 'Erreur lors du chargement du PDF';
|
|
560
|
-
this._renderErrorState();
|
|
561
|
-
this.onErrorCb(this.error);
|
|
645
|
+
y += ph + pad;
|
|
562
646
|
}
|
|
563
|
-
}
|
|
564
647
|
|
|
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;
|
|
648
|
+
this._drawScrollbar(ctx, vr);
|
|
612
649
|
}
|
|
613
650
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
});
|
|
651
|
+
_drawScrollbar(ctx, vr) {
|
|
652
|
+
if (this._maxScrollY <= 0) return;
|
|
653
|
+
const tw = 5, tr = 3;
|
|
654
|
+
const tx = vr.x + vr.w - tw - 3;
|
|
655
|
+
const ty = vr.y + 4, th = vr.h - 8;
|
|
656
|
+
const ratio = vr.h / (vr.h + this._maxScrollY);
|
|
657
|
+
const thumbH = Math.max(24, th * ratio);
|
|
658
|
+
const thumbY = ty + (this._scrollY / this._maxScrollY) * (th - thumbH);
|
|
659
|
+
ctx.fillStyle = 'rgba(0,0,0,0.08)';
|
|
660
|
+
this._rr(ctx, tx, ty, tw, th, tr);
|
|
661
|
+
ctx.fill();
|
|
662
|
+
ctx.fillStyle = 'rgba(0,0,0,0.32)';
|
|
663
|
+
this._rr(ctx, tx, thumbY, tw, thumbH, tr);
|
|
664
|
+
ctx.fill();
|
|
665
|
+
}
|
|
638
666
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const ctx = thumbCanvas.getContext('2d');
|
|
660
|
-
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
667
|
+
// ─── États visuels ───────────────────────────────────────────────────────────
|
|
668
|
+
|
|
669
|
+
_drawLoading(ctx, vr) {
|
|
670
|
+
const cx = vr.x + vr.w / 2, cy = vr.y + vr.h / 2 - 20;
|
|
671
|
+
ctx.strokeStyle = 'rgba(0,0,0,0.1)';
|
|
672
|
+
ctx.lineWidth = 4;
|
|
673
|
+
ctx.beginPath();
|
|
674
|
+
ctx.arc(cx, cy, 22, 0, Math.PI * 2);
|
|
675
|
+
ctx.stroke();
|
|
676
|
+
ctx.strokeStyle = this._primary;
|
|
677
|
+
ctx.lineWidth = 4;
|
|
678
|
+
ctx.lineCap = 'round';
|
|
679
|
+
ctx.beginPath();
|
|
680
|
+
ctx.arc(cx, cy, 22, this._spinAngle, this._spinAngle + 1.3);
|
|
681
|
+
ctx.stroke();
|
|
682
|
+
ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
|
|
683
|
+
ctx.font = '13px sans-serif';
|
|
684
|
+
ctx.textAlign = 'center';
|
|
685
|
+
ctx.textBaseline = 'top';
|
|
686
|
+
ctx.fillText('Chargement...', cx, cy + 32);
|
|
661
687
|
}
|
|
662
688
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
689
|
+
_drawEmpty(ctx, vr) {
|
|
690
|
+
const cx = vr.x + vr.w / 2, cy = vr.y + vr.h / 2 - 20;
|
|
691
|
+
ctx.globalAlpha = 0.35;
|
|
692
|
+
ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
|
|
693
|
+
ctx.beginPath();
|
|
694
|
+
const fx = cx - 20, fy = cy - 26;
|
|
695
|
+
ctx.moveTo(fx + 6, fy);
|
|
696
|
+
ctx.lineTo(fx + 28, fy);
|
|
697
|
+
ctx.lineTo(fx + 40, fy + 14);
|
|
698
|
+
ctx.lineTo(fx + 40, fy + 54);
|
|
699
|
+
ctx.quadraticCurveTo(fx + 40, fy + 58, fx + 36, fy + 58);
|
|
700
|
+
ctx.lineTo(fx + 4, fy + 58);
|
|
701
|
+
ctx.quadraticCurveTo(fx, fy + 58, fx, fy + 54);
|
|
702
|
+
ctx.lineTo(fx, fy + 6);
|
|
703
|
+
ctx.quadraticCurveTo(fx, fy, fx + 6, fy);
|
|
704
|
+
ctx.closePath();
|
|
705
|
+
ctx.fill();
|
|
706
|
+
ctx.globalAlpha = 1;
|
|
707
|
+
ctx.fillStyle = this._colors.onSurfaceVariant || '#49454F';
|
|
708
|
+
ctx.font = '13px sans-serif';
|
|
709
|
+
ctx.textAlign = 'center';
|
|
710
|
+
ctx.textBaseline = 'top';
|
|
711
|
+
ctx.fillText('Aucun document chargé', cx, cy + 42);
|
|
675
712
|
}
|
|
676
713
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
714
|
+
_drawError(ctx, vr) {
|
|
715
|
+
const cx = vr.x + vr.w / 2, cy = vr.y + vr.h / 2 - 30;
|
|
716
|
+
const err = this._colors.error || '#BA1A1A';
|
|
717
|
+
ctx.fillStyle = err;
|
|
718
|
+
ctx.beginPath();
|
|
719
|
+
ctx.arc(cx, cy, 24, 0, Math.PI * 2);
|
|
720
|
+
ctx.fill();
|
|
721
|
+
ctx.fillStyle = '#fff';
|
|
722
|
+
ctx.font = 'bold 26px sans-serif';
|
|
723
|
+
ctx.textAlign = 'center';
|
|
724
|
+
ctx.textBaseline = 'middle';
|
|
725
|
+
ctx.fillText('!', cx, cy);
|
|
726
|
+
ctx.fillStyle = err;
|
|
727
|
+
ctx.font = '13px sans-serif';
|
|
728
|
+
ctx.textBaseline = 'top';
|
|
729
|
+
ctx.fillText((this.error || 'Erreur').substring(0, 60), cx, cy + 34);
|
|
730
|
+
const bx = cx - 48, by = cy + 62;
|
|
731
|
+
this._retryBtn = { x: bx, y: by, w: 96, h: 30 };
|
|
732
|
+
|
|
733
|
+
// Hover effect
|
|
734
|
+
if (this._hoveredBtn === 'retry') {
|
|
735
|
+
ctx.fillStyle = this._primary + 'dd';
|
|
736
|
+
} else {
|
|
737
|
+
ctx.fillStyle = this._primary;
|
|
738
|
+
}
|
|
739
|
+
this._rr(ctx, bx, by, 96, 30, this._isMat ? 15 : 8);
|
|
740
|
+
ctx.fill();
|
|
741
|
+
ctx.fillStyle = '#fff';
|
|
742
|
+
ctx.font = '13px sans-serif';
|
|
743
|
+
ctx.textBaseline = 'middle';
|
|
744
|
+
ctx.fillText('Réessayer', cx, by + 15);
|
|
745
|
+
}
|
|
692
746
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
747
|
+
// ─── Miniatures ──────────────────────────────────────────────────────────────
|
|
748
|
+
|
|
749
|
+
_drawThumbs(ctx) {
|
|
750
|
+
const tr = this._thumbRect;
|
|
751
|
+
ctx.fillStyle = this._colors.thumbsBg;
|
|
752
|
+
ctx.fillRect(tr.x, tr.y, tr.w, tr.h);
|
|
753
|
+
ctx.strokeStyle = this._colors.thumbBorder;
|
|
754
|
+
ctx.lineWidth = 1;
|
|
755
|
+
ctx.beginPath();
|
|
756
|
+
ctx.moveTo(tr.x + tr.w, tr.y);
|
|
757
|
+
ctx.lineTo(tr.x + tr.w, tr.y + tr.h);
|
|
758
|
+
ctx.stroke();
|
|
759
|
+
|
|
760
|
+
ctx.save();
|
|
761
|
+
ctx.beginPath();
|
|
762
|
+
ctx.rect(tr.x, tr.y, tr.w, tr.h);
|
|
763
|
+
ctx.clip();
|
|
764
|
+
let ty = tr.y + 8;
|
|
765
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
766
|
+
const img = this._thumbImages[p];
|
|
767
|
+
const tw = tr.w - 16;
|
|
768
|
+
const th = img ? Math.round(img.height * tw / img.width) : 70;
|
|
769
|
+
const tx = tr.x + 8;
|
|
770
|
+
if (ty + th > tr.y && ty < tr.y + tr.h) {
|
|
771
|
+
const active = p === this.currentPage;
|
|
772
|
+
ctx.strokeStyle = active ? this._colors.thumbActive : this._colors.thumbBorder;
|
|
773
|
+
ctx.lineWidth = active ? 2 : 1;
|
|
774
|
+
this._rr(ctx, tx, ty, tw, th, 3);
|
|
775
|
+
if (img) {
|
|
776
|
+
ctx.drawImage(img, tx, ty, tw, th);
|
|
777
|
+
ctx.stroke();
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
ctx.fillStyle = '#fff';
|
|
781
|
+
ctx.fill();
|
|
782
|
+
ctx.stroke();
|
|
783
|
+
}
|
|
784
|
+
ctx.fillStyle = '#666';
|
|
785
|
+
ctx.font = '10px sans-serif';
|
|
786
|
+
ctx.textAlign = 'center';
|
|
787
|
+
ctx.textBaseline = 'top';
|
|
788
|
+
ctx.fillText(p, tx + tw / 2, ty + th + 2);
|
|
789
|
+
}
|
|
790
|
+
ty += th + 18;
|
|
698
791
|
}
|
|
792
|
+
ctx.restore();
|
|
699
793
|
}
|
|
700
794
|
|
|
701
|
-
// ───
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
795
|
+
// ─── Toolbar ─────────────────────────────────────────────────────────────────
|
|
796
|
+
|
|
797
|
+
_drawToolbar(ctx) {
|
|
798
|
+
const tb = this._tbRect;
|
|
799
|
+
ctx.fillStyle = this._primary;
|
|
800
|
+
ctx.fillRect(tb.x, tb.y, tb.w, tb.h);
|
|
801
|
+
if (!this._isMat) {
|
|
802
|
+
ctx.strokeStyle = this._colors.toolbarBorder || '#C6C6C8';
|
|
803
|
+
ctx.lineWidth = 1;
|
|
804
|
+
ctx.beginPath();
|
|
805
|
+
ctx.moveTo(tb.x, tb.y + tb.h);
|
|
806
|
+
ctx.lineTo(tb.x + tb.w, tb.y + tb.h);
|
|
807
|
+
ctx.stroke();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
ctx.save();
|
|
811
|
+
ctx.beginPath();
|
|
812
|
+
ctx.rect(tb.x, tb.y, tb.w, tb.h);
|
|
813
|
+
ctx.clip();
|
|
814
|
+
|
|
815
|
+
this._tbButtons = [];
|
|
816
|
+
this._pageInputRect = null;
|
|
817
|
+
let cx = tb.x + 12;
|
|
818
|
+
const my = tb.y + tb.h / 2;
|
|
819
|
+
|
|
820
|
+
// Titre
|
|
821
|
+
const title = typeof this.src === 'string' ? (this.src.split('/').pop() || 'Document.pdf') : 'Document.pdf';
|
|
822
|
+
ctx.fillStyle = 'rgba(255,255,255,0.92)';
|
|
823
|
+
ctx.font = `${this._isMat ? '500' : '600'} ${this._isMat ? 15 : 16}px sans-serif`;
|
|
824
|
+
ctx.textAlign = 'left';
|
|
825
|
+
ctx.textBaseline = 'middle';
|
|
826
|
+
|
|
827
|
+
// Calcul de la largeur disponible pour le titre
|
|
828
|
+
let availableWidth = tb.w - 200; // Réserver de l'espace pour les boutons
|
|
829
|
+
|
|
830
|
+
let t = title;
|
|
831
|
+
while (ctx.measureText(t).width > availableWidth && t.length > 2) {
|
|
832
|
+
t = t.slice(0, -1);
|
|
833
|
+
}
|
|
834
|
+
if (t !== title) t += '…';
|
|
835
|
+
ctx.fillText(t, cx, my);
|
|
836
|
+
|
|
837
|
+
// Boutons de navigation (si plus d'une page)
|
|
838
|
+
if (this.totalPages > 1 && this.allowNavigation) {
|
|
839
|
+
cx = tb.x + tb.w - 240;
|
|
840
|
+
|
|
841
|
+
// Page précédente
|
|
842
|
+
cx = this._btn(ctx, cx, my, 'prev', this._icChevron('left'), 0, this.currentPage <= 1);
|
|
843
|
+
|
|
844
|
+
// Indicateur de page
|
|
845
|
+
const pageText = `${this.currentPage}/${this.totalPages}`;
|
|
846
|
+
ctx.fillStyle = 'rgba(255,255,255,0.88)';
|
|
847
|
+
ctx.font = '13px sans-serif';
|
|
848
|
+
ctx.textAlign = 'center';
|
|
849
|
+
ctx.textBaseline = 'middle';
|
|
850
|
+
ctx.fillText(pageText, cx + 20, my);
|
|
851
|
+
|
|
852
|
+
// Enregistrer la zone cliquable pour l'indicateur de page
|
|
853
|
+
this._pageInputRect = { x: cx, y: my - 16, w: 44, h: 32 };
|
|
854
|
+
|
|
855
|
+
cx += 44;
|
|
856
|
+
|
|
857
|
+
// Page suivante
|
|
858
|
+
cx = this._btn(ctx, cx, my, 'next', this._icChevron('right'), 0, this.currentPage >= this.totalPages);
|
|
859
|
+
|
|
860
|
+
cx += 8; // Séparateur
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Boutons de zoom (toujours à droite)
|
|
864
|
+
cx = tb.x + tb.w - 120;
|
|
865
|
+
|
|
866
|
+
// Zoom out
|
|
867
|
+
if (this.allowZoom) {
|
|
868
|
+
cx = this._btn(ctx, cx, my, 'zoomOut', '−', 20, this.scale <= this.minScale);
|
|
869
|
+
|
|
870
|
+
// Pourcentage
|
|
871
|
+
const zl = `${Math.round(this.scale * 100)}%`;
|
|
872
|
+
ctx.fillStyle = 'rgba(255,255,255,0.88)';
|
|
873
|
+
ctx.font = '12px sans-serif';
|
|
874
|
+
ctx.textAlign = 'center';
|
|
875
|
+
ctx.textBaseline = 'middle';
|
|
876
|
+
ctx.fillText(zl, cx + 20, my);
|
|
877
|
+
|
|
878
|
+
cx += 44;
|
|
879
|
+
|
|
880
|
+
// Zoom in
|
|
881
|
+
cx = this._btn(ctx, cx, my, 'zoomIn', '+', 20, this.scale >= this.maxScale);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
ctx.restore();
|
|
721
885
|
}
|
|
722
886
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
887
|
+
_btn(ctx, x, y, id, content, fontSize = 0, disabled = false) {
|
|
888
|
+
const bw = 32, bh = 32, bx = x, by = y - 16;
|
|
889
|
+
|
|
890
|
+
// Hover effect
|
|
891
|
+
if (this._hoveredBtn === id && !disabled) {
|
|
892
|
+
ctx.fillStyle = 'rgba(255,255,255,0.22)';
|
|
893
|
+
this._rr(ctx, bx, by, bw, bh, this._isMat ? 4 : 6);
|
|
894
|
+
ctx.fill();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
this._tbButtons.push({ id, x: bx, y: by, w: bw, h: bh, disabled });
|
|
898
|
+
ctx.globalAlpha = disabled ? 0.35 : 1;
|
|
899
|
+
|
|
900
|
+
const isText = typeof content === 'string' && content.length <= 2;
|
|
901
|
+
if (isText) {
|
|
902
|
+
ctx.fillStyle = '#fff';
|
|
903
|
+
ctx.font = `${fontSize || 18}px sans-serif`;
|
|
904
|
+
ctx.textAlign = 'center';
|
|
905
|
+
ctx.textBaseline = 'middle';
|
|
906
|
+
ctx.fillText(content, bx + bw / 2, y);
|
|
907
|
+
} else {
|
|
908
|
+
// C'est un chemin SVG
|
|
909
|
+
ctx.save();
|
|
910
|
+
ctx.translate(bx + 7, y - 9);
|
|
911
|
+
ctx.strokeStyle = '#fff';
|
|
912
|
+
ctx.lineWidth = 2;
|
|
913
|
+
ctx.lineCap = 'round';
|
|
914
|
+
ctx.lineJoin = 'round';
|
|
915
|
+
ctx.stroke(new Path2D(content));
|
|
916
|
+
ctx.restore();
|
|
745
917
|
}
|
|
918
|
+
|
|
919
|
+
ctx.globalAlpha = 1;
|
|
920
|
+
return x + bw + 4;
|
|
921
|
+
}
|
|
746
922
|
|
|
747
|
-
|
|
748
|
-
label.style.cssText = `margin:0; font-size:15px; color:${this._colors.onSurfaceVariant || '#49454F'};`;
|
|
749
|
-
label.textContent = 'Chargement du document...';
|
|
923
|
+
// ─── Barre de recherche ───────────────────────────────────────────────────────
|
|
750
924
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
925
|
+
_drawSearchBar(ctx) {
|
|
926
|
+
const sb = this._sbRect;
|
|
927
|
+
if (sb.h === 0) return;
|
|
928
|
+
|
|
929
|
+
ctx.fillStyle = this._isMat ? this.m3Colors.surfaceVariant : '#F2F2F7';
|
|
930
|
+
ctx.fillRect(sb.x, sb.y, sb.w, sb.h);
|
|
931
|
+
ctx.strokeStyle = this._colors.outlineVariant || '#C6C6C8';
|
|
932
|
+
ctx.lineWidth = 1;
|
|
933
|
+
ctx.beginPath();
|
|
934
|
+
ctx.moveTo(sb.x, sb.y + sb.h);
|
|
935
|
+
ctx.lineTo(sb.x + sb.w, sb.y + sb.h);
|
|
936
|
+
ctx.stroke();
|
|
937
|
+
|
|
938
|
+
const fw = sb.w - 24 - 38, fh = sb.h - 16;
|
|
939
|
+
ctx.fillStyle = '#fff';
|
|
940
|
+
this._rr(ctx, sb.x + 12, sb.y + 8, fw, fh, this._isMat ? 4 : 8);
|
|
941
|
+
ctx.fill();
|
|
942
|
+
ctx.strokeStyle = this._colors.outline || '#C6C6C8';
|
|
943
|
+
this._rr(ctx, sb.x + 12, sb.y + 8, fw, fh, this._isMat ? 4 : 8);
|
|
944
|
+
ctx.stroke();
|
|
945
|
+
ctx.fillStyle = this._searchQuery ? '#000' : '#999';
|
|
946
|
+
ctx.font = '13px sans-serif';
|
|
947
|
+
ctx.textAlign = 'left';
|
|
948
|
+
ctx.textBaseline = 'middle';
|
|
949
|
+
ctx.fillText(this._searchQuery || 'Rechercher...', sb.x + 20, sb.y + sb.h / 2);
|
|
950
|
+
|
|
951
|
+
const clx = sb.x + sb.w - 34;
|
|
952
|
+
this._searchCloseRect = { x: clx, y: sb.y, w: 34, h: sb.h };
|
|
953
|
+
|
|
954
|
+
// Hover effect pour le bouton de fermeture
|
|
955
|
+
if (this._hoveredBtn === 'searchClose') {
|
|
956
|
+
ctx.fillStyle = '#444';
|
|
957
|
+
} else {
|
|
958
|
+
ctx.fillStyle = '#666';
|
|
959
|
+
}
|
|
960
|
+
ctx.font = '15px sans-serif';
|
|
961
|
+
ctx.textAlign = 'center';
|
|
962
|
+
ctx.textBaseline = 'middle';
|
|
963
|
+
ctx.fillText('✕', clx + 17, sb.y + sb.h / 2);
|
|
754
964
|
}
|
|
755
965
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
966
|
+
// ─── Gestion des clics ───────────────────────────────────────────────────────
|
|
967
|
+
|
|
968
|
+
_handleClick(x, y) {
|
|
969
|
+
console.log('Click at', x, y); // Debug
|
|
970
|
+
|
|
971
|
+
// Retry
|
|
972
|
+
if (this._retryBtn) {
|
|
973
|
+
const r = this._retryBtn;
|
|
974
|
+
if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
|
|
975
|
+
console.log('Retry clicked');
|
|
976
|
+
if (this.src) this._loadPDF(this.src);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Fermer recherche
|
|
982
|
+
if (this._searchCloseRect) {
|
|
983
|
+
const r = this._searchCloseRect;
|
|
984
|
+
if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
|
|
985
|
+
console.log('Search close clicked');
|
|
986
|
+
this._toggleSearch();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Toolbar
|
|
992
|
+
for (const b of this._tbButtons) {
|
|
993
|
+
if (!b.disabled && x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
|
|
994
|
+
console.log('Button clicked:', b.id);
|
|
995
|
+
this._handleBtn(b.id);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Indicateur de page (pour navigation)
|
|
1001
|
+
if (this._pageInputRect) {
|
|
1002
|
+
const r = this._pageInputRect;
|
|
1003
|
+
if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) {
|
|
1004
|
+
console.log('Page indicator clicked');
|
|
1005
|
+
this._promptPage();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Miniatures
|
|
1011
|
+
if (this.totalPages > 1) {
|
|
1012
|
+
const tr = this._thumbRect;
|
|
1013
|
+
if (x >= tr.x && x <= tr.x + tr.w) {
|
|
1014
|
+
let ty = tr.y + 8;
|
|
1015
|
+
for (let p = 1; p <= this.totalPages; p++) {
|
|
1016
|
+
const img = this._thumbImages[p];
|
|
1017
|
+
const tw = tr.w - 16;
|
|
1018
|
+
const th = img ? Math.round(img.height * tw / img.width) : 70;
|
|
1019
|
+
if (y >= ty && y <= ty + th) {
|
|
1020
|
+
console.log('Thumbnail clicked:', p);
|
|
1021
|
+
this.goToPage(p);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
ty += th + 18;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
789
1029
|
|
|
790
|
-
|
|
791
|
-
|
|
1030
|
+
_handleBtn(id) {
|
|
1031
|
+
console.log('Handling button:', id);
|
|
1032
|
+
switch (id) {
|
|
1033
|
+
case 'prev':
|
|
1034
|
+
this.goToPreviousPage();
|
|
1035
|
+
break;
|
|
1036
|
+
case 'next':
|
|
1037
|
+
this.goToNextPage();
|
|
1038
|
+
break;
|
|
1039
|
+
case 'zoomIn':
|
|
1040
|
+
this._setScale(this.scale + 0.25);
|
|
1041
|
+
break;
|
|
1042
|
+
case 'zoomOut':
|
|
1043
|
+
this._setScale(this.scale - 0.25);
|
|
1044
|
+
break;
|
|
1045
|
+
case 'zoomFit':
|
|
1046
|
+
this._fitToWidth();
|
|
1047
|
+
break;
|
|
1048
|
+
case 'rotate':
|
|
1049
|
+
this._rotate();
|
|
1050
|
+
break;
|
|
1051
|
+
case 'search':
|
|
1052
|
+
this._toggleSearch();
|
|
1053
|
+
break;
|
|
1054
|
+
case 'download':
|
|
1055
|
+
this._download();
|
|
1056
|
+
break;
|
|
1057
|
+
case 'print':
|
|
1058
|
+
this._print();
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
this._redraw();
|
|
792
1062
|
}
|
|
793
1063
|
|
|
794
|
-
// ─── Actions
|
|
1064
|
+
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
795
1065
|
|
|
796
|
-
|
|
797
|
-
|
|
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)}%`;
|
|
1066
|
+
_setScale(s) {
|
|
1067
|
+
this.scale = Math.max(this.minScale, Math.min(this.maxScale, Math.round(s * 100) / 100));
|
|
805
1068
|
this.onScaleChange(this.scale);
|
|
806
|
-
|
|
1069
|
+
this._reRenderAll();
|
|
807
1070
|
}
|
|
808
1071
|
|
|
809
|
-
/**
|
|
810
|
-
* Ajuste le zoom à la largeur du viewer
|
|
811
|
-
* @private
|
|
812
|
-
*/
|
|
813
1072
|
async _fitToWidth() {
|
|
814
1073
|
if (!this._pdfDoc) return;
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
const availW = this._viewerEl.clientWidth - 32;
|
|
818
|
-
const ratio = availW / viewport.width;
|
|
819
|
-
this._setScale(ratio);
|
|
1074
|
+
const vp = (await this._pdfDoc.getPage(1)).getViewport({ scale: 1 });
|
|
1075
|
+
this._setScale((this._viewerRect.w - 32) / vp.width);
|
|
820
1076
|
}
|
|
821
1077
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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();
|
|
1078
|
+
_rotate() {
|
|
1079
|
+
this.rotation = (this.rotation + 90) % 360;
|
|
1080
|
+
this._reRenderAll();
|
|
842
1081
|
}
|
|
843
1082
|
|
|
844
|
-
/**
|
|
845
|
-
* Télécharge le PDF
|
|
846
|
-
* @private
|
|
847
|
-
*/
|
|
848
1083
|
_download() {
|
|
849
|
-
if (
|
|
1084
|
+
if (typeof this.src !== 'string') return;
|
|
850
1085
|
const a = document.createElement('a');
|
|
851
|
-
a.href = this.src;
|
|
852
|
-
a.download = this.src.split('/').pop() || 'document.pdf';
|
|
1086
|
+
a.href = this.src;
|
|
1087
|
+
a.download = this.src.split('/').pop() || 'document.pdf';
|
|
853
1088
|
a.click();
|
|
854
1089
|
}
|
|
855
1090
|
|
|
856
|
-
/**
|
|
857
|
-
* Imprime le PDF
|
|
858
|
-
* @private
|
|
859
|
-
*/
|
|
860
1091
|
_print() {
|
|
861
|
-
if (
|
|
1092
|
+
if (typeof this.src !== 'string') return;
|
|
862
1093
|
const w = window.open(this.src);
|
|
863
1094
|
if (w) w.addEventListener('load', () => w.print());
|
|
864
1095
|
}
|
|
865
1096
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
}
|
|
1097
|
+
_toggleSearch() {
|
|
1098
|
+
this._searchOpen = !this._searchOpen;
|
|
1099
|
+
this._computeMaxScroll();
|
|
1100
|
+
this._redraw();
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
_promptPage() {
|
|
1104
|
+
const v = prompt(`Aller à la page (1–${this.totalPages}) :`, this.currentPage);
|
|
1105
|
+
if (v !== null) {
|
|
1106
|
+
const p = parseInt(v);
|
|
1107
|
+
if (p >= 1 && p <= this.totalPages) this.goToPage(p);
|
|
894
1108
|
}
|
|
895
1109
|
}
|
|
896
1110
|
|
|
897
|
-
// ─── Navigation
|
|
1111
|
+
// ─── Navigation publique ─────────────────────────────────────────────────────
|
|
898
1112
|
|
|
899
|
-
/**
|
|
900
|
-
* Navigue vers une page spécifique
|
|
901
|
-
* @param {number} page
|
|
902
|
-
*/
|
|
903
1113
|
goToPage(page) {
|
|
904
1114
|
if (!this._pdfDoc || page < 1 || page > this.totalPages) return;
|
|
905
1115
|
this.currentPage = page;
|
|
906
|
-
|
|
907
|
-
this.
|
|
908
|
-
this.
|
|
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' });
|
|
1116
|
+
this._scrollY = Math.max(0, Math.min(this._maxScrollY, this._pageScrollOffset(page) - 8));
|
|
1117
|
+
this.onPageChange(page, this.totalPages);
|
|
1118
|
+
this._redraw();
|
|
919
1119
|
}
|
|
920
1120
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
*/
|
|
924
|
-
goToPreviousPage() {
|
|
925
|
-
if (this.currentPage > 1) this.goToPage(this.currentPage - 1);
|
|
1121
|
+
goToPreviousPage() {
|
|
1122
|
+
if (this.currentPage > 1) this.goToPage(this.currentPage - 1);
|
|
926
1123
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
*/
|
|
931
|
-
goToNextPage() {
|
|
932
|
-
if (this.currentPage < this.totalPages) this.goToPage(this.currentPage + 1);
|
|
1124
|
+
|
|
1125
|
+
goToNextPage() {
|
|
1126
|
+
if (this.currentPage < this.totalPages) this.goToPage(this.currentPage + 1);
|
|
933
1127
|
}
|
|
934
1128
|
|
|
935
1129
|
// ─── API publique ─────────────────────────────────────────────────────────────
|
|
936
1130
|
|
|
937
|
-
/**
|
|
938
|
-
* Charge un nouveau document PDF
|
|
939
|
-
* @param {string|Uint8Array} src - URL ou données binaires
|
|
940
|
-
*/
|
|
941
1131
|
load(src) {
|
|
942
|
-
this.src = src;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
this._loadPDF(src);
|
|
949
|
-
}
|
|
1132
|
+
this.src = src;
|
|
1133
|
+
this._pdfDoc = null;
|
|
1134
|
+
this._pageImages = {};
|
|
1135
|
+
this._thumbImages = {};
|
|
1136
|
+
this._scrollY = 0;
|
|
1137
|
+
this._loadPDF(src);
|
|
950
1138
|
}
|
|
951
1139
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
* @param {number} scale - Facteur de zoom (ex: 1.5 = 150%)
|
|
955
|
-
*/
|
|
956
|
-
setScale(scale) {
|
|
957
|
-
this._setScale(scale);
|
|
1140
|
+
setScale(s) {
|
|
1141
|
+
this._setScale(s);
|
|
958
1142
|
}
|
|
959
1143
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
* @returns {Promise<Object>}
|
|
963
|
-
*/
|
|
964
|
-
async getMetadata() {
|
|
965
|
-
if (!this._pdfDoc) return null;
|
|
966
|
-
return this._pdfDoc.getMetadata();
|
|
1144
|
+
async getMetadata() {
|
|
1145
|
+
return this._pdfDoc ? this._pdfDoc.getMetadata() : null;
|
|
967
1146
|
}
|
|
968
1147
|
|
|
969
|
-
// ─── SVG
|
|
1148
|
+
// ─── Icônes SVG paths (18×18 viewbox) ────────────────────────────────────────
|
|
970
1149
|
|
|
971
|
-
|
|
972
|
-
|
|
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>`;
|
|
1150
|
+
_icChevron(d) {
|
|
1151
|
+
return d === 'left' ? 'M12 3 L5 9 L12 15' : 'M6 3 L13 9 L6 15';
|
|
976
1152
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
return
|
|
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>`;
|
|
1153
|
+
|
|
1154
|
+
_icFit() {
|
|
1155
|
+
return 'M11 1 L17 1 L17 7 M1 11 L1 17 L7 17 M17 1 L10 8 M1 17 L8 10';
|
|
983
1156
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
return
|
|
987
|
-
<path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 .49-4.46"/>
|
|
988
|
-
</svg>`;
|
|
1157
|
+
|
|
1158
|
+
_icRotate() {
|
|
1159
|
+
return 'M1 4 L1 9 L6 9 M2 11.5 A8 8 0 1 0 4 4.5';
|
|
989
1160
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
return
|
|
993
|
-
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
994
|
-
</svg>`;
|
|
1161
|
+
|
|
1162
|
+
_icSearch() {
|
|
1163
|
+
return 'M7.5 13.5 A6 6 0 1 0 7.5 1.5 A6 6 0 1 0 7.5 13.5 M12 12 L17 17';
|
|
995
1164
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
return
|
|
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>`;
|
|
1165
|
+
|
|
1166
|
+
_icDownload() {
|
|
1167
|
+
return 'M9 1 L9 11 M5 7 L9 11 L13 7 M1 14 L1 17 L17 17 L17 14';
|
|
1002
1168
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
return
|
|
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>`;
|
|
1169
|
+
|
|
1170
|
+
_icPrint() {
|
|
1171
|
+
return 'M4 7 L4 1 L14 1 L14 7 M4 15 L14 15 L14 11 L4 11 Z M1 7 L17 7 L17 14 L1 14 Z';
|
|
1010
1172
|
}
|
|
1011
1173
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1174
|
+
// ─── Utilitaire roundRect ─────────────────────────────────────────────────────
|
|
1175
|
+
|
|
1176
|
+
_rr(ctx, x, y, w, h, r) {
|
|
1177
|
+
r = Math.min(r, w / 2, h / 2);
|
|
1178
|
+
ctx.beginPath();
|
|
1179
|
+
ctx.moveTo(x + r, y);
|
|
1180
|
+
ctx.lineTo(x + w - r, y);
|
|
1181
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
1182
|
+
ctx.lineTo(x + w, y + h - r);
|
|
1183
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
1184
|
+
ctx.lineTo(x + r, y + h);
|
|
1185
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
1186
|
+
ctx.lineTo(x, y + r);
|
|
1187
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
1188
|
+
ctx.closePath();
|
|
1017
1189
|
}
|
|
1018
1190
|
|
|
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
1191
|
isPointInside(x, y) {
|
|
1051
1192
|
return x >= this.x && x <= this.x + this.width &&
|
|
1052
1193
|
y >= this.y && y <= this.y + this.height;
|
|
1053
1194
|
}
|
|
1054
1195
|
|
|
1055
|
-
/**
|
|
1056
|
-
* Nettoie les ressources
|
|
1057
|
-
*/
|
|
1058
1196
|
destroy() {
|
|
1059
|
-
if (this.
|
|
1060
|
-
|
|
1197
|
+
if (this._spinRAF) cancelAnimationFrame(this._spinRAF);
|
|
1198
|
+
if (this._momentumRAF) cancelAnimationFrame(this._momentumRAF);
|
|
1199
|
+
const canvas = this.framework && this.framework.canvas;
|
|
1200
|
+
if (canvas && this._eventsRegistered) {
|
|
1201
|
+
const h = this._boundHandlers;
|
|
1202
|
+
canvas.removeEventListener('mousedown', h.onMouseDown);
|
|
1203
|
+
canvas.removeEventListener('mousemove', h.onMouseMove);
|
|
1204
|
+
canvas.removeEventListener('mouseup', h.onMouseUp);
|
|
1205
|
+
canvas.removeEventListener('mouseleave', h.onMouseLeave);
|
|
1206
|
+
canvas.removeEventListener('click', h.onClick);
|
|
1207
|
+
canvas.removeEventListener('wheel', h.onWheel);
|
|
1208
|
+
canvas.removeEventListener('touchstart', h.onTouchStart);
|
|
1209
|
+
canvas.removeEventListener('touchmove', h.onTouchMove);
|
|
1210
|
+
canvas.removeEventListener('touchend', h.onTouchEnd);
|
|
1211
|
+
canvas.removeEventListener('touchcancel', h.onTouchEnd);
|
|
1061
1212
|
}
|
|
1062
|
-
this._pdfDoc
|
|
1063
|
-
|
|
1064
|
-
this._containerEl = null;
|
|
1065
|
-
super.destroy && super.destroy();
|
|
1213
|
+
this._pdfDoc = null;
|
|
1214
|
+
if (super.destroy) super.destroy();
|
|
1066
1215
|
}
|
|
1067
1216
|
}
|
|
1068
1217
|
|