Skip to content
39 changes: 30 additions & 9 deletions navigator-html-injectables/src/helpers/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function deselect(wnd: ReadiumWindow) {

const interactiveTags = [
"a",
"area",
"audio",
"button",
"canvas",
Expand All @@ -43,17 +44,14 @@ const interactiveTags = [
"video",
];

const interactiveRoles = ["dialog", "radiogroup", "radio", "menu", "menuitem"];

// See https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling
export function nearestInteractiveElement(element: Element): Element | null {
if (interactiveTags.indexOf(element.nodeName.toLowerCase()) !== -1) {
return element;
}

// Checks whether the element is editable by the user.
if (
element.hasAttribute("contenteditable") &&
element.getAttribute("contenteditable")?.toLowerCase() !== "false"
) {
// If the element or any ancestor is blocked, return null immediately
if (isElementBlocked(element)) return null;

if (isInteractiveElement(element)) {
return element;
}

Expand All @@ -65,6 +63,29 @@ export function nearestInteractiveElement(element: Element): Element | null {
return null;
}

export function isElementBlocked(element: Element | null): boolean {
if (!element) {
return true;
}

return element.closest("[inert]") !== null || element.hasAttribute("disabled");
}

export function isInteractiveElement(element: Element | null): boolean {
if (!element) {
return false;
}

// Check for interactive roles
if (element.role && interactiveRoles.includes(element.role)) return true;

if ((element as HTMLElement).tabIndex && (element as HTMLElement).tabIndex >= 0) return true;

// Use existing interactive tags logic
return interactiveTags.includes(element.nodeName.toLowerCase()) ||
element.hasAttribute("contenteditable") && element.getAttribute("contenteditable")?.toLowerCase() !== "false";
}

/// Returns the `Locator` object to the first block element that is visible on
/// the screen.
export function findFirstVisibleLocator(wnd: ReadiumWindow, scrolling: boolean) {
Expand Down
3 changes: 2 additions & 1 deletion navigator-html-injectables/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './comms';
export * from './modules';
export * from './Loader';
export * from './protection';
export * from './protection';
export * from './keyboard';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KeyCombo, KeyboardPeripheral } from "./KeyboardCombinations";
import { BaseKeyboardPeripheralEvent, KeyboardEventData, BasicTextSelection } from "../modules/Peripherals";
import { ReadiumWindow } from "../helpers/dom";
import { ReadiumWindow, nearestInteractiveElement } from "../helpers/dom";

export type KeyHandler = (event: KeyboardEvent) => void;
export type ActivityEventDispatcher = (event: KeyboardPeripheralEvent) => void;
Expand Down Expand Up @@ -56,6 +56,8 @@ export class KeyCombinationManager {
): KeyboardPeripheralEvent {
// Capture selected text if window is available
let selectedText: Omit<BasicTextSelection, "targetFrameSrc"> | undefined;
let interactiveElement: string | undefined;

if (wnd) {
const selection = wnd.getSelection();
const selectedTextStr = selection?.toString() || '';
Expand All @@ -71,6 +73,12 @@ export class KeyCombinationManager {
height: rect.height
};
}

// Capture interactive element information following the same pattern as onPointUp
const activeElement = wnd.document.activeElement;
if (activeElement && activeElement !== wnd.document.body) {
interactiveElement = nearestInteractiveElement(activeElement)?.outerHTML;
}
}

return {
Expand All @@ -84,7 +92,8 @@ export class KeyCombinationManager {
shiftKey: event.shiftKey,
metaKey: event.metaKey,
targetFrameSrc: targetFrameSrc,
selectedText
selectedText,
interactiveElement
};
}

Expand Down
2 changes: 2 additions & 0 deletions navigator-html-injectables/src/keyboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./KeyboardCombinations";
export * from "./KeyCombinationManager";
11 changes: 5 additions & 6 deletions navigator-html-injectables/src/modules/Peripherals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import { ReadiumWindow, nearestInteractiveElement } from "../helpers/dom";
import { BulkCopyProtector, BulkCopyProtectionOptions } from "../protection/BulkCopyProtector";
import { SelectionAnalyzer, SelectionAnalyzerOptions } from "../protection/SelectionAnalyzer";
import {
KeyboardPeripheral
} from "../protection";
import { SuspiciousActivityType } from "../comms";
import { BULK_COPY_CONFIG, SELECTION_ANALYZER_CONFIG } from "../protection/config";
import {
KeyboardPeripheral,
KeyCombinationManager,
ActivityEventDispatcher,
KeyboardPeripheralEvent
} from "../protection/KeyCombinationManager";
} from "../keyboard";
import { SuspiciousActivityType } from "../comms";
import { BULK_COPY_CONFIG, SELECTION_ANALYZER_CONFIG } from "../protection/config";
import { SuspiciousScrollingEvent } from "./snapper/ScrollSnapper";
import { SuspiciousSnappingEvent } from "./snapper/ColumnSnapper";

Expand Down Expand Up @@ -59,6 +57,7 @@ export interface BaseKeyboardPeripheralEvent {
timestamp: number;
targetFrameSrc: string;
selectedText?: Omit<BasicTextSelection, "targetFrameSrc">;
interactiveElement?: string;
}

export interface DeveloperToolsEvent extends BaseSuspiciousActivityEvent, KeyboardEventData {
Expand Down
2 changes: 0 additions & 2 deletions navigator-html-injectables/src/protection/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export * from "./BulkCopyProtector";
export * from "./KeyCombinationManager";
export * from "./KeyboardCombinations";
export * from "./PatternAnalyzer";
export * from "./SelectionAnalyzer";
export * from "./PrintProtector";
3 changes: 2 additions & 1 deletion navigator/docs/epub/CustomizingListeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Fires when a configured keyboard shortcut is triggered within the EPUB content.
The event provides detailed information about the keyboard interaction:

```ts
interface KeyboardPeripheralEvent {
interface KeyboardPeripheralEventData {
type: string; // The type of peripheral (e.g., "developer_tools", "select_all", "print", "save")
timestamp: number; // When the event occurred
targetFrameSrc: string; // The source of the frame where the event originated
Expand All @@ -143,6 +143,7 @@ interface KeyboardPeripheralEvent {
width: number;
height: number;
};
interactiveElement?: Element; // The interactive element (if any) that is currently focused
key: string; // The key that was pressed
code: string; // The physical key code
keyCode: number; // The numeric key code
Expand Down
1 change: 1 addition & 0 deletions navigator/docs/epub/KeyboardPeripherals.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ interface KeyboardPeripheralEvent {
width: number;
height: number;
};
interactiveElement?: string; // The interactive element (if any) that is currently focused
// Keyboard-specific data
key: string;
code: string;
Expand Down
18 changes: 15 additions & 3 deletions navigator/src/epub/EpubNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Layout, Link, Locator, Profile, Publication, ReadingProgression } from
import { Configurable, ConfigurablePreferences, ConfigurableSettings, LineLengths, ProgressionRange, VisualNavigator, VisualNavigatorViewport } from "../";
import { FramePoolManager } from "./frame/FramePoolManager";
import { FXLFramePoolManager } from "./fxl/FXLFramePoolManager";
import { CommsEventKey, ContextMenuEvent, FXLModules, KeyboardEventData, ModuleLibrary, ModuleName, ReflowableModules } from "@readium/navigator-html-injectables";
import { CommsEventKey, ContextMenuEvent, FXLModules, KeyboardPeripheralEvent, ModuleLibrary, ModuleName, ReflowableModules } from "@readium/navigator-html-injectables";
import { BasicTextSelection, FrameClickEvent, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
import * as path from "path-browserify";
import { FXLFrameManager } from "./fxl/FXLFrameManager";
Expand All @@ -23,6 +23,10 @@ import { KeyboardPeripherals, NAVIGATOR_KEYBOARD_PERIPHERAL_EVENT } from "../per

export type ManagerEventKey = "zoom";

export interface KeyboardPeripheralEventData extends Omit<KeyboardPeripheralEvent, 'interactiveElement'> {
interactiveElement?: Element;
}

export interface EpubNavigatorConfiguration {
preferences: IEpubPreferences;
defaults: IEpubDefaults;
Expand All @@ -44,7 +48,7 @@ export interface EpubNavigatorListeners {
textSelected: (selection: BasicTextSelection) => void;
contentProtection: (type: string, data: SuspiciousActivityEvent) => void;
contextMenu: (data: ContextMenuEvent) => void;
peripheral: (data: KeyboardEventData) => void;
peripheral: (data: KeyboardPeripheralEventData) => void;
// showToc: () => void;
}

Expand Down Expand Up @@ -507,7 +511,15 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
this.listeners.contextMenu(data as ContextMenuEvent);
break;
case "keyboard_peripherals":
this.listeners.peripheral(data as KeyboardEventData);
const event = data as KeyboardPeripheralEvent;
const parsedEvent: KeyboardPeripheralEventData = { ...event, interactiveElement: undefined };
if (event.interactiveElement) {
parsedEvent.interactiveElement = new DOMParser().parseFromString(
event.interactiveElement,
"text/html"
).body.children[0] as Element;
}
this.listeners.peripheral(parsedEvent);
break;
case "log":
console.log(this._cframes[0]?.source?.split("/")[3], ...(data as any[]));
Expand Down
1 change: 0 additions & 1 deletion navigator/src/epub/css/Properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "../../css/Properties";

export interface IUserProperties {
advancedSettings?: boolean | null;
a11yNormalize?: boolean | null;
backgroundColor?: string | null;
blendFilter?: boolean | null;
Expand Down
16 changes: 12 additions & 4 deletions navigator/src/webpub/WebPubNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Feature, Link, Locator, Publication, ReadingProgression, LocatorLocatio
import { VisualNavigator, VisualNavigatorViewport, ProgressionRange } from "../Navigator";
import { Configurable } from "../preferences/Configurable";
import { WebPubFramePoolManager } from "./WebPubFramePoolManager";
import { BasicTextSelection, CommsEventKey, ContextMenuEvent, FrameClickEvent, KeyboardEventData, ModuleLibrary, ModuleName, SuspiciousActivityEvent, WebPubModules } from "@readium/navigator-html-injectables";
import { BasicTextSelection, CommsEventKey, ContextMenuEvent, FrameClickEvent, KeyboardPeripheralEvent, ModuleLibrary, ModuleName, SuspiciousActivityEvent, WebPubModules } from "@readium/navigator-html-injectables";
import * as path from "path-browserify";
import { WebPubFrameManager } from "./WebPubFrameManager";

import { ManagerEventKey } from "../epub/EpubNavigator";
import { KeyboardPeripheralEventData, ManagerEventKey } from "../epub/EpubNavigator";
import { WebPubCSS } from "./css/WebPubCSS";
import { WebUserProperties, WebRSProperties } from "./css/Properties";
import { IWebPubPreferences, WebPubPreferences } from "./preferences/WebPubPreferences";
Expand Down Expand Up @@ -41,7 +41,7 @@ export interface WebPubNavigatorListeners {
textSelected: (selection: BasicTextSelection) => void;
contentProtection: (type: string, data: SuspiciousActivityEvent) => void;
contextMenu: (data: ContextMenuEvent) => void;
peripheral: (data: KeyboardEventData) => void;
peripheral: (data: KeyboardPeripheralEventData) => void;
}

const defaultListeners = (listeners: WebPubNavigatorListeners): WebPubNavigatorListeners => ({
Expand Down Expand Up @@ -352,7 +352,15 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
this.listeners.contextMenu(data as ContextMenuEvent);
break;
case "keyboard_peripherals":
this.listeners.peripheral(data as KeyboardEventData);
const event = data as KeyboardPeripheralEvent;
const parsedEvent: KeyboardPeripheralEventData = { ...event, interactiveElement: undefined };
if (event.interactiveElement) {
parsedEvent.interactiveElement = new DOMParser().parseFromString(
event.interactiveElement,
"text/html"
).body.children[0] as Element;
}
this.listeners.peripheral(parsedEvent);
break;
case "log":
console.log(this.framePool.currentFrames[0]?.source?.split("/")[3], ...(data as any[]));
Expand Down