@privacyscrubber/sdk 2.0.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.
- package/README.md +258 -0
- package/index.d.ts +217 -0
- package/index.js +581 -0
- package/package.json +51 -0
- package/polyfill.js +33 -0
- package/ps-license-manager.js +257 -0
- package/ps-pii-engine.cjs +1297 -0
- package/ps-pii-engine.js +1297 -0
- package/scrubber-core.cjs +1711 -0
- package/shared-ui.js +533 -0
- package/ui-modals.js +819 -0
package/ui-modals.js
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrivacyScrubber UI Modal Orchestrator
|
|
3
|
+
* Handles smooth animations and state management for all site-wide modals.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const activeModals = [];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hydrates a lazy-loaded modal by extracting its template content into the DOM.
|
|
10
|
+
* @param {HTMLElement|string} modal - Modal element or ID
|
|
11
|
+
* @returns {HTMLElement} The modal element
|
|
12
|
+
*/
|
|
13
|
+
export const hydrateLazyModal = (modal) => {
|
|
14
|
+
const m = typeof modal === 'string' ? document.getElementById(modal) : modal;
|
|
15
|
+
if (!m) return null;
|
|
16
|
+
|
|
17
|
+
const template = m.querySelector('template[data-lazy-modal]');
|
|
18
|
+
if (template) {
|
|
19
|
+
m.appendChild(template.content.cloneNode(true));
|
|
20
|
+
template.remove();
|
|
21
|
+
}
|
|
22
|
+
return m;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Opens a modal with scale and fade animations.
|
|
27
|
+
* @param {HTMLElement|string} modal - Modal element or ID
|
|
28
|
+
* @param {HTMLElement|string} content - Content element or ID
|
|
29
|
+
*/
|
|
30
|
+
export const openModal = (modal, content) => {
|
|
31
|
+
const m = hydrateLazyModal(modal);
|
|
32
|
+
const c = typeof content === 'string' ? document.getElementById(content) : content;
|
|
33
|
+
|
|
34
|
+
if (!m || !c) return;
|
|
35
|
+
|
|
36
|
+
const trigger = document.activeElement;
|
|
37
|
+
|
|
38
|
+
// Add to active stack
|
|
39
|
+
if (!activeModals.find(item => item.modal === m)) {
|
|
40
|
+
activeModals.push({ modal: m, content: c, trigger: trigger });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 1. Ensure the modal is in the flex layout
|
|
44
|
+
m.classList.remove('hidden');
|
|
45
|
+
m.classList.add('flex');
|
|
46
|
+
|
|
47
|
+
// Force reflow
|
|
48
|
+
void m.offsetWidth;
|
|
49
|
+
|
|
50
|
+
// 2. Trigger transitions
|
|
51
|
+
m.classList.add('opacity-100');
|
|
52
|
+
m.classList.remove('opacity-0', 'pointer-events-none');
|
|
53
|
+
|
|
54
|
+
c.classList.remove('scale-95', 'opacity-0', 'translate-y-4');
|
|
55
|
+
c.classList.add('scale-100', 'opacity-100', 'translate-y-0');
|
|
56
|
+
|
|
57
|
+
document.body.style.overflow = 'hidden';
|
|
58
|
+
|
|
59
|
+
// 3. Auto-focus logic for inputs (great for desktop usability)
|
|
60
|
+
const firstInput = c.querySelector('input[type="text"], input[type="password"], input[type="email"], textarea');
|
|
61
|
+
if (firstInput) {
|
|
62
|
+
setTimeout(() => firstInput.focus(), 150); // delay ensures it's visible before focus
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Closes a modal with scale and fade animations.
|
|
68
|
+
* @param {HTMLElement|string} modal - Modal element or ID
|
|
69
|
+
* @param {HTMLElement|string} content - Content element or ID
|
|
70
|
+
*/
|
|
71
|
+
export const closeModal = (modal, content) => {
|
|
72
|
+
const m = typeof modal === 'string' ? document.getElementById(modal) : modal;
|
|
73
|
+
const c = typeof content === 'string' ? document.getElementById(content) : content;
|
|
74
|
+
|
|
75
|
+
if (!m || !c) return;
|
|
76
|
+
|
|
77
|
+
// Remove from active stack
|
|
78
|
+
let triggerToRestore = null;
|
|
79
|
+
const index = activeModals.findIndex(item => item.modal === m);
|
|
80
|
+
if (index !== -1) {
|
|
81
|
+
triggerToRestore = activeModals[index].trigger;
|
|
82
|
+
activeModals.splice(index, 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 1. Trigger transitions
|
|
86
|
+
c.classList.add('scale-95', 'opacity-0', 'translate-y-4');
|
|
87
|
+
c.classList.remove('scale-100', 'opacity-100', 'translate-y-0');
|
|
88
|
+
|
|
89
|
+
m.classList.remove('opacity-100');
|
|
90
|
+
m.classList.add('opacity-0', 'pointer-events-none');
|
|
91
|
+
|
|
92
|
+
// 2. Clean up after animation
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
m.classList.add('hidden');
|
|
95
|
+
m.classList.remove('flex');
|
|
96
|
+
if (activeModals.length === 0) {
|
|
97
|
+
document.body.style.overflow = '';
|
|
98
|
+
}
|
|
99
|
+
if (triggerToRestore && typeof triggerToRestore.focus === 'function') {
|
|
100
|
+
triggerToRestore.focus();
|
|
101
|
+
}
|
|
102
|
+
}, 300);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Opens the native Teams Activation/Pilot modal.
|
|
107
|
+
*/
|
|
108
|
+
export const openTeamsModal = () => {
|
|
109
|
+
const modal = document.getElementById('teamModal');
|
|
110
|
+
const content = document.getElementById('teamModalContent');
|
|
111
|
+
if (modal && content) {
|
|
112
|
+
openModal(modal, content);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Closes the native Teams Activation/Pilot modal.
|
|
118
|
+
*/
|
|
119
|
+
export const closeTeamsModal = () => {
|
|
120
|
+
const modal = document.getElementById('teamModal');
|
|
121
|
+
const content = document.getElementById('teamModalContent');
|
|
122
|
+
if (modal && content) {
|
|
123
|
+
closeModal(modal, content);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Global hooks for non-module compatibility
|
|
128
|
+
if (typeof window !== 'undefined') {
|
|
129
|
+
window.openModal = openModal;
|
|
130
|
+
window.closeModal = closeModal;
|
|
131
|
+
window.openTeamsModal = openTeamsModal;
|
|
132
|
+
window.closeTeamsModal = closeTeamsModal;
|
|
133
|
+
window.closeTeamModal = closeTeamsModal; // Alias to match older HTML usage
|
|
134
|
+
window.openTeamModal = openTeamsModal; // Alias to match older HTML usage
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Session Handoff (Team) Modals
|
|
138
|
+
*/
|
|
139
|
+
window.openSessionHandoffModal = function(e) {
|
|
140
|
+
if (e && e.preventDefault) e.preventDefault();
|
|
141
|
+
if (typeof window.openTeamModal === 'function') {
|
|
142
|
+
window.openTeamModal(true, 'teams');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof window.showProRecoveryModal === 'function') {
|
|
146
|
+
window.showProRecoveryModal('teams');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const m = document.getElementById('teamModal');
|
|
150
|
+
const c = document.getElementById('teamModalContent');
|
|
151
|
+
if (m && c) {
|
|
152
|
+
openModal(m, c);
|
|
153
|
+
if (typeof window.switchTeamTab === 'function') {
|
|
154
|
+
window.switchTeamTab('teams');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
window.closeSessionHandoffModal = function() {
|
|
160
|
+
if (typeof window.closeTeamModal === 'function') {
|
|
161
|
+
window.closeTeamModal();
|
|
162
|
+
} else {
|
|
163
|
+
const e = document.getElementById("teamModal");
|
|
164
|
+
const t = document.getElementById("teamModalContent");
|
|
165
|
+
if (e && t) {
|
|
166
|
+
closeModal(e, t);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
window.closeTeamRecoveryModal = function () {
|
|
171
|
+
const e = document.getElementById("teamRecoveryModal");
|
|
172
|
+
const t = document.getElementById("teamRecoveryModalContent") || e.children[0];
|
|
173
|
+
if (e) closeModal(e, t);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Global Escape Key Listener
|
|
177
|
+
window.addEventListener('keydown', (e) => {
|
|
178
|
+
if (e.key === 'Escape' && activeModals.length > 0) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
const lastModal = activeModals[activeModals.length - 1];
|
|
181
|
+
closeModal(lastModal.modal, lastModal.content);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Switches between Monthly and Lifetime PRO tiers in the modal.
|
|
187
|
+
*/
|
|
188
|
+
window.setProTier = (tier) => {
|
|
189
|
+
const priceDisplay = document.getElementById('pro-price-display');
|
|
190
|
+
const descDisplay = document.getElementById('pro-desc-display');
|
|
191
|
+
const monthlyBtn = document.getElementById('btn-pro-monthly');
|
|
192
|
+
const lifetimeBtn = document.getElementById('btn-pro-lifetime');
|
|
193
|
+
const subContainer = document.getElementById('paypal-pro-btn-container');
|
|
194
|
+
const captureContainer = document.getElementById('paypal-lifetime-btn-container');
|
|
195
|
+
|
|
196
|
+
if (!priceDisplay) return;
|
|
197
|
+
|
|
198
|
+
if (tier === 'lifetime') {
|
|
199
|
+
const lifetimePrice = window.PII_CONFIG?.pricing?.PRO_LIFETIME || 110;
|
|
200
|
+
priceDisplay.innerHTML = `$${lifetimePrice}<span class="text-xs font-medium text-slate-500 uppercase ml-2">Lifetime</span>`;
|
|
201
|
+
descDisplay.innerText = "One-time payment for permanent PRO access. No recurring fees, ever.";
|
|
202
|
+
|
|
203
|
+
monthlyBtn.classList.remove('bg-neon-green', 'text-dark-900');
|
|
204
|
+
monthlyBtn.classList.add('text-slate-500', 'hover:text-white');
|
|
205
|
+
|
|
206
|
+
lifetimeBtn.classList.add('bg-neon-green', 'text-dark-900');
|
|
207
|
+
lifetimeBtn.classList.remove('text-slate-500', 'hover:text-white');
|
|
208
|
+
|
|
209
|
+
subContainer.classList.add('hidden');
|
|
210
|
+
captureContainer.classList.remove('hidden');
|
|
211
|
+
|
|
212
|
+
// Trigger PayPal button initialization for lifetime if not already done
|
|
213
|
+
if (typeof window.initLifetimePayPal === 'function') {
|
|
214
|
+
window.initLifetimePayPal();
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
const monthlyPrice = window.PII_CONFIG?.pricing?.PRO_MONTHLY || 15;
|
|
218
|
+
priceDisplay.innerHTML = `$${monthlyPrice}<span class="text-lg font-medium text-slate-500">/mo</span>`;
|
|
219
|
+
descDisplay.innerText = "Professional grade PII scrubbing for high-volume AI power users.";
|
|
220
|
+
|
|
221
|
+
lifetimeBtn.classList.remove('bg-neon-green', 'text-dark-900');
|
|
222
|
+
lifetimeBtn.classList.add('text-slate-500', 'hover:text-white');
|
|
223
|
+
|
|
224
|
+
monthlyBtn.classList.add('bg-neon-green', 'text-dark-900');
|
|
225
|
+
monthlyBtn.classList.remove('text-slate-500', 'hover:text-white');
|
|
226
|
+
|
|
227
|
+
captureContainer.classList.add('hidden');
|
|
228
|
+
subContainer.classList.remove('hidden');
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
window.psSetQuickTemplate = function(el) {
|
|
235
|
+
var lbl = document.getElementById('teamNewRuleLabel');
|
|
236
|
+
var inp = document.getElementById('teamNewRuleInput');
|
|
237
|
+
if (lbl && inp) {
|
|
238
|
+
lbl.value = el.dataset.label || '';
|
|
239
|
+
inp.value = el.dataset.pattern || '';
|
|
240
|
+
inp.dispatchEvent(new Event('input'));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
window.downloadSessionFile = async function() {
|
|
245
|
+
let tokenMap = {};
|
|
246
|
+
if (window.piiWorkerManager) {
|
|
247
|
+
try {
|
|
248
|
+
const res = await window.piiWorkerManager.send("EXPORT_SESSION");
|
|
249
|
+
tokenMap = res.plaintextMap || res.tokenMap || {};
|
|
250
|
+
} catch(e) {}
|
|
251
|
+
}
|
|
252
|
+
if (!tokenMap || Object.keys(tokenMap).length === 0) {
|
|
253
|
+
const raw = sessionStorage.getItem(`sessionTokenMap_${window.tabId || 1}`);
|
|
254
|
+
if (raw) tokenMap = JSON.parse(raw);
|
|
255
|
+
}
|
|
256
|
+
const count = Object.keys(tokenMap).length;
|
|
257
|
+
if (count === 0) {
|
|
258
|
+
if (typeof window.showStatus === 'function') window.showStatus("No active session tokens in RAM to save.", "info", 3000);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const payload = {
|
|
262
|
+
pssession: true,
|
|
263
|
+
version: "1.7.0",
|
|
264
|
+
timestamp: new Date().toISOString(),
|
|
265
|
+
tokenMap: tokenMap,
|
|
266
|
+
count: count
|
|
267
|
+
};
|
|
268
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
|
269
|
+
const url = URL.createObjectURL(blob);
|
|
270
|
+
const a = document.createElement("a");
|
|
271
|
+
a.href = url;
|
|
272
|
+
a.download = `privacyscrubber-session-${new Date().toISOString().slice(0,10)}.pssession`;
|
|
273
|
+
document.body.appendChild(a);
|
|
274
|
+
a.click();
|
|
275
|
+
document.body.removeChild(a);
|
|
276
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
277
|
+
if (typeof window.showStatus === "function") {
|
|
278
|
+
window.showStatus(`Session saved! (${count} tokens stored in .pssession file)`, "success", 4000);
|
|
279
|
+
}
|
|
280
|
+
window.closeSessionSaveModal();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
window.importSessionFileFromInput = function(input) {
|
|
284
|
+
if (!input || !input.files || !input.files[0]) return;
|
|
285
|
+
const file = input.files[0];
|
|
286
|
+
const reader = new FileReader();
|
|
287
|
+
reader.onload = async function(e) {
|
|
288
|
+
try {
|
|
289
|
+
const data = JSON.parse(e.target.result);
|
|
290
|
+
const map = data.tokenMap || data.plaintextMap || data.tm;
|
|
291
|
+
if (map && typeof map === 'object') {
|
|
292
|
+
if (window.piiWorkerManager) {
|
|
293
|
+
await window.piiWorkerManager.send("IMPORT_SESSION", { tokenMap: map });
|
|
294
|
+
}
|
|
295
|
+
const count = Object.keys(map).length;
|
|
296
|
+
const entCount = document.getElementById("entityCount");
|
|
297
|
+
if (entCount) entCount.textContent = `${count} items protected`;
|
|
298
|
+
if (typeof window.showStatus === "function") {
|
|
299
|
+
window.showStatus(`Session restored! (${count} tokens loaded from file)`, "success", 4000);
|
|
300
|
+
}
|
|
301
|
+
window.closeSessionSaveModal();
|
|
302
|
+
} else {
|
|
303
|
+
throw new Error("Invalid format");
|
|
304
|
+
}
|
|
305
|
+
} catch(err) {
|
|
306
|
+
if (typeof window.showStatus === "function") {
|
|
307
|
+
window.showStatus("Invalid session file format. Upload a valid .pssession file.", "warning", 4000);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
input.value = '';
|
|
311
|
+
};
|
|
312
|
+
reader.readAsText(file);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
window.openSessionSaveModal = function() {
|
|
316
|
+
const modal = document.getElementById('sessionSaveModal');
|
|
317
|
+
if (!modal) return;
|
|
318
|
+
modal.classList.remove('hidden');
|
|
319
|
+
modal.classList.add('flex');
|
|
320
|
+
setTimeout(() => {
|
|
321
|
+
modal.classList.remove('opacity-0');
|
|
322
|
+
if (modal.children[0]) {
|
|
323
|
+
modal.children[0].classList.remove('scale-95');
|
|
324
|
+
modal.children[0].classList.add('scale-100');
|
|
325
|
+
}
|
|
326
|
+
}, 10);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
window.closeSessionSaveModal = function() {
|
|
330
|
+
const modal = document.getElementById('sessionSaveModal');
|
|
331
|
+
if (!modal) return;
|
|
332
|
+
modal.classList.add('opacity-0');
|
|
333
|
+
if (modal.children[0]) {
|
|
334
|
+
modal.children[0].classList.remove('scale-100');
|
|
335
|
+
modal.children[0].classList.add('scale-95');
|
|
336
|
+
}
|
|
337
|
+
setTimeout(() => {
|
|
338
|
+
modal.classList.add('hidden');
|
|
339
|
+
modal.classList.remove('flex');
|
|
340
|
+
}, 200);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
window.closeAuditModal = function() {
|
|
344
|
+
const modal = document.getElementById('devSecOpsAuditModal');
|
|
345
|
+
if (!modal) return;
|
|
346
|
+
modal.classList.add('opacity-0');
|
|
347
|
+
if (modal.children[0]) {
|
|
348
|
+
modal.children[0].classList.remove('scale-100');
|
|
349
|
+
modal.children[0].classList.add('scale-95');
|
|
350
|
+
}
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
modal.classList.add('hidden');
|
|
353
|
+
modal.classList.remove('flex');
|
|
354
|
+
}, 200);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
let currentDocZoom = 1;
|
|
358
|
+
let isDocPanning = false;
|
|
359
|
+
let startPanX = 0, startPanY = 0;
|
|
360
|
+
let scrollPanLeft = 0, scrollPanTop = 0;
|
|
361
|
+
|
|
362
|
+
window.setPdfRedactionStyle = function(style) {
|
|
363
|
+
window.pdfRedactionStyle = style;
|
|
364
|
+
const isTok = style === 'tokenized';
|
|
365
|
+
|
|
366
|
+
// Main UI toggle
|
|
367
|
+
const btnTok = document.getElementById('btnStyleTokenized');
|
|
368
|
+
const btnBlk = document.getElementById('btnStyleBlackout');
|
|
369
|
+
if (btnTok && btnBlk) {
|
|
370
|
+
if (isTok) {
|
|
371
|
+
btnTok.className = 'px-2 py-0.5 rounded-md font-bold transition-all bg-neon-blue/20 text-neon-blue border border-neon-blue/30 cursor-pointer shadow-sm';
|
|
372
|
+
btnBlk.className = 'px-2 py-0.5 rounded-md font-bold transition-all text-slate-400 hover:text-white border border-transparent cursor-pointer';
|
|
373
|
+
} else {
|
|
374
|
+
btnBlk.className = 'px-2 py-0.5 rounded-md font-bold transition-all bg-neon-blue/20 text-neon-blue border border-neon-blue/30 cursor-pointer shadow-sm';
|
|
375
|
+
btnTok.className = 'px-2 py-0.5 rounded-md font-bold transition-all text-slate-400 hover:text-white border border-transparent cursor-pointer';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Modal UI toggle
|
|
380
|
+
const mBtnTok = document.getElementById('modalBtnStyleTokenized');
|
|
381
|
+
const mBtnBlk = document.getElementById('modalBtnStyleBlackout');
|
|
382
|
+
if (mBtnTok && mBtnBlk) {
|
|
383
|
+
if (isTok) {
|
|
384
|
+
mBtnTok.className = 'px-2 py-0.5 rounded-md font-bold transition-all bg-neon-blue/20 text-neon-blue border border-neon-blue/30 cursor-pointer shadow-sm';
|
|
385
|
+
mBtnBlk.className = 'px-2 py-0.5 rounded-md font-bold transition-all text-slate-400 hover:text-white border border-transparent cursor-pointer';
|
|
386
|
+
} else {
|
|
387
|
+
mBtnBlk.className = 'px-2 py-0.5 rounded-md font-bold transition-all bg-neon-blue/20 text-neon-blue border border-neon-blue/30 cursor-pointer shadow-sm';
|
|
388
|
+
mBtnTok.className = 'px-2 py-0.5 rounded-md font-bold transition-all text-slate-400 hover:text-white border border-transparent cursor-pointer';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Re-render previews
|
|
393
|
+
const docSnippet = document.getElementById('docModeLiveSnippet');
|
|
394
|
+
if (docSnippet && window._lastPDFBuffer && typeof window.renderPdfLivePreview === 'function') {
|
|
395
|
+
window.renderPdfLivePreview(window._lastPDFBuffer, docSnippet, style);
|
|
396
|
+
}
|
|
397
|
+
if (document.getElementById('imageZoomModal')?.classList.contains('flex')) {
|
|
398
|
+
window.renderPdfZoomInspection?.();
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
window.renderPdfZoomInspection = async function() {
|
|
403
|
+
if (!window._lastPDFBuffer) return false;
|
|
404
|
+
const modal = document.getElementById('imageZoomModal');
|
|
405
|
+
const img = document.getElementById('imageZoomImg');
|
|
406
|
+
const overlay = document.getElementById('imageZoomBadgeOverlay');
|
|
407
|
+
const styleGroup = document.getElementById('modalPdfStyleToggleGroup');
|
|
408
|
+
if (!modal || !img) return false;
|
|
409
|
+
|
|
410
|
+
if (styleGroup) {
|
|
411
|
+
styleGroup.classList.remove('hidden');
|
|
412
|
+
styleGroup.classList.add('flex');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
if (typeof loadPdfJs === 'function') await loadPdfJs();
|
|
417
|
+
const pdfjsDoc = await pdfjsLib.getDocument({ data: window._lastPDFBuffer.slice(0) }).promise;
|
|
418
|
+
const page = await pdfjsDoc.getPage(1);
|
|
419
|
+
|
|
420
|
+
const viewport = page.getViewport({ scale: 2.0 });
|
|
421
|
+
const canvas = document.createElement('canvas');
|
|
422
|
+
canvas.width = viewport.width;
|
|
423
|
+
canvas.height = viewport.height;
|
|
424
|
+
const ctx = canvas.getContext('2d');
|
|
425
|
+
await page.render({ canvasContext: ctx, viewport }).promise;
|
|
426
|
+
|
|
427
|
+
// Fetch active token map
|
|
428
|
+
let tokenMap = (window.AppState && window.AppState.sessionMap && Object.keys(window.AppState.sessionMap).length > 0)
|
|
429
|
+
? window.AppState.sessionMap
|
|
430
|
+
: {};
|
|
431
|
+
|
|
432
|
+
if (!tokenMap || Object.keys(tokenMap).length === 0) {
|
|
433
|
+
if (window.piiWorkerManager) {
|
|
434
|
+
try {
|
|
435
|
+
const sessionData = await window.piiWorkerManager.send('EXPORT_SESSION');
|
|
436
|
+
tokenMap = sessionData?.plaintextMap || sessionData || {};
|
|
437
|
+
} catch (_) {}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!tokenMap || Object.keys(tokenMap).length === 0) {
|
|
441
|
+
const tabId = window._tabId || 'default';
|
|
442
|
+
try {
|
|
443
|
+
const raw = sessionStorage.getItem(`sessionTokenMap_${tabId}`);
|
|
444
|
+
if (raw) tokenMap = JSON.parse(raw);
|
|
445
|
+
} catch (_) {}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const piiEntries = Object.entries(tokenMap)
|
|
449
|
+
.filter(([k, v]) => typeof v === 'string' && v.trim().length >= 2)
|
|
450
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
451
|
+
|
|
452
|
+
const textContent = await page.getTextContent();
|
|
453
|
+
const lines = {};
|
|
454
|
+
const tolerance = 4;
|
|
455
|
+
|
|
456
|
+
textContent.items.forEach((item) => {
|
|
457
|
+
const [a, , , d, tx, ty] = item.transform;
|
|
458
|
+
const fontSz = Math.abs(d) || Math.abs(a) || 10;
|
|
459
|
+
const y = Math.round(ty);
|
|
460
|
+
let foundY = null;
|
|
461
|
+
for (let existingY in lines) {
|
|
462
|
+
if (Math.abs(y - existingY) <= tolerance) {
|
|
463
|
+
foundY = existingY;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!foundY) {
|
|
468
|
+
lines[y] = [];
|
|
469
|
+
foundY = y;
|
|
470
|
+
}
|
|
471
|
+
lines[foundY].push({
|
|
472
|
+
str: item.str,
|
|
473
|
+
x: tx,
|
|
474
|
+
y: ty,
|
|
475
|
+
w: item.width || fontSz * item.str.length * 0.55,
|
|
476
|
+
h: fontSz * 1.15,
|
|
477
|
+
fontSz
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const sortedY = Object.keys(lines).map(Number).sort((a, b) => b - a);
|
|
482
|
+
|
|
483
|
+
const badges = [];
|
|
484
|
+
const addBadge = (x, y, w, h, tokenKey, piiVal) => {
|
|
485
|
+
const pt1 = viewport.convertToViewportPoint(x, y + h * 0.85);
|
|
486
|
+
const pt2 = viewport.convertToViewportPoint(x + Math.max(w, 4), y - h * 0.15);
|
|
487
|
+
const cX = Math.min(pt1[0], pt2[0]);
|
|
488
|
+
const cY = Math.min(pt1[1], pt2[1]);
|
|
489
|
+
const cW = Math.max(Math.abs(pt1[0] - pt2[0]), 42);
|
|
490
|
+
const cH = Math.abs(pt1[1] - pt2[1]);
|
|
491
|
+
badges.push({ x: cX, y: cY, w: cW, h: cH, tokenKey, piiVal });
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
for (const [tokenKey, piiVal] of piiEntries) {
|
|
495
|
+
for (const y of sortedY) {
|
|
496
|
+
const lineItems = lines[y].sort((a, b) => a.x - b.x);
|
|
497
|
+
|
|
498
|
+
// Strategy A: full PII within single text item
|
|
499
|
+
let matchedInLine = false;
|
|
500
|
+
for (const item of lineItems) {
|
|
501
|
+
if (!item.str) continue;
|
|
502
|
+
const hits = window.findPIIWordBoundary ? window.findPIIWordBoundary(item.str, piiVal) : [];
|
|
503
|
+
for (const idx of hits) {
|
|
504
|
+
const geom = window.calcSubStringGeometry ? window.calcSubStringGeometry(item.str, idx, piiVal.length, item.w) : { prefixX: 0, targetW: item.w };
|
|
505
|
+
addBadge(item.x + geom.prefixX, item.y, geom.targetW, item.h, tokenKey, piiVal);
|
|
506
|
+
matchedInLine = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Strategy B: PII spans 2-20 consecutive items on the SAME line
|
|
511
|
+
if (!matchedInLine && lineItems.length > 1) {
|
|
512
|
+
for (let i = 0; i < lineItems.length; i++) {
|
|
513
|
+
let concat = '';
|
|
514
|
+
let span = [];
|
|
515
|
+
for (let j = i; j < lineItems.length; j++) {
|
|
516
|
+
if (span.length > 0) {
|
|
517
|
+
const prev = span[span.length - 1];
|
|
518
|
+
const gap = lineItems[j].x - (prev.x + prev.w);
|
|
519
|
+
const spaceThreshold = Math.max(3.5, prev.fontSz * 0.22);
|
|
520
|
+
if (gap >= spaceThreshold) concat += ' ';
|
|
521
|
+
}
|
|
522
|
+
concat += lineItems[j].str;
|
|
523
|
+
span.push(lineItems[j]);
|
|
524
|
+
if (span.length < 2) continue;
|
|
525
|
+
|
|
526
|
+
const wbHits = window.findPIIWordBoundary ? window.findPIIWordBoundary(concat, piiVal) : [];
|
|
527
|
+
if (wbHits.length === 0) continue;
|
|
528
|
+
const matchIdx = wbHits[0];
|
|
529
|
+
const matchEnd = matchIdx + piiVal.length;
|
|
530
|
+
let charCursor = 0;
|
|
531
|
+
let barX = null, barEndX = null;
|
|
532
|
+
|
|
533
|
+
for (let si_idx = 0; si_idx < span.length; si_idx++) {
|
|
534
|
+
const si = span[si_idx];
|
|
535
|
+
if (si_idx > 0) {
|
|
536
|
+
const prevSi = span[si_idx - 1];
|
|
537
|
+
const gapPx = si.x - (prevSi.x + prevSi.w);
|
|
538
|
+
const spaceThresh = Math.max(3.5, prevSi.fontSz * 0.22);
|
|
539
|
+
if (gapPx >= spaceThresh) charCursor += 1;
|
|
540
|
+
}
|
|
541
|
+
const siLen = si.str.length;
|
|
542
|
+
const siStart = charCursor;
|
|
543
|
+
const siEnd = charCursor + siLen;
|
|
544
|
+
|
|
545
|
+
const overlapStart = Math.max(siStart, matchIdx);
|
|
546
|
+
const overlapEnd = Math.min(siEnd, matchEnd);
|
|
547
|
+
if (overlapStart < overlapEnd) {
|
|
548
|
+
const localStart = overlapStart - siStart;
|
|
549
|
+
const localLen = overlapEnd - overlapStart;
|
|
550
|
+
const geom = window.calcSubStringGeometry ? window.calcSubStringGeometry(si.str, localStart, localLen, si.w || 0) : { prefixX: 0, targetW: si.w };
|
|
551
|
+
const xStart = si.x + geom.prefixX;
|
|
552
|
+
const xEnd = xStart + geom.targetW;
|
|
553
|
+
if (barX === null || xStart < barX) barX = xStart;
|
|
554
|
+
if (barEndX === null || xEnd > barEndX) barEndX = xEnd;
|
|
555
|
+
}
|
|
556
|
+
charCursor = siEnd;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (barX !== null && barEndX !== null && barEndX > barX) {
|
|
560
|
+
addBadge(barX, span[0].y, barEndX - barX, span[0].h, tokenKey, piiVal);
|
|
561
|
+
i = j;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const style = window.pdfRedactionStyle || 'tokenized';
|
|
571
|
+
|
|
572
|
+
// Draw base image onto canvas
|
|
573
|
+
for (const b of badges) {
|
|
574
|
+
if (style === 'tokenized') {
|
|
575
|
+
const entityType = window.getEntityTypeFromToken ? window.getEntityTypeFromToken(b.tokenKey) : 'DEFAULT';
|
|
576
|
+
const colorScheme = (window.ENTITY_PDF_COLORS && window.ENTITY_PDF_COLORS[entityType])
|
|
577
|
+
? window.ENTITY_PDF_COLORS[entityType]
|
|
578
|
+
: { hexBg: '#facc15', hexBorder: '#ca8a04', hexText: '#000000' };
|
|
579
|
+
|
|
580
|
+
ctx.fillStyle = colorScheme.hexBg;
|
|
581
|
+
ctx.strokeStyle = colorScheme.hexBorder;
|
|
582
|
+
ctx.lineWidth = 1.5;
|
|
583
|
+
if (typeof ctx.roundRect === 'function') {
|
|
584
|
+
ctx.beginPath();
|
|
585
|
+
ctx.roundRect(b.x - 1, b.y - 1, b.w + 2, b.h + 2, 3);
|
|
586
|
+
ctx.fill();
|
|
587
|
+
ctx.stroke();
|
|
588
|
+
} else {
|
|
589
|
+
ctx.fillRect(b.x - 1, b.y - 1, b.w + 2, b.h + 2);
|
|
590
|
+
ctx.strokeRect(b.x - 1, b.y - 1, b.w + 2, b.h + 2);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
ctx.fillStyle = colorScheme.hexText;
|
|
594
|
+
const fontSz = Math.min(Math.max(b.h * 0.65, 10), 16);
|
|
595
|
+
ctx.font = `bold ${Math.round(fontSz)}px monospace`;
|
|
596
|
+
ctx.textAlign = 'center';
|
|
597
|
+
ctx.textBaseline = 'middle';
|
|
598
|
+
ctx.fillText(b.tokenKey, b.x + b.w / 2, b.y + b.h / 2);
|
|
599
|
+
} else {
|
|
600
|
+
ctx.fillStyle = '#000000';
|
|
601
|
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
606
|
+
img.src = dataUrl;
|
|
607
|
+
|
|
608
|
+
// Build interactive overlay for clicking false positives
|
|
609
|
+
if (overlay) {
|
|
610
|
+
overlay.innerHTML = '';
|
|
611
|
+
const cW = canvas.width;
|
|
612
|
+
const cH = canvas.height;
|
|
613
|
+
const escapeHTML = window.escapeHTML || (str => (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'));
|
|
614
|
+
|
|
615
|
+
badges.forEach((b) => {
|
|
616
|
+
const entityType = window.getEntityTypeFromToken ? window.getEntityTypeFromToken(b.tokenKey) : 'DEFAULT';
|
|
617
|
+
const colorScheme = (window.ENTITY_PDF_COLORS && window.ENTITY_PDF_COLORS[entityType])
|
|
618
|
+
? window.ENTITY_PDF_COLORS[entityType]
|
|
619
|
+
: { hexBg: '#facc15', hexBorder: '#ca8a04', hexText: '#000000' };
|
|
620
|
+
|
|
621
|
+
const leftPct = (b.x / cW) * 100;
|
|
622
|
+
const topPct = (b.y / cH) * 100;
|
|
623
|
+
const widthPct = (b.w / cW) * 100;
|
|
624
|
+
const heightPct = (b.h / cH) * 100;
|
|
625
|
+
|
|
626
|
+
const badgeEl = document.createElement('div');
|
|
627
|
+
badgeEl.className = 'absolute cursor-pointer flex items-center justify-center font-mono font-bold select-none transition-all duration-150 hover:scale-110 hover:ring-2 hover:ring-white z-20 group/badge shadow-md active:scale-95';
|
|
628
|
+
badgeEl.style.left = `${leftPct}%`;
|
|
629
|
+
badgeEl.style.top = `${topPct}%`;
|
|
630
|
+
badgeEl.style.width = `${widthPct}%`;
|
|
631
|
+
badgeEl.style.height = `${heightPct}%`;
|
|
632
|
+
badgeEl.style.backgroundColor = style === 'tokenized' ? colorScheme.hexBg : 'rgba(0,0,0,0.95)';
|
|
633
|
+
badgeEl.style.color = style === 'tokenized' ? colorScheme.hexText : '#ffffff';
|
|
634
|
+
badgeEl.style.border = `1.5px solid ${style === 'tokenized' ? colorScheme.hexBorder : '#ffffff'}`;
|
|
635
|
+
badgeEl.style.borderRadius = '3px';
|
|
636
|
+
badgeEl.dataset.token = b.tokenKey;
|
|
637
|
+
badgeEl.title = `Click to remove false positive: ${b.piiVal}`;
|
|
638
|
+
|
|
639
|
+
badgeEl.innerHTML = `
|
|
640
|
+
<span class="truncate px-0.5 text-[10px] sm:text-xs pointer-events-none">${style === 'tokenized' ? b.tokenKey : '⬛'}</span>
|
|
641
|
+
<span class="hidden group-hover/badge:flex absolute -top-8 left-1/2 -translate-x-1/2 bg-dark-950 border border-white/20 text-white text-[10px] px-2 py-0.5 rounded shadow-xl whitespace-nowrap pointer-events-none z-30 font-sans">
|
|
642
|
+
Unmask "${escapeHTML(b.piiVal.slice(0, 24))}"
|
|
643
|
+
</span>
|
|
644
|
+
`;
|
|
645
|
+
|
|
646
|
+
badgeEl.onclick = async (e) => {
|
|
647
|
+
e.stopPropagation();
|
|
648
|
+
// Immediate cleanup in AppState and session
|
|
649
|
+
if (window.AppState && window.AppState.sessionMap) {
|
|
650
|
+
delete window.AppState.sessionMap[b.tokenKey];
|
|
651
|
+
delete window.AppState.sessionMap[`[${b.tokenKey}]`];
|
|
652
|
+
}
|
|
653
|
+
if (window.ignoreToken) {
|
|
654
|
+
await window.ignoreToken(b.tokenKey);
|
|
655
|
+
}
|
|
656
|
+
if (window.showStatus) {
|
|
657
|
+
window.showStatus(`Revealed false positive: ${b.piiVal}`, 'success', 3500);
|
|
658
|
+
}
|
|
659
|
+
// Re-render both modal and live snippet
|
|
660
|
+
await window.renderPdfZoomInspection();
|
|
661
|
+
const docSnippet = document.getElementById('docModeLiveSnippet');
|
|
662
|
+
if (docSnippet && window._lastPDFBuffer && typeof window.renderPdfLivePreview === 'function') {
|
|
663
|
+
window.renderPdfLivePreview(window._lastPDFBuffer, docSnippet, window.pdfRedactionStyle || 'tokenized');
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
overlay.appendChild(badgeEl);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.warn('[renderPdfZoomInspection] error:', err);
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
window.openPdfInspectionModal = async function() {
|
|
678
|
+
const modal = document.getElementById('imageZoomModal');
|
|
679
|
+
if (!modal) return;
|
|
680
|
+
const ok = await window.renderPdfZoomInspection();
|
|
681
|
+
if (ok) {
|
|
682
|
+
window.openImageZoomModal(document.getElementById('imageZoomImg')?.src || '');
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
window.openImageZoomModal = function(imageUrl) {
|
|
687
|
+
const modal = document.getElementById('imageZoomModal');
|
|
688
|
+
const img = document.getElementById('imageZoomImg');
|
|
689
|
+
const wrapper = document.getElementById('imageZoomWrapper');
|
|
690
|
+
const content = document.getElementById('imageZoomContent');
|
|
691
|
+
const viewport = document.getElementById('imageZoomViewport');
|
|
692
|
+
const zoomText = document.getElementById('zoomLevelText');
|
|
693
|
+
const downloadBtn = document.getElementById('modalDownloadImageBtn');
|
|
694
|
+
if (!modal || !img) return;
|
|
695
|
+
|
|
696
|
+
if (imageUrl && !window._lastPDFBuffer) {
|
|
697
|
+
img.src = imageUrl;
|
|
698
|
+
const overlay = document.getElementById('imageZoomBadgeOverlay');
|
|
699
|
+
if (overlay) overlay.innerHTML = '';
|
|
700
|
+
const styleGroup = document.getElementById('modalPdfStyleToggleGroup');
|
|
701
|
+
if (styleGroup) { styleGroup.classList.add('hidden'); styleGroup.classList.remove('flex'); }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
currentDocZoom = 1;
|
|
705
|
+
if (wrapper) wrapper.style.transform = `scale(${currentDocZoom})`;
|
|
706
|
+
else img.style.transform = `scale(${currentDocZoom})`;
|
|
707
|
+
if (zoomText) zoomText.textContent = '100%';
|
|
708
|
+
|
|
709
|
+
if (downloadBtn) {
|
|
710
|
+
downloadBtn.onclick = () => {
|
|
711
|
+
const isProUser = window.isPro || (window.AppState && window.AppState.isPro) || localStorage.getItem('isPro') === 'true' || !!localStorage.getItem('ps_pro_sub') || !!localStorage.getItem('ps_pro_key') || !!localStorage.getItem('ps_team_key');
|
|
712
|
+
if (isProUser) {
|
|
713
|
+
const link = document.createElement("a");
|
|
714
|
+
link.download = `redacted_document_${Date.now()}.png`;
|
|
715
|
+
link.href = img.src || imageUrl;
|
|
716
|
+
link.click();
|
|
717
|
+
} else if (typeof window.openProModalAction === "function") {
|
|
718
|
+
window.openProModalAction();
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
modal.classList.remove('hidden');
|
|
724
|
+
modal.classList.add('flex');
|
|
725
|
+
setTimeout(() => {
|
|
726
|
+
modal.classList.remove('opacity-0', 'pointer-events-none');
|
|
727
|
+
if (content) {
|
|
728
|
+
content.classList.remove('scale-95');
|
|
729
|
+
content.classList.add('scale-100');
|
|
730
|
+
}
|
|
731
|
+
}, 10);
|
|
732
|
+
document.body.style.overflow = 'hidden';
|
|
733
|
+
|
|
734
|
+
// Set up interactive zoom & pan once
|
|
735
|
+
if (!modal._hasZoomListeners) {
|
|
736
|
+
modal._hasZoomListeners = true;
|
|
737
|
+
|
|
738
|
+
const updateZoom = (newZoom) => {
|
|
739
|
+
currentDocZoom = Math.min(Math.max(newZoom, 0.5), 3.5);
|
|
740
|
+
const targetEl = document.getElementById('imageZoomWrapper') || img;
|
|
741
|
+
targetEl.style.transform = `scale(${currentDocZoom})`;
|
|
742
|
+
if (zoomText) zoomText.textContent = `${Math.round(currentDocZoom * 100)}%`;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const zoomInBtn = document.getElementById('zoomInBtn');
|
|
746
|
+
const zoomOutBtn = document.getElementById('zoomOutBtn');
|
|
747
|
+
const zoomResetBtn = document.getElementById('zoomResetBtn');
|
|
748
|
+
|
|
749
|
+
if (zoomInBtn) zoomInBtn.onclick = () => updateZoom(currentDocZoom + 0.25);
|
|
750
|
+
if (zoomOutBtn) zoomOutBtn.onclick = () => updateZoom(currentDocZoom - 0.25);
|
|
751
|
+
if (zoomResetBtn) zoomResetBtn.onclick = () => updateZoom(1);
|
|
752
|
+
|
|
753
|
+
// Wire modal style toggles
|
|
754
|
+
const mBtnTok = document.getElementById('modalBtnStyleTokenized');
|
|
755
|
+
const mBtnBlk = document.getElementById('modalBtnStyleBlackout');
|
|
756
|
+
if (mBtnTok) mBtnTok.onclick = () => window.setPdfRedactionStyle('tokenized');
|
|
757
|
+
if (mBtnBlk) mBtnBlk.onclick = () => window.setPdfRedactionStyle('blackout');
|
|
758
|
+
|
|
759
|
+
// Wire main style toggles
|
|
760
|
+
const btnTok = document.getElementById('btnStyleTokenized');
|
|
761
|
+
const btnBlk = document.getElementById('btnStyleBlackout');
|
|
762
|
+
if (btnTok) btnTok.onclick = () => window.setPdfRedactionStyle('tokenized');
|
|
763
|
+
if (btnBlk) btnBlk.onclick = () => window.setPdfRedactionStyle('blackout');
|
|
764
|
+
|
|
765
|
+
if (viewport) {
|
|
766
|
+
viewport.addEventListener('wheel', (e) => {
|
|
767
|
+
if (modal.classList.contains('flex')) {
|
|
768
|
+
e.preventDefault();
|
|
769
|
+
const delta = e.deltaY < 0 ? 0.15 : -0.15;
|
|
770
|
+
updateZoom(currentDocZoom + delta);
|
|
771
|
+
}
|
|
772
|
+
}, { passive: false });
|
|
773
|
+
|
|
774
|
+
viewport.addEventListener('mousedown', (e) => {
|
|
775
|
+
if (e.target.tagName === 'BUTTON' || e.target.closest('#imageZoomBadgeOverlay')) return;
|
|
776
|
+
isDocPanning = true;
|
|
777
|
+
startPanX = e.pageX - viewport.offsetLeft;
|
|
778
|
+
startPanY = e.pageY - viewport.offsetTop;
|
|
779
|
+
scrollPanLeft = viewport.scrollLeft;
|
|
780
|
+
scrollPanTop = viewport.scrollTop;
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
viewport.addEventListener('mouseleave', () => { isDocPanning = false; });
|
|
784
|
+
viewport.addEventListener('mouseup', () => { isDocPanning = false; });
|
|
785
|
+
viewport.addEventListener('mousemove', (e) => {
|
|
786
|
+
if (!isDocPanning) return;
|
|
787
|
+
e.preventDefault();
|
|
788
|
+
const x = e.pageX - viewport.offsetLeft;
|
|
789
|
+
const y = e.pageY - viewport.offsetTop;
|
|
790
|
+
const walkX = (x - startPanX) * 1.5;
|
|
791
|
+
const walkY = (y - startPanY) * 1.5;
|
|
792
|
+
viewport.scrollLeft = scrollPanLeft - walkX;
|
|
793
|
+
viewport.scrollTop = scrollPanTop - walkY;
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
document.addEventListener('keydown', (e) => {
|
|
798
|
+
if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
|
|
799
|
+
window.closeImageZoomModal();
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
window.closeImageZoomModal = function() {
|
|
806
|
+
const modal = document.getElementById('imageZoomModal');
|
|
807
|
+
const content = document.getElementById('imageZoomContent');
|
|
808
|
+
if (!modal) return;
|
|
809
|
+
modal.classList.add('opacity-0', 'pointer-events-none');
|
|
810
|
+
if (content) {
|
|
811
|
+
content.classList.remove('scale-100');
|
|
812
|
+
content.classList.add('scale-95');
|
|
813
|
+
}
|
|
814
|
+
setTimeout(() => {
|
|
815
|
+
modal.classList.add('hidden');
|
|
816
|
+
modal.classList.remove('flex');
|
|
817
|
+
document.body.style.overflow = '';
|
|
818
|
+
}, 200);
|
|
819
|
+
};
|