@readium/decorator 1.0.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.
- package/README.md +172 -0
- package/dist/comms/direct.js +1 -0
- package/dist/controller/DecorationController.js +1 -0
- package/dist/index.js +1 -0
- package/dist/styles.js +1 -0
- package/package.json +48 -0
- package/src/Decoration.ts +16 -0
- package/src/comms/direct.ts +130 -0
- package/src/controller/DecorationController.ts +123 -0
- package/src/index.ts +16 -0
- package/src/styles.ts +121 -0
- package/types/src/Decoration.d.ts +16 -0
- package/types/src/comms/direct.d.ts +34 -0
- package/types/src/controller/DecorationController.d.ts +19 -0
- package/types/src/index.d.ts +8 -0
- package/types/src/styles.d.ts +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @readium/decorator
|
|
2
|
+
|
|
3
|
+
Renders visual annotations over text ranges in HTML content — highlights, underlines, strikethroughs, and more. Works on any HTML document.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @readium/decorator
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@readium/decorator` depends on [`@readium/shared`](https://www.npmjs.com/package/@readium/shared) (for the `Locator` type, which identifies *where* a decoration goes) and `@readium/navigator-html-injectables` (the DOM renderer it wraps). Both are installed automatically as dependencies.
|
|
12
|
+
|
|
13
|
+
### Requirements
|
|
14
|
+
|
|
15
|
+
- A browser environment. The renderer (`Decorator`) mounts on a real `Window`/`document` and uses `ResizeObserver` and `MutationObserver` to keep decorations positioned as the page reflows — there's no server-side/Node rendering path.
|
|
16
|
+
|
|
17
|
+
## Concepts
|
|
18
|
+
|
|
19
|
+
- **Comms**: `DecorationController` (your app logic) and `Decorator` (the DOM renderer) talk over a small message-passing interface, handled for you by `DirectCommsChannel` (below).
|
|
20
|
+
- **Groups**: every decoration belongs to a `group` (an arbitrary string you choose — `"tts"`, `"search-results"`, `"annotations"`, etc). `applyDecorations` replaces *all* decorations for one group at a time, diffing against that group's previous state. Separate groups don't interfere with each other, and each can independently opt into activation (tap/click) and hover tracking depending on which callbacks its `DecorationObserver` declares.
|
|
21
|
+
- **Locators**: each decoration's `locator` is a `Locator` instance from `@readium/shared`, identifying the text range (or other target) to decorate within a resource.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { Locator, LocatorLocations } from "@readium/shared";
|
|
27
|
+
import { DirectCommsChannel, Decorator, DecorationController, DecorationStyleType } from "@readium/decorator";
|
|
28
|
+
|
|
29
|
+
// 1. Create the comms channel
|
|
30
|
+
const channel = new DirectCommsChannel();
|
|
31
|
+
|
|
32
|
+
// 2. Mount the Decorator module on the page
|
|
33
|
+
const decorator = new Decorator();
|
|
34
|
+
decorator.mount(window, channel.frame);
|
|
35
|
+
|
|
36
|
+
// 3. Create the controller
|
|
37
|
+
const ctrl = new DecorationController(channel.host);
|
|
38
|
+
|
|
39
|
+
// 4. Check style support before applying (TextColor requires the CSS Highlight API)
|
|
40
|
+
if (ctrl.supportsDecorationStyle(DecorationStyleType.TextColor)) {
|
|
41
|
+
// safe to use
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 5. Apply decorations — call again with a new array to update, empty array to clear
|
|
45
|
+
const locator = new Locator({
|
|
46
|
+
href: "chapter1.xhtml",
|
|
47
|
+
type: "application/xhtml+xml",
|
|
48
|
+
locations: new LocatorLocations({ /* e.g. fragments, progression, ... */ }),
|
|
49
|
+
});
|
|
50
|
+
ctrl.applyDecorations([
|
|
51
|
+
{
|
|
52
|
+
id: "tts-0",
|
|
53
|
+
locator,
|
|
54
|
+
style: { type: DecorationStyleType.Highlight, tint: "#FFFF00" },
|
|
55
|
+
}
|
|
56
|
+
], "tts");
|
|
57
|
+
|
|
58
|
+
// 6. Cleanup
|
|
59
|
+
decorator.unmount(window, channel.frame);
|
|
60
|
+
ctrl.destroy();
|
|
61
|
+
channel.frame.destroy();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `DecorationController`
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
class DecorationController {
|
|
70
|
+
constructor(host: DirectCommsHost, config?: DecorationControllerConfig)
|
|
71
|
+
|
|
72
|
+
// Returns true if the given style ID can be rendered.
|
|
73
|
+
// Returns false for TextColor when the CSS Highlight API is unavailable.
|
|
74
|
+
// Returns true for any ID registered in config.decorationTemplates.
|
|
75
|
+
supportsDecorationStyle(styleTypeId: string): boolean
|
|
76
|
+
|
|
77
|
+
// Replaces all decorations for a group. Diffs against previous state.
|
|
78
|
+
applyDecorations(decorations: Decoration[], group: string): void
|
|
79
|
+
|
|
80
|
+
// Registers an observer for a group.
|
|
81
|
+
// Activation is enabled for the group only when the observer declares onDecorationActivated.
|
|
82
|
+
// Hover tracking is enabled when the observer declares onDecorationPointerEnter or onDecorationPointerLeave.
|
|
83
|
+
registerDecorationObserver(group: string, observer: DecorationObserver): void
|
|
84
|
+
unregisterDecorationObserver(observer: DecorationObserver): void
|
|
85
|
+
|
|
86
|
+
destroy(): void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface DecorationControllerConfig {
|
|
90
|
+
// Named style templates. Register a template here and reference it by ID in decoration styles.
|
|
91
|
+
decorationTemplates?: Record<string, HTMLDecorationTemplate>;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `DecorationObserver`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface DecorationObserver {
|
|
99
|
+
// Called when a decoration is tapped/clicked. Return true to consume the event.
|
|
100
|
+
// Activation is enabled for the group only when this method is declared.
|
|
101
|
+
onDecorationActivated?(event: OnDecorationActivatedEvent): boolean;
|
|
102
|
+
|
|
103
|
+
// Called when the pointer enters a decoration.
|
|
104
|
+
// Registering either hover method automatically enables hover tracking for the group.
|
|
105
|
+
onDecorationPointerEnter?(event: OnDecorationPointerEnterEvent): void;
|
|
106
|
+
|
|
107
|
+
// Called when the pointer leaves a decoration.
|
|
108
|
+
// rect is absent if the decoration was removed from the DOM before leave fired.
|
|
109
|
+
// point is the current pointer position at the moment of leave.
|
|
110
|
+
onDecorationPointerLeave?(event: OnDecorationPointerLeaveEvent): void;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `DirectCommsChannel`
|
|
115
|
+
|
|
116
|
+
Connects the `DecorationController` to the `Decorator` module in the same JS context (e.g. controller and renderer sharing one `window`). For a controller and renderer split across a real iframe boundary, implement `IComms` yourself (e.g. over `postMessage`) instead.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const channel = new DirectCommsChannel();
|
|
120
|
+
channel.frame // pass to Decorator.mount
|
|
121
|
+
channel.host // pass to DecorationController constructor
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `Decorator`
|
|
125
|
+
|
|
126
|
+
Mounts and unmounts the decoration renderer on a `Window`/document.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
class Decorator {
|
|
130
|
+
mount(wnd: Window, comms: IComms): boolean
|
|
131
|
+
unmount(wnd: Window, comms: IComms): boolean
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `IComms`
|
|
136
|
+
|
|
137
|
+
The message-passing interface between a `DecorationController` (host side) and a `Decorator` (frame side). `DirectCommsChannel` implements this for you when both sides share a JS context; implement it yourself over `postMessage` when the renderer lives in a real iframe.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
interface IComms {
|
|
141
|
+
// Registers a callback for one or more command keys, scoped to a module name.
|
|
142
|
+
register(key: string | string[], module: string, callback: (data: unknown, ack: (ok: boolean) => void) => void): void
|
|
143
|
+
unregister(key: string | string[], module: string): void
|
|
144
|
+
unregisterAll(module: string): void
|
|
145
|
+
|
|
146
|
+
// Sends an event with a payload. The receiving side dispatches it to matching register()'d callbacks.
|
|
147
|
+
send(key: string, data: unknown): void
|
|
148
|
+
|
|
149
|
+
log(...data: unknown[]): void
|
|
150
|
+
readonly ready: boolean
|
|
151
|
+
destroy(): void
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Types
|
|
156
|
+
|
|
157
|
+
| Name | Notes |
|
|
158
|
+
|------|-------|
|
|
159
|
+
| `Decoration` | `{ id, locator, style, extras? }` — `locator` is a `Locator` from `@readium/shared` |
|
|
160
|
+
| `DecorationStyle` | `BuiltinDecorationStyle \| HTMLDecorationTemplate \| NamedDecorationStyle` |
|
|
161
|
+
| `BuiltinDecorationStyle` | `{ type?, tint?, layout?, width?, enforceContrast?, expand? }` |
|
|
162
|
+
| `HTMLDecorationTemplate` | `{ type: "template", layout, width, element, stylesheet? }` — `element` is a function `(decoration) => string`, resolved to HTML per decoration before rendering |
|
|
163
|
+
| `NamedDecorationStyle` | `{ type: string }` — reference to a style registered in `DecoratorConfig.decorationTemplates` |
|
|
164
|
+
| `DecoratorConfig` | `{ decorationTemplates? }` — same shape as `DecorationControllerConfig` (the latter is a type alias of this) |
|
|
165
|
+
| `DecorationStyleType` | `"highlight" \| "highlightUnderline" \| "underline" \| "strikethrough" \| "outline" \| "textColor" \| "mask" \| "template"` |
|
|
166
|
+
| `DecorationLayout` | `"boxes" \| "bounds"` |
|
|
167
|
+
| `DecorationWidth` | `"wrap" \| "viewport" \| "bounds" \| "page"` |
|
|
168
|
+
| `DecorationObserver` | `{ onDecorationActivated?, onDecorationPointerEnter?, onDecorationPointerLeave? }` |
|
|
169
|
+
| `OnDecorationActivatedEvent` | `{ decoration, group, rect, point }` |
|
|
170
|
+
| `OnDecorationPointerEnterEvent` | `{ decoration, group, rect, point }` |
|
|
171
|
+
| `OnDecorationPointerLeaveEvent` | `{ decoration, group, rect?, point? }` — rect absent if decoration removed from DOM before leave fired |
|
|
172
|
+
| `IComms` | Interface for the comms channel module side — see [`IComms`](#icomms) above |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class o{constructor(){this.queue=[],this.channel=typeof MessageChannel<"u"?new MessageChannel:void 0,this.channel&&(this.channel.port1.onmessage=()=>this.flush())}push(s){const t=this.queue.length===0;this.queue.push(s),t&&(this.channel?this.channel.port2.postMessage(null):setTimeout(()=>this.flush(),0))}flush(){const s=this.queue;this.queue=[],s.forEach(t=>t())}clear(){this.queue=[]}}class f{constructor(){this.frame=new c(this),this.host=new l(this)}}class c{constructor(s){this.channel=s,this.registrar=new Map,this.outbox=new o,this.ready=!0}register(s,t,e){(Array.isArray(s)?s:[s]).forEach(i=>{const h=this.registrar.get(i)??[];if(h.find(a=>a.module===t))throw new Error(`Duplicate callback for "${i}" in module "${t}"`);h.push({module:t,cb:e}),this.registrar.set(i,h)})}unregister(s,t){(Array.isArray(s)?s:[s]).forEach(r=>{const i=this.registrar.get(r);i&&this.registrar.set(r,i.filter(h=>h.module!==t))})}unregisterAll(s){this.registrar.forEach((t,e)=>{this.registrar.set(e,t.filter(r=>r.module!==s))})}_dispatch(s,t,e){const r=this.registrar.get(s);if(!r?.length){e(!1);return}r.forEach(i=>i.cb(t,e))}send(s,t){this.outbox.push(()=>this.channel.host._receive(s,t))}log(...s){this.outbox.push(()=>this.channel.host._receive("log",s))}destroy(){this.registrar.clear(),this.outbox.clear()}}class l{constructor(s){this.channel=s,this.listeners=new Map,this.outbox=new o,this.ready=!0}send(s,t,e){this.outbox.push(()=>this.channel.frame._dispatch(s,t,e??(()=>{})))}on(s,t){const e=this.listeners.get(s)??[];e.push(t),this.listeners.set(s,e)}off(s,t){const e=this.listeners.get(s);e&&this.listeners.set(s,e.filter(r=>r!==t))}_receive(s,t){this.listeners.get(s)?.forEach(e=>e(t))}}export{f as DirectCommsChannel,c as DirectCommsFrame,l as DirectCommsHost};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{supportsDecorationStyle as v,decorationsEqual as _,resolveDecorationForWire as d}from"../styles.js";class p{constructor(o,e={}){this.host=o,this._decorations=new Map,this._activationState=new Map,this._hoverState=new Map,this._observers=new Map,this._hoveredDecorations=new Map,this._config=e,o.on("decoration_activated",a=>{const t=a,r=this._decorations.get(t.group)?.find(i=>i.id===t.decorationId);r&&this._observers.get(t.group)?.forEach(i=>i.onDecorationActivated?.({group:t.group,decoration:r,rect:t.rect,point:t.point}))}),o.on("decoration_pointer_enter",a=>{const t=a,r=this._decorations.get(t.group)?.find(i=>i.id===t.decorationId);r&&(this._hoveredDecorations.set(t.group,r),this._observers.get(t.group)?.forEach(i=>i.onDecorationPointerEnter?.({group:t.group,decoration:r,rect:t.rect,point:t.point})))}),o.on("decoration_pointer_leave",a=>{const t=a,r=this._decorations.get(t.group)?.find(i=>i.id===t.decorationId)??this._hoveredDecorations.get(t.group);this._hoveredDecorations.delete(t.group),r&&this._observers.get(t.group)?.forEach(i=>i.onDecorationPointerLeave?.({group:t.group,decoration:r,rect:t.rect,point:t.point}))})}supportsDecorationStyle(o){return v(o,this._config.decorationTemplates)}applyDecorations(o,e){const a=this._decorations.get(e)??[],t=new Map(a.map(n=>[n.id,n])),r=new Map(o.map(n=>[n.id,n]));for(const[n,s]of t){const c=r.get(n);c?_(s,c)||this.host.send("decorate",{group:e,action:"update",decoration:d(c,this._config.decorationTemplates)}):this.host.send("decorate",{group:e,action:"remove",decoration:{id:n}})}for(const[n,s]of r)t.has(n)||this.host.send("decorate",{group:e,action:"add",decoration:d(s,this._config.decorationTemplates)});this._decorations.set(e,o);const i=this._activationState.get(e);i!==void 0&&this.host.send("decoration_activatable",{group:e,activatable:i});const h=this._hoverState.get(e);h!==void 0&&this.host.send("decoration_hoverable",{group:e,hoverable:h})}registerDecorationObserver(o,e){this._observers.has(o)||this._observers.set(o,new Set),this._observers.get(o).add(e),e.onDecorationActivated&&(this._activationState.set(o,!0),this.host.send("decoration_activatable",{group:o,activatable:!0})),(e.onDecorationPointerEnter||e.onDecorationPointerLeave)&&(this._hoverState.set(o,!0),this.host.send("decoration_hoverable",{group:o,hoverable:!0}))}unregisterDecorationObserver(o){this._observers.forEach((e,a)=>{if(!e.has(o))return;e.delete(o);const t=[...e].some(i=>i.onDecorationActivated);this._activationState.has(a)&&!t&&(this._activationState.delete(a),this.host.send("decoration_activatable",{group:a,activatable:!1}));const r=[...e].some(i=>i.onDecorationPointerEnter||i.onDecorationPointerLeave);this._hoverState.has(a)&&!r&&(this._hoverState.delete(a),this.host.send("decoration_hoverable",{group:a,hoverable:!1}))})}destroy(){this._decorations.clear(),this._activationState.clear(),this._hoverState.clear(),this._observers.clear(),this._hoveredDecorations.clear()}}export{p as DecorationController};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DecorationLayout as e,DecorationStyleType as t,DecorationWidth as a,Decorator as i}from"@readium/navigator-html-injectables";import{BUILTIN_DECORATION_TYPES as m,decorationsEqual as D,resolveDecorationForWire as n,supportsDecorationStyle as s}from"./styles.js";import{DirectCommsChannel as p,DirectCommsFrame as C,DirectCommsHost as f}from"./comms/direct.js";import{DecorationController as y}from"./controller/DecorationController.js";export{m as BUILTIN_DECORATION_TYPES,y as DecorationController,e as DecorationLayout,t as DecorationStyleType,a as DecorationWidth,i as Decorator,p as DirectCommsChannel,C as DirectCommsFrame,f as DirectCommsHost,D as decorationsEqual,n as resolveDecorationForWire,s as supportsDecorationStyle};
|
package/dist/styles.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DecorationStyleType as r}from"@readium/navigator-html-injectables";const u=new Set(Object.values(r));function f(t,e){return t===r.TextColor?typeof window<"u"&&"Highlight"in window:u.has(t)?!0:!!e?.[t]}function a(t,e){const{style:o}=t;if(o.type===r.Template){const n=o;return{...t,style:{...n,element:s(n,t)}}}if(o.type&&e?.[o.type]){const n=e[o.type];return{...t,style:{type:r.Template,layout:n.layout,width:n.width,stylesheet:n.stylesheet,element:s(n,t)}}}return t}function s(t,e){return typeof t.element=="function"?t.element(e):t.element}function y(t,e){if(t.type!==e.type)return!1;if(t.type===r.Template){const l=t,i=e;return l.layout===i.layout&&l.width===i.width&&l.stylesheet===i.stylesheet}const o=t,n=e;return o.tint===n.tint&&o.layout===n.layout&&o.width===n.width&&(o.enforceContrast??!0)===(n.enforceContrast??!0)&&(o.expand??0)===(n.expand??0)}function p(t,e){return t.locator.href===e.locator.href&&JSON.stringify(t.locator.locations?.serialize?.()??t.locator.locations)===JSON.stringify(e.locator.locations?.serialize?.()??e.locator.locations)&&JSON.stringify(t.locator.text??null)===JSON.stringify(e.locator.text??null)&&y(t.style,e.style)&&JSON.stringify(t.extras??null)===JSON.stringify(e.extras??null)}export{u as BUILTIN_DECORATION_TYPES,p as decorationsEqual,a as resolveDecorationForWire,f as supportsDecorationStyle};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readium/decorator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Standalone decoration controller and renderer for HTML content",
|
|
6
|
+
"author": "readium",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/readium/ts-toolkit.git",
|
|
10
|
+
"directory": "decorator"
|
|
11
|
+
},
|
|
12
|
+
"license": "BSD-3-Clause",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/readium/ts-toolkit/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/readium/ts-toolkit",
|
|
17
|
+
"types": "./types/src/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"ts-toolkit-source": "./src/index.ts",
|
|
21
|
+
"types": "./types/src/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src",
|
|
29
|
+
"types"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"clean": "rimraf types dist",
|
|
36
|
+
"build": "pnpm clean && tsc && vite build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@readium/navigator-html-injectables": "workspace:*",
|
|
40
|
+
"@readium/shared": "workspace:*"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"rimraf": "^6.1.2",
|
|
44
|
+
"tslib": "^2.8.1",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vite": "^7.3.1"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData } from "@readium/navigator-html-injectables";
|
|
2
|
+
import type { Decoration } from "./styles.ts";
|
|
3
|
+
|
|
4
|
+
export interface OnDecorationActivatedEvent<D = Decoration> extends Omit<DecorationActivatedEvent, "decorationId"> {
|
|
5
|
+
decoration: D;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type OnDecorationPointerEnterEvent<D = Decoration> = Omit<DecorationPointerEnterData, "decorationId"> & { decoration: D };
|
|
9
|
+
|
|
10
|
+
export type OnDecorationPointerLeaveEvent<D = Decoration> = Omit<DecorationPointerLeaveData, "decorationId"> & { decoration: D };
|
|
11
|
+
|
|
12
|
+
export interface DecorationObserver<D = Decoration> {
|
|
13
|
+
onDecorationActivated?(event: OnDecorationActivatedEvent<D>): boolean;
|
|
14
|
+
onDecorationPointerEnter?(event: OnDecorationPointerEnterEvent<D>): void;
|
|
15
|
+
onDecorationPointerLeave?(event: OnDecorationPointerLeaveEvent<D>): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { IComms, CommsCallback } from "@readium/navigator-html-injectables";
|
|
2
|
+
|
|
3
|
+
type AckFn = (ok: boolean) => void;
|
|
4
|
+
type EventListener = (data: unknown) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Defers pushed tasks to a macrotask, mirroring postMessage's scheduling (as opposed to a
|
|
8
|
+
* microtask) so the standalone direct-comms path behaves like the iframe/postMessage path:
|
|
9
|
+
* it never reenters the caller's stack, and it interleaves with rAF/observer callbacks the
|
|
10
|
+
* same way postMessage does. Tasks pushed within the same tick are flushed together, in order.
|
|
11
|
+
*/
|
|
12
|
+
class MacrotaskQueue {
|
|
13
|
+
private queue: (() => void)[] = [];
|
|
14
|
+
private readonly channel = typeof MessageChannel !== "undefined" ? new MessageChannel() : undefined;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
if (this.channel) this.channel.port1.onmessage = () => this.flush();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
push(task: () => void): void {
|
|
21
|
+
const wasEmpty = this.queue.length === 0;
|
|
22
|
+
this.queue.push(task);
|
|
23
|
+
if (wasEmpty) {
|
|
24
|
+
if (this.channel) this.channel.port2.postMessage(null);
|
|
25
|
+
else setTimeout(() => this.flush(), 0);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private flush(): void {
|
|
30
|
+
const tasks = this.queue;
|
|
31
|
+
this.queue = [];
|
|
32
|
+
tasks.forEach(task => task());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clear(): void {
|
|
36
|
+
this.queue = [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class DirectCommsChannel {
|
|
41
|
+
readonly frame: DirectCommsFrame;
|
|
42
|
+
readonly host: DirectCommsHost;
|
|
43
|
+
|
|
44
|
+
constructor() {
|
|
45
|
+
this.frame = new DirectCommsFrame(this);
|
|
46
|
+
this.host = new DirectCommsHost(this);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class DirectCommsFrame implements IComms {
|
|
51
|
+
private registrar = new Map<string, { module: string; cb: CommsCallback }[]>();
|
|
52
|
+
private readonly outbox = new MacrotaskQueue();
|
|
53
|
+
|
|
54
|
+
constructor(private readonly channel: DirectCommsChannel) {}
|
|
55
|
+
|
|
56
|
+
register(key: string | string[], module: string, callback: CommsCallback): void {
|
|
57
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
58
|
+
keys.forEach(k => {
|
|
59
|
+
const listeners = this.registrar.get(k) ?? [];
|
|
60
|
+
const existing = listeners.find(l => l.module === module);
|
|
61
|
+
if (existing) throw new Error(`Duplicate callback for "${k}" in module "${module}"`);
|
|
62
|
+
listeners.push({ module, cb: callback });
|
|
63
|
+
this.registrar.set(k, listeners);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
unregister(key: string | string[], module: string): void {
|
|
68
|
+
const keys = Array.isArray(key) ? key : [key];
|
|
69
|
+
keys.forEach(k => {
|
|
70
|
+
const ls = this.registrar.get(k);
|
|
71
|
+
if (!ls) return;
|
|
72
|
+
this.registrar.set(k, ls.filter(l => l.module !== module));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
unregisterAll(module: string): void {
|
|
77
|
+
this.registrar.forEach((ls, k) => {
|
|
78
|
+
this.registrar.set(k, ls.filter(l => l.module !== module));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_dispatch(key: string, data: unknown, ack: AckFn): void {
|
|
83
|
+
const ls = this.registrar.get(key);
|
|
84
|
+
if (!ls?.length) { ack(false); return; }
|
|
85
|
+
ls.forEach(l => l.cb(data, ack));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
send(key: string, data: unknown): void {
|
|
89
|
+
this.outbox.push(() => this.channel.host._receive(key, data));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log(...data: unknown[]): void {
|
|
93
|
+
this.outbox.push(() => this.channel.host._receive("log", data));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
readonly ready = true;
|
|
97
|
+
|
|
98
|
+
destroy(): void {
|
|
99
|
+
this.registrar.clear();
|
|
100
|
+
this.outbox.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class DirectCommsHost {
|
|
105
|
+
private listeners = new Map<string, EventListener[]>();
|
|
106
|
+
private readonly outbox = new MacrotaskQueue();
|
|
107
|
+
|
|
108
|
+
constructor(private readonly channel: DirectCommsChannel) {}
|
|
109
|
+
|
|
110
|
+
send(key: string, data: unknown, callback?: AckFn): void {
|
|
111
|
+
this.outbox.push(() => this.channel.frame._dispatch(key, data, callback ?? (() => {})));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
on(key: string, cb: EventListener): void {
|
|
115
|
+
const ls = this.listeners.get(key) ?? [];
|
|
116
|
+
ls.push(cb);
|
|
117
|
+
this.listeners.set(key, ls);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
off(key: string, cb: EventListener): void {
|
|
121
|
+
const ls = this.listeners.get(key);
|
|
122
|
+
if (ls) this.listeners.set(key, ls.filter(l => l !== cb));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_receive(key: string, data: unknown): void {
|
|
126
|
+
this.listeners.get(key)?.forEach(cb => cb(data));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
readonly ready = true;
|
|
130
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData } from "@readium/navigator-html-injectables";
|
|
2
|
+
import type { DirectCommsHost } from "../comms/direct.ts";
|
|
3
|
+
import type { DecorationObserver } from "../Decoration.ts";
|
|
4
|
+
import type { Decoration, DecoratorConfig } from "../styles.ts";
|
|
5
|
+
import { decorationsEqual, resolveDecorationForWire, supportsDecorationStyle } from "../styles.ts";
|
|
6
|
+
|
|
7
|
+
export type DecorationControllerConfig = DecoratorConfig;
|
|
8
|
+
|
|
9
|
+
export class DecorationController {
|
|
10
|
+
private _decorations = new Map<string, Decoration[]>();
|
|
11
|
+
private _activationState = new Map<string, boolean>();
|
|
12
|
+
private _hoverState = new Map<string, boolean>();
|
|
13
|
+
private _observers = new Map<string, Set<DecorationObserver>>();
|
|
14
|
+
private _hoveredDecorations = new Map<string, Decoration>();
|
|
15
|
+
private readonly _config: DecorationControllerConfig;
|
|
16
|
+
|
|
17
|
+
constructor(private readonly host: DirectCommsHost, config: DecorationControllerConfig = {}) {
|
|
18
|
+
this._config = config;
|
|
19
|
+
host.on("decoration_activated", (raw) => {
|
|
20
|
+
const ev = raw as DecorationActivatedEvent;
|
|
21
|
+
const decoration = this._decorations.get(ev.group)?.find(d => d.id === ev.decorationId);
|
|
22
|
+
if (!decoration) return;
|
|
23
|
+
this._observers.get(ev.group)?.forEach(obs =>
|
|
24
|
+
obs.onDecorationActivated?.({ group: ev.group, decoration, rect: ev.rect, point: ev.point })
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
host.on("decoration_pointer_enter", (raw) => {
|
|
29
|
+
const ev = raw as DecorationPointerEnterData;
|
|
30
|
+
const decoration = this._decorations.get(ev.group)?.find(d => d.id === ev.decorationId);
|
|
31
|
+
if (!decoration) return;
|
|
32
|
+
this._hoveredDecorations.set(ev.group, decoration);
|
|
33
|
+
this._observers.get(ev.group)?.forEach(obs =>
|
|
34
|
+
obs.onDecorationPointerEnter?.({ group: ev.group, decoration, rect: ev.rect, point: ev.point })
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
host.on("decoration_pointer_leave", (raw) => {
|
|
39
|
+
const ev = raw as DecorationPointerLeaveData;
|
|
40
|
+
const decoration = this._decorations.get(ev.group)?.find(d => d.id === ev.decorationId)
|
|
41
|
+
?? this._hoveredDecorations.get(ev.group);
|
|
42
|
+
this._hoveredDecorations.delete(ev.group);
|
|
43
|
+
if (!decoration) return;
|
|
44
|
+
this._observers.get(ev.group)?.forEach(obs =>
|
|
45
|
+
obs.onDecorationPointerLeave?.({ group: ev.group, decoration, rect: ev.rect, point: ev.point })
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
supportsDecorationStyle(styleTypeId: string): boolean {
|
|
51
|
+
return supportsDecorationStyle(styleTypeId, this._config.decorationTemplates);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
applyDecorations(decorations: Decoration[], group: string): void {
|
|
55
|
+
const previous = this._decorations.get(group) ?? [];
|
|
56
|
+
const prevById = new Map(previous.map(d => [d.id, d]));
|
|
57
|
+
const nextById = new Map(decorations.map(d => [d.id, d]));
|
|
58
|
+
|
|
59
|
+
for (const [id, prev] of prevById) {
|
|
60
|
+
const next = nextById.get(id);
|
|
61
|
+
if (!next) {
|
|
62
|
+
this.host.send("decorate", { group, action: "remove", decoration: { id } });
|
|
63
|
+
} else if (!decorationsEqual(prev, next)) {
|
|
64
|
+
this.host.send("decorate", { group, action: "update", decoration: resolveDecorationForWire(next, this._config.decorationTemplates) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const [id, next] of nextById) {
|
|
68
|
+
if (!prevById.has(id)) {
|
|
69
|
+
this.host.send("decorate", { group, action: "add", decoration: resolveDecorationForWire(next, this._config.decorationTemplates) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._decorations.set(group, decorations);
|
|
74
|
+
const activatable = this._activationState.get(group);
|
|
75
|
+
if (activatable !== undefined) {
|
|
76
|
+
this.host.send("decoration_activatable", { group, activatable });
|
|
77
|
+
}
|
|
78
|
+
const hoverable = this._hoverState.get(group);
|
|
79
|
+
if (hoverable !== undefined) {
|
|
80
|
+
this.host.send("decoration_hoverable", { group, hoverable });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
registerDecorationObserver(group: string, observer: DecorationObserver): void {
|
|
85
|
+
if (!this._observers.has(group)) this._observers.set(group, new Set());
|
|
86
|
+
this._observers.get(group)!.add(observer);
|
|
87
|
+
if (observer.onDecorationActivated) {
|
|
88
|
+
this._activationState.set(group, true);
|
|
89
|
+
this.host.send("decoration_activatable", { group, activatable: true });
|
|
90
|
+
}
|
|
91
|
+
if (observer.onDecorationPointerEnter || observer.onDecorationPointerLeave) {
|
|
92
|
+
this._hoverState.set(group, true);
|
|
93
|
+
this.host.send("decoration_hoverable", { group, hoverable: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
unregisterDecorationObserver(observer: DecorationObserver): void {
|
|
98
|
+
this._observers.forEach((set, group) => {
|
|
99
|
+
if (!set.has(observer)) return;
|
|
100
|
+
set.delete(observer);
|
|
101
|
+
|
|
102
|
+
const stillActivatable = [...set].some(o => o.onDecorationActivated);
|
|
103
|
+
if (this._activationState.has(group) && !stillActivatable) {
|
|
104
|
+
this._activationState.delete(group);
|
|
105
|
+
this.host.send("decoration_activatable", { group, activatable: false });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const stillHoverable = [...set].some(o => o.onDecorationPointerEnter || o.onDecorationPointerLeave);
|
|
109
|
+
if (this._hoverState.has(group) && !stillHoverable) {
|
|
110
|
+
this._hoverState.delete(group);
|
|
111
|
+
this.host.send("decoration_hoverable", { group, hoverable: false });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
destroy(): void {
|
|
117
|
+
this._decorations.clear();
|
|
118
|
+
this._activationState.clear();
|
|
119
|
+
this._hoverState.clear();
|
|
120
|
+
this._observers.clear();
|
|
121
|
+
this._hoveredDecorations.clear();
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { IComms } from "@readium/navigator-html-injectables";
|
|
2
|
+
export { Decorator } from "@readium/navigator-html-injectables";
|
|
3
|
+
export type {
|
|
4
|
+
DecoratorRequest,
|
|
5
|
+
DecorationActivatedEvent as DecorationActivatedWireEvent,
|
|
6
|
+
} from "@readium/navigator-html-injectables";
|
|
7
|
+
export {
|
|
8
|
+
DecorationStyleType,
|
|
9
|
+
DecorationLayout,
|
|
10
|
+
DecorationWidth,
|
|
11
|
+
} from "@readium/navigator-html-injectables";
|
|
12
|
+
|
|
13
|
+
export * from "./styles.ts";
|
|
14
|
+
export * from "./comms/direct.ts";
|
|
15
|
+
export * from "./Decoration.ts";
|
|
16
|
+
export * from "./controller/DecorationController.ts";
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Decoration as InjectableDecoration,
|
|
3
|
+
BuiltinDecorationStyle,
|
|
4
|
+
HTMLDecorationTemplate as WireHTMLDecorationTemplate,
|
|
5
|
+
} from "@readium/navigator-html-injectables";
|
|
6
|
+
import { DecorationStyleType } from "@readium/navigator-html-injectables";
|
|
7
|
+
|
|
8
|
+
export type { BuiltinDecorationStyle };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Author-level decoration template. `element` is a function called once per decoration to
|
|
12
|
+
* generate the HTML string that the injectable will render. The result is resolved
|
|
13
|
+
* (via {@link resolveDecorationForWire}) before postMessage and sanitized by the injectable.
|
|
14
|
+
*/
|
|
15
|
+
export interface HTMLDecorationTemplate extends Omit<WireHTMLDecorationTemplate, "element"> {
|
|
16
|
+
element: (decoration: Decoration) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A reference to a named style registered in `DecoratorConfig.decorationTemplates`. */
|
|
20
|
+
export interface NamedDecorationStyle {
|
|
21
|
+
type: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DecorationStyle = BuiltinDecorationStyle | HTMLDecorationTemplate | NamedDecorationStyle;
|
|
25
|
+
|
|
26
|
+
export interface Decoration extends Omit<InjectableDecoration, "style"> {
|
|
27
|
+
style: DecorationStyle;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Configuration for decoration rendering. */
|
|
31
|
+
export interface DecoratorConfig {
|
|
32
|
+
/**
|
|
33
|
+
* Named custom styles. Each key is a style type ID; the value is the template that
|
|
34
|
+
* generates the HTML for decorations of that type. When a decoration's `style.type`
|
|
35
|
+
* matches a key here, the template is resolved and sent to the injectable as a
|
|
36
|
+
* Template-type decoration.
|
|
37
|
+
*/
|
|
38
|
+
decorationTemplates?: Record<string, HTMLDecorationTemplate>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const BUILTIN_DECORATION_TYPES = new Set<string>(Object.values(DecorationStyleType));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns whether the given style type ID can be rendered.
|
|
45
|
+
* True for all built-in types and any IDs registered in `decorationTemplates`.
|
|
46
|
+
* TextColor additionally requires the CSS Highlight API.
|
|
47
|
+
*/
|
|
48
|
+
export function supportsDecorationStyle(
|
|
49
|
+
styleTypeId: string,
|
|
50
|
+
decorationTemplates?: Record<string, HTMLDecorationTemplate>
|
|
51
|
+
): boolean {
|
|
52
|
+
if (styleTypeId === DecorationStyleType.TextColor) return typeof window !== "undefined" && "Highlight" in window;
|
|
53
|
+
if (BUILTIN_DECORATION_TYPES.has(styleTypeId)) return true;
|
|
54
|
+
return !!decorationTemplates?.[styleTypeId];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolves an author-level Decoration to a wire-safe plain object for postMessage.
|
|
59
|
+
* For Template styles, calls `element(decoration)` (or passes through an already-resolved
|
|
60
|
+
* string) and embeds the resulting HTML string. For registered custom style IDs (found in
|
|
61
|
+
* `decorationTemplates`), resolves the template and converts the style to a Template wire object.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveDecorationForWire(
|
|
64
|
+
decoration: Decoration,
|
|
65
|
+
decorationTemplates?: Record<string, HTMLDecorationTemplate>
|
|
66
|
+
): unknown {
|
|
67
|
+
const { style } = decoration;
|
|
68
|
+
if (style.type === DecorationStyleType.Template) {
|
|
69
|
+
const tpl = style as HTMLDecorationTemplate;
|
|
70
|
+
return { ...decoration, style: { ...tpl, element: resolveElement(tpl, decoration) } };
|
|
71
|
+
}
|
|
72
|
+
if (style.type && decorationTemplates?.[style.type]) {
|
|
73
|
+
const tpl = decorationTemplates[style.type];
|
|
74
|
+
return {
|
|
75
|
+
...decoration,
|
|
76
|
+
style: {
|
|
77
|
+
type: DecorationStyleType.Template,
|
|
78
|
+
layout: tpl.layout,
|
|
79
|
+
width: tpl.width,
|
|
80
|
+
stylesheet: tpl.stylesheet,
|
|
81
|
+
element: resolveElement(tpl, decoration),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return decoration;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveElement(tpl: HTMLDecorationTemplate, decoration: Decoration): string {
|
|
89
|
+
// A standalone caller may pass an already-resolved (string) element.
|
|
90
|
+
return typeof tpl.element === "function" ? tpl.element(decoration) : (tpl.element as unknown as string);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stylesEqual(a: DecorationStyle, b: DecorationStyle): boolean {
|
|
94
|
+
if (a.type !== b.type) return false;
|
|
95
|
+
if (a.type === DecorationStyleType.Template) {
|
|
96
|
+
const ta = a as HTMLDecorationTemplate;
|
|
97
|
+
const tb = b as HTMLDecorationTemplate;
|
|
98
|
+
// element is a function — not comparable by value; excluded from equality
|
|
99
|
+
return ta.layout === tb.layout &&
|
|
100
|
+
ta.width === tb.width &&
|
|
101
|
+
ta.stylesheet === tb.stylesheet;
|
|
102
|
+
}
|
|
103
|
+
const ba = a as BuiltinDecorationStyle;
|
|
104
|
+
const bb = b as BuiltinDecorationStyle;
|
|
105
|
+
return ba.tint === bb.tint &&
|
|
106
|
+
ba.layout === bb.layout &&
|
|
107
|
+
ba.width === bb.width &&
|
|
108
|
+
(ba.enforceContrast ?? true) === (bb.enforceContrast ?? true) &&
|
|
109
|
+
(ba.expand ?? 0) === (bb.expand ?? 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function decorationsEqual(a: Decoration, b: Decoration): boolean {
|
|
113
|
+
return (
|
|
114
|
+
a.locator.href === b.locator.href &&
|
|
115
|
+
JSON.stringify((a.locator.locations as any)?.serialize?.() ?? a.locator.locations) ===
|
|
116
|
+
JSON.stringify((b.locator.locations as any)?.serialize?.() ?? b.locator.locations) &&
|
|
117
|
+
JSON.stringify(a.locator.text ?? null) === JSON.stringify(b.locator.text ?? null) &&
|
|
118
|
+
stylesEqual(a.style, b.style) &&
|
|
119
|
+
JSON.stringify(a.extras ?? null) === JSON.stringify(b.extras ?? null)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DecorationActivatedEvent, DecorationPointerEnterData, DecorationPointerLeaveData } from "@readium/navigator-html-injectables";
|
|
2
|
+
import type { Decoration } from "./styles.ts";
|
|
3
|
+
export interface OnDecorationActivatedEvent<D = Decoration> extends Omit<DecorationActivatedEvent, "decorationId"> {
|
|
4
|
+
decoration: D;
|
|
5
|
+
}
|
|
6
|
+
export type OnDecorationPointerEnterEvent<D = Decoration> = Omit<DecorationPointerEnterData, "decorationId"> & {
|
|
7
|
+
decoration: D;
|
|
8
|
+
};
|
|
9
|
+
export type OnDecorationPointerLeaveEvent<D = Decoration> = Omit<DecorationPointerLeaveData, "decorationId"> & {
|
|
10
|
+
decoration: D;
|
|
11
|
+
};
|
|
12
|
+
export interface DecorationObserver<D = Decoration> {
|
|
13
|
+
onDecorationActivated?(event: OnDecorationActivatedEvent<D>): boolean;
|
|
14
|
+
onDecorationPointerEnter?(event: OnDecorationPointerEnterEvent<D>): void;
|
|
15
|
+
onDecorationPointerLeave?(event: OnDecorationPointerLeaveEvent<D>): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IComms, CommsCallback } from "@readium/navigator-html-injectables";
|
|
2
|
+
type AckFn = (ok: boolean) => void;
|
|
3
|
+
type EventListener = (data: unknown) => void;
|
|
4
|
+
export declare class DirectCommsChannel {
|
|
5
|
+
readonly frame: DirectCommsFrame;
|
|
6
|
+
readonly host: DirectCommsHost;
|
|
7
|
+
constructor();
|
|
8
|
+
}
|
|
9
|
+
export declare class DirectCommsFrame implements IComms {
|
|
10
|
+
private readonly channel;
|
|
11
|
+
private registrar;
|
|
12
|
+
private readonly outbox;
|
|
13
|
+
constructor(channel: DirectCommsChannel);
|
|
14
|
+
register(key: string | string[], module: string, callback: CommsCallback): void;
|
|
15
|
+
unregister(key: string | string[], module: string): void;
|
|
16
|
+
unregisterAll(module: string): void;
|
|
17
|
+
_dispatch(key: string, data: unknown, ack: AckFn): void;
|
|
18
|
+
send(key: string, data: unknown): void;
|
|
19
|
+
log(...data: unknown[]): void;
|
|
20
|
+
readonly ready = true;
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class DirectCommsHost {
|
|
24
|
+
private readonly channel;
|
|
25
|
+
private listeners;
|
|
26
|
+
private readonly outbox;
|
|
27
|
+
constructor(channel: DirectCommsChannel);
|
|
28
|
+
send(key: string, data: unknown, callback?: AckFn): void;
|
|
29
|
+
on(key: string, cb: EventListener): void;
|
|
30
|
+
off(key: string, cb: EventListener): void;
|
|
31
|
+
_receive(key: string, data: unknown): void;
|
|
32
|
+
readonly ready = true;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DirectCommsHost } from "../comms/direct.ts";
|
|
2
|
+
import type { DecorationObserver } from "../Decoration.ts";
|
|
3
|
+
import type { Decoration, DecoratorConfig } from "../styles.ts";
|
|
4
|
+
export type DecorationControllerConfig = DecoratorConfig;
|
|
5
|
+
export declare class DecorationController {
|
|
6
|
+
private readonly host;
|
|
7
|
+
private _decorations;
|
|
8
|
+
private _activationState;
|
|
9
|
+
private _hoverState;
|
|
10
|
+
private _observers;
|
|
11
|
+
private _hoveredDecorations;
|
|
12
|
+
private readonly _config;
|
|
13
|
+
constructor(host: DirectCommsHost, config?: DecorationControllerConfig);
|
|
14
|
+
supportsDecorationStyle(styleTypeId: string): boolean;
|
|
15
|
+
applyDecorations(decorations: Decoration[], group: string): void;
|
|
16
|
+
registerDecorationObserver(group: string, observer: DecorationObserver): void;
|
|
17
|
+
unregisterDecorationObserver(observer: DecorationObserver): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { IComms } from "@readium/navigator-html-injectables";
|
|
2
|
+
export { Decorator } from "@readium/navigator-html-injectables";
|
|
3
|
+
export type { DecoratorRequest, DecorationActivatedEvent as DecorationActivatedWireEvent, } from "@readium/navigator-html-injectables";
|
|
4
|
+
export { DecorationStyleType, DecorationLayout, DecorationWidth, } from "@readium/navigator-html-injectables";
|
|
5
|
+
export * from "./styles.ts";
|
|
6
|
+
export * from "./comms/direct.ts";
|
|
7
|
+
export * from "./Decoration.ts";
|
|
8
|
+
export * from "./controller/DecorationController.ts";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Decoration as InjectableDecoration, BuiltinDecorationStyle, HTMLDecorationTemplate as WireHTMLDecorationTemplate } from "@readium/navigator-html-injectables";
|
|
2
|
+
export type { BuiltinDecorationStyle };
|
|
3
|
+
/**
|
|
4
|
+
* Author-level decoration template. `element` is a function called once per decoration to
|
|
5
|
+
* generate the HTML string that the injectable will render. The result is resolved
|
|
6
|
+
* (via {@link resolveDecorationForWire}) before postMessage and sanitized by the injectable.
|
|
7
|
+
*/
|
|
8
|
+
export interface HTMLDecorationTemplate extends Omit<WireHTMLDecorationTemplate, "element"> {
|
|
9
|
+
element: (decoration: Decoration) => string;
|
|
10
|
+
}
|
|
11
|
+
/** A reference to a named style registered in `DecoratorConfig.decorationTemplates`. */
|
|
12
|
+
export interface NamedDecorationStyle {
|
|
13
|
+
type: string;
|
|
14
|
+
}
|
|
15
|
+
export type DecorationStyle = BuiltinDecorationStyle | HTMLDecorationTemplate | NamedDecorationStyle;
|
|
16
|
+
export interface Decoration extends Omit<InjectableDecoration, "style"> {
|
|
17
|
+
style: DecorationStyle;
|
|
18
|
+
}
|
|
19
|
+
/** Configuration for decoration rendering. */
|
|
20
|
+
export interface DecoratorConfig {
|
|
21
|
+
/**
|
|
22
|
+
* Named custom styles. Each key is a style type ID; the value is the template that
|
|
23
|
+
* generates the HTML for decorations of that type. When a decoration's `style.type`
|
|
24
|
+
* matches a key here, the template is resolved and sent to the injectable as a
|
|
25
|
+
* Template-type decoration.
|
|
26
|
+
*/
|
|
27
|
+
decorationTemplates?: Record<string, HTMLDecorationTemplate>;
|
|
28
|
+
}
|
|
29
|
+
export declare const BUILTIN_DECORATION_TYPES: Set<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Returns whether the given style type ID can be rendered.
|
|
32
|
+
* True for all built-in types and any IDs registered in `decorationTemplates`.
|
|
33
|
+
* TextColor additionally requires the CSS Highlight API.
|
|
34
|
+
*/
|
|
35
|
+
export declare function supportsDecorationStyle(styleTypeId: string, decorationTemplates?: Record<string, HTMLDecorationTemplate>): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves an author-level Decoration to a wire-safe plain object for postMessage.
|
|
38
|
+
* For Template styles, calls `element(decoration)` (or passes through an already-resolved
|
|
39
|
+
* string) and embeds the resulting HTML string. For registered custom style IDs (found in
|
|
40
|
+
* `decorationTemplates`), resolves the template and converts the style to a Template wire object.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveDecorationForWire(decoration: Decoration, decorationTemplates?: Record<string, HTMLDecorationTemplate>): unknown;
|
|
43
|
+
export declare function decorationsEqual(a: Decoration, b: Decoration): boolean;
|