@readium/navigator-html-injectables 2.2.5 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.js +1107 -548
  2. package/dist/index.umd.cjs +23 -9
  3. package/package.json +1 -1
  4. package/src/comms/keys.ts +22 -3
  5. package/src/index.ts +2 -1
  6. package/src/modules/ModuleLibrary.ts +10 -4
  7. package/src/modules/Peripherals.ts +458 -13
  8. package/src/modules/snapper/ColumnSnapper.ts +63 -2
  9. package/src/modules/snapper/ScrollSnapper.ts +59 -1
  10. package/src/modules/snapper/WebPubSnapper.ts +57 -3
  11. package/src/protection/BulkCopyProtector.ts +134 -0
  12. package/src/protection/KeyCombinationManager.ts +139 -0
  13. package/src/protection/KeyboardCombinations.ts +60 -0
  14. package/src/protection/PatternAnalyzer.ts +117 -0
  15. package/src/protection/PrintProtector.ts +87 -0
  16. package/src/protection/SelectionAnalyzer.ts +133 -0
  17. package/src/protection/config.ts +33 -0
  18. package/src/protection/index.ts +6 -0
  19. package/types/src/comms/keys.d.ts +3 -2
  20. package/types/src/index.d.ts +1 -0
  21. package/types/src/modules/ModuleLibrary.d.ts +1 -1
  22. package/types/src/modules/Peripherals.d.ts +102 -2
  23. package/types/src/modules/snapper/ColumnSnapper.d.ts +10 -0
  24. package/types/src/modules/snapper/ScrollSnapper.d.ts +13 -0
  25. package/types/src/modules/snapper/WebPubSnapper.d.ts +6 -0
  26. package/types/src/protection/BulkCopyProtector.d.ts +20 -0
  27. package/types/src/protection/KeyCombinationManager.d.ts +33 -0
  28. package/types/src/protection/KeyboardCombinations.d.ts +15 -0
  29. package/types/src/protection/PatternAnalyzer.d.ts +18 -0
  30. package/types/src/protection/PrintProtector.d.ts +20 -0
  31. package/types/src/protection/SelectionAnalyzer.d.ts +20 -0
  32. package/types/src/protection/config.d.ts +7 -0
  33. package/types/src/protection/index.d.ts +6 -0
  34. package/src/types/protection.ts +0 -16
  35. package/types/src/types/protection.d.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@readium/navigator-html-injectables",
3
- "version": "2.2.5",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator",
6
6
  "author": "readium",
package/src/comms/keys.ts CHANGED
@@ -16,8 +16,11 @@ export type CommsEventKey =
16
16
  "progress" |
17
17
  "first_visible_locator" |
18
18
  "text_selected" |
19
+ "context_menu" |
19
20
  "media_play" |
20
- "media_pause";
21
+ "media_pause" |
22
+ "content_protection" |
23
+ "keyboard_peripherals";
21
24
  ;
22
25
 
23
26
  export type CommsCommandKey =
@@ -42,5 +45,21 @@ export type CommsCommandKey =
42
45
  "focus" |
43
46
  "activate" |
44
47
  "shake" |
45
- "force_webkit_recalc";
46
- ;
48
+ "force_webkit_recalc" |
49
+ "peripherals_protection" |
50
+ "keyboard_peripherals" |
51
+ "scroll_protection" |
52
+ "print_protection";
53
+ ;
54
+
55
+ export type SuspiciousActivityType =
56
+ "developer_tools" |
57
+ "select_all" |
58
+ "save" |
59
+ "suspicious_selection" |
60
+ "bulk_copy" |
61
+ "suspicious_scrolling" |
62
+ "suspicious_snapping" |
63
+ "drag_detected" |
64
+ "drop_detected" |
65
+ "print";
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './comms';
2
2
  export * from './modules';
3
- export * from './Loader';
3
+ export * from './Loader';
4
+ export * from './protection';
@@ -7,6 +7,7 @@ import { ReflowableSetup } from "./setup/ReflowableSetup";
7
7
  import { FixedSetup } from "./setup/FixedSetup";
8
8
  import { Decorator } from "./Decorator";
9
9
  import { WebPubSetup } from "./setup/WebPubSetup";
10
+ import { PrintProtector } from "../protection/PrintProtector";
10
11
 
11
12
  // All the module names. TODO: Come up with a better way of collecting these in a way TS will recognize
12
13
  export type ModuleName =
@@ -19,13 +20,15 @@ export type ModuleName =
19
20
  "decorator" |
20
21
  "reflowable_setup" |
21
22
  "peripherals" |
22
- "webpub_setup";
23
+ "webpub_setup" |
24
+ "print_protection";
23
25
 
24
26
  // Modules that are valid for FXL publications
25
27
  export const FXLModules: ModuleName[] = [
26
28
  "fixed_setup",
27
29
  "decorator",
28
- "peripherals"
30
+ "peripherals",
31
+ "print_protection"
29
32
  ];
30
33
 
31
34
  // Modules that are valid for reflowable publications
@@ -34,7 +37,8 @@ export const ReflowableModules: ModuleName[] = [
34
37
  "decorator",
35
38
  "peripherals",
36
39
  "column_snapper",
37
- "scroll_snapper"
40
+ "scroll_snapper",
41
+ "print_protection"
38
42
  ];
39
43
 
40
44
  // Modules that are valid for WebPub publications (simple scroll-based)
@@ -42,7 +46,8 @@ export const WebPubModules: ModuleName[] = [
42
46
  "webpub_setup",
43
47
  "webpub_snapper",
44
48
  "decorator",
45
- "peripherals"
49
+ "peripherals",
50
+ "print_protection"
46
51
  ];
47
52
 
48
53
  export const ModuleLibrary = new Map<string, ModuleDerived>([
@@ -55,4 +60,5 @@ export const ModuleLibrary = new Map<string, ModuleDerived>([
55
60
  Decorator,
56
61
  ColumnSnapper,
57
62
  ScrollSnapper,
63
+ PrintProtector
58
64
  ].map(m => [m.moduleName, m])); // Turn module list into K/V list for quick access by name
@@ -1,6 +1,20 @@
1
1
  import { Comms } from "../comms/comms";
2
2
  import { Module } from "./Module";
3
3
  import { ReadiumWindow, nearestInteractiveElement } from "../helpers/dom";
4
+ import { BulkCopyProtector, BulkCopyProtectionOptions } from "../protection/BulkCopyProtector";
5
+ import { SelectionAnalyzer, SelectionAnalyzerOptions } from "../protection/SelectionAnalyzer";
6
+ import {
7
+ KeyboardPeripheral
8
+ } from "../protection";
9
+ import { SuspiciousActivityType } from "../comms";
10
+ import { BULK_COPY_CONFIG, SELECTION_ANALYZER_CONFIG } from "../protection/config";
11
+ import {
12
+ KeyCombinationManager,
13
+ ActivityEventDispatcher,
14
+ KeyboardPeripheralEvent
15
+ } from "../protection/KeyCombinationManager";
16
+ import { SuspiciousScrollingEvent } from "./snapper/ScrollSnapper";
17
+ import { SuspiciousSnappingEvent } from "./snapper/ColumnSnapper";
4
18
 
5
19
  export interface FrameClickEvent {
6
20
  defaultPrevented: boolean;
@@ -22,12 +36,363 @@ export interface BasicTextSelection {
22
36
  targetFrameSrc: string;
23
37
  }
24
38
 
39
+ export interface BaseSuspiciousActivityEvent {
40
+ type: SuspiciousActivityType;
41
+ timestamp: number;
42
+ targetFrameSrc: string;
43
+ }
44
+
45
+ // Common interface for all keyboard-related events
46
+ export interface KeyboardEventData {
47
+ key: string;
48
+ code: string;
49
+ keyCode: number;
50
+ ctrlKey: boolean;
51
+ altKey: boolean;
52
+ shiftKey: boolean;
53
+ metaKey: boolean;
54
+ }
55
+
56
+ // Base interface for keyboard peripheral events
57
+ export interface BaseKeyboardPeripheralEvent {
58
+ type: string;
59
+ timestamp: number;
60
+ targetFrameSrc: string;
61
+ selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
62
+ }
63
+
64
+ export interface DeveloperToolsEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
65
+ type: "developer_tools";
66
+ }
67
+
68
+ export interface SelectAllEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
69
+ type: "select_all";
70
+ }
71
+
72
+ export interface PrintEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
73
+ type: "print";
74
+ }
75
+
76
+ export interface SaveEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
77
+ type: "save";
78
+ }
79
+
80
+ export interface BulkCopyEvent extends BaseSuspiciousActivityEvent {
81
+ type: "bulk_copy";
82
+ clipboardTypes: readonly string[];
83
+ selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
84
+ selectionLength?: number;
85
+ }
86
+
87
+ export interface SuspiciousSelectionEvent extends BaseSuspiciousActivityEvent {
88
+ type: "suspicious_selection";
89
+ selectionLength: number;
90
+ selectedText: Omit<BasicTextSelection, "targetFrameSrc">;
91
+ eventType: string;
92
+ }
93
+
94
+ export interface DragDetectedEvent extends BaseSuspiciousActivityEvent {
95
+ type: "drag_detected";
96
+ dataTransferTypes: readonly string[];
97
+ }
98
+
99
+ export interface DropDetectedEvent extends BaseSuspiciousActivityEvent {
100
+ type: "drop_detected";
101
+ dataTransferTypes: readonly string[];
102
+ fileCount: number;
103
+ }
104
+
105
+ export interface ContextMenuEvent extends Omit<BaseSuspiciousActivityEvent, "type"> {
106
+ clientX: number;
107
+ clientY: number;
108
+ selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
109
+ }
110
+
111
+ export type SuspiciousActivityEvent =
112
+ | DeveloperToolsEvent
113
+ | SelectAllEvent
114
+ | BulkCopyEvent
115
+ | SuspiciousSelectionEvent
116
+ | DragDetectedEvent
117
+ | DropDetectedEvent
118
+ | PrintEvent
119
+ | SaveEvent
120
+ | SuspiciousScrollingEvent
121
+ | SuspiciousSnappingEvent
122
+ | KeyboardPeripheralEvent;
123
+
124
+ export interface ContentProtectionConfig {
125
+ monitorSelection?: boolean | SelectionAnalyzerOptions;
126
+ protectCopy?: boolean | Omit<BulkCopyProtectionOptions, "enabled">;
127
+ disableContextMenu?: boolean;
128
+ disableDragAndDrop?: boolean;
129
+ disableSelectAll?: boolean;
130
+ disableSave?: boolean;
131
+ monitorDevTools?: boolean;
132
+ monitorScrollingExperimental?: boolean;
133
+ }
134
+
25
135
  export class Peripherals extends Module {
26
136
  static readonly moduleName = "peripherals";
27
137
  private wnd!: ReadiumWindow;
28
138
  private comms!: Comms;
139
+ private configApplied = false; // Track if config has been applied
29
140
 
141
+ // State management
142
+ private cleanupCallbacks: (() => void)[] = [];
30
143
  private pointerMoved = false;
144
+
145
+ // Feature flags
146
+ private isContextMenuEnabled = false;
147
+ private isDragAndDropEnabled = false;
148
+ private isSelectionMonitoringEnabled = false;
149
+ private isBulkCopyProtectionEnabled = false;
150
+
151
+ // Selection analysis
152
+ private selectionAnalyzer: SelectionAnalyzer | null = null;
153
+ private currentSelection: string | null = null;
154
+
155
+ // Bulk copy protection
156
+ private bulkCopyProtector: BulkCopyProtector | null = null;
157
+
158
+ // Key combination manager
159
+ private keyManager = new KeyCombinationManager();
160
+
161
+ private addContextMenuPrevention(): void {
162
+ if (this.isContextMenuEnabled || !this.wnd) return;
163
+ this.wnd.document.addEventListener("contextmenu", this.onContext);
164
+ this.isContextMenuEnabled = true;
165
+ }
166
+
167
+ private removeContextMenuPrevention(): void {
168
+ if (!this.isContextMenuEnabled || !this.wnd) return;
169
+ this.wnd.document.removeEventListener("contextmenu", this.onContext);
170
+ this.isContextMenuEnabled = false;
171
+ }
172
+
173
+ private addDragAndDropPrevention(): void {
174
+ if (this.isDragAndDropEnabled || !this.wnd) return;
175
+ this.wnd.document.addEventListener("dragstart", this.onDragStart);
176
+ this.wnd.document.addEventListener("drop", this.onDrop);
177
+ this.isDragAndDropEnabled = true;
178
+ }
179
+
180
+ private removeDragAndDropPrevention(): void {
181
+ if (!this.isDragAndDropEnabled || !this.wnd) return;
182
+ this.wnd.document.removeEventListener("dragstart", this.onDragStart);
183
+ this.wnd.document.removeEventListener("drop", this.onDrop);
184
+ this.isDragAndDropEnabled = false;
185
+ }
186
+
187
+ private keyDownHandler: ((event: KeyboardEvent) => void) | null = null;
188
+
189
+ private enableKeyboardPeripherals(shortcuts: KeyboardPeripheral[] = []): void {
190
+ // Clear any existing state
191
+ this.disableKeyboardPeripherals();
192
+
193
+ // Create activity event dispatcher
194
+ const dispatcher: ActivityEventDispatcher = (activityEvent) => {
195
+ this.comms?.send("keyboard_peripherals", activityEvent);
196
+ };
197
+
198
+ // Create unified handler using centralized KeyCombinationManager
199
+ this.keyDownHandler = this.keyManager.createUnifiedHandler(this.wnd.location.href, shortcuts, dispatcher, this.wnd);
200
+
201
+ // Add the event listener
202
+ if (this.wnd) {
203
+ this.wnd.document.addEventListener("keydown", this.keyDownHandler, {
204
+ capture: true
205
+ });
206
+ }
207
+ }
208
+
209
+ private disableKeyboardPeripherals(): void {
210
+ if (this.wnd && this.keyDownHandler) {
211
+ this.wnd.document.removeEventListener("keydown", this.keyDownHandler, {
212
+ capture: true
213
+ });
214
+ this.keyDownHandler = null;
215
+ }
216
+ }
217
+
218
+ private addBulkCopyProtection(options: Partial<BulkCopyProtectionOptions> = {}): void {
219
+ if (this.isBulkCopyProtectionEnabled || !this.wnd) return;
220
+
221
+ const defaultOptions = BULK_COPY_CONFIG;
222
+
223
+ const finalOptions = options ? { ...defaultOptions, ...options } : defaultOptions;
224
+
225
+ this.bulkCopyProtector = new BulkCopyProtector(this.wnd, finalOptions);
226
+ this.wnd.document.addEventListener("copy", this.preventBulkCopy, true);
227
+ this.wnd.document.addEventListener("cut", this.preventBulkCopy, true);
228
+ this.isBulkCopyProtectionEnabled = true;
229
+ }
230
+
231
+ private removeBulkCopyProtection(): void {
232
+ if (!this.isBulkCopyProtectionEnabled || !this.wnd) return;
233
+
234
+ this.wnd.document.removeEventListener("copy", this.preventBulkCopy, true);
235
+ this.wnd.document.removeEventListener("cut", this.preventBulkCopy, true);
236
+ this.bulkCopyProtector?.destroy();
237
+ this.bulkCopyProtector = null;
238
+ this.isBulkCopyProtectionEnabled = false;
239
+ }
240
+
241
+ private preventBulkCopy = (event: ClipboardEvent) => {
242
+ if (!this.isBulkCopyProtectionEnabled || !this.bulkCopyProtector) {
243
+ return true;
244
+ }
245
+
246
+ if (!this.bulkCopyProtector.shouldAllowCopy(event)) {
247
+ event.preventDefault();
248
+
249
+ const selection = this.wnd.getSelection();
250
+ const selectedText = selection?.toString() || '';
251
+ const domRectList = selectedText ? selection?.getRangeAt(0)?.getClientRects() : null;
252
+ const rect = domRectList?.[0];
253
+
254
+ const activityEvent: BulkCopyEvent = {
255
+ type: "bulk_copy",
256
+ timestamp: Date.now(),
257
+ clipboardTypes: event.clipboardData?.types ? [...event.clipboardData.types] : [],
258
+ selectedText: rect ? {
259
+ text: selectedText,
260
+ x: rect.x,
261
+ y: rect.y,
262
+ width: rect.width,
263
+ height: rect.height
264
+ } : undefined,
265
+ selectionLength: selectedText.length,
266
+ targetFrameSrc: this.wnd.location.href
267
+
268
+ };
269
+ this.comms?.send("content_protection", activityEvent);
270
+ return false;
271
+ }
272
+ return true;
273
+ };
274
+
275
+ private handleSelection = (event?: Event) => {
276
+ if (!this.isSelectionMonitoringEnabled || !this.wnd || !this.selectionAnalyzer) {
277
+ return;
278
+ }
279
+
280
+ const selection = this.wnd.getSelection();
281
+ if (selection) {
282
+ this.currentSelection = selection.toString();
283
+ const isSuspicious = this.selectionAnalyzer.analyze(selection);
284
+
285
+ if (isSuspicious && this.currentSelection) {
286
+ const selection = this.wnd.getSelection();
287
+ const selectedText = selection?.toString() || '';
288
+ const domRectList = (selectedText && selection?.rangeCount) ? selection.getRangeAt(0)?.getClientRects() : null;
289
+ const rect = domRectList?.[0];
290
+
291
+ const activityEvent: SuspiciousSelectionEvent = {
292
+ type: "suspicious_selection",
293
+ timestamp: Date.now(),
294
+ selectionLength: selectedText.length,
295
+ selectedText: {
296
+ text: selectedText,
297
+ x: rect?.x ?? 0,
298
+ y: rect?.y ?? 0,
299
+ width: rect?.width ?? 0,
300
+ height: rect?.height ?? 0
301
+ },
302
+ eventType: event?.type || "selectionchange",
303
+ targetFrameSrc: this.wnd.location.href
304
+ };
305
+ this.comms?.send("content_protection", activityEvent);
306
+ }
307
+ } else {
308
+ this.currentSelection = null;
309
+ }
310
+ };
311
+
312
+ private addSelectionMonitoring(options?: SelectionAnalyzerOptions): void {
313
+ if (this.isSelectionMonitoringEnabled || !this.wnd) return;
314
+
315
+ // Use provided options or fall back to default config
316
+ const analyzerOptions = options || SELECTION_ANALYZER_CONFIG;
317
+ this.selectionAnalyzer = new SelectionAnalyzer(analyzerOptions);
318
+ this.wnd.document.addEventListener("selectionchange", this.handleSelection);
319
+ this.isSelectionMonitoringEnabled = true;
320
+ }
321
+
322
+ private removeSelectionMonitoring(): void {
323
+ if (!this.isSelectionMonitoringEnabled || !this.wnd) {
324
+ return;
325
+ }
326
+
327
+ this.wnd.document.removeEventListener("selectionchange", this.handleSelection);
328
+ this.selectionAnalyzer?.clear();
329
+ this.selectionAnalyzer = null;
330
+ this.isSelectionMonitoringEnabled = false;
331
+ }
332
+
333
+ private onDragStart = (event: DragEvent) => {
334
+ if (this.isDragAndDropEnabled) {
335
+ // Drag and drop protection is enabled - prevent it
336
+ event.preventDefault();
337
+ const activityEvent: DragDetectedEvent = {
338
+ type: "drag_detected",
339
+ timestamp: Date.now(),
340
+ dataTransferTypes: event.dataTransfer?.types ? [...event.dataTransfer.types] : [],
341
+ targetFrameSrc: this.wnd.location.href
342
+ };
343
+ this.comms?.send("content_protection", activityEvent);
344
+ return false;
345
+ } else {
346
+ return true;
347
+ }
348
+ };
349
+
350
+ private onDrop = (event: DragEvent) => {
351
+ if (this.isDragAndDropEnabled) {
352
+ // Drag and drop protection is enabled - prevent it
353
+ event.preventDefault();
354
+ const dataTransfer = event.dataTransfer;
355
+ const activityEvent: DropDetectedEvent = {
356
+ type: "drop_detected",
357
+ timestamp: Date.now(),
358
+ dataTransferTypes: dataTransfer?.types ? [...dataTransfer.types] : [],
359
+ fileCount: dataTransfer?.files?.length || 0,
360
+ targetFrameSrc: this.wnd.location.href
361
+ };
362
+ this.comms?.send("content_protection", activityEvent);
363
+ return false;
364
+ } else {
365
+ return true;
366
+ }
367
+ };
368
+
369
+ private onContext = (event: MouseEvent) => {
370
+ if (this.isContextMenuEnabled) {
371
+ // Context menu protection is enabled - prevent it
372
+ event.preventDefault();
373
+ const selection = this.wnd.getSelection();
374
+ const selectedText = selection?.toString() || '';
375
+ const domRectList = (selectedText && selection?.rangeCount) ? selection.getRangeAt(0)?.getClientRects() : null;
376
+ const rect = domRectList?.[0];
377
+
378
+ const activityEvent: ContextMenuEvent = {
379
+ timestamp: Date.now(),
380
+ clientX: event.clientX,
381
+ clientY: event.clientY,
382
+ ...(rect && {
383
+ selectedText: {
384
+ text: selectedText,
385
+ x: rect.x,
386
+ y: rect.y,
387
+ width: rect.width,
388
+ height: rect.height
389
+ }
390
+ }),
391
+ targetFrameSrc: this.wnd.location.href
392
+ };
393
+ this.comms?.send("context_menu", activityEvent);
394
+ }
395
+ };
31
396
 
32
397
  onPointUp(event: PointerEvent) {
33
398
  const selection = this.wnd.getSelection();
@@ -44,7 +409,7 @@ export class Peripherals extends Module {
44
409
  y: rect.y,
45
410
  width: rect.width,
46
411
  height: rect.height,
47
- targetFrameSrc: this.wnd?.location?.href,
412
+ targetFrameSrc: this.wnd?.location?.href
48
413
  }
49
414
  this.comms.send("text_selected", textSelection as BasicTextSelection);
50
415
  }
@@ -95,13 +460,6 @@ export class Peripherals extends Module {
95
460
  }
96
461
  private readonly onPointerDown = this.onPointDown.bind(this);
97
462
 
98
- onContext(event: MouseEvent) {
99
- // If the context menu is triggered by a long press, we manage the event using the pointup event
100
- this.onPointUp(event as PointerEvent);
101
- this.pointerMoved = false;
102
- }
103
- private readonly onContextMenu = this.onContext.bind(this);
104
-
105
463
  onClick(event: MouseEvent) {
106
464
  event.preventDefault(); // To prevent certain browser actions. May have side-effects
107
465
  if(!event.isTrusted) {
@@ -114,20 +472,90 @@ export class Peripherals extends Module {
114
472
  clientY: event.clientY,
115
473
  });
116
474
  // Override properties we cannot set in the constructor
117
- Object.defineProperty(synthEvent, 'target', {writable: false, value: event.target});
118
- Object.defineProperty(synthEvent, 'defaultPrevented', {writable: false, value: event.defaultPrevented });
475
+ Object.defineProperty(synthEvent, "target", {writable: false, value: event.target});
476
+ Object.defineProperty(synthEvent, "defaultPrevented", {writable: false, value: event.defaultPrevented });
119
477
  // Trigger `pointerup` event
120
478
  this.onPointUp(synthEvent);
121
479
  }
122
480
  }
123
481
  private readonly onClicker = this.onClick.bind(this);
124
482
 
483
+ private registerProtectionHandlers() {
484
+ // Single handler for all content protection features
485
+ this.comms?.register("peripherals_protection", Peripherals.moduleName, (data: unknown, ack) => {
486
+ const config = data as ContentProtectionConfig;
487
+
488
+ // Apply config only on first call, then ignore subsequent calls (immutable)
489
+ if (!this.configApplied) {
490
+ this.configApplied = true;
491
+
492
+ // Selection monitoring with optional configuration
493
+ if (config.monitorSelection) {
494
+ const options = typeof config.monitorSelection === "boolean"
495
+ ? undefined
496
+ : config.monitorSelection;
497
+ this.addSelectionMonitoring(options);
498
+ this.comms?.log("Selection monitoring enabled");
499
+ }
500
+
501
+ // Copy
502
+ if (typeof config.protectCopy === "object") {
503
+ // Limited copying with custom thresholds
504
+ this.addBulkCopyProtection({
505
+ enabled: true,
506
+ ...config.protectCopy
507
+ });
508
+ this.comms?.log("Copy protection enabled (limited)");
509
+ } else if (config.protectCopy === true) {
510
+ // Block all copying
511
+ this.addBulkCopyProtection({
512
+ enabled: true,
513
+ maxSelectionPercent: 0,
514
+ minThreshold: 0,
515
+ absoluteMaxChars: 0
516
+ });
517
+ this.comms?.log("Copy protection enabled");
518
+ }
519
+
520
+ // Context menu
521
+ if (config.disableContextMenu) {
522
+ this.addContextMenuPrevention();
523
+ this.comms?.log("Context menu protection enabled");
524
+ }
525
+
526
+ // Drag and drop
527
+ if (config.disableDragAndDrop) {
528
+ this.addDragAndDropPrevention();
529
+ this.comms?.log("Drag and drop protection enabled");
530
+ }
531
+ }
532
+
533
+ ack(true);
534
+ });
535
+
536
+ // Separate handler for keyboard peripherals
537
+ this.comms?.register("keyboard_peripherals", Peripherals.moduleName, (data: unknown, ack) => {
538
+ const keyboardPeripherals = data as KeyboardPeripheral[];
539
+
540
+ if (keyboardPeripherals && keyboardPeripherals.length > 0) {
541
+ this.enableKeyboardPeripherals(keyboardPeripherals);
542
+ this.comms?.log(`Keyboard peripherals enabled: ${keyboardPeripherals.map(p => p.type).join(", ")}`);
543
+ }
544
+
545
+ ack(true);
546
+ });
547
+ }
548
+
125
549
  mount(wnd: ReadiumWindow, comms: Comms): boolean {
126
550
  this.wnd = wnd;
127
551
  this.comms = comms;
552
+
553
+ // Register protection handlers
554
+ this.registerProtectionHandlers();
555
+
556
+ // Core event listeners (always active)
128
557
  wnd.document.addEventListener("pointerdown", this.onPointerDown);
129
558
  wnd.document.addEventListener("pointerup", this.onPointerUp);
130
- wnd.document.addEventListener("contextmenu", this.onContextMenu);
131
559
  wnd.document.addEventListener("pointermove", this.onPointerMove);
132
560
  wnd.document.addEventListener("click", this.onClicker);
133
561
 
@@ -136,12 +564,29 @@ export class Peripherals extends Module {
136
564
  }
137
565
 
138
566
  unmount(wnd: ReadiumWindow, comms: Comms): boolean {
567
+ // Clean up optional features
568
+ this.removeBulkCopyProtection();
569
+ this.removeSelectionMonitoring();
570
+ this.removeContextMenuPrevention();
571
+ this.removeDragAndDropPrevention();
572
+ this.disableKeyboardPeripherals();
573
+
574
+ // Clean up event listeners
575
+ this.cleanupCallbacks.forEach(cleanup => cleanup());
576
+ this.cleanupCallbacks = [];
577
+
578
+ // Remove core event listeners
139
579
  wnd.document.removeEventListener("pointerdown", this.onPointerDown);
140
580
  wnd.document.removeEventListener("pointerup", this.onPointerUp);
141
- wnd.document.removeEventListener("contextmenu", this.onContextMenu);
142
581
  wnd.document.removeEventListener("pointermove", this.onPointerMove);
143
582
  wnd.document.removeEventListener("click", this.onClicker);
144
-
583
+
584
+ // Unregister all handlers
585
+ comms.unregisterAll(Peripherals.moduleName);
586
+
587
+ // Reset config applied flag for fresh instances
588
+ this.configApplied = false;
589
+
145
590
  comms.log("Peripherals Unmounted");
146
591
  return true;
147
592
  }