From 2226513713431b64df219adc8aa47f77922a00e1 Mon Sep 17 00:00:00 2001 From: Hatton Date: Wed, 4 Dec 2024 14:52:55 -0700 Subject: [PATCH 1/3] feat: Navigation between pages and books with history --- .storybook/main.ts | 52 +- .storybook/preview.ts | 6 + README-advanced.md | 21 +- README.md | 63 +- package.json | 20 +- .../testBooks/multibook-index/Basic Book.css | 348 ++++ .../testBooks/multibook-index/BloomLocal.svg | 9 + .../multibook-index/Traditional-XMatter.css | 262 +++ .../testBooks/multibook-index/appearance.css | 230 +++ .../testBooks/multibook-index/appearance.json | 10 + .../multibook-index/back-cover-local.svg | 12 + public/testBooks/multibook-index/basePage.css | 1304 +++++++++++++++ .../testBooks/multibook-index/book.userPrefs | 1 + public/testBooks/multibook-index/branding.css | 59 + .../customCollectionStyles.css | 14 + .../multibook-index/defaultLangStyles.css | 24 + .../multibook-index/happy-pirate.png | Bin 0 -> 18354 bytes public/testBooks/multibook-index/history.db | Bin 0 -> 24576 bytes public/testBooks/multibook-index/image.jpg | Bin 0 -> 1844 bytes public/testBooks/multibook-index/index.htm | 473 ++++++ public/testBooks/multibook-index/license.png | Bin 0 -> 9856 bytes public/testBooks/multibook-index/meta.json | 1 + public/testBooks/multibook-index/origami.css | 304 ++++ .../testBooks/multibook-index/placeHolder.png | Bin 0 -> 6734 bytes .../testBooks/multibook-index/previewMode.css | 115 ++ .../multibook-index/publish-settings.json | 1 + .../testBooks/multibook-index/thumbnail.png | Bin 0 -> 2491 bytes .../testBooks/multibook-target1/.distribution | 0 .../multibook-target1/Basic Book.css | 348 ++++ .../multibook-target1/BloomLocal.svg | 9 + .../multibook-target1/Traditional-XMatter.css | 262 +++ .../multibook-target1/appearance.css | 230 +++ .../multibook-target1/appearance.json | 10 + .../multibook-target1/back-cover-local.svg | 12 + .../testBooks/multibook-target1/basePage.css | 1304 +++++++++++++++ .../multibook-target1/book.userPrefs | 1 + .../testBooks/multibook-target1/branding.css | 59 + .../customCollectionStyles.css | 14 + .../multibook-target1/defaultLangStyles.css | 24 + public/testBooks/multibook-target1/history.db | Bin 0 -> 24576 bytes public/testBooks/multibook-target1/image.png | Bin 0 -> 35995 bytes public/testBooks/multibook-target1/index.htm | 432 +++++ .../testBooks/multibook-target1/license.png | Bin 0 -> 9856 bytes public/testBooks/multibook-target1/meta.json | 1 + .../testBooks/multibook-target1/origami.css | 304 ++++ .../multibook-target1/placeHolder.png | Bin 0 -> 6734 bytes .../multibook-target1/previewMode.css | 115 ++ .../multibook-target1/publish-settings.json | 1 + .../testBooks/multibook-target1/thumbnail.png | Bin 0 -> 2461 bytes src/bloom-player-controls.tsx | 36 +- src/bloom-player-core.tsx | 530 ++++-- src/bloomPlayerAnalytics.tsx | 2 +- src/bookInfo.ts | 2 +- src/controlBar.tsx | 45 +- src/externalContext.ts | 2 +- src/navigation.test.ts | 152 ++ src/navigation.ts | 112 ++ src/stories/BloomPlayerIframe.tsx | 42 + src/stories/BloomPlayerTester.ts | 118 ++ src/stories/multiBook.stories.tsx | 47 + src/stories/navigation.stories.tsx | 106 ++ yarn.lock | 1452 +++++------------ 62 files changed, 7882 insertions(+), 1219 deletions(-) create mode 100644 public/testBooks/multibook-index/Basic Book.css create mode 100644 public/testBooks/multibook-index/BloomLocal.svg create mode 100644 public/testBooks/multibook-index/Traditional-XMatter.css create mode 100644 public/testBooks/multibook-index/appearance.css create mode 100644 public/testBooks/multibook-index/appearance.json create mode 100644 public/testBooks/multibook-index/back-cover-local.svg create mode 100644 public/testBooks/multibook-index/basePage.css create mode 100644 public/testBooks/multibook-index/book.userPrefs create mode 100644 public/testBooks/multibook-index/branding.css create mode 100644 public/testBooks/multibook-index/customCollectionStyles.css create mode 100644 public/testBooks/multibook-index/defaultLangStyles.css create mode 100644 public/testBooks/multibook-index/happy-pirate.png create mode 100644 public/testBooks/multibook-index/history.db create mode 100644 public/testBooks/multibook-index/image.jpg create mode 100644 public/testBooks/multibook-index/index.htm create mode 100644 public/testBooks/multibook-index/license.png create mode 100644 public/testBooks/multibook-index/meta.json create mode 100644 public/testBooks/multibook-index/origami.css create mode 100644 public/testBooks/multibook-index/placeHolder.png create mode 100644 public/testBooks/multibook-index/previewMode.css create mode 100644 public/testBooks/multibook-index/publish-settings.json create mode 100644 public/testBooks/multibook-index/thumbnail.png create mode 100644 public/testBooks/multibook-target1/.distribution create mode 100644 public/testBooks/multibook-target1/Basic Book.css create mode 100644 public/testBooks/multibook-target1/BloomLocal.svg create mode 100644 public/testBooks/multibook-target1/Traditional-XMatter.css create mode 100644 public/testBooks/multibook-target1/appearance.css create mode 100644 public/testBooks/multibook-target1/appearance.json create mode 100644 public/testBooks/multibook-target1/back-cover-local.svg create mode 100644 public/testBooks/multibook-target1/basePage.css create mode 100644 public/testBooks/multibook-target1/book.userPrefs create mode 100644 public/testBooks/multibook-target1/branding.css create mode 100644 public/testBooks/multibook-target1/customCollectionStyles.css create mode 100644 public/testBooks/multibook-target1/defaultLangStyles.css create mode 100644 public/testBooks/multibook-target1/history.db create mode 100644 public/testBooks/multibook-target1/image.png create mode 100644 public/testBooks/multibook-target1/index.htm create mode 100644 public/testBooks/multibook-target1/license.png create mode 100644 public/testBooks/multibook-target1/meta.json create mode 100644 public/testBooks/multibook-target1/origami.css create mode 100644 public/testBooks/multibook-target1/placeHolder.png create mode 100644 public/testBooks/multibook-target1/previewMode.css create mode 100644 public/testBooks/multibook-target1/publish-settings.json create mode 100644 public/testBooks/multibook-target1/thumbnail.png create mode 100644 src/navigation.test.ts create mode 100644 src/navigation.ts create mode 100644 src/stories/BloomPlayerIframe.tsx create mode 100644 src/stories/BloomPlayerTester.ts create mode 100644 src/stories/multiBook.stories.tsx create mode 100644 src/stories/navigation.stories.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index c702b1e3..aed5006b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,5 +1,4 @@ import type { StorybookConfig } from "@storybook/react-vite"; -// get the vite config one level up import viteConfigFn from "../vite.config"; const proxy = viteConfigFn({ @@ -8,6 +7,9 @@ const proxy = viteConfigFn({ }).server!.proxy; const config: StorybookConfig = { stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + // For stories that include an iframe and thus need access to bloomplayer.htm and the bundles it loads. + // When using those stories, use `yarn buildForStorybook` to update non-storybook code. + staticDirs: ["../dist"], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", @@ -18,15 +20,49 @@ const config: StorybookConfig = { options: {}, }, viteFinal: (config, options) => { - // setup the same proxy that we have in "vite dev" to avoid CORS issues - const server = { - ...config.server, - proxy: { - ...proxy, - }, + const idToBookName = { + "2e492eb1-bcc5-4b2b-b756-6cda33e1eee4": "multibook-index", + "2c1b71ac-f399-446d-8398-e61a8efd4e83": "multibook-target1", }; + const updatedProxy = { + // setup the same proxy that we have in "vite dev" to avoid CORS issues. (THIS IS NOT WORKING) + // Note that it requires that paths to book on BloomLibrary.org start with "s3/" in place of "https://s3.amazonaws.com/" + ...proxy, + + // Simulate what a bloom-player host must do to provide books via their instanceId + // Note that this code is not giving a path all the way to the html file, so that + // it works with paths that are asking for all the other resources, too. That works + // so long as the html file of the book is named "index.htm". + "/book/": { + target: `http://localhost:6006`, + rewrite: (path) => { + const bookId = path.split("/")[2]; + const bookName = idToBookName[bookId]; + const rewrittenPath = path.replace( + `/book/${bookId}`, + `testBooks/${bookName}`, + ); + + // console.log( + // `[Storybook Proxy] ${path} --> ${rewrittenPath}`, + // ); + return rewrittenPath; + }, + }, - return { ...config, server }; + "/bloom/": { + target: "file:///", + rewrite: (path) => { + console.log(`proxy got request ${path}`); + return path.replace("/bloom/", ""); + }, + }, + }; + return { + ...config, + server: { ...config.server, proxy: updatedProxy }, + addons: ["@storybook/addon-interactions"], + }; }, }; export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 19d27680..244bc423 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -8,6 +8,12 @@ const preview: Preview = { date: /Date$/i, }, }, + + options: { + storySort: { + order: ["Automated"], + }, + }, }, }; diff --git a/README-advanced.md b/README-advanced.md index 380832b7..da9a05a9 100644 --- a/README-advanced.md +++ b/README-advanced.md @@ -2,27 +2,14 @@ ## Getting the pieces - yarn add bloom-player + `yarn add bloom-player` This puts the files you need at - node_modules/bloom-player/dist + `node_modules/bloom-player/dist` -Note, however, that you won't typically import or require the bloom-player javascript. That javascript is there to be used by the bloomplayer.htm, which is intended to be the source of an iframe or WebView. So typically you will need to do some step to get the files from node_modules/bloom-player/dist to your output directory. +Copy those wherever you need them for your project. -For example, if gulp is part of your build process, you might use - - gulp.src(paths.nodeFilesNeededInOutput) - .pipe(gulpCopy(outputDir, { prefix: 1 })); - -along with declaring one of your paths to be - - nodeFilesNeededInOutput: [ - "./**/bloom-player/dist/bloomPlayer.min.js", - "./**/bloom-player/dist/simpleComprehensionQuiz.js", - "./**/bloom-player/dist/bloomplayer.htm", - "./**/bloom-player/dist/*.mp3" - ] ### Other options @@ -38,7 +25,7 @@ The url parameter points to the HTML file that is the core of the bloom book. It The display of the book will automatically grow to be as large as will fit in the given space. If the book can rotate, it will pick an orientation depending on whether the window is wider then it is tall. If the book has special behaviors, such as motion/animation when in landscape mode, they will be triggered based on the chosen orientation. -`Bloom-player` is designed to be published using `npm` so as to be readily available to a variety of clients. The published version includes both `dest/bloomPlayer.js` and `dest/bloomPlayer.min.js`, as well as an un-minified version of the code and some mp3 files used for responding to comprehension questions. There is also a file simpleComprehensionQuiz.js, but this is needed only for books containing an obsolete kind of comprehension questions. +`Bloom-player` is designed to be published using `npm` so as to be readily available to a variety of clients. The published version includes both `dist/bloomPlayer-HASH.js`, as well as some mp3 files used by activities. We deliberately require this component to be used in an `iframe` so that the containing web page is safe from any scripts that might get embedded in Bloom books. For this reason `bloom-player` deliberately manipulates (with `React`) the body of its HTML document. diff --git a/README.md b/README.md index 3f15d19c..cfbba00d 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,13 @@ This project, _bloom-player_, lets users view and interact with [Bloom](https://bloomlibrary.org) books in any browser. -Specifically, the books must be in _BloomPub_ format (.bloompub or .bloomd, which are zipped Bloom Digital books). - -The Bloom project uses _bloom-player_ is used in the following places: +The Bloom project uses _bloom-player_ in the following places: - In [Bloom Editor](https://github.com/bloombooks/bloomdesktop) / Publish / Bloom Reader tab, for previewing what the book will look like on a device. -- In [Bloom Reader](https://github.com/bloombooks/bloomreader) itself (starting with BR version 2.0) +- In the [Bloom Reader](https://github.com/bloombooks/bloomreader) Android app - On [BloomLibrary.org](https://bloomlibrary.org) - In [BloomPUB Viewer](https://github.com/bloombooks/bloompub-viewer) +- In Android and IOS apps created with Reading App Builder # Using bloom-player in a website @@ -55,12 +54,17 @@ Default: `false` #### showBackButton -If true, displays an arrow in the upper left corner of the app bar. When clicked, bloom-player will post a message "backButtonClicked" that the surrounding context can catch and respond to. +If true, displays a button in the upper left corner of the app bar. When clicked, bloom-player will post a message "backButtonClicked" that the your host can catch and respond to. Example: `showBackButton=true` Default: `false` +> [!NOTE] +> Normally this button will be a left arrow. However if bloom-player is the top level window (i.e. it is not embedded in another page), it will show as an ellipsis. +> +> This is used in BloomLibrary.org when the user has used a link from somewhere else to jump directly into to reading a book. In that case, this button isn't really taking them "back", it's instead taking them to the "Detail" page for that same book on BloomLibrary.org. + #### lang If set, determines the initial language to be displayed. The user can still change the display language using the language picker. @@ -95,16 +99,45 @@ Examples: `host=bloomlibrary` or `host=bloomreader` or `host=bloompubviewer` or Default: `nothing/undefined` +# How to support books that link to other books + +Some books are designed to be published together as a collection. These books may link to each other using underlined phrases or buttons. Users can click on these links and also backtrack. You can see examples of this in the StoryBook stories under "MultiBook". + +Instead of asking your host to handle the navigation, history, etc., this navigation happens within the one instance of Bloom Player. However, Bloom Player needs your help, because it does not have a way to know what the URL to each book is in the context of your host, which could be a website, an app, a desktop program, etc. So if you want to support these collections of linked books, your host has to do some extra work. + +Bloom books link to each other using each book's "Instance ID". This is a guid that is given as a property in the `meta.json` file: +```json +{ + "bookInstanceId":"0b82aab3-61fb-429e-864f-ce7671a3d372", + "title":"หมู่บ้านแห่งความดี\r\nVillage of good", + "credits":"มิได้จัดจำหน่าย  แต่จัดทำเพื่อส่งเสริมการเรียนรู้", + "tags":["topic:Fiction"], + "pageCount":16, +``` + +When the user clicks on a link to another book, Bloom Player will first request a resource from `/book/THE-INSTANCE-ID/index.htm`. This will be followed by requests for things like `/book/THE-INSTANCE-ID/basePage.css` and `/book/THE-INSTANCE-ID/image1.png`. + +To support this, your host needs to: +1. intercept that request +2. extract the Instance ID +3. figure out the path to that book in your system +4. if the target is still zipped in a BloomPUB, you may need to unzip it or read the resource directly out of the archive +5. return the requested file (if your host is running the server) or redirect to a new url (if it is not). + +If these steps are slow, you might need to cache or even pre-prepare an index for use at runtime. + +As an example, the file `.storybook/main.ts` sets up Storybook's vite dev server to proxy a couple instance ids to correct folders in this repository's `/public/testBooks` directory. + # Development +If you haven't already, install `volta` globally. Volta takes care of getting all the correct versions of things like node and yarn to match what this repository expects. + Run `yarn` to get the dependencies. -Either, run `yarn storybook` (which has multiple books), +Either run `yarn storybook` (which has multiple books), or run `yarn dev` (which will use `index-for-developing.html`). -Note: you need to have `chrome` on your path. - See package.json for other scripts. ### Testing with a book hosted on the web @@ -115,15 +148,17 @@ Both `yarn storybook` and `yarn dev` do this for you. ### Testing with a book hosted by Bloom -Note that while testing, one option is to run Bloom, select your book, go to the publish tab, and choose Bloom Reader. Bloom will make the book available through its local fileserver. Modify index.html to use a path like this - - ``` @@ -32,7 +32,7 @@ You can customize some aspects of the bloom-player to fit your context. For exam ```html ``` @@ -75,7 +75,7 @@ Default: none (The initial language will be the vernacular language when the boo #### hideFullScreenButton -If true, the Full Screen icon button will not appear in the upper right hand corner of the window. Otherwise, a Full Screen icon button is displayed and allows the user to toggle between full screen and window mode. +If true, the Full Screen icon button will not appear in the upper right hand corner of the window. Otherwise, a Full Screen icon button is displayed and allows the user to toggle between full screen and window mode. Example: `hideFullScreenButton=true` @@ -83,7 +83,7 @@ Default: `false` #### independent -If true, bloom-player reports its own analytics directly to segment. If false, bloom-player sends messages to its host, and the host is responsible for reporting analytics. +If true, bloom-player reports its own analytics directly to segment. If false, bloom-player sends messages to its host, and the host is responsible for reporting analytics. Example: `independent=false` @@ -91,7 +91,7 @@ Default: `true` #### host -If set, provides a host value for analytics. If not set, bloom-player attempts to derive a value from the available information if independent is `true` (information can be limited in an iframe) and does nothing otherwise. +If set, provides a host value for analytics. If not set, bloom-player attempts to derive a value from the available information if independent is `true` (information can be limited in an iframe) and does nothing otherwise. Examples: `host=bloomlibrary` or `host=bloomreader` or `host=bloompubviewer` or `host=readerapp` @@ -106,6 +106,7 @@ Some books are designed to be published together as a collection. These books ma Instead of asking your host to handle the navigation, history, etc., this navigation happens within the one instance of Bloom Player. However, Bloom Player needs your help, because it does not have a way to know what the URL to each book is in the context of your host, which could be a website, an app, a desktop program, etc. So if you want to support these collections of linked books, your host has to do some extra work. Bloom books link to each other using each book's "Instance ID". This is a guid that is given as a property in the `meta.json` file: + ```json { "bookInstanceId":"0b82aab3-61fb-429e-864f-ce7671a3d372", @@ -115,14 +116,15 @@ Bloom books link to each other using each book's "Instance ID". This is a guid t "pageCount":16, ``` -When the user clicks on a link to another book, Bloom Player will first request a resource from `/book/THE-INSTANCE-ID/index.htm`. This will be followed by requests for things like `/book/THE-INSTANCE-ID/basePage.css` and `/book/THE-INSTANCE-ID/image1.png`. +When the user clicks on a link to another book, Bloom Player will first request a resource from `/book/THE-INSTANCE-ID/index.htm`. If you do a redirect to `C:/mybook/index.htm`, then this will be followed by requests for things like `C:/mybook/basePage.css` and `C:/mybook/image1.png`. To support this, your host needs to: + 1. intercept that request 2. extract the Instance ID 3. figure out the path to that book in your system 4. if the target is still zipped in a BloomPUB, you may need to unzip it or read the resource directly out of the archive -5. return the requested file (if your host is running the server) or redirect to a new url (if it is not). +5. redirect to a new url If these steps are slow, you might need to cache or even pre-prepare an index for use at runtime. @@ -149,6 +151,7 @@ Both `yarn storybook` and `yarn dev` do this for you. ### Testing with a book hosted by Bloom To test Bloom Player on a book in the Bloom Editor, follow these steps: + 1. Go to the Publish tab in Bloom, choose "BloomPUB", and click "Preview". 2. Back in this repository, run storybook (`yarn storybook`). 3. Choose the "Live From Bloom Editor" story. @@ -158,6 +161,7 @@ To test Bloom Player on a book in the Bloom Editor, follow these steps: To run unit tests use `yarn test`. This will run all `*.test.ts`, which should be collocated with the thing being tested. ### More info + For more information, see README-advanced.md ## License diff --git a/package.json b/package.json index 45909b7a..e8dfe3f5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "prepare": "husky install && git config blame.ignoreRevsFile .git-blame-ignore-revs", "storybook": "storybook dev -p 6006", "// run this when working with stories that use an iframe and bloomplayer.htm instead of the raw react components": " ", - "buildForStorybook": "yarn vite build --watch --mode development", + "watchForStorybook": "yarn vite build --watch --mode development", "strings:download": "crowdin download --config src/l10n/crowdin.yml && yarn strings:compile", "strings:upload": "crowdin upload --config src/l10n/crowdin.yml", "strings:compile": "yarn tsx ./src/l10n/compileL10Strings.ts", diff --git a/src/bloom-player-controls.tsx b/src/bloom-player-controls.tsx index 8a83b27a..21017553 100644 --- a/src/bloom-player-controls.tsx +++ b/src/bloom-player-controls.tsx @@ -581,9 +581,7 @@ export const BloomPlayerControls: React.FunctionComponent = ( const amountToMoveDown = (winHeight - actualPageHeight - controlsHeight) / 2; // don't count controlsHeight in what we move down if (amountToMoveDown > 0) { - translateString = `translate(0, ${amountToMoveDown.toFixed( - 0, - )}px) `; + translateString = `translate(0, ${amountToMoveDown.toFixed(0)}px) `; // console.log(`** translating down ${amountToMoveDown}px`); // console.log(` winHeight ${winHeight}px`); // console.log(` desiredPageHeight ${desiredPageHeight}px`); @@ -915,8 +913,8 @@ export const BloomPlayerControls: React.FunctionComponent = ( // we have a page or book jump on our bloom player history stack return BackButtonState.showArrow; else if (showBackButton) { - // so our internal history is empty, but the container is - // wants us to offer a "back" to it. Bloom Reader uses this. + // so our internal history is empty, but the container + // wants us to offer a way "back" to it. Bloom Reader uses this. if (window === window.top) return BackButtonState.showArrow; // Used by bloomlibrary.org if you went straight to a book via some URL @@ -932,7 +930,9 @@ export const BloomPlayerControls: React.FunctionComponent = ( playLabel={playLabel} preferredLanguages={preferredUiLanguages} backClicked={() => { - if (!coreRef.current?.HandleBackButtonClicked()) { + if ( + !coreRef.current?.HandleBackButtonClickedIfHavePlayerHistory() + ) { sendBackToHost(); } }} diff --git a/src/bloom-player-core.tsx b/src/bloom-player-core.tsx index 3084d691..1e1c1223 100644 --- a/src/bloom-player-core.tsx +++ b/src/bloom-player-core.tsx @@ -286,7 +286,7 @@ export class BloomPlayerCore extends React.Component { const parsedUrl = new URL(props.url, window.location.origin); this.state.bookUrl = parsedUrl.pathname; if (parsedUrl.hash) { - this.state.startPageId = parsedUrl.hash.substring(1); + this.state.startPageId = parsedUrl.hash.substring(1); // strip the "#" } else { // Copy into state because in a multi-book scenario, the prop.startPageIndex will // always be the initial value, but we don't want to just got to that page of @@ -355,6 +355,22 @@ export class BloomPlayerCore extends React.Component { this.handleDocumentLevelKeyDown(e), ); + // Prevent unwanted behavior on link clicks that accidentally move a bit. + document.addEventListener( + "pointerdown", + (event) => { + if ( + (event.target as HTMLElement).closest("[href], [data-href]") + ) { + // Stop the swiper from starting a drag + event.stopPropagation(); + // Stop the browser from showing a thing like you're trying to drag the link to some other window + event.preventDefault(); + } + }, + { capture: true }, // Let us see this before children see it. + ); + // Clicking on any element that has a data-href attribute will be treated as a link. // This is the simplest way to handle such links that may be scattered on different // types of elements throughout the book. See BL-13879. @@ -411,7 +427,7 @@ export class BloomPlayerCore extends React.Component { return canGoBack(); } // called by bloom-player-controls - public HandleBackButtonClicked(): boolean { + public HandleBackButtonClickedIfHavePlayerHistory(): boolean { const backLocation = goBackInHistoryIfPossible( this.bookInfo.bookInstanceId, ); @@ -543,11 +559,10 @@ export class BloomPlayerCore extends React.Component { // parse the url const u = new URL(newSourceUrl, window.location.origin); - // TODO: the URL parse always introduces a leading slash if it's missing, but currently that breaks... something. - // I feel like I'm hacking around something I should understand better. + // The URL parsing can introduce a slash we don't want when the input URL is not relative. this.sourceUrl = newSourceUrl.startsWith("/") - ? u.pathname - : u.pathname.substring(1); + ? u.pathname // if the original was relative, fine + : u.pathname.substring(1); // if the original was not relative, don't make it look relative // We support a two ways of interpreting URLs. // If the url ends in .htm, it is assumed to be the URL of the htm file that @@ -612,7 +627,7 @@ export class BloomPlayerCore extends React.Component { }, ); const htmlPromise = axios.get(urlOfBookHtmlFile); - console.log("urlOfBookHtmlFile", urlOfBookHtmlFile); + // console.log("urlOfBookHtmlFile", urlOfBookHtmlFile); const metadataPromise = axios.get(this.fullUrl("meta.json")); Promise.all([htmlPromise, metadataPromise, distributionPromise]) .then((result) => { @@ -894,7 +909,7 @@ export class BloomPlayerCore extends React.Component { ); }; - // TODO: most of this should be moved into bookInfo, including the pageIdToIndex map. + // ENHANCE: most of this should be moved into bookInfo, including the pageIdToIndex map. // clear the pageIdToIndex map pageIdToIndex.length = 0; pageIdToIndex["cover"] = 0; @@ -2235,12 +2250,7 @@ export class BloomPlayerCore extends React.Component { .hasAppearanceSystem ? "appearance-system" : "" - } ${ - this.state - .importedBodyAttributes[ - "class" - ] ?? "" - }`} + } ${this.state.importedBodyAttributes["class"] ?? ""}`} // Note: the contents of `slide` are what was in the .htm originally. // So you would expect that this would replace any changes made to the dom by the activity or other code. // You would expect that we would lose the answers a user made in an activity. @@ -2361,37 +2371,6 @@ export class BloomPlayerCore extends React.Component { // Other internal links won't work and are disabled; external ones are forced to // open a new tab as the results of trying to display a web page in the bloom-player // iframe or webview can be confusing. - // private fixInternalHyperlinks(div: HTMLDivElement | null): void { - // if (!div) { - // return; - // } - // const anchors = Array.from(div.getElementsByTagName("a") ?? []); - // anchors.forEach((a) => { - // const href = a.getAttribute("href"); // not a.href, which has the full page address prepended. - // if (href?.startsWith("#")) { - // const pageId = href.substring(1); - // const pageNum = this.state.pageIdToIndexMap[pageId]; - // if (pageNum !== undefined) { - // a.href = ""; // may not be needed, on its own was unsuccessful in stopping attempted default navigation - // a.onclick = (ev) => { - // ev.preventDefault(); // don't try to follow the link, other than by the slideTo below - // ev.stopPropagation(); // don't allow parent listeners, particularly the one that toggles the nav bar - // this.swiperInstance.slideTo(pageNum); - // }; - // } else { - // // no other kind of internal link makes sense, so let them be ignored. - // a.onclick = (ev) => { - // ev.preventDefault(); - // ev.stopPropagation(); - // }; - // } - // } else { - // // an external link. It will likely confuse things to follow it inside whatever iframe or webview - // // bloom player may be running in, so encourage opening a new tab or similar action. - // a.setAttribute("target", "blank"); - // } - // }); - // } // What we need to do when the page narration is completed (if autoadvance, go to next page). public handlePageNarrationComplete = (page: HTMLElement | undefined) => { diff --git a/src/navigation.test.ts b/src/navigation.test.ts index c996e2a3..64cf2bcd 100644 --- a/src/navigation.test.ts +++ b/src/navigation.test.ts @@ -132,6 +132,16 @@ describe("Navigation functions", () => { const thirdBack = goBackInHistoryIfPossible("book1"); expect(thirdBack).toBeUndefined(); }); + + test("doesn't die if an href is empty", () => { + const event = createClickEvent(""); + const result = checkClickForBookOrPageJump( + event, + "doesn't matter", + () => "doesn't matter", + ); + expect(result).toEqual(undefined); + }); }); }); diff --git a/src/navigation.ts b/src/navigation.ts index 37f23189..78aaa297 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -5,15 +5,15 @@ type ILocation = { bookUrl?: string; pageId?: string }; export function canGoBack() { return jumpHistory.length > 0; } -export function goBackInHistoryIfPossible( +export function tryPopPlayerHistory( currentBookId: string, ): ILocation | undefined { const previousLocation = jumpHistory.pop(); if (previousLocation) { - console.log( - "Going Back. Current History is: ", - JSON.stringify(jumpHistory, null, 2), - ); + // console.log( + // "Going Back. Current History is: ", + // JSON.stringify(jumpHistory, null, 2), + // ); return { bookUrl: currentBookId === previousLocation.bookId @@ -24,15 +24,12 @@ export function goBackInHistoryIfPossible( } return undefined; } -function urlFromBookId(bookId: string) { - return `/book/${bookId}/index.htm`; -} export function checkClickForBookOrPageJump( event: any, currentBookInstanceId: string, - getCurentPageId: () => string, -): ILocation { + getCurrentPageId: () => string, +): ILocation | undefined { const linkElement = (event.target as HTMLElement).closest( "[href], [data-href]", ); @@ -41,10 +38,9 @@ export function checkClickForBookOrPageJump( event.stopPropagation(); const href: string = - (linkElement.attributes["href"] && - linkElement.attributes["href"].nodeValue) || - (linkElement.attributes["data-href"] && - linkElement.attributes["data-href"].nodeValue); + (linkElement.getAttribute("href") || + linkElement.getAttribute("data-href")) ?? + ""; if (href.startsWith("http://") || href.startsWith("https://")) { // This is a generic external link. We open it in a new window or tab. @@ -66,7 +62,7 @@ export function checkClickForBookOrPageJump( : urlFromBookId(previousLocation.bookId!), pageId: previousLocation.pageId, }; - } + } else return undefined; } else if (href.startsWith("/book/")) { const target = parseTargetBookUrl(href); targetBookId = target.bookId; @@ -93,16 +89,21 @@ export function checkClickForBookOrPageJump( return { pageId: targetPageId }; } - return {}; // nothing to do as far as navigation goes + return undefined; // nothing to do as far as navigation goes } function pushLocation(bookId: string, pageId: string, comment: string) { jumpHistory.push({ bookId, pageId }); - console.log( - comment + " Current History is: ", - JSON.stringify(jumpHistory, null, 2), - ); + // console.log( + // comment + " Current History is: ", + // JSON.stringify(jumpHistory, null, 2) + // ); } + +function urlFromBookId(bookId: string) { + return `/book/${bookId}/index.htm`; +} + function parseTargetBookUrl(url: string) { // the format is "/book/BOOKID#PAGEID" where the page id is optional const bloomUrl = new URL(url, window.location.origin); diff --git a/src/stories/BloomPlayerIframe.tsx b/src/stories/BloomPlayerIframe.tsx index c85c487e..5342d0f8 100644 --- a/src/stories/BloomPlayerIframe.tsx +++ b/src/stories/BloomPlayerIframe.tsx @@ -6,7 +6,7 @@ export function BloomPlayerIframe({ bookUrl, bookPageIndex, showBackButton }) { if (!useBuildIsReady()) { return (
- Waiting for Bloom Player to build. See "yarn buildForStorybook" + Waiting for Bloom Player to build. See "yarn watchForStorybook"
); } diff --git a/src/stories/multiBook.stories.tsx b/src/stories/multiBook.stories.tsx index d3135aa3..17c44a00 100644 --- a/src/stories/multiBook.stories.tsx +++ b/src/stories/multiBook.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { BloomPlayerIframe } from "./BloomPlayerIframe"; /* This uses an iframe, so normal dev server compilation is not effective. -Therefore, when developing with this, use `yarn buildForStorybook` to update non-storybook code. +Therefore, when developing with this, use `yarn watchForStorybook` to update non-storybook code. The point of this story is really just to host a book that has links to a second book. What makes that possible is the proxy defined in .storybook/main.ts, not here. diff --git a/src/stories/navigation.stories.tsx b/src/stories/navigation.stories.tsx index 841ba6c8..ca5fd1ce 100644 --- a/src/stories/navigation.stories.tsx +++ b/src/stories/navigation.stories.tsx @@ -3,7 +3,7 @@ import { BloomPlayerIframe } from "./BloomPlayerIframe"; import { BloomPlayerTester } from "./BloomPlayerTester"; /* This uses an iframe, so normal dev server compilation is not effective. -Therefore, when developing with this, use `yarn buildForStorybook` to update non-storybook code. +Therefore, when developing with this, use `yarn watchForStorybook` to update non-storybook code. The point of this story is really just to host a book that has links to a second book. What makes that possible is the proxy defined in .storybook/main.ts, not here. From 3df06c8fd24f3f614b2a24d57c28340dbd0ddfd8 Mon Sep 17 00:00:00 2001 From: Hatton Date: Tue, 10 Dec 2024 11:14:01 -0700 Subject: [PATCH 3/3] chore: final review session with JohnT --- public/testBooks/multibook-index/history.db | Bin 24576 -> 0 bytes .../multibook-index/publish-settings.json | 1 - src/bloom-player-controls.tsx | 4 +- src/bloom-player-core.tsx | 290 +----------------- src/externalContext.ts | 2 +- src/navigation.test.ts | 17 +- src/navigation.ts | 22 +- src/stories/BloomPlayerTester.ts | 8 +- 8 files changed, 36 insertions(+), 308 deletions(-) delete mode 100644 public/testBooks/multibook-index/history.db delete mode 100644 public/testBooks/multibook-index/publish-settings.json diff --git a/public/testBooks/multibook-index/history.db b/public/testBooks/multibook-index/history.db deleted file mode 100644 index ce655a89498d4045f1b798347b57ea02d7541dfa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI)%Wu;#90zcFeQd3?1rAeGaj|k?)z)fjyGF2oMbR= z6@L(a1}8Xk;m?@F5hra?TLhZ41Do`X>eR7p_xJnMmps~+&-X>l*-@whJ7!ICi%?3o z7$bxfbSdgG+H$&*A06m-I#&N%wLt10Pv?!Fq%i-P7!QrF3$GT^DmDl}00Izz00bZa z0SG_<0ucDOz(JvCRx30S(&Hca8JBU?al-JpBfP;;arZ@g`=HJCo<3;5X4YgEi-mI3 zW-VF6fmZcdzGyBl(>Jm0_&hrG^%-F&;-?9h?(|_{IRC-+&VHLs^<9#7z*qFhYTwVz znw1Lu)Xe4_gfZCXCuSH%XtAZTwdYw($e4GzVkb%jwmN6W{G8cI917_w9_W0T40TC- zpRJUwB;rcwQD?Std$t-irD04o#^)!xVKnz}U?<>FWOpZfdUmWE9MRRC)9KE*ibNlP08g)$Y@I}%wJe78Tz)QHYV*2K;|tL*Y!H9|1Rwwb2tWV=5P$## zAOHafTyKF*nm=474c@xf;LcjjaozP=%V{_@XLEg{w&8kqv&q*u=dB>|W0771sAX3P z$~rG^1(>MeGp!a(Isvd{nj0=l>`P5=M^ diff --git a/public/testBooks/multibook-index/publish-settings.json b/public/testBooks/multibook-index/publish-settings.json deleted file mode 100644 index 261f25e2..00000000 --- a/public/testBooks/multibook-index/publish-settings.json +++ /dev/null @@ -1 +0,0 @@ -{"audioVideo":{"pageTurnDelay":3000,"format":"facebook","motion":false,"playerSettings":"","pageRange":[]},"epub":{"howToPublishImageDescriptions":0,"removeFontSizes":false,"mode":"fixed"},"bloomPUB":{"motion":true,"textLangs":{},"audioLangs":{},"signLangs":{},"imageSettings":{"maxWidth":1280,"maxHeight":720}},"bloomLibrary":{"textLangs":{},"audioLangs":{},"signLangs":{},"comic":true}} \ No newline at end of file diff --git a/src/bloom-player-controls.tsx b/src/bloom-player-controls.tsx index 21017553..7e07a6d2 100644 --- a/src/bloom-player-controls.tsx +++ b/src/bloom-player-controls.tsx @@ -5,7 +5,7 @@ book inside of the Bloom:Publish:Android screen. import { BloomPlayerCore } from "./bloom-player-core"; import * as ReactDOM from "react-dom"; import { - sendBackToHost, + informHostOfBackAction, showNavBar, hideNavBar, reportBookProperties, @@ -933,7 +933,7 @@ export const BloomPlayerControls: React.FunctionComponent = ( if ( !coreRef.current?.HandleBackButtonClickedIfHavePlayerHistory() ) { - sendBackToHost(); + informHostOfBackAction(); } }} showPlayPause={hasAudio || hasMusic || hasVideo} diff --git a/src/bloom-player-core.tsx b/src/bloom-player-core.tsx index 1e1c1223..79072380 100644 --- a/src/bloom-player-core.tsx +++ b/src/bloom-player-core.tsx @@ -3,8 +3,7 @@ bloom-player-core is responsible for all the behavior of working through a book, (other than page turning). */ import * as React from "react"; -import * as ReactDOM from "react-dom"; -import axios, { AxiosPromise } from "axios"; +import axios from "axios"; import Swiper, { SwiperInstance } from "react-id-swiper"; // This loads some JS right here that is a polyfill for the (otherwise discontinued) scoped-styles html feature import "style-scoped/scoped.min.js"; @@ -20,7 +19,6 @@ import { LocalizationManager } from "./l10n/localizationManager"; import { reportAnalytics, reportPlaybackComplete, - sendMessageToHost, setAmbientAnalyticsProperties, updateBookProgressReport, } from "./externalContext"; @@ -30,7 +28,6 @@ import { sendStringToBloomApi, } from "./videoRecordingSupport"; import LangData from "./langData"; -import Replay from "@material-ui/icons/Replay"; // See related comments in controlBar.tsx import IconButton from "@material-ui/core/IconButton"; @@ -83,7 +80,7 @@ import { assembleStyleSheets } from "./stylesheets"; import { canGoBack, checkClickForBookOrPageJump, - goBackInHistoryIfPossible, + tryPopPlayerHistory, } from "./navigation"; // BloomPlayer takes a URL param that directs it to Bloom book. @@ -428,9 +425,7 @@ export class BloomPlayerCore extends React.Component { } // called by bloom-player-controls public HandleBackButtonClickedIfHavePlayerHistory(): boolean { - const backLocation = goBackInHistoryIfPossible( - this.bookInfo.bookInstanceId, - ); + const backLocation = tryPopPlayerHistory(this.bookInfo.bookInstanceId); if (backLocation) { this.navigate(backLocation.bookUrl, backLocation.pageId); return true; @@ -1667,285 +1662,6 @@ export class BloomPlayerCore extends React.Component { } } - // Make a