forked from readium/ts-toolkit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Navigator + navigator-html-injectables implementation (readium#12)
* initial navigator-html * add basic swipe handling in ColumnSnapper * address PR review issues * updated column snapper * add will-change for animation * fix safari columnsnapper behavior * use transform3d instead of left for snapper * make using transform an option * more work on scroll snapper engine * switch to yarn pnp for dependencies * Work on EPUB Navigator * adjustments to epub navigator for interoperability * framepool improvements * make iframe pool link to mounted elements - moves frame display management from navigator to framepoolmanager * add logging to comms, add message buffers, bugfix * add stricter frame-side comms enforcement * move single js package to individual packages in a workspace * revert CI path changes * add basic builds for packages, readmes, other tweaks * attempt to fix size-limit * move to using pnpm * update CI to use pnpm * temporarily disable size.yml * fix * fix lint command * be explicit about commands in CI * prepare for demo reader * add unregisterAll function * add progress event to ColumnSnapper * reject non-matching comms version * fix iframe resuming * add more progress reports to columnsnapper * epubnavigator improvements * demo -> testapp * navigator-html -> navigator-html-injectables * conslidate imports * ColumnSnapper fixes * comment out showToc logic * use more accurate progression * add protect/unprotect commands, implement in snapper * bugfix * distinguish between touch and tap * test attempt to fix tapping issue * columnsnapper/general iframe improvements * add pointer events disable to hidden iframes * columnsnapper/frame tweaks * block parallel update requests for frame pool * fix accuracy of log source in navigator * more columnsnapper fixes * reflowable reader improvments * reduce FOUC * replace wentBack with reduced FOUC * add Oscar's MVP ScrollSnapper, address bugs * add initialPosition to EpubNavigator * replace unknown event with customEvent callback * scrollsnapper changes (by Oscar) * export HttpResource * support live alterations to self of publication * change destination origin * Oscar: remove scrollbars in ScrollSnapper * add "HttpClient" equivalent to HttpFetcher * Oscar: remove duplicate reportprogress call * implement goto specific progression in resource * PR suggestions from @dysbulic * address PR comments from @mickael-menu
- Loading branch information
1 parent
44d9473
commit ca5f465
Showing
59 changed files
with
11,041 additions
and
25,441 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,16 @@ on: | |
push: | ||
paths: | ||
- 'shared/**' | ||
- 'streamer/**' | ||
- 'navigator/**' | ||
- 'src/**' | ||
- '.github/workflows/CI-shared.yml' | ||
- '.github/workflows/CI.yml' | ||
|
||
jobs: | ||
build: | ||
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} | ||
|
||
runs-on: ${{ matrix.os }} | ||
defaults: | ||
run: | ||
working-directory: ./shared | ||
strategy: | ||
matrix: | ||
node: ['14.x', '16.x'] | ||
|
@@ -22,19 +22,23 @@ jobs: | |
- name: Checkout repo | ||
uses: actions/checkout@v2 | ||
|
||
- name: Set up pnpm | ||
uses: pnpm/[email protected] | ||
|
||
- name: Use Node ${{ matrix.node }} | ||
uses: actions/setup-node@v1 | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: ${{ matrix.node }} | ||
cache: 'pnpm' | ||
|
||
- name: Install deps and build (with cache) | ||
uses: bahmutov/npm-install@v1 | ||
- name: Install deps and build | ||
run: pnpm install | ||
|
||
- name: Lint | ||
run: yarn lint | ||
run: pnpm run lint | ||
|
||
- name: Test | ||
run: yarn test --ci --coverage --maxWorkers=2 | ||
run: pnpm run test --ci --coverage --maxWorkers=2 | ||
|
||
- name: Build | ||
run: yarn build | ||
run: pnpm run build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,3 @@ | ||
*.log | ||
.DS_Store | ||
node_modules | ||
dist | ||
types | ||
coverage | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"typescript.tsdk": "node_modules/typescript/lib" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
root = true | ||
|
||
[*] | ||
end_of_line = lf | ||
insert_final_newline = true | ||
|
||
[*.{js,json,yml}] | ||
charset = utf-8 | ||
indent_style = space | ||
indent_size = 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
*.log | ||
.DS_Store | ||
node_modules | ||
dist | ||
types | ||
coverage | ||
|
||
# Yarn | ||
.yarn/* | ||
!.yarn/cache | ||
!.yarn/releases | ||
!.yarn/plugins | ||
!.yarn/sdks | ||
!.yarn/versions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# navigator-html-injectables | ||
|
||
This package can be used either inside a reflowable (X)HTML or outside (in a javascript environment) to provide access and control over a resource from a navigator on any modern browser or embedded browser frame. This is a replacement for the javascript stubs found in the mobile SDKs, such as [this](https://github.com/readium/kotlin-toolkit/tree/develop/readium/navigator/src/main/assets/_scripts/src). | ||
|
||
Special care should be taken to make the final produced build compatible with a set of browsers that are older than what you'd typically support on the web, since this SDK can be used in a mobile app's webview, which is an environment that tends to run on an outdated web engine. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
{ | ||
"name": "@readium/navigator-html-injectables", | ||
"version": "0.0.1", | ||
"description": "An embeddable solution for connecting frames of HTML publications with a Readium Navigator", | ||
"author": "readium", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/readium/web.git" | ||
}, | ||
"license": "BSD-3-Clause", | ||
"bugs": { | ||
"url": "https://github.com/readium/ts-toolkit/issues" | ||
}, | ||
"homepage": "https://github.com/readium/ts-toolkit", | ||
"keywords": [ | ||
"readium", | ||
"web", | ||
"epub", | ||
"html", | ||
"reflowable", | ||
"embedded" | ||
], | ||
"scripts": { | ||
"build": "esbuild src/index.ts --bundle --minify --sourcemap --target=es6,chrome58,firefox57,safari11,edge16 --outfile=dist/index.js" | ||
}, | ||
"main": "dist/index.js", | ||
"types": "types/index.d.ts", | ||
"files": [ | ||
"dist", | ||
"src", | ||
"types" | ||
], | ||
"engines": { | ||
"node": ">=14" | ||
}, | ||
"prettier": { | ||
"printWidth": 80, | ||
"semi": true, | ||
"singleQuote": true, | ||
"trailingComma": "es5" | ||
}, | ||
"module": "dist/index.js", | ||
"devDependencies": { | ||
"tslib": "^2.3.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { Comms } from "./comms/comms"; | ||
import { Module, ModuleDerived, ModuleLibrary, ModuleName } from "./modules"; | ||
|
||
/** | ||
* The Module loader. Handles initialization of the HTML injectables | ||
* in the target Window, which could be an IFrame, or the current window). | ||
*/ | ||
export class Loader<T extends string = ModuleName> { | ||
private loadedModules: Module[] = []; | ||
private readonly wnd: Window; | ||
private readonly comms: Comms; | ||
|
||
/** | ||
* @param wnd Window instance to operate on | ||
* @param initialModules List of initial modules to load | ||
*/ | ||
constructor(wnd: Window = window, initialModules: string[] = []) { | ||
this.wnd = wnd; // Window instance | ||
this.comms = new Comms(wnd); | ||
|
||
const uniqueModules = [...new Set(initialModules)]; // Deduplicate initial module list | ||
if(!uniqueModules.length) return; // No initial modules | ||
|
||
if(typeof wnd === 'undefined') // Detect accidental Node/SSR usage | ||
throw Error("Loader is not in a web browser"); | ||
|
||
if(wnd.parent !== wnd) this.comms.log("Loader is probably in a frame"); | ||
|
||
this.loadedModules = uniqueModules.map(name => { | ||
const nm = this.loadModule(name); | ||
if(!nm) return; | ||
nm.mount(this.wnd, this.comms); // Mount module | ||
return nm; | ||
}).filter(m => m !== undefined) as Module[]; // Filter out all modules not found | ||
} | ||
|
||
private loadModule(moduleName: string) { | ||
const m = ModuleLibrary.get(moduleName); // Find a module with this name | ||
if(m === undefined) { | ||
this.comms.log(`Module "${name}" does not exist in the library`) | ||
return m; | ||
} | ||
return new m(); // Construct module | ||
} | ||
|
||
/** | ||
* Add a module by name | ||
* @param moduleName Module name | ||
* @returns Success | ||
*/ | ||
public addModule(moduleName: T): boolean { | ||
const nm = this.loadModule(moduleName); | ||
if(!nm || !nm.mount(this.wnd, this.comms)) return false; // Mount module | ||
this.loadedModules.push(nm); // Add module to list | ||
return true; | ||
} | ||
|
||
/** | ||
* Remove a module by name | ||
* @param moduleName Module name | ||
* @returns Success | ||
*/ | ||
public removeModule(moduleName: T): boolean { | ||
const m = ModuleLibrary.get(moduleName) as ModuleDerived; // Get the right class | ||
if(m === undefined) { | ||
this.comms.log(`Module "${moduleName}" does not exist in the library`) | ||
return false; | ||
} | ||
const index = this.loadedModules.findIndex(lm => lm instanceof m); // Find module | ||
if(index < 0) return false; // Module not found | ||
this.loadedModules[index].unmount(this.wnd, this.comms); // Unmount module | ||
this.loadedModules.splice(index, 1); // Remove module | ||
return true; | ||
} | ||
|
||
/** | ||
* Unmount and remove all modules | ||
*/ | ||
public destroy() { | ||
this.comms.destroy(); | ||
this.loadedModules.forEach(m => m.unmount(this.wnd, this.comms)); | ||
this.loadedModules = []; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { CommsEventKey, CommsCommandKey } from "./keys"; | ||
import { mid } from "./mid"; | ||
|
||
export const COMMS_VERSION = 1; | ||
|
||
export interface CommsMessage { | ||
_readium: number; // Sanity/version-checking field | ||
_channel: string; // Channel ID | ||
id?: string; // Optional (but recommended!) unique identifier | ||
strict?: boolean; // Whether or not the event *must* be handled by the receiver | ||
key: CommsEventKey | CommsCommandKey; // The "key" for identification to the listener | ||
data: unknown; // The data to be sent to the module | ||
} | ||
|
||
export interface Registrant { | ||
module: string; | ||
cb: CommsCallback; | ||
} | ||
export type CommsAck = (ok: boolean) => void; | ||
export type CommsCallback = (data: unknown, ack: CommsAck) => void; // TODO: maybe more than void? | ||
|
||
/** | ||
* Comms is basically a wrapper around window.postMessage that | ||
* adds structure to the messages and lets modules register callbacks. | ||
*/ | ||
export class Comms { | ||
private readonly wnd: Window; | ||
private destination: MessageEventSource | null = null; | ||
private registrar = new Map<CommsCommandKey, Registrant[]>(); | ||
private origin: string = ""; | ||
private channelId: string = ""; | ||
|
||
constructor(wnd: Window) { | ||
this.wnd = wnd; | ||
wnd.addEventListener("message", this.receiver); | ||
} | ||
|
||
private receive(event: MessageEvent) { | ||
if(event.source === null) throw Error("Event source is null"); | ||
if(typeof event.data !== "object") return; | ||
const data = event.data as CommsMessage; // Cast it as a CommsMessage | ||
if(!("_readium" in data) || !data._readium || data._readium <= 0) return; // Not for us | ||
if(data.key === "_ping") { | ||
// The "ping" gives us a destination we bind to for posting events | ||
if(!this.destination) { | ||
this.destination = event.source; | ||
this.origin = event.origin; | ||
this.channelId = data._channel; | ||
|
||
// Make sure we're communicating with a host on the same comms version | ||
if(data._readium !== COMMS_VERSION) { | ||
if(data._readium > COMMS_VERSION) | ||
this.send("error", `received comms version ${data._readium} higher than ${COMMS_VERSION}`); | ||
else | ||
this.send("error", `received comms version ${data._readium} lower than ${COMMS_VERSION}`); | ||
|
||
this.destination = null; | ||
this.origin = ""; | ||
this.channelId = ""; | ||
return; | ||
} | ||
|
||
this.send("_pong", undefined); | ||
this.preLog.forEach(d => this.send("log", d)); | ||
this.preLog = []; | ||
} | ||
return; | ||
} else if(this.channelId) { | ||
// Enforce matching channel ID and origin | ||
if( | ||
data._channel !== this.channelId || | ||
event.origin !== this.origin | ||
) return; | ||
} else { | ||
// Ignore any messages beside _ping if not initialized | ||
return; | ||
} | ||
this.handle(data); | ||
} | ||
private receiver = this.receive.bind(this); | ||
|
||
private handle(data: CommsMessage) { | ||
const listeners = this.registrar.get(data.key as CommsCommandKey); | ||
if(!listeners || listeners.length === 0) { | ||
if(data.strict) this.send("_unhandled", data); // Let the sender know the data was not handled by any listener | ||
return; | ||
} | ||
listeners.forEach(l => l.cb(data.data, (ok: boolean) => { | ||
this.send("_ack", ok, data.id); // Acknowledge handling of the event | ||
})); | ||
} | ||
|
||
public register(key: CommsCommandKey, module: string, callback: CommsCallback) { | ||
const listeners = this.registrar.get(key); | ||
if(listeners && listeners.length >= 0) { | ||
const existing = listeners.find(l => l.module === module); | ||
if(existing) throw new Error(`Trying to register another callback for combination of event ${key} and module ${module}`); | ||
listeners.push({ | ||
cb: callback, | ||
module | ||
}) | ||
this.registrar.set(key, listeners); | ||
} else | ||
this.registrar.set(key, [{ | ||
cb: callback, | ||
module | ||
}]); | ||
} | ||
|
||
public unregister(key: CommsCommandKey, module: string) { | ||
const listeners = this.registrar.get(key); | ||
if(!listeners || listeners.length === 0) return; | ||
listeners.splice(listeners.findIndex(l => l.module === module), 1); | ||
} | ||
|
||
public unregisterAll(module: string) { | ||
this.registrar.forEach((v, k) => this.registrar.set(k, v.filter(r => r.module !== module))); | ||
} | ||
|
||
// Convenience function for logging data | ||
private preLog: any[] = []; | ||
public log(...data: any[]) { | ||
if(!this.destination) this.preLog.push(data); | ||
else this.send("log", data); | ||
} | ||
|
||
public get ready() { | ||
return !!this.destination; | ||
} | ||
|
||
public destroy() { | ||
this.destination = null; | ||
this.channelId = ""; | ||
this.preLog = []; | ||
this.registrar.clear(); | ||
this.wnd.removeEventListener("message", this.receiver); | ||
} | ||
|
||
public send(key: CommsEventKey, data: unknown, id: unknown = undefined, transfer: Transferable[] = []) { | ||
if(!this.destination) throw Error("Attempted to send comms message before destination has been initialized"); | ||
this.destination.postMessage({ | ||
_readium: COMMS_VERSION, | ||
_channel: this.channelId, | ||
id: id ?? mid(), | ||
// scrict, | ||
key, | ||
data | ||
} as CommsMessage, { | ||
targetOrigin: this.origin, | ||
transfer | ||
}); | ||
} | ||
} |
Oops, something went wrong.