Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/BookReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ModeThumb } from './BookReader/ModeThumb.js';
import { ImageCache } from './BookReader/ImageCache.js';
import { PageContainer } from './BookReader/PageContainer.js';
import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet.js';
import {BookReaderTextFragment} from './util/TextSelectionManager.js';

/**
* BookReader
Expand Down Expand Up @@ -645,6 +646,8 @@ BookReader.prototype.init = function() {

const params = this.initParams();

// Make a copy of it
this.firstParams = JSON.parse(JSON.stringify(params));
this.firstIndex = params.index ? params.index : 0;

// Setup Navbars and other UI
Expand Down Expand Up @@ -1976,15 +1979,15 @@ BookReader.prototype.queryStringFromParams = function(
// the browser seems not to handle with the text fragment
if (newParams.get('text')) {
newParams.delete('text');
textFragmentParam = `text=${this.urlPlugin.retrieveTextFragment(currQueryString)}`;
textFragmentParam = BookReaderTextFragment.fromUrl(currQueryString, this.book, this.firstParams.index);
}

// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString
// Note: This method returns the query string without the question mark.
let result = newParams.toString();
if (textFragmentParam) {
if (result) result += '&';
result += textFragmentParam;
result += textFragmentParam.toString();
}
if (result) result = '?' + result;

Expand Down
7 changes: 5 additions & 2 deletions src/css/_BRpages.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,11 @@ svg.BRPageLayer {
}

// Hides page layers during page flip animation
.BRpageFlipping .BRtextLayer {
display: none;
.BRpageFlipping {
// If the text layer has an annotation, don't hide it
.BRpagecontainer:not(:has(hypothesis-highlight)):not(:has(.BRhighlight)) .BRtextLayer {
display: none;
}
}

.br-mode-2up__root {
Expand Down
24 changes: 15 additions & 9 deletions src/css/_TextSelection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@
}
}

// Style URI TextFragments, eg #:~:text=example
.BRtextLayer ::target-text {
// Similar colour to the default one used in Safari, Firefox. Note Chrome uses a purple colour
background-color: hsla(45, 80%, 66%, 0.6);
color: transparent;
}

.BRtranslateLayer ::selection {
background: hsla(210, 74%, 62%, 0.4);
}
Expand All @@ -82,7 +75,7 @@
// Hide text layer for performance during zooming & scrolling
.BRsmooth-zooming, .BRscrolling-active {
// If the text layer has an annotation, don't hide it
.BRpagecontainer:not(.BRpagecontainer--hasSelection):not(:has(hypothesis-highlight)) .BRtextLayer {
.BRpagecontainer:not(.BRpagecontainer--hasSelection):not(:has(hypothesis-highlight)):not(:has(.BRhighlight)) .BRtextLayer {
display: none;
}
}
Expand Down Expand Up @@ -209,4 +202,17 @@
width: auto;
margin-left: 4px;
opacity: 1;
}
}

.BRtextLayer .BRhighlight {
background-color: yellow;
pointer-events: all;
color: transparent;
}

// Style URI TextFragments, eg #:~:text=example
.BRtextLayer .BRhighlight--target-text, .BRtextLayer ::target-text {
// Similar colour to the default one used in Safari, Firefox. Note Chrome uses a purple colour
background-color: hsla(45, 80%, 66%, 0.6);
color: transparent;
}
20 changes: 17 additions & 3 deletions src/plugins/plugin.experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ export class ExperimentsPlugin extends BookReaderPlugin {
allExperiments = [
new class extends ExperimentModel {
name = 'copyLinkToHighlight';
title = 'Copy to Selection URL';
description = 'Share text selection via URL';
learnMore = 'none';
title = 'Copy Link to Highlight';
description = 'Shareable link to a text selection';
icon = null;
enabled = false;
async enable ({ manual = false }) {
Expand All @@ -72,6 +71,21 @@ export class ExperimentsPlugin extends BookReaderPlugin {
});
}
}(),
new class extends ExperimentModel {
name = 'annotateHighlight';
title = 'Highlight and annotate';
description = 'Create private highlights and annotations for this book';
icon = null;
enabled = false;
async enable ({ manual = false }) {
this.br.plugins.textSelection.enableHighlightMenu();
}
async disable() {
sleep(0).then(() => {
window.location.reload();
});
}
}(),
new class extends ExperimentModel {
name = 'translate';
title = 'Translate Plugin';
Expand Down
51 changes: 5 additions & 46 deletions src/plugins/plugin.text_selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export class TextSelectionPlugin extends BookReaderPlugin {
this.textSelectionManager.renderSelectionMenu();
}

enableHighlightMenu() {
this.textSelectionManager.highlightAnnotationEnabled = true;
this.textSelectionManager.renderHighlightMenu();
}

/**
* @override
* @param {PageContainer} pageContainer
Expand Down Expand Up @@ -575,49 +580,3 @@ class Rect {
get top() { return this.y; }
get left() { return this.x; }
}

/**
* Depth traverse the DOM tree starting at `start`, and ending at `end`.
* @param {Node} start
* @param {Node} end
* @returns {Generator<Node>}
*/
export function* walkBetweenNodes(start, end) {
let done = false;

/**
* @param {Node} node
*/
function* walk(node, {children = true, parents = true, siblings = true} = {}) {
if (node === end) {
done = true;
yield node;
return;
}

// yield self
yield node;

// First iterate children (depth-first traversal)
if (children && node.firstChild) {
yield* walk(node.firstChild, {children: true, parents: false, siblings: true});
if (done) return;
}

// Then iterate siblings
if (siblings) {
for (let sib = node.nextSibling; sib; sib = sib.nextSibling) {
yield* walk(sib, {children: true, parents: false, siblings: false});
if (done) return;
}
}

// Finally, move up the tree
if (parents && node.parentNode) {
yield* walk(node.parentNode, {children: false, parents: true, siblings: true});
if (done) return;
}
}

yield* walk(start);
}
17 changes: 2 additions & 15 deletions src/plugins/url/UrlPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,22 +190,9 @@ export class UrlPlugin {
}

/**
* Get the hash out of the current URL. Also augments it with the text
* from the main part of the URL, since that is not readable by JS
* from the actual hash
* @returns
* Get the hash out of the current URL
*/
getHash() {
const text = this.retrieveTextFragment(window.location.search);
const textFragment = text ? `:~:text=${text[0]}` : '';
return `${window.location.hash.slice(1)}${textFragment}`;
}

/**
* @param {string} urlString
* @returns {string}
*/
retrieveTextFragment(urlString) {
return urlString.match(/(?<=[&?]?text=)[^&]*/);
return window.location.hash.slice(1);
}
}
83 changes: 24 additions & 59 deletions src/plugins/url/plugin.url.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* global BookReader */

import { UrlPlugin } from "./UrlPlugin.js";
import { sleep } from "../../BookReader/utils.js";

import { BookReaderTextFragment } from "../../util/TextSelectionManager.js";
import { renderHighlight } from "../../util/TextSelectionManager.js";
/**
* Plugin for URL management in BookReader
* Note read more about the url "fragment" here:
Expand Down Expand Up @@ -43,10 +43,6 @@ BookReader.prototype.setup = (function(super_) {
this.locationPollId = null;
this.oldLocationHash = null;
this.oldUserHash = null;
// Should include the :~:text= prefix
this.textFragment = null;
// Tracks the original textFragment page num when first loaded
this.textFragmentPage = null;
};
})(BookReader.prototype.setup);

Expand Down Expand Up @@ -146,22 +142,16 @@ BookReader.prototype.urlUpdateFragment = function() {
}, {});

// eg 'page/3/mode/2up'; no query params (in hash mode, it might have /search/term)
// Does NOT have the :~:text fragment
const newFragment = this.fragmentFromParams(params, this.options.urlMode);
const newFragmentWithSlash = newFragment === '' ? '' : `/${newFragment}`;
// eg 'page/3/mode/2up'; no query params
// WILL CONTAIN the :~:text fragment in hash mode (!)
const currFragment = this.urlReadFragment();
// This should have both ?q=foo&text=bar (and any other params) as an encoded string
const currQueryString = this.getLocationSearch();
// Eg ?q=foo&text=bar; only query params, no fragment
const newQueryString = this.queryStringFromParams(params, currQueryString, this.options.urlMode);

// NOTE: If ?text is in the URL, we will fire fragment change events on every render; which is
// not desireable, but currently don't have a way to handle re-writing ?text to the hash text
// fragment form, :~:text=foo.
const hasTextParam = this.urlPlugin.retrieveTextFragment(currQueryString);
if (currFragment === newFragment && currQueryString === newQueryString && !hasTextParam) {
// Avoid infinite loop if there are no changes
if (currFragment === newFragment && currQueryString === newQueryString) {
return;
}

Expand All @@ -170,19 +160,12 @@ BookReader.prototype.urlUpdateFragment = function() {
this.options.urlMode = 'hash';
} else {
const baseWithoutSlash = this.options.urlHistoryBasePath.replace(/\/+$/, '');
const textFragment = this.urlPlugin.retrieveTextFragment(newQueryString);
this.targetTextFragment = BookReaderTextFragment.fromUrl(newQueryString, this.book, this.firstParams.index);
const newUrlPath = `${baseWithoutSlash}${newFragmentWithSlash}${newQueryString}`;
const extractedPage = this.urlPlugin.urlStringToUrlState(newFragmentWithSlash)?.page;
if (!this.textFragmentPage && textFragment) {
this.textFragmentPage = extractedPage ? extractedPage : null;
this.textFragment = `:~:text=${textFragment}`;
}

try {
window.history.replaceState({}, null, newUrlPath);
this.oldLocationHash = newFragment + newQueryString;
if (textFragment) {
this.oldLocationHash += `:~:text=${textFragment[0]}`;
}
} catch (e) {
// DOMException on Chrome when in sandboxed iframe
this.options.urlMode = 'hash';
Expand All @@ -192,22 +175,9 @@ BookReader.prototype.urlUpdateFragment = function() {

if (this.options.urlMode === 'hash') {
const newQueryStringSearch = this.urlParamsFiltersOnlySearch(this.readQueryString());
let textFragment = this.urlPlugin.retrieveTextFragment(this.readQueryString());
const extractedPage = this.urlPlugin.urlStringToUrlState(newFragmentWithSlash)?.page;

if (textFragment) {
textFragment = `:~:text=${textFragment[0]}`;
} else {
textFragment = '';
}
if (!this.textFragmentPage && textFragment) {
this.textFragmentPage = extractedPage ? extractedPage : null;
this.textFragment = textFragment;
} else if (this.textFragmentPage && extractedPage != this.textFragmentPage) {
textFragment = '';
}
window.location.replace('#' + newFragment + newQueryStringSearch + textFragment);
this.oldLocationHash = newFragment + newQueryStringSearch + textFragment;
this.targetTextFragment = BookReaderTextFragment.fromUrl(this.readQueryString(), this.book, this.firstParams.index);
window.location.replace('#' + newFragment + newQueryStringSearch);
this.oldLocationHash = newFragment + newQueryStringSearch;
}
};

Expand Down Expand Up @@ -245,31 +215,26 @@ BookReader.prototype.urlReadHashFragment = function() {
return window.location.hash.substr(1);
};
export class BookreaderUrlPlugin extends BookReader {
/** @type {BookReaderTextFragment} */
targetTextFragment;

init() {
if (this.options.enableUrlPlugin) {
this.urlPlugin = new UrlPlugin(this.options);
const location = this.getLocationSearch();
if (location.includes("text=")) {
this.on('textLayerVisible', async (_, {pageContainerEl}) => {
const visiblePageNum = pageContainerEl.getAttribute('data-page-num');

// Hack: More time mode 1up page "settle down" from user scrolling
await sleep(this.mode === 1 ? 900 : 100);

// No textFragment found or the textFragment stored doesn't match current visible page loaded
if (!this.textFragment || this.textFragmentPage !== visiblePageNum) return;
if (this.options.urlMode === 'history') {
window.location.replace(`#${this.textFragment}`);
} else {
// for urlMode hash, textFragment is stored in oldLocationHash already
window.location.replace(`#${this.oldLocationHash}`);
}
});
}

this.bind(BookReader.eventNames.PostInit, () => {
const { urlMode } = this.options;
if (this.targetTextFragment) {
this.on('textLayerVisible', async (_, {pageContainerEl}) => {
const pageIndex = this.targetTextFragment.pageNumber ? this.book.getPageIndex(this.targetTextFragment.pageNumber) : this.firstParams.index;
const hasTargetText = pageIndex === parseFloat(pageContainerEl.getAttribute('data-index'));
if (hasTargetText) {
const textLayer = pageContainerEl.querySelector('.BRtextLayer');
Comment on lines +230 to +231
renderHighlight(textLayer, this.targetTextFragment, 'BRhighlight--target-text');
}
});
}

if (urlMode === 'hash') {
if (this.options.urlMode === 'hash') {
this.urlPlugin.listenForHashChanges();
}
});
Expand Down
Loading
Loading