nodebb-plugin-pdf-secure2 1.4.5 → 1.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-pdf-secure2",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Secure PDF viewer plugin for NodeBB - prevents downloading, enables canvas-only rendering with Premium group support",
5
5
  "main": "library.js",
6
6
  "repository": {
@@ -33,6 +33,8 @@
33
33
  document.documentElement.webkitRequestFullscreen
34
34
  );
35
35
  var simulatedFullscreenIframe = null;
36
+ var windowedModeIframe = null;
37
+ var windowedBackdrop = null;
36
38
  var savedBodyOverflow = '';
37
39
 
38
40
  // Loading queue - only load one PDF at a time
@@ -103,6 +105,9 @@
103
105
  });
104
106
  if (!sourceIframe) return;
105
107
 
108
+ // Exit windowed mode first if active
109
+ if (windowedModeIframe) exitWindowedMode();
110
+
106
111
  if (fullscreenApiSupported) {
107
112
  // Native fullscreen path
108
113
  var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
@@ -123,6 +128,28 @@
123
128
  }
124
129
  }
125
130
 
131
+ // Windowed mode toggle request from iframe viewer
132
+ if (event.data && event.data.type === 'pdf-secure-windowed-toggle') {
133
+ var sourceIframe = document.querySelector('.pdf-secure-iframe');
134
+ document.querySelectorAll('.pdf-secure-iframe').forEach(function (f) {
135
+ if (f.contentWindow === event.source) sourceIframe = f;
136
+ });
137
+ if (!sourceIframe) return;
138
+
139
+ if (windowedModeIframe) {
140
+ exitWindowedMode();
141
+ } else {
142
+ // Exit fullscreen/simulated fullscreen first if active
143
+ var fsEl = document.fullscreenElement || document.webkitFullscreenElement;
144
+ if (fsEl) {
145
+ if (document.exitFullscreen) document.exitFullscreen();
146
+ else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
147
+ }
148
+ if (simulatedFullscreenIframe) exitSimulatedFullscreen();
149
+ enterWindowedMode(sourceIframe);
150
+ }
151
+ }
152
+
126
153
  // Fullscreen state query from iframe
127
154
  if (event.data && event.data.type === 'pdf-secure-fullscreen-query') {
128
155
  var fsActive = !!(document.fullscreenElement || document.webkitFullscreenElement) ||
@@ -270,6 +297,90 @@
270
297
  }
271
298
  }
272
299
 
300
+ // Enter windowed overlay mode (large overlay without hiding browser chrome/tabs)
301
+ function enterWindowedMode(iframe) {
302
+ if (windowedModeIframe) return;
303
+ windowedModeIframe = iframe;
304
+
305
+ // Save original styles
306
+ iframe._savedWindowedStyle = {
307
+ position: iframe.style.position,
308
+ top: iframe.style.top,
309
+ left: iframe.style.left,
310
+ width: iframe.style.width,
311
+ height: iframe.style.height,
312
+ zIndex: iframe.style.zIndex,
313
+ borderRadius: iframe.style.borderRadius,
314
+ boxShadow: iframe.style.boxShadow
315
+ };
316
+
317
+ // Create backdrop
318
+ windowedBackdrop = document.createElement('div');
319
+ windowedBackdrop.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:2147483646;transition:opacity 0.2s;';
320
+ windowedBackdrop.onclick = function () { exitWindowedMode(); };
321
+ document.body.appendChild(windowedBackdrop);
322
+
323
+ // Position iframe as overlay with margins (browser tabs stay visible)
324
+ iframe.style.position = 'fixed';
325
+ iframe.style.top = '24px';
326
+ iframe.style.left = '24px';
327
+ iframe.style.width = 'calc(100vw - 48px)';
328
+ iframe.style.height = 'calc(100vh - 48px)';
329
+ iframe.style.zIndex = '2147483647';
330
+ iframe.style.borderRadius = '12px';
331
+ iframe.style.boxShadow = '0 8px 48px rgba(0,0,0,0.5)';
332
+
333
+ // Lock body scroll
334
+ savedBodyOverflow = document.body.style.overflow;
335
+ document.body.style.overflow = 'hidden';
336
+
337
+ // Notify iframe
338
+ if (iframe.contentWindow) {
339
+ iframe.contentWindow.postMessage({
340
+ type: 'pdf-secure-windowed-state',
341
+ isWindowed: true
342
+ }, window.location.origin);
343
+ }
344
+ }
345
+
346
+ // Exit windowed overlay mode
347
+ function exitWindowedMode() {
348
+ if (!windowedModeIframe) return;
349
+ var iframe = windowedModeIframe;
350
+ windowedModeIframe = null;
351
+
352
+ // Restore original styles
353
+ if (iframe._savedWindowedStyle) {
354
+ iframe.style.position = iframe._savedWindowedStyle.position;
355
+ iframe.style.top = iframe._savedWindowedStyle.top;
356
+ iframe.style.left = iframe._savedWindowedStyle.left;
357
+ iframe.style.width = iframe._savedWindowedStyle.width;
358
+ iframe.style.height = iframe._savedWindowedStyle.height;
359
+ iframe.style.zIndex = iframe._savedWindowedStyle.zIndex;
360
+ iframe.style.borderRadius = iframe._savedWindowedStyle.borderRadius;
361
+ iframe.style.boxShadow = iframe._savedWindowedStyle.boxShadow;
362
+ delete iframe._savedWindowedStyle;
363
+ }
364
+
365
+ // Remove backdrop
366
+ if (windowedBackdrop && windowedBackdrop.parentNode) {
367
+ windowedBackdrop.remove();
368
+ windowedBackdrop = null;
369
+ }
370
+
371
+ // Restore body scroll
372
+ document.body.style.overflow = savedBodyOverflow;
373
+ savedBodyOverflow = '';
374
+
375
+ // Notify iframe
376
+ if (iframe.contentWindow) {
377
+ iframe.contentWindow.postMessage({
378
+ type: 'pdf-secure-windowed-state',
379
+ isWindowed: false
380
+ }, window.location.origin);
381
+ }
382
+ }
383
+
273
384
  async function processQueue() {
274
385
  if (isLoading || loadQueue.length === 0) return;
275
386
 
@@ -302,8 +413,9 @@
302
413
  currentResolver = null;
303
414
  // Clear decrypted PDF buffer cache on navigation
304
415
  pdfBufferCache.clear();
305
- // Exit simulated fullscreen on SPA navigation
416
+ // Exit simulated fullscreen / windowed mode on SPA navigation
306
417
  exitSimulatedFullscreen();
418
+ exitWindowedMode();
307
419
  interceptPdfLinks();
308
420
  });
309
421
  } catch (err) {
@@ -687,12 +687,14 @@
687
687
 
688
688
  const dropdownBackdrop = document.getElementById('dropdownBackdrop');
689
689
  const overflowDropdown = document.getElementById('overflowDropdown');
690
+ const fullscreenDropdown = document.getElementById('fullscreenDropdown');
690
691
 
691
692
  function closeAllDropdowns() {
692
693
  highlightDropdown.classList.remove('visible');
693
694
  drawDropdown.classList.remove('visible');
694
695
  shapesDropdown.classList.remove('visible');
695
696
  overflowDropdown.classList.remove('visible');
697
+ fullscreenDropdown.classList.remove('visible');
696
698
  dropdownBackdrop.classList.remove('visible');
697
699
  }
698
700
 
@@ -726,6 +728,11 @@
726
728
  document.getElementById('drawArrow').onclick = (e) => toggleDropdown(drawDropdown, e);
727
729
  document.getElementById('shapesArrow').onclick = (e) => toggleDropdown(shapesDropdown, e);
728
730
 
731
+ // Fullscreen dropdown toggle
732
+ document.getElementById('fullscreenArrow').onclick = (e) => toggleDropdown(fullscreenDropdown, e);
733
+ fullscreenDropdown.addEventListener('click', (e) => e.stopPropagation());
734
+ fullscreenDropdown.addEventListener('pointerup', (e) => e.stopPropagation());
735
+
729
736
  // Overflow menu toggle
730
737
  document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
731
738
  overflowDropdown.addEventListener('click', (e) => e.stopPropagation());
@@ -762,9 +769,27 @@
762
769
  closeAllDropdowns();
763
770
  });
764
771
  attachOverflowAction('overflowFullscreen', () => {
772
+ if (isWindowed) toggleWindowed();
773
+ toggleFullscreen();
774
+ closeAllDropdowns();
775
+ });
776
+ attachOverflowAction('overflowWindowed', () => {
777
+ if (isFullscreen) toggleFullscreen();
778
+ toggleWindowed();
779
+ closeAllDropdowns();
780
+ });
781
+
782
+ // Fullscreen dropdown options
783
+ attachOverflowAction('fullscreenOptionFull', () => {
784
+ if (isWindowed) toggleWindowed();
765
785
  toggleFullscreen();
766
786
  closeAllDropdowns();
767
787
  });
788
+ attachOverflowAction('fullscreenOptionWindowed', () => {
789
+ if (isFullscreen) toggleFullscreen();
790
+ toggleWindowed();
791
+ closeAllDropdowns();
792
+ });
768
793
 
769
794
  // Close dropdowns when clicking outside
770
795
  document.addEventListener('click', (e) => {
@@ -2487,7 +2512,7 @@
2487
2512
  const isLiteMode = _cfg && _cfg.isLite;
2488
2513
 
2489
2514
  // Fullscreen shortcut (always allowed)
2490
- if (key === 'f') { toggleFullscreen(); e.preventDefault(); }
2515
+ if (key === 'f') { if (isWindowed) toggleWindowed(); else toggleFullscreen(); e.preventDefault(); }
2491
2516
 
2492
2517
  // Tool shortcuts (blocked in Lite mode)
2493
2518
  if (!isLiteMode) {
@@ -2577,9 +2602,11 @@
2577
2602
  return;
2578
2603
  }
2579
2604
 
2580
- // Escape to deselect tool
2605
+ // Escape to deselect tool / exit windowed mode
2581
2606
  if (key === 'escape') {
2582
- if (currentTool) {
2607
+ if (isWindowed) {
2608
+ toggleWindowed();
2609
+ } else if (currentTool) {
2583
2610
  setTool(currentTool); // Toggle off
2584
2611
  }
2585
2612
  closeAllDropdowns();
@@ -2669,8 +2696,9 @@
2669
2696
  // ==========================================
2670
2697
 
2671
2698
 
2672
- // Fullscreen state (tracks both native and simulated fullscreen)
2699
+ // Fullscreen & Windowed state (tracks native, simulated fullscreen, and windowed overlay)
2673
2700
  let isFullscreen = false;
2701
+ let isWindowed = false;
2674
2702
 
2675
2703
  // Fullscreen toggle function — delegates to parent iframe handler via postMessage
2676
2704
  function toggleFullscreen() {
@@ -2687,23 +2715,41 @@
2687
2715
  }
2688
2716
  }
2689
2717
 
2718
+ // Windowed mode toggle — delegates to parent
2719
+ function toggleWindowed() {
2720
+ if (window.self !== window.top) {
2721
+ window.parent.postMessage({ type: 'pdf-secure-windowed-toggle' }, window.location.origin);
2722
+ }
2723
+ }
2724
+
2690
2725
  // Update fullscreen button icon
2691
2726
  function updateFullscreenIcon() {
2692
2727
  const icon = document.getElementById('fullscreenIcon');
2693
2728
  const btn = document.getElementById('fullscreenBtn');
2729
+ const exitPath = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
2730
+ const enterPath = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
2731
+ const windowedPath = '<path d="M19 4H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H5V8h14v12z"/>';
2694
2732
  if (isFullscreen) {
2695
- icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
2733
+ icon.innerHTML = exitPath;
2734
+ btn.classList.add('active');
2735
+ } else if (isWindowed) {
2736
+ icon.innerHTML = windowedPath;
2696
2737
  btn.classList.add('active');
2697
2738
  } else {
2698
- icon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
2739
+ icon.innerHTML = enterPath;
2699
2740
  btn.classList.remove('active');
2700
2741
  }
2742
+ // Highlight active option in dropdown
2743
+ const optFull = document.getElementById('fullscreenOptionFull');
2744
+ const optWindowed = document.getElementById('fullscreenOptionWindowed');
2745
+ if (optFull) optFull.classList.toggle('active', isFullscreen);
2746
+ if (optWindowed) optWindowed.classList.toggle('active', isWindowed);
2701
2747
  }
2702
2748
 
2703
2749
  // Apply fullscreen CSS class and touch restrictions
2704
2750
  function applyFullscreenTouchRestrictions() {
2705
- document.body.classList.toggle('viewer-fullscreen', isFullscreen);
2706
- if (isFullscreen) {
2751
+ document.body.classList.toggle('viewer-fullscreen', isFullscreen || isWindowed);
2752
+ if (isFullscreen || isWindowed) {
2707
2753
  document.documentElement.style.touchAction = 'pan-y pinch-zoom';
2708
2754
  document.documentElement.style.overscrollBehavior = 'none';
2709
2755
  } else {
@@ -2712,13 +2758,18 @@
2712
2758
  }
2713
2759
  }
2714
2760
 
2715
- // Listen for fullscreen state from parent (simulated fullscreen)
2761
+ // Listen for fullscreen/windowed state from parent (iframe mode)
2716
2762
  window.addEventListener('message', (event) => {
2717
2763
  if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
2718
2764
  isFullscreen = event.data.isFullscreen;
2719
2765
  updateFullscreenIcon();
2720
2766
  applyFullscreenTouchRestrictions();
2721
2767
  }
2768
+ if (event.data && event.data.type === 'pdf-secure-windowed-state') {
2769
+ isWindowed = event.data.isWindowed;
2770
+ updateFullscreenIcon();
2771
+ applyFullscreenTouchRestrictions();
2772
+ }
2722
2773
  });
2723
2774
 
2724
2775
  // Local fullscreen events (standalone mode)
@@ -2728,7 +2779,11 @@
2728
2779
  applyFullscreenTouchRestrictions();
2729
2780
  });
2730
2781
 
2731
- document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
2782
+ // Fullscreen button click: toggle current active mode, or fullscreen by default
2783
+ document.getElementById('fullscreenBtn').onclick = () => {
2784
+ if (isWindowed) toggleWindowed();
2785
+ else toggleFullscreen();
2786
+ };
2732
2787
 
2733
2788
 
2734
2789
  // Mouse wheel / touchpad pinch zoom with Ctrl (clamped 0.5x-5x)
@@ -757,11 +757,24 @@
757
757
  .chatMsg .tableWrap {
758
758
  overflow-x: auto;
759
759
  margin: 6px 0;
760
+ -webkit-overflow-scrolling: touch;
761
+ position: relative;
762
+ }
763
+ .chatMsg .tableWrap.scrollable::after {
764
+ content: '';
765
+ position: absolute;
766
+ top: 0;
767
+ right: 0;
768
+ bottom: 0;
769
+ width: 24px;
770
+ background: linear-gradient(to right, transparent, rgba(0,0,0,0.4));
771
+ pointer-events: none;
760
772
  }
761
773
  .chatMsg table {
762
- width: 100%;
763
774
  border-collapse: collapse;
764
775
  font-size: 12px;
776
+ min-width: 100%;
777
+ width: max-content;
765
778
  }
766
779
  .chatMsg th, .chatMsg td {
767
780
  border: 1px solid rgba(255,255,255,0.15);
@@ -771,6 +784,7 @@
771
784
  .chatMsg th {
772
785
  background: rgba(255,255,255,0.08);
773
786
  font-weight: 600;
787
+ white-space: nowrap;
774
788
  }
775
789
  .chatMsg tbody tr:nth-child(even) {
776
790
  background: rgba(255,255,255,0.03);
@@ -2742,12 +2756,33 @@
2742
2756
  </svg>
2743
2757
  </button>
2744
2758
 
2745
- <!-- Fullscreen Toggle -->
2746
- <button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
2747
- <svg viewBox="0 0 24 24" id="fullscreenIcon">
2748
- <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
2749
- </svg>
2750
- </button>
2759
+ <!-- Fullscreen Toggle with Dropdown -->
2760
+ <div class="toolbarBtnWithDropdown" id="fullscreenWrapper">
2761
+ <button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
2762
+ <svg viewBox="0 0 24 24" id="fullscreenIcon">
2763
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
2764
+ </svg>
2765
+ </button>
2766
+ <button class="dropdownArrow" id="fullscreenArrow">
2767
+ <svg viewBox="0 0 24 24">
2768
+ <path d="M7 10l5 5 5-5z" />
2769
+ </svg>
2770
+ </button>
2771
+ <div class="toolDropdown" id="fullscreenDropdown">
2772
+ <button class="overflowItem" id="fullscreenOptionFull">
2773
+ <svg viewBox="0 0 24 24">
2774
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
2775
+ </svg>
2776
+ <span>Tam Ekran</span>
2777
+ </button>
2778
+ <button class="overflowItem" id="fullscreenOptionWindowed">
2779
+ <svg viewBox="0 0 24 24">
2780
+ <path d="M19 4H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H5V8h14v12z" />
2781
+ </svg>
2782
+ <span>Pencereli Mod</span>
2783
+ </button>
2784
+ </div>
2785
+ </div>
2751
2786
 
2752
2787
  <!-- AI Chat Toggle -->
2753
2788
  <button class="toolbarBtn" id="chatBtn" data-tooltip="PDF Chat (C)" style="display:none;">
@@ -3060,6 +3095,12 @@
3060
3095
  </svg>
3061
3096
  <span>Tam Ekran</span>
3062
3097
  </button>
3098
+ <button class="overflowItem" id="overflowWindowed">
3099
+ <svg viewBox="0 0 24 24">
3100
+ <path d="M19 4H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H5V8h14v12z" />
3101
+ </svg>
3102
+ <span>Pencereli Mod</span>
3103
+ </button>
3063
3104
  </div>
3064
3105
  </div>
3065
3106
  </div>
@@ -4219,6 +4260,14 @@
4219
4260
  // Truncate excessively long AI responses
4220
4261
  if (text.length > 10000) text = text.slice(0, 10000) + '…';
4221
4262
 
4263
+ // Fix multi-line inline math: join $...$ that span line breaks
4264
+ // e.g. "$V_o\n= V_i$" → "$V_o = V_i$"
4265
+ text = text.replace(/\$([^$]*?\n[^$]*?)\$/g, function (match, inner) {
4266
+ // Only join if the inner content is short (likely math, not prose)
4267
+ if (inner.length > 80) return match;
4268
+ return '$' + inner.replace(/\n/g, ' ') + '$';
4269
+ });
4270
+
4222
4271
  var frag = document.createDocumentFragment();
4223
4272
 
4224
4273
  // Split into code blocks, LaTeX blocks ($$...$$), and normal text
@@ -4492,15 +4541,19 @@
4492
4541
  }
4493
4542
 
4494
4543
  // Render LaTeX math expression (inline or block)
4544
+ // Tracks pending elements to re-render once KaTeX loads
4545
+ var pendingMathElements = [];
4495
4546
  function renderMath(latex, displayMode) {
4496
- if (typeof katex === 'undefined') {
4497
- // KaTeX not loaded, show raw LaTeX
4498
- var fallback = document.createElement('code');
4499
- fallback.textContent = (displayMode ? '$$' : '$') + latex + (displayMode ? '$$' : '$');
4500
- return fallback;
4501
- }
4502
4547
  var container = document.createElement('span');
4503
4548
  container.className = displayMode ? 'katex-block' : 'katex-inline';
4549
+ if (typeof katex === 'undefined') {
4550
+ // KaTeX not loaded yet — show placeholder, queue for re-render
4551
+ container.textContent = (displayMode ? '$$' : '$') + latex + (displayMode ? '$$' : '$');
4552
+ container.dataset.latex = latex;
4553
+ container.dataset.displayMode = displayMode ? '1' : '0';
4554
+ pendingMathElements.push(container);
4555
+ return container;
4556
+ }
4504
4557
  try {
4505
4558
  katex.render(latex, container, { displayMode: displayMode, throwOnError: false });
4506
4559
  } catch (e) {
@@ -4508,6 +4561,27 @@
4508
4561
  }
4509
4562
  return container;
4510
4563
  }
4564
+ // Re-render pending math elements once KaTeX loads
4565
+ function flushPendingMath() {
4566
+ if (typeof katex === 'undefined' || pendingMathElements.length === 0) return;
4567
+ pendingMathElements.forEach(function (el) {
4568
+ if (!el.dataset.latex) return;
4569
+ try {
4570
+ var dm = el.dataset.displayMode === '1';
4571
+ katex.render(el.dataset.latex, el, { displayMode: dm, throwOnError: false });
4572
+ delete el.dataset.latex;
4573
+ delete el.dataset.displayMode;
4574
+ } catch (e) { /* keep fallback text */ }
4575
+ });
4576
+ pendingMathElements = [];
4577
+ }
4578
+ // Check periodically if KaTeX has loaded (for deferred loading)
4579
+ var katexCheckInterval = setInterval(function () {
4580
+ if (typeof katex !== 'undefined') {
4581
+ clearInterval(katexCheckInterval);
4582
+ flushPendingMath();
4583
+ }
4584
+ }, 500);
4511
4585
 
4512
4586
  function tokenizeInline(line) {
4513
4587
  var nodes = [];
@@ -5195,6 +5269,7 @@
5195
5269
 
5196
5270
  const dropdownBackdrop = document.getElementById('dropdownBackdrop');
5197
5271
  const overflowDropdown = document.getElementById('overflowDropdown');
5272
+ const fullscreenDropdown = document.getElementById('fullscreenDropdown');
5198
5273
 
5199
5274
  // Swipe-to-dismiss controller (declared before closeAllDropdowns which references it)
5200
5275
  let swipeAbortController = null;
@@ -5206,7 +5281,7 @@
5206
5281
  // Clean up swipe listeners
5207
5282
  if (swipeAbortController) { swipeAbortController.abort(); swipeAbortController = null; }
5208
5283
  // Reset inline styles and move dropdowns back to original parents
5209
- [highlightDropdown, drawDropdown, shapesDropdown, overflowDropdown].forEach(dd => {
5284
+ [highlightDropdown, drawDropdown, shapesDropdown, overflowDropdown, fullscreenDropdown].forEach(dd => {
5210
5285
  dd.style.transform = '';
5211
5286
  dd.style.transition = '';
5212
5287
  dd.style.position = '';
@@ -5223,6 +5298,7 @@
5223
5298
  drawDropdown.classList.remove('visible');
5224
5299
  shapesDropdown.classList.remove('visible');
5225
5300
  overflowDropdown.classList.remove('visible');
5301
+ fullscreenDropdown.classList.remove('visible');
5226
5302
  dropdownBackdrop.classList.remove('visible');
5227
5303
  }
5228
5304
 
@@ -5313,6 +5389,11 @@
5313
5389
  document.getElementById('drawArrow').onclick = (e) => toggleDropdown(drawDropdown, e);
5314
5390
  document.getElementById('shapesArrow').onclick = (e) => toggleDropdown(shapesDropdown, e);
5315
5391
 
5392
+ // Fullscreen dropdown toggle
5393
+ document.getElementById('fullscreenArrow').onclick = (e) => toggleDropdown(fullscreenDropdown, e);
5394
+ fullscreenDropdown.addEventListener('click', (e) => e.stopPropagation());
5395
+ fullscreenDropdown.addEventListener('pointerup', (e) => e.stopPropagation());
5396
+
5316
5397
  // Overflow menu toggle
5317
5398
  document.getElementById('overflowBtn').onclick = (e) => toggleDropdown(overflowDropdown, e);
5318
5399
  overflowDropdown.addEventListener('click', (e) => e.stopPropagation());
@@ -5351,9 +5432,27 @@
5351
5432
  closeAllDropdowns();
5352
5433
  });
5353
5434
  attachOverflowAction('overflowFullscreen', () => {
5435
+ if (isWindowed) toggleWindowed();
5354
5436
  toggleFullscreen();
5355
5437
  closeAllDropdowns();
5356
5438
  });
5439
+ attachOverflowAction('overflowWindowed', () => {
5440
+ if (isFullscreen) toggleFullscreen();
5441
+ toggleWindowed();
5442
+ closeAllDropdowns();
5443
+ });
5444
+
5445
+ // Fullscreen dropdown options
5446
+ attachOverflowAction('fullscreenOptionFull', () => {
5447
+ if (isWindowed) toggleWindowed(); // exit windowed first
5448
+ toggleFullscreen();
5449
+ closeAllDropdowns();
5450
+ });
5451
+ attachOverflowAction('fullscreenOptionWindowed', () => {
5452
+ if (isFullscreen) toggleFullscreen(); // exit fullscreen first
5453
+ toggleWindowed();
5454
+ closeAllDropdowns();
5455
+ });
5357
5456
 
5358
5457
  // Close dropdowns when clicking outside
5359
5458
  document.addEventListener('click', (e) => {
@@ -7180,7 +7279,7 @@
7180
7279
  if (key === 't') { setTool('text'); e.preventDefault(); }
7181
7280
  if (key === 'r') { setTool('shape'); e.preventDefault(); }
7182
7281
  if (key === 'v') { setTool('select'); e.preventDefault(); }
7183
- if (key === 'f') { toggleFullscreen(); e.preventDefault(); }
7282
+ if (key === 'f') { if (isWindowed) toggleWindowed(); else toggleFullscreen(); e.preventDefault(); }
7184
7283
 
7185
7284
  // Delete selected annotation(s)
7186
7285
  if ((key === 'delete' || key === 'backspace') && (selectedAnnotation || multiSelectedAnnotations.length > 0)) {
@@ -7271,9 +7370,11 @@
7271
7370
  return;
7272
7371
  }
7273
7372
 
7274
- // Escape to deselect tool
7373
+ // Escape to deselect tool / exit windowed mode
7275
7374
  if (key === 'escape') {
7276
- if (currentTool) {
7375
+ if (isWindowed) {
7376
+ toggleWindowed();
7377
+ } else if (currentTool) {
7277
7378
  setTool(currentTool); // Toggle off
7278
7379
  }
7279
7380
  closeAllDropdowns();
@@ -7362,8 +7463,9 @@
7362
7463
  // ERGONOMIC FEATURES
7363
7464
  // ==========================================
7364
7465
 
7365
- // Fullscreen: track state (parent-managed when in iframe)
7466
+ // Fullscreen & Windowed: track state (parent-managed when in iframe)
7366
7467
  let isFullscreen = false;
7468
+ let isWindowed = false;
7367
7469
  const inIframe = window.self !== window.top;
7368
7470
 
7369
7471
  function toggleFullscreen() {
@@ -7385,6 +7487,12 @@
7385
7487
  }
7386
7488
  }
7387
7489
 
7490
+ function toggleWindowed() {
7491
+ if (inIframe) {
7492
+ window.parent.postMessage({ type: 'pdf-secure-windowed-toggle' }, window.location.origin);
7493
+ }
7494
+ }
7495
+
7388
7496
  // Update fullscreen button icon based on state
7389
7497
  function updateFullscreenIcon() {
7390
7498
  const icon = document.getElementById('fullscreenIcon');
@@ -7392,6 +7500,7 @@
7392
7500
  const bottomBtn = document.getElementById('bottomFullscreenBtn');
7393
7501
  const exitPath = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
7394
7502
  const enterPath = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
7503
+ const windowedPath = '<path d="M19 4H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H5V8h14v12z"/>';
7395
7504
  if (isFullscreen) {
7396
7505
  icon.innerHTML = exitPath;
7397
7506
  btn.classList.add('active');
@@ -7399,6 +7508,13 @@
7399
7508
  bottomBtn.querySelector('svg').innerHTML = exitPath;
7400
7509
  bottomBtn.classList.add('active');
7401
7510
  }
7511
+ } else if (isWindowed) {
7512
+ icon.innerHTML = windowedPath;
7513
+ btn.classList.add('active');
7514
+ if (bottomBtn) {
7515
+ bottomBtn.querySelector('svg').innerHTML = windowedPath;
7516
+ bottomBtn.classList.add('active');
7517
+ }
7402
7518
  } else {
7403
7519
  icon.innerHTML = enterPath;
7404
7520
  btn.classList.remove('active');
@@ -7407,12 +7523,17 @@
7407
7523
  bottomBtn.classList.remove('active');
7408
7524
  }
7409
7525
  }
7526
+ // Highlight active option in dropdown
7527
+ const optFull = document.getElementById('fullscreenOptionFull');
7528
+ const optWindowed = document.getElementById('fullscreenOptionWindowed');
7529
+ if (optFull) optFull.classList.toggle('active', isFullscreen);
7530
+ if (optWindowed) optWindowed.classList.toggle('active', isWindowed);
7410
7531
  }
7411
7532
 
7412
7533
  // Apply touch restrictions when entering/exiting fullscreen
7413
7534
  function applyFullscreenTouchRestrictions() {
7414
- document.body.classList.toggle('viewer-fullscreen', isFullscreen);
7415
- if (isFullscreen) {
7535
+ document.body.classList.toggle('viewer-fullscreen', isFullscreen || isWindowed);
7536
+ if (isFullscreen || isWindowed) {
7416
7537
  document.documentElement.style.touchAction = 'pan-y pinch-zoom';
7417
7538
  document.documentElement.style.overscrollBehavior = 'none';
7418
7539
  } else {
@@ -7421,13 +7542,18 @@
7421
7542
  }
7422
7543
  }
7423
7544
 
7424
- // Listen for fullscreen state from parent (iframe mode)
7545
+ // Listen for fullscreen/windowed state from parent (iframe mode)
7425
7546
  window.addEventListener('message', (event) => {
7426
7547
  if (event.data && event.data.type === 'pdf-secure-fullscreen-state') {
7427
7548
  isFullscreen = event.data.isFullscreen;
7428
7549
  updateFullscreenIcon();
7429
7550
  applyFullscreenTouchRestrictions();
7430
7551
  }
7552
+ if (event.data && event.data.type === 'pdf-secure-windowed-state') {
7553
+ isWindowed = event.data.isWindowed;
7554
+ updateFullscreenIcon();
7555
+ applyFullscreenTouchRestrictions();
7556
+ }
7431
7557
  });
7432
7558
 
7433
7559
  // Local fullscreen events (standalone mode)
@@ -7442,9 +7568,15 @@
7442
7568
  applyFullscreenTouchRestrictions();
7443
7569
  });
7444
7570
 
7445
- // Fullscreen button click (top toolbar + bottom toolbar)
7446
- document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
7447
- document.getElementById('bottomFullscreenBtn').onclick = () => toggleFullscreen();
7571
+ // Fullscreen button click: toggle current active mode, or fullscreen by default
7572
+ document.getElementById('fullscreenBtn').onclick = () => {
7573
+ if (isWindowed) toggleWindowed();
7574
+ else toggleFullscreen();
7575
+ };
7576
+ document.getElementById('bottomFullscreenBtn').onclick = () => {
7577
+ if (isWindowed) toggleWindowed();
7578
+ else toggleFullscreen();
7579
+ };
7448
7580
 
7449
7581
 
7450
7582