@progress/kendo-pdfviewer-common 1.0.0-develop.1 → 1.0.0-develop.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,6 +47,7 @@ export class PdfViewer extends Component {
47
47
  maxZoom: 4,
48
48
  zoomRate: 0.25,
49
49
  zoomLevel: DEFAULT_ZOOM_LEVEL,
50
+ pinchToZoom: true,
50
51
  zoomBeforePrint: false,
51
52
  zoomLevelForPrint: 3,
52
53
  renderForms: false,
@@ -148,6 +149,14 @@ export class PdfViewer extends Component {
148
149
  this.state = {};
149
150
  this.pdfDocument = null;
150
151
  this.pages = [];
152
+ // Pinch-to-zoom state
153
+ this._isCtrlKeyDown = false;
154
+ this._isPinching = false;
155
+ this._touchInfo = null;
156
+ this._wheelUnusedFactor = 1;
157
+ this._touchUnusedFactor = 1;
158
+ this._pendingPinchFactor = 1;
159
+ this._pinchAC = null;
151
160
  this.triggerError = (e) => {
152
161
  this.trigger(ERROR, {
153
162
  error: e
@@ -223,6 +232,18 @@ export class PdfViewer extends Component {
223
232
  }
224
233
  e.preventDefault();
225
234
  e.stopPropagation();
235
+ if (this.options.pinchToZoom && this._isTrackpadPinch(e)) {
236
+ const scaleFactor = Math.exp(-e.deltaY / 100);
237
+ const newScaleFactor = this._accumulateFactor(this.state.zoomLevel, scaleFactor, '_wheelUnusedFactor');
238
+ if (newScaleFactor === 1) {
239
+ return;
240
+ }
241
+ const zoomLevel = this.state.zoomLevel * newScaleFactor;
242
+ this.triggerZoomStart({ zoomLevel });
243
+ this.zoom({ zoomLevel });
244
+ this.triggerZoomEnd({ zoomLevel });
245
+ return;
246
+ }
226
247
  const wheelDelta = mousewheelDelta(e);
227
248
  const zoomModifier = wheelDelta < 0 ? 1 : -1;
228
249
  const zoomLevel = this.state.zoomLevel + (zoomModifier * this.options.zoomRate);
@@ -236,6 +257,102 @@ export class PdfViewer extends Component {
236
257
  zoomLevel: zoomLevel
237
258
  });
238
259
  };
260
+ this._onKeyDown = (e) => {
261
+ if (e.key === 'Control') {
262
+ this._isCtrlKeyDown = true;
263
+ }
264
+ };
265
+ this._onKeyUp = (e) => {
266
+ if (e.key === 'Control') {
267
+ this._isCtrlKeyDown = false;
268
+ }
269
+ };
270
+ this._onGestureEvent = (e) => {
271
+ e.preventDefault();
272
+ };
273
+ this._onTouchStart = (e) => {
274
+ var _a;
275
+ if (!this.options.pinchToZoom) {
276
+ return;
277
+ }
278
+ if (e.touches.length !== 2) {
279
+ return;
280
+ }
281
+ // Prevent browser-native pinch-zoom from starting (required for iOS Safari
282
+ // where the gesture decision is made at touchstart time)
283
+ e.preventDefault();
284
+ const touch0 = e.touches[0];
285
+ const touch1 = e.touches[1];
286
+ this._touchInfo = {
287
+ touch0X: touch0.screenX,
288
+ touch0Y: touch0.screenY,
289
+ touch1X: touch1.screenX,
290
+ touch1Y: touch1.screenY
291
+ };
292
+ this._pendingPinchFactor = 1;
293
+ this._touchUnusedFactor = 1;
294
+ const documentContainer = this.getDocumentContainer();
295
+ if (!documentContainer) {
296
+ return;
297
+ }
298
+ const signal = (_a = this._pinchAC) === null || _a === void 0 ? void 0 : _a.signal;
299
+ documentContainer.addEventListener('touchmove', this._onTouchMove, { passive: false, signal });
300
+ documentContainer.addEventListener('touchend', this._onTouchEnd, { signal });
301
+ documentContainer.addEventListener('touchcancel', this._onTouchEnd, { signal });
302
+ };
303
+ this._onTouchMove = (e) => {
304
+ if (!this._touchInfo || e.touches.length !== 2) {
305
+ return;
306
+ }
307
+ e.preventDefault();
308
+ e.stopPropagation();
309
+ const touch0 = e.touches[0];
310
+ const touch1 = e.touches[1];
311
+ const prevGapX = this._touchInfo.touch1X - this._touchInfo.touch0X;
312
+ const prevGapY = this._touchInfo.touch1Y - this._touchInfo.touch0Y;
313
+ const currGapX = touch1.screenX - touch0.screenX;
314
+ const currGapY = touch1.screenY - touch0.screenY;
315
+ const prevDist = Math.hypot(prevGapX, prevGapY) || 1;
316
+ const currDist = Math.hypot(currGapX, currGapY) || 1;
317
+ if (Math.abs(prevDist - currDist) <= this._minTouchDistanceToPinch) {
318
+ return;
319
+ }
320
+ if (!this._isPinching) {
321
+ this._isPinching = true;
322
+ // Suppress panning during pinch
323
+ this.disableScrollerEventsTracking();
324
+ // Fire zoom start on first pinch movement
325
+ this.triggerZoomStart({ zoomLevel: this.state.zoomLevel });
326
+ }
327
+ this._applyPinchTransform(prevDist, currDist);
328
+ // Update stored positions for next move
329
+ this._touchInfo = {
330
+ touch0X: touch0.screenX,
331
+ touch0Y: touch0.screenY,
332
+ touch1X: touch1.screenX,
333
+ touch1Y: touch1.screenY
334
+ };
335
+ };
336
+ this._onTouchEnd = (e) => {
337
+ if (e.touches.length >= 2) {
338
+ return;
339
+ }
340
+ const documentContainer = this.getDocumentContainer();
341
+ if (documentContainer) {
342
+ documentContainer.removeEventListener('touchmove', this._onTouchMove);
343
+ documentContainer.removeEventListener('touchend', this._onTouchEnd);
344
+ documentContainer.removeEventListener('touchcancel', this._onTouchEnd);
345
+ }
346
+ if (this._isPinching) {
347
+ this._clearPinchTransform();
348
+ const zoomLevel = clamp(this.state.zoomLevel * this._pendingPinchFactor, this.options.minZoom, this.options.maxZoom);
349
+ this.zoom({ zoomLevel });
350
+ this.triggerZoomEnd({ zoomLevel });
351
+ // Re-enable panning
352
+ this.enableScrollerEventsTracking();
353
+ }
354
+ this._resetPinchState();
355
+ };
239
356
  this.extendOptions(options);
240
357
  this.throwIfInvalidOptions();
241
358
  this.wrapper = this.element;
@@ -362,6 +479,7 @@ export class PdfViewer extends Component {
362
479
  }
363
480
  bindEvents() {
364
481
  this.bindPagesWheel();
482
+ this.bindPinchToZoomEvents();
365
483
  }
366
484
  bindPagesWheel() {
367
485
  const documentContainer = this.getDocumentContainer();
@@ -381,6 +499,7 @@ export class PdfViewer extends Component {
381
499
  }
382
500
  unbindEvents() {
383
501
  this.unbindPagesWheel();
502
+ this.unbindPinchToZoomEvents();
384
503
  }
385
504
  unbindPagesWheel() {
386
505
  const documentContainer = this.getDocumentContainer();
@@ -1343,6 +1462,102 @@ export class PdfViewer extends Component {
1343
1462
  this.element.style.setProperty('--scale-round-x', '1px');
1344
1463
  this.element.style.setProperty('--scale-round-y', '1px');
1345
1464
  }
1465
+ _isTrackpadPinch(e) {
1466
+ // Trackpad pinch gestures generate wheel events with synthetic ctrlKey=true.
1467
+ // Real Ctrl+scroll has ctrlKey=true but the physical key is down.
1468
+ // Heuristic: ctrlKey is set, physical Ctrl is NOT pressed, pixel-level delta,
1469
+ // deltaX is 0 (no horizontal scroll), and the scale factor is small.
1470
+ if (this._isCtrlKeyDown) {
1471
+ return false;
1472
+ }
1473
+ if (e.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
1474
+ return false;
1475
+ }
1476
+ if (e.deltaX !== 0) {
1477
+ return false;
1478
+ }
1479
+ const scaleFactor = Math.exp(-e.deltaY / 100);
1480
+ if (Math.abs(scaleFactor - 1) >= 0.05) {
1481
+ return false;
1482
+ }
1483
+ return true;
1484
+ }
1485
+ _accumulateFactor(previousScale, factor, prop) {
1486
+ if (factor === 1) {
1487
+ return 1;
1488
+ }
1489
+ if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) {
1490
+ this[prop] = 1;
1491
+ }
1492
+ const newFactor = Math.floor(previousScale * factor * this[prop] * 100) / (100 * previousScale);
1493
+ this[prop] = factor / (newFactor || factor);
1494
+ return newFactor || 1;
1495
+ }
1496
+ bindPinchToZoomEvents() {
1497
+ if (!this.options.pinchToZoom) {
1498
+ return;
1499
+ }
1500
+ document.addEventListener('keydown', this._onKeyDown);
1501
+ document.addEventListener('keyup', this._onKeyUp);
1502
+ const documentContainer = this.getDocumentContainer();
1503
+ if (!documentContainer) {
1504
+ return;
1505
+ }
1506
+ this._pinchAC = new AbortController();
1507
+ const signal = this._pinchAC.signal;
1508
+ // Prevent the browser from handling pinch-zoom natively
1509
+ documentContainer.style.touchAction = 'none';
1510
+ documentContainer.addEventListener('touchstart', this._onTouchStart, { passive: false, signal });
1511
+ // iOS Safari fires proprietary GestureEvents for pinch gestures.
1512
+ // Suppressing them prevents the browser from zooming the page.
1513
+ documentContainer.addEventListener('gesturestart', this._onGestureEvent, { signal });
1514
+ documentContainer.addEventListener('gesturechange', this._onGestureEvent, { signal });
1515
+ }
1516
+ unbindPinchToZoomEvents() {
1517
+ document.removeEventListener('keydown', this._onKeyDown);
1518
+ document.removeEventListener('keyup', this._onKeyUp);
1519
+ this._isCtrlKeyDown = false;
1520
+ const documentContainer = this.getDocumentContainer();
1521
+ if (documentContainer) {
1522
+ documentContainer.style.touchAction = '';
1523
+ }
1524
+ if (this._pinchAC) {
1525
+ this._pinchAC.abort();
1526
+ this._pinchAC = null;
1527
+ }
1528
+ this._resetPinchState();
1529
+ }
1530
+ _resetPinchState() {
1531
+ this._isPinching = false;
1532
+ this._touchInfo = null;
1533
+ this._touchUnusedFactor = 1;
1534
+ this._wheelUnusedFactor = 1;
1535
+ this._pendingPinchFactor = 1;
1536
+ }
1537
+ get _minTouchDistanceToPinch() {
1538
+ return 32 / (window.devicePixelRatio || 1);
1539
+ }
1540
+ _applyPinchTransform(prevDist, currDist) {
1541
+ const rawFactor = currDist / prevDist;
1542
+ const newFactor = this._accumulateFactor(this.state.zoomLevel, rawFactor, '_touchUnusedFactor');
1543
+ this._pendingPinchFactor *= newFactor;
1544
+ // Clamp the visual factor to the allowed zoom range
1545
+ const targetZoom = this.state.zoomLevel * this._pendingPinchFactor;
1546
+ const clampedZoom = clamp(targetZoom, this.options.minZoom, this.options.maxZoom);
1547
+ const visualFactor = clampedZoom / this.state.zoomLevel;
1548
+ const pagesContainer = this.getPagesContainer();
1549
+ if (pagesContainer) {
1550
+ pagesContainer.style.transform = `scale(${visualFactor})`;
1551
+ pagesContainer.style.transformOrigin = 'center top';
1552
+ }
1553
+ }
1554
+ _clearPinchTransform() {
1555
+ const pagesContainer = this.getPagesContainer();
1556
+ if (pagesContainer) {
1557
+ pagesContainer.style.transform = '';
1558
+ pagesContainer.style.transformOrigin = '';
1559
+ }
1560
+ }
1346
1561
  activatePageNumber(pageNumber) {
1347
1562
  const page = this.getPageByNumber(pageNumber);
1348
1563
  if (!page) {
@@ -47,6 +47,7 @@ export class PdfViewer extends Component {
47
47
  maxZoom: 4,
48
48
  zoomRate: 0.25,
49
49
  zoomLevel: DEFAULT_ZOOM_LEVEL,
50
+ pinchToZoom: true,
50
51
  zoomBeforePrint: false,
51
52
  zoomLevelForPrint: 3,
52
53
  renderForms: false,
@@ -148,6 +149,14 @@ export class PdfViewer extends Component {
148
149
  this.state = {};
149
150
  this.pdfDocument = null;
150
151
  this.pages = [];
152
+ // Pinch-to-zoom state
153
+ this._isCtrlKeyDown = false;
154
+ this._isPinching = false;
155
+ this._touchInfo = null;
156
+ this._wheelUnusedFactor = 1;
157
+ this._touchUnusedFactor = 1;
158
+ this._pendingPinchFactor = 1;
159
+ this._pinchAC = null;
151
160
  this.triggerError = (e) => {
152
161
  this.trigger(ERROR, {
153
162
  error: e
@@ -223,6 +232,18 @@ export class PdfViewer extends Component {
223
232
  }
224
233
  e.preventDefault();
225
234
  e.stopPropagation();
235
+ if (this.options.pinchToZoom && this._isTrackpadPinch(e)) {
236
+ const scaleFactor = Math.exp(-e.deltaY / 100);
237
+ const newScaleFactor = this._accumulateFactor(this.state.zoomLevel, scaleFactor, '_wheelUnusedFactor');
238
+ if (newScaleFactor === 1) {
239
+ return;
240
+ }
241
+ const zoomLevel = this.state.zoomLevel * newScaleFactor;
242
+ this.triggerZoomStart({ zoomLevel });
243
+ this.zoom({ zoomLevel });
244
+ this.triggerZoomEnd({ zoomLevel });
245
+ return;
246
+ }
226
247
  const wheelDelta = mousewheelDelta(e);
227
248
  const zoomModifier = wheelDelta < 0 ? 1 : -1;
228
249
  const zoomLevel = this.state.zoomLevel + (zoomModifier * this.options.zoomRate);
@@ -236,6 +257,102 @@ export class PdfViewer extends Component {
236
257
  zoomLevel: zoomLevel
237
258
  });
238
259
  };
260
+ this._onKeyDown = (e) => {
261
+ if (e.key === 'Control') {
262
+ this._isCtrlKeyDown = true;
263
+ }
264
+ };
265
+ this._onKeyUp = (e) => {
266
+ if (e.key === 'Control') {
267
+ this._isCtrlKeyDown = false;
268
+ }
269
+ };
270
+ this._onGestureEvent = (e) => {
271
+ e.preventDefault();
272
+ };
273
+ this._onTouchStart = (e) => {
274
+ var _a;
275
+ if (!this.options.pinchToZoom) {
276
+ return;
277
+ }
278
+ if (e.touches.length !== 2) {
279
+ return;
280
+ }
281
+ // Prevent browser-native pinch-zoom from starting (required for iOS Safari
282
+ // where the gesture decision is made at touchstart time)
283
+ e.preventDefault();
284
+ const touch0 = e.touches[0];
285
+ const touch1 = e.touches[1];
286
+ this._touchInfo = {
287
+ touch0X: touch0.screenX,
288
+ touch0Y: touch0.screenY,
289
+ touch1X: touch1.screenX,
290
+ touch1Y: touch1.screenY
291
+ };
292
+ this._pendingPinchFactor = 1;
293
+ this._touchUnusedFactor = 1;
294
+ const documentContainer = this.getDocumentContainer();
295
+ if (!documentContainer) {
296
+ return;
297
+ }
298
+ const signal = (_a = this._pinchAC) === null || _a === void 0 ? void 0 : _a.signal;
299
+ documentContainer.addEventListener('touchmove', this._onTouchMove, { passive: false, signal });
300
+ documentContainer.addEventListener('touchend', this._onTouchEnd, { signal });
301
+ documentContainer.addEventListener('touchcancel', this._onTouchEnd, { signal });
302
+ };
303
+ this._onTouchMove = (e) => {
304
+ if (!this._touchInfo || e.touches.length !== 2) {
305
+ return;
306
+ }
307
+ e.preventDefault();
308
+ e.stopPropagation();
309
+ const touch0 = e.touches[0];
310
+ const touch1 = e.touches[1];
311
+ const prevGapX = this._touchInfo.touch1X - this._touchInfo.touch0X;
312
+ const prevGapY = this._touchInfo.touch1Y - this._touchInfo.touch0Y;
313
+ const currGapX = touch1.screenX - touch0.screenX;
314
+ const currGapY = touch1.screenY - touch0.screenY;
315
+ const prevDist = Math.hypot(prevGapX, prevGapY) || 1;
316
+ const currDist = Math.hypot(currGapX, currGapY) || 1;
317
+ if (Math.abs(prevDist - currDist) <= this._minTouchDistanceToPinch) {
318
+ return;
319
+ }
320
+ if (!this._isPinching) {
321
+ this._isPinching = true;
322
+ // Suppress panning during pinch
323
+ this.disableScrollerEventsTracking();
324
+ // Fire zoom start on first pinch movement
325
+ this.triggerZoomStart({ zoomLevel: this.state.zoomLevel });
326
+ }
327
+ this._applyPinchTransform(prevDist, currDist);
328
+ // Update stored positions for next move
329
+ this._touchInfo = {
330
+ touch0X: touch0.screenX,
331
+ touch0Y: touch0.screenY,
332
+ touch1X: touch1.screenX,
333
+ touch1Y: touch1.screenY
334
+ };
335
+ };
336
+ this._onTouchEnd = (e) => {
337
+ if (e.touches.length >= 2) {
338
+ return;
339
+ }
340
+ const documentContainer = this.getDocumentContainer();
341
+ if (documentContainer) {
342
+ documentContainer.removeEventListener('touchmove', this._onTouchMove);
343
+ documentContainer.removeEventListener('touchend', this._onTouchEnd);
344
+ documentContainer.removeEventListener('touchcancel', this._onTouchEnd);
345
+ }
346
+ if (this._isPinching) {
347
+ this._clearPinchTransform();
348
+ const zoomLevel = clamp(this.state.zoomLevel * this._pendingPinchFactor, this.options.minZoom, this.options.maxZoom);
349
+ this.zoom({ zoomLevel });
350
+ this.triggerZoomEnd({ zoomLevel });
351
+ // Re-enable panning
352
+ this.enableScrollerEventsTracking();
353
+ }
354
+ this._resetPinchState();
355
+ };
239
356
  this.extendOptions(options);
240
357
  this.throwIfInvalidOptions();
241
358
  this.wrapper = this.element;
@@ -362,6 +479,7 @@ export class PdfViewer extends Component {
362
479
  }
363
480
  bindEvents() {
364
481
  this.bindPagesWheel();
482
+ this.bindPinchToZoomEvents();
365
483
  }
366
484
  bindPagesWheel() {
367
485
  const documentContainer = this.getDocumentContainer();
@@ -381,6 +499,7 @@ export class PdfViewer extends Component {
381
499
  }
382
500
  unbindEvents() {
383
501
  this.unbindPagesWheel();
502
+ this.unbindPinchToZoomEvents();
384
503
  }
385
504
  unbindPagesWheel() {
386
505
  const documentContainer = this.getDocumentContainer();
@@ -1343,6 +1462,102 @@ export class PdfViewer extends Component {
1343
1462
  this.element.style.setProperty('--scale-round-x', '1px');
1344
1463
  this.element.style.setProperty('--scale-round-y', '1px');
1345
1464
  }
1465
+ _isTrackpadPinch(e) {
1466
+ // Trackpad pinch gestures generate wheel events with synthetic ctrlKey=true.
1467
+ // Real Ctrl+scroll has ctrlKey=true but the physical key is down.
1468
+ // Heuristic: ctrlKey is set, physical Ctrl is NOT pressed, pixel-level delta,
1469
+ // deltaX is 0 (no horizontal scroll), and the scale factor is small.
1470
+ if (this._isCtrlKeyDown) {
1471
+ return false;
1472
+ }
1473
+ if (e.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
1474
+ return false;
1475
+ }
1476
+ if (e.deltaX !== 0) {
1477
+ return false;
1478
+ }
1479
+ const scaleFactor = Math.exp(-e.deltaY / 100);
1480
+ if (Math.abs(scaleFactor - 1) >= 0.05) {
1481
+ return false;
1482
+ }
1483
+ return true;
1484
+ }
1485
+ _accumulateFactor(previousScale, factor, prop) {
1486
+ if (factor === 1) {
1487
+ return 1;
1488
+ }
1489
+ if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) {
1490
+ this[prop] = 1;
1491
+ }
1492
+ const newFactor = Math.floor(previousScale * factor * this[prop] * 100) / (100 * previousScale);
1493
+ this[prop] = factor / (newFactor || factor);
1494
+ return newFactor || 1;
1495
+ }
1496
+ bindPinchToZoomEvents() {
1497
+ if (!this.options.pinchToZoom) {
1498
+ return;
1499
+ }
1500
+ document.addEventListener('keydown', this._onKeyDown);
1501
+ document.addEventListener('keyup', this._onKeyUp);
1502
+ const documentContainer = this.getDocumentContainer();
1503
+ if (!documentContainer) {
1504
+ return;
1505
+ }
1506
+ this._pinchAC = new AbortController();
1507
+ const signal = this._pinchAC.signal;
1508
+ // Prevent the browser from handling pinch-zoom natively
1509
+ documentContainer.style.touchAction = 'none';
1510
+ documentContainer.addEventListener('touchstart', this._onTouchStart, { passive: false, signal });
1511
+ // iOS Safari fires proprietary GestureEvents for pinch gestures.
1512
+ // Suppressing them prevents the browser from zooming the page.
1513
+ documentContainer.addEventListener('gesturestart', this._onGestureEvent, { signal });
1514
+ documentContainer.addEventListener('gesturechange', this._onGestureEvent, { signal });
1515
+ }
1516
+ unbindPinchToZoomEvents() {
1517
+ document.removeEventListener('keydown', this._onKeyDown);
1518
+ document.removeEventListener('keyup', this._onKeyUp);
1519
+ this._isCtrlKeyDown = false;
1520
+ const documentContainer = this.getDocumentContainer();
1521
+ if (documentContainer) {
1522
+ documentContainer.style.touchAction = '';
1523
+ }
1524
+ if (this._pinchAC) {
1525
+ this._pinchAC.abort();
1526
+ this._pinchAC = null;
1527
+ }
1528
+ this._resetPinchState();
1529
+ }
1530
+ _resetPinchState() {
1531
+ this._isPinching = false;
1532
+ this._touchInfo = null;
1533
+ this._touchUnusedFactor = 1;
1534
+ this._wheelUnusedFactor = 1;
1535
+ this._pendingPinchFactor = 1;
1536
+ }
1537
+ get _minTouchDistanceToPinch() {
1538
+ return 32 / (window.devicePixelRatio || 1);
1539
+ }
1540
+ _applyPinchTransform(prevDist, currDist) {
1541
+ const rawFactor = currDist / prevDist;
1542
+ const newFactor = this._accumulateFactor(this.state.zoomLevel, rawFactor, '_touchUnusedFactor');
1543
+ this._pendingPinchFactor *= newFactor;
1544
+ // Clamp the visual factor to the allowed zoom range
1545
+ const targetZoom = this.state.zoomLevel * this._pendingPinchFactor;
1546
+ const clampedZoom = clamp(targetZoom, this.options.minZoom, this.options.maxZoom);
1547
+ const visualFactor = clampedZoom / this.state.zoomLevel;
1548
+ const pagesContainer = this.getPagesContainer();
1549
+ if (pagesContainer) {
1550
+ pagesContainer.style.transform = `scale(${visualFactor})`;
1551
+ pagesContainer.style.transformOrigin = 'center top';
1552
+ }
1553
+ }
1554
+ _clearPinchTransform() {
1555
+ const pagesContainer = this.getPagesContainer();
1556
+ if (pagesContainer) {
1557
+ pagesContainer.style.transform = '';
1558
+ pagesContainer.style.transformOrigin = '';
1559
+ }
1560
+ }
1346
1561
  activatePageNumber(pageNumber) {
1347
1562
  const page = this.getPageByNumber(pageNumber);
1348
1563
  if (!page) {
@@ -33,6 +33,18 @@ export declare class PdfViewer extends Component {
33
33
  searchService: SearchService;
34
34
  shouldPreventScroll: boolean;
35
35
  eventBus: EventBus;
36
+ _isCtrlKeyDown: boolean;
37
+ _isPinching: boolean;
38
+ _touchInfo: {
39
+ touch0X: number;
40
+ touch0Y: number;
41
+ touch1X: number;
42
+ touch1Y: number;
43
+ } | null;
44
+ _wheelUnusedFactor: number;
45
+ _touchUnusedFactor: number;
46
+ _pendingPinchFactor: number;
47
+ _pinchAC: AbortController | null;
36
48
  constructor(element: any, options: any);
37
49
  destroy(): void;
38
50
  throwIfInvalidOptions(): void;
@@ -183,6 +195,20 @@ export declare class PdfViewer extends Component {
183
195
  disableScrollerEventsTracking(): void;
184
196
  setScaleFactor(scaleFactor: number): void;
185
197
  onDocumentWheel: (e: any) => void;
198
+ _isTrackpadPinch(e: WheelEvent): boolean;
199
+ _accumulateFactor(previousScale: number, factor: number, prop: '_wheelUnusedFactor' | '_touchUnusedFactor'): number;
200
+ _onKeyDown: (e: KeyboardEvent) => void;
201
+ _onKeyUp: (e: KeyboardEvent) => void;
202
+ bindPinchToZoomEvents(): void;
203
+ unbindPinchToZoomEvents(): void;
204
+ _resetPinchState(): void;
205
+ get _minTouchDistanceToPinch(): number;
206
+ _onGestureEvent: (e: Event) => void;
207
+ _onTouchStart: (e: TouchEvent) => void;
208
+ _onTouchMove: (e: TouchEvent) => void;
209
+ _onTouchEnd: (e: TouchEvent) => void;
210
+ _applyPinchTransform(prevDist: number, currDist: number): void;
211
+ _clearPinchTransform(): void;
186
212
  activatePageNumber(pageNumber: any): void;
187
213
  scrollToPage({ pageNumber }: {
188
214
  pageNumber: any;
@@ -50,6 +50,7 @@ class PdfViewer extends main_1.Component {
50
50
  maxZoom: 4,
51
51
  zoomRate: 0.25,
52
52
  zoomLevel: DEFAULT_ZOOM_LEVEL,
53
+ pinchToZoom: true,
53
54
  zoomBeforePrint: false,
54
55
  zoomLevelForPrint: 3,
55
56
  renderForms: false,
@@ -151,6 +152,14 @@ class PdfViewer extends main_1.Component {
151
152
  this.state = {};
152
153
  this.pdfDocument = null;
153
154
  this.pages = [];
155
+ // Pinch-to-zoom state
156
+ this._isCtrlKeyDown = false;
157
+ this._isPinching = false;
158
+ this._touchInfo = null;
159
+ this._wheelUnusedFactor = 1;
160
+ this._touchUnusedFactor = 1;
161
+ this._pendingPinchFactor = 1;
162
+ this._pinchAC = null;
154
163
  this.triggerError = (e) => {
155
164
  this.trigger(ERROR, {
156
165
  error: e
@@ -226,6 +235,18 @@ class PdfViewer extends main_1.Component {
226
235
  }
227
236
  e.preventDefault();
228
237
  e.stopPropagation();
238
+ if (this.options.pinchToZoom && this._isTrackpadPinch(e)) {
239
+ const scaleFactor = Math.exp(-e.deltaY / 100);
240
+ const newScaleFactor = this._accumulateFactor(this.state.zoomLevel, scaleFactor, '_wheelUnusedFactor');
241
+ if (newScaleFactor === 1) {
242
+ return;
243
+ }
244
+ const zoomLevel = this.state.zoomLevel * newScaleFactor;
245
+ this.triggerZoomStart({ zoomLevel });
246
+ this.zoom({ zoomLevel });
247
+ this.triggerZoomEnd({ zoomLevel });
248
+ return;
249
+ }
229
250
  const wheelDelta = (0, main_1.mousewheelDelta)(e);
230
251
  const zoomModifier = wheelDelta < 0 ? 1 : -1;
231
252
  const zoomLevel = this.state.zoomLevel + (zoomModifier * this.options.zoomRate);
@@ -239,6 +260,102 @@ class PdfViewer extends main_1.Component {
239
260
  zoomLevel: zoomLevel
240
261
  });
241
262
  };
263
+ this._onKeyDown = (e) => {
264
+ if (e.key === 'Control') {
265
+ this._isCtrlKeyDown = true;
266
+ }
267
+ };
268
+ this._onKeyUp = (e) => {
269
+ if (e.key === 'Control') {
270
+ this._isCtrlKeyDown = false;
271
+ }
272
+ };
273
+ this._onGestureEvent = (e) => {
274
+ e.preventDefault();
275
+ };
276
+ this._onTouchStart = (e) => {
277
+ var _a;
278
+ if (!this.options.pinchToZoom) {
279
+ return;
280
+ }
281
+ if (e.touches.length !== 2) {
282
+ return;
283
+ }
284
+ // Prevent browser-native pinch-zoom from starting (required for iOS Safari
285
+ // where the gesture decision is made at touchstart time)
286
+ e.preventDefault();
287
+ const touch0 = e.touches[0];
288
+ const touch1 = e.touches[1];
289
+ this._touchInfo = {
290
+ touch0X: touch0.screenX,
291
+ touch0Y: touch0.screenY,
292
+ touch1X: touch1.screenX,
293
+ touch1Y: touch1.screenY
294
+ };
295
+ this._pendingPinchFactor = 1;
296
+ this._touchUnusedFactor = 1;
297
+ const documentContainer = this.getDocumentContainer();
298
+ if (!documentContainer) {
299
+ return;
300
+ }
301
+ const signal = (_a = this._pinchAC) === null || _a === void 0 ? void 0 : _a.signal;
302
+ documentContainer.addEventListener('touchmove', this._onTouchMove, { passive: false, signal });
303
+ documentContainer.addEventListener('touchend', this._onTouchEnd, { signal });
304
+ documentContainer.addEventListener('touchcancel', this._onTouchEnd, { signal });
305
+ };
306
+ this._onTouchMove = (e) => {
307
+ if (!this._touchInfo || e.touches.length !== 2) {
308
+ return;
309
+ }
310
+ e.preventDefault();
311
+ e.stopPropagation();
312
+ const touch0 = e.touches[0];
313
+ const touch1 = e.touches[1];
314
+ const prevGapX = this._touchInfo.touch1X - this._touchInfo.touch0X;
315
+ const prevGapY = this._touchInfo.touch1Y - this._touchInfo.touch0Y;
316
+ const currGapX = touch1.screenX - touch0.screenX;
317
+ const currGapY = touch1.screenY - touch0.screenY;
318
+ const prevDist = Math.hypot(prevGapX, prevGapY) || 1;
319
+ const currDist = Math.hypot(currGapX, currGapY) || 1;
320
+ if (Math.abs(prevDist - currDist) <= this._minTouchDistanceToPinch) {
321
+ return;
322
+ }
323
+ if (!this._isPinching) {
324
+ this._isPinching = true;
325
+ // Suppress panning during pinch
326
+ this.disableScrollerEventsTracking();
327
+ // Fire zoom start on first pinch movement
328
+ this.triggerZoomStart({ zoomLevel: this.state.zoomLevel });
329
+ }
330
+ this._applyPinchTransform(prevDist, currDist);
331
+ // Update stored positions for next move
332
+ this._touchInfo = {
333
+ touch0X: touch0.screenX,
334
+ touch0Y: touch0.screenY,
335
+ touch1X: touch1.screenX,
336
+ touch1Y: touch1.screenY
337
+ };
338
+ };
339
+ this._onTouchEnd = (e) => {
340
+ if (e.touches.length >= 2) {
341
+ return;
342
+ }
343
+ const documentContainer = this.getDocumentContainer();
344
+ if (documentContainer) {
345
+ documentContainer.removeEventListener('touchmove', this._onTouchMove);
346
+ documentContainer.removeEventListener('touchend', this._onTouchEnd);
347
+ documentContainer.removeEventListener('touchcancel', this._onTouchEnd);
348
+ }
349
+ if (this._isPinching) {
350
+ this._clearPinchTransform();
351
+ const zoomLevel = (0, main_1.clamp)(this.state.zoomLevel * this._pendingPinchFactor, this.options.minZoom, this.options.maxZoom);
352
+ this.zoom({ zoomLevel });
353
+ this.triggerZoomEnd({ zoomLevel });
354
+ // Re-enable panning
355
+ this.enableScrollerEventsTracking();
356
+ }
357
+ this._resetPinchState();
358
+ };
242
359
  this.extendOptions(options);
243
360
  this.throwIfInvalidOptions();
244
361
  this.wrapper = this.element;
@@ -365,6 +482,7 @@ class PdfViewer extends main_1.Component {
365
482
  }
366
483
  bindEvents() {
367
484
  this.bindPagesWheel();
485
+ this.bindPinchToZoomEvents();
368
486
  }
369
487
  bindPagesWheel() {
370
488
  const documentContainer = this.getDocumentContainer();
@@ -384,6 +502,7 @@ class PdfViewer extends main_1.Component {
384
502
  }
385
503
  unbindEvents() {
386
504
  this.unbindPagesWheel();
505
+ this.unbindPinchToZoomEvents();
387
506
  }
388
507
  unbindPagesWheel() {
389
508
  const documentContainer = this.getDocumentContainer();
@@ -1346,6 +1465,102 @@ class PdfViewer extends main_1.Component {
1346
1465
  this.element.style.setProperty('--scale-round-x', '1px');
1347
1466
  this.element.style.setProperty('--scale-round-y', '1px');
1348
1467
  }
1468
+ _isTrackpadPinch(e) {
1469
+ // Trackpad pinch gestures generate wheel events with synthetic ctrlKey=true.
1470
+ // Real Ctrl+scroll has ctrlKey=true but the physical key is down.
1471
+ // Heuristic: ctrlKey is set, physical Ctrl is NOT pressed, pixel-level delta,
1472
+ // deltaX is 0 (no horizontal scroll), and the scale factor is small.
1473
+ if (this._isCtrlKeyDown) {
1474
+ return false;
1475
+ }
1476
+ if (e.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
1477
+ return false;
1478
+ }
1479
+ if (e.deltaX !== 0) {
1480
+ return false;
1481
+ }
1482
+ const scaleFactor = Math.exp(-e.deltaY / 100);
1483
+ if (Math.abs(scaleFactor - 1) >= 0.05) {
1484
+ return false;
1485
+ }
1486
+ return true;
1487
+ }
1488
+ _accumulateFactor(previousScale, factor, prop) {
1489
+ if (factor === 1) {
1490
+ return 1;
1491
+ }
1492
+ if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) {
1493
+ this[prop] = 1;
1494
+ }
1495
+ const newFactor = Math.floor(previousScale * factor * this[prop] * 100) / (100 * previousScale);
1496
+ this[prop] = factor / (newFactor || factor);
1497
+ return newFactor || 1;
1498
+ }
1499
+ bindPinchToZoomEvents() {
1500
+ if (!this.options.pinchToZoom) {
1501
+ return;
1502
+ }
1503
+ document.addEventListener('keydown', this._onKeyDown);
1504
+ document.addEventListener('keyup', this._onKeyUp);
1505
+ const documentContainer = this.getDocumentContainer();
1506
+ if (!documentContainer) {
1507
+ return;
1508
+ }
1509
+ this._pinchAC = new AbortController();
1510
+ const signal = this._pinchAC.signal;
1511
+ // Prevent the browser from handling pinch-zoom natively
1512
+ documentContainer.style.touchAction = 'none';
1513
+ documentContainer.addEventListener('touchstart', this._onTouchStart, { passive: false, signal });
1514
+ // iOS Safari fires proprietary GestureEvents for pinch gestures.
1515
+ // Suppressing them prevents the browser from zooming the page.
1516
+ documentContainer.addEventListener('gesturestart', this._onGestureEvent, { signal });
1517
+ documentContainer.addEventListener('gesturechange', this._onGestureEvent, { signal });
1518
+ }
1519
+ unbindPinchToZoomEvents() {
1520
+ document.removeEventListener('keydown', this._onKeyDown);
1521
+ document.removeEventListener('keyup', this._onKeyUp);
1522
+ this._isCtrlKeyDown = false;
1523
+ const documentContainer = this.getDocumentContainer();
1524
+ if (documentContainer) {
1525
+ documentContainer.style.touchAction = '';
1526
+ }
1527
+ if (this._pinchAC) {
1528
+ this._pinchAC.abort();
1529
+ this._pinchAC = null;
1530
+ }
1531
+ this._resetPinchState();
1532
+ }
1533
+ _resetPinchState() {
1534
+ this._isPinching = false;
1535
+ this._touchInfo = null;
1536
+ this._touchUnusedFactor = 1;
1537
+ this._wheelUnusedFactor = 1;
1538
+ this._pendingPinchFactor = 1;
1539
+ }
1540
+ get _minTouchDistanceToPinch() {
1541
+ return 32 / (window.devicePixelRatio || 1);
1542
+ }
1543
+ _applyPinchTransform(prevDist, currDist) {
1544
+ const rawFactor = currDist / prevDist;
1545
+ const newFactor = this._accumulateFactor(this.state.zoomLevel, rawFactor, '_touchUnusedFactor');
1546
+ this._pendingPinchFactor *= newFactor;
1547
+ // Clamp the visual factor to the allowed zoom range
1548
+ const targetZoom = this.state.zoomLevel * this._pendingPinchFactor;
1549
+ const clampedZoom = (0, main_1.clamp)(targetZoom, this.options.minZoom, this.options.maxZoom);
1550
+ const visualFactor = clampedZoom / this.state.zoomLevel;
1551
+ const pagesContainer = this.getPagesContainer();
1552
+ if (pagesContainer) {
1553
+ pagesContainer.style.transform = `scale(${visualFactor})`;
1554
+ pagesContainer.style.transformOrigin = 'center top';
1555
+ }
1556
+ }
1557
+ _clearPinchTransform() {
1558
+ const pagesContainer = this.getPagesContainer();
1559
+ if (pagesContainer) {
1560
+ pagesContainer.style.transform = '';
1561
+ pagesContainer.style.transformOrigin = '';
1562
+ }
1563
+ }
1349
1564
  activatePageNumber(pageNumber) {
1350
1565
  const page = this.getPageByNumber(pageNumber);
1351
1566
  if (!page) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@progress/kendo-pdfviewer-common",
3
3
  "description": "Kendo UI TypeScript package exporting functions for PDFViewer component",
4
- "version": "1.0.0-develop.1",
4
+ "version": "1.0.0-develop.2",
5
5
  "keywords": [
6
6
  "Kendo UI"
7
7
  ],