Skip to content
Open
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 @@ -1976,9 +1976,12 @@ 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 = `text=${this.urlPlugin.retrieveTextFragment(currQueryString, 'text')}`;
}
if (newParams.get('dIndex')) {
newParams.delete('dIndex');
textFragmentParam += `&dIndex=${this.urlPlugin.retrieveDIndex(currQueryString)}`;
}

// 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();
Expand Down
21 changes: 13 additions & 8 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 Down Expand Up @@ -209,4 +202,16 @@
width: auto;
margin-left: 4px;
opacity: 1;
}
}

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

// 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;
}
18 changes: 16 additions & 2 deletions src/plugins/plugin.experiments.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class ExperimentsPlugin extends BookReaderPlugin {
localStorageKey: 'BrExperiments',

/** The experiments that should be shown in the experiments panel */
enabledExperiments: ['translate', 'copyLinkToHighlight'],
enabledExperiments: ['translate', 'copyLinkToHighlight', 'annotateHighlight'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enabledExperiments: ['translate', 'copyLinkToHighlight', 'annotateHighlight'],
enabledExperiments: ['translate', 'copyLinkToHighlight'],

}

/** @type {ExperimentModel[]} */
Expand All @@ -60,7 +60,6 @@ export class ExperimentsPlugin extends BookReaderPlugin {
name = 'copyLinkToHighlight';
title = 'Copy to Selection URL';
description = 'Share text selection via URL';
learnMore = 'none';
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
5 changes: 5 additions & 0 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
64 changes: 57 additions & 7 deletions src/plugins/url/UrlPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,10 @@ 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}`;
return window.location.hash.slice(1);
}

/**
Expand All @@ -208,4 +203,59 @@ export class UrlPlugin {
retrieveTextFragment(urlString) {
return urlString.match(/(?<=[&?]?text=)[^&]*/);
}

retrieveDIndex(urlString) {
return urlString.match(/(?<=[&?]?dIndex=)[^&]*/);
}

/**
* @param {string} urlString
* @param {string} type
* @returns {string}
*/

retrieveHighlightContext(urlString, type) {
const regexString = new RegExp(String.raw`(?<=[&?]?${type}=)[^&]*`, "gis");
return urlString.match(regexString);
}

parseToText(urlString) {
const quoteMatch = urlString.match(/(?<=[&?]?text=)[^&]*/);
let dIndex = urlString.match(/(?<=[&?]?dIndex=)[^&]*/);
let quote, prefix, suffix;
if (quoteMatch) {
const prefixMatch = quoteMatch[0].match(/.*?(?=(-,))/);
const suffixMatch = quoteMatch[0].match(/(?<=,-).*/);
if (prefixMatch) prefix = decodeURIComponent(prefixMatch[0]);
if (suffixMatch) suffix = decodeURIComponent(suffixMatch[0]);
if (prefixMatch && suffixMatch) {
quote = decodeURIComponent(quoteMatch[0].match(/(?<=(-,)).*(?=(,-))/)[0]);
} else if (prefixMatch && !suffixMatch) { // Prefix only
quote = decodeURIComponent(quoteMatch[0].match(/(?<=-,).*/)[0]);
} else if (!prefixMatch && suffixMatch) { // Suffix only
quote = decodeURIComponent(quoteMatch[0].match(/.*(?<=,-)/)[0]);
} else { // Somehow no prefix or suffix
quote = decodeURIComponent(quoteMatch[0]);
}
}
if (dIndex) dIndex = decodeURIComponent(dIndex[0]);
return {prefix, quote, suffix, dIndex};
}
}

/**
* @typedef {Object} BookReaderTextFragment
* An extension of the fields defined by the browser-native TextFragment;
* See https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment/Text_fragments
*
* @property {string} prefix
* @property {string} quote
* @property {string} suffix
* @property {string} dIndex Page index
* @property {string} dPageNum Page num ; eg. asserted page number or the n-prefixed page index
*/

/**
* @typedef {BookReaderTextFragment} BookReaderSavedHighlight
* @property {string} uuid
*/
72 changes: 22 additions & 50 deletions src/plugins/url/plugin.url.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

import { convertRangeToDOMSelection } 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,18 @@ 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) {
// Adding a check to the urlMode for now. Without the check, the highlight does not work if shared and the page is refreshed
if (currFragment === newFragment && currQueryString === newQueryString
&& !newQueryString.includes('search=')
) {
return;
}

Expand All @@ -170,19 +162,12 @@ BookReader.prototype.urlUpdateFragment = function() {
this.options.urlMode = 'hash';
} else {
const baseWithoutSlash = this.options.urlHistoryBasePath.replace(/\/+$/, '');
const textFragment = this.urlPlugin.retrieveTextFragment(newQueryString);
this.targetTextFragment = this.urlPlugin.parseToText(newQueryString);
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 +177,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 = this.urlPlugin.parseToText(this.readQueryString());
window.location.replace('#' + newFragment + newQueryStringSearch);
this.oldLocationHash = newFragment + newQueryStringSearch;
}
};

Expand Down Expand Up @@ -245,24 +217,24 @@ BookReader.prototype.urlReadHashFragment = function() {
return window.location.hash.substr(1);
};
export class BookreaderUrlPlugin extends BookReader {
/** @type {import('./UrlPlugin.js').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}`);
const hasTargetText = this.targetTextFragment?.dIndex === pageContainerEl.getAttribute('data-index');
if (hasTargetText) {
if (!this.targetTextFragment['dPageNum']) {
this.targetTextFragment['dPageNum'] = pageContainerEl.getAttribute('data-page-num');
}

// Hack: More time mode 1up page "settle down" from user scrolling
await sleep(this.mode === 1 ? 900 : 200);
convertRangeToDOMSelection(this.targetTextFragment);
}
});
}
Expand Down
Loading
Loading