Skip to content

Commit 2908b1e

Browse files
authored
Merge pull request #1512 from schu96/custom-live-server
"Copy link to highlight" experiment
2 parents 1c6792b + c86d4c7 commit 2908b1e

15 files changed

Lines changed: 920 additions & 58 deletions

package-lock.json

Lines changed: 23 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@internetarchive/icon-magnify-minus": "^1.4.0",
4343
"@internetarchive/icon-magnify-plus": "^1.4.0",
4444
"@internetarchive/icon-search": "^1.4.0",
45+
"@internetarchive/icon-share": "1.4.0",
4546
"@internetarchive/icon-toc": "^1.4.0",
4647
"@internetarchive/icon-visual-adjustment": "^1.4.0",
4748
"@internetarchive/modal-manager": "2.0.5",

src/BookReader.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,10 +1970,25 @@ BookReader.prototype.queryStringFromParams = function(
19701970
if (params.search && urlMode === 'history') {
19711971
newParams.set('q', params.search);
19721972
}
1973+
1974+
let textFragmentParam = '';
1975+
// Need to pull out text separately to avoid the spaces becoming encoded as +, which
1976+
// the browser seems not to handle with the text fragment
1977+
if (newParams.get('text')) {
1978+
newParams.delete('text');
1979+
textFragmentParam = `text=${this.urlPlugin.retrieveTextFragment(currQueryString)}`;
1980+
}
1981+
19731982
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString
19741983
// Note: This method returns the query string without the question mark.
1975-
const result = newParams.toString();
1976-
return result ? '?' + result : '';
1984+
let result = newParams.toString();
1985+
if (textFragmentParam) {
1986+
if (result) result += '&';
1987+
result += textFragmentParam;
1988+
}
1989+
if (result) result = '?' + result;
1990+
1991+
return result;
19771992
};
19781993

19791994
/**

src/BookReader/Mode2UpLit.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -628,9 +628,7 @@ export class Mode2UpLit extends LitElement {
628628
progression == 'lr' ? [nextSpread.left, nextSpread.right] : [nextSpread.right, nextSpread.left]
629629
).filter(x => x);
630630
nextPageContainers.forEach(c => {
631-
this.br.trigger('pageVisible', {
632-
pageContainerEl: c.$container[0],
633-
});
631+
this.br.trigger('pageVisible', {pageContainerEl: c.$container[0]});
634632
});
635633
}
636634

src/BookReader/utils/SelectionObserver.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ export class SelectionObserver {
44
startedInSelector = false;
55
/** @type {HTMLElement} */
66
target = null;
7+
/** @type {Node} */
8+
lastKnownFocusNode = null;
79

810
/**
911
* @param {string} selector
10-
* @param {function('started' | 'cleared', HTMLElement): any} handler
12+
* @param {function('started' | 'cleared' | 'focusChanged', HTMLElement): any} handler
1113
*/
1214
constructor(selector, handler) {
1315
this.selector = selector;
@@ -34,9 +36,15 @@ export class SelectionObserver {
3436
if (!target) return;
3537
this.target = target;
3638
this.selecting = true;
39+
this.lastKnownFocusNode = sel.focusNode;
3740
this.handler('started', this.target);
3841
}
3942

43+
if (this.selecting && (this.lastKnownFocusNode != sel.focusNode || sel.toString() && !sel.isCollapsed)) {
44+
this.lastKnownFocusNode = sel.focusNode;
45+
this.handler('focusChanged', this.target);
46+
}
47+
4048
if (this.selecting && (sel.isCollapsed || !sel.toString() || !$(sel.anchorNode).closest(this.selector)[0])) {
4149
this.selecting = false;
4250
this.handler('cleared', this.target);

src/css/_TextSelection.scss

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,63 @@
150150

151151
.BRtranslateLayer .BRparagraphElement.BRtranslateHidden {
152152
display: none;
153+
}
154+
155+
.br-select-menu__root {
156+
display: none;
157+
border-radius: 20px;
158+
background-color: #333;
159+
color-scheme: dark;
160+
padding: 2px;
161+
overflow: hidden;
162+
transition: border-radius 0.2s;
163+
}
164+
.br-select-menu__root:hover {
165+
border-radius: 8px;
166+
}
167+
168+
.br-select-menu__root:hover .br-select-menu__option {
169+
border-radius: 6px;
170+
}
171+
172+
.br-select-menu__option {
173+
--iconWidth: 15px;
174+
--iconHeight: 15px;
175+
--iconFillColor: currentColor;
176+
background: transparent;
177+
display: flex;
178+
align-items: center;
179+
border-radius: 20px;
180+
font-family: inherit;
181+
border: 0;
182+
transition: background-color 0.2s, border-radius 0.2s;
183+
cursor: pointer;
184+
text-wrap: nowrap;
185+
padding: 4px;
186+
}
187+
188+
.br-select-menu__option:hover {
189+
background-color: rgba(255, 255, 255, 0.1);
190+
}
191+
192+
.br-select-menu__icon {
193+
display: block;
194+
flex-shrink: 0;
195+
width: 17px;
196+
height: 17px;
197+
}
198+
199+
.br-select-menu__label {
200+
width: 0px;
201+
opacity: 0;
202+
interpolate-size: allow-keywords;
203+
transition: width 0.2s;
204+
font-size: 12px;
205+
}
206+
207+
208+
.br-select-menu__root:hover .br-select-menu__label {
209+
width: auto;
210+
margin-left: 4px;
211+
opacity: 1;
153212
}

src/plugins/plugin.experiments.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,27 @@ export class ExperimentsPlugin extends BookReaderPlugin {
5151
localStorageKey: 'BrExperiments',
5252

5353
/** The experiments that should be shown in the experiments panel */
54-
enabledExperiments: ['translate'],
54+
enabledExperiments: ['translate', 'copyLinkToHighlight'],
5555
}
5656

5757
/** @type {ExperimentModel[]} */
5858
allExperiments = [
59+
new class extends ExperimentModel {
60+
name = 'copyLinkToHighlight';
61+
title = 'Copy to Selection URL';
62+
description = 'Share text selection via URL';
63+
learnMore = 'none';
64+
icon = null;
65+
enabled = false;
66+
async enable ({ manual = false }) {
67+
this.br.plugins.textSelection.enableSelectionMenu();
68+
}
69+
async disable() {
70+
sleep(0).then(() => {
71+
window.location.reload();
72+
});
73+
}
74+
}(),
5975
new class extends ExperimentModel {
6076
name = 'translate';
6177
title = 'Translate Plugin';
@@ -123,7 +139,9 @@ export class ExperimentsPlugin extends BookReaderPlugin {
123139
for (const experiment of this.allExperiments) {
124140
// TODO: imagesBaseURL should be replaced with assetRoot everywhere
125141
experiment.assetRoot = this.br.options.imagesBaseURL.replace(/images\/$/, '');
126-
experiment.icon = experiment.buildAssetPath(experiment.icon);
142+
if (experiment.icon) {
143+
experiment.icon = experiment.buildAssetPath(experiment.icon);
144+
}
127145
experiment.br = this.br;
128146
}
129147

@@ -269,7 +287,7 @@ export class BrExperimentToggle extends LitElement {
269287
return html`
270288
<div class="experiment-card" style="margin-bottom: 10px;">
271289
<div style="display: flex; align-items: center; gap: 10px;">
272-
<img src="${this.icon}" style="width: 20px; height: 20px;" alt="" />
290+
${this.icon ? html`<img src="${this.icon}" style="width: 20px; height: 20px;" alt="" />` : ''}
273291
<div style="flex-grow: 1; font-weight: bold;">${this.title}</div>
274292
</div>
275293
<p style="opacity: 0.9">

src/plugins/plugin.text_selection.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,21 @@ export class TextSelectionPlugin extends BookReaderPlugin {
5353
init() {
5454
if (!this.options.enabled) return;
5555

56+
this.br.on('pageVisible', (_, {pageContainerEl}) => {
57+
if (pageContainerEl.querySelector('.BRtextLayer')) {
58+
this.br.trigger('textLayerVisible', {pageContainerEl});
59+
}
60+
});
61+
5662
this.loadData();
5763
this.textSelectionManager.init();
5864
}
5965

66+
enableSelectionMenu() {
67+
this.textSelectionManager.selectionMenuEnabled = true;
68+
this.textSelectionManager.renderSelectionMenu();
69+
}
70+
6071
/**
6172
* @override
6273
* @param {PageContainer} pageContainer
@@ -188,13 +199,19 @@ export class TextSelectionPlugin extends BookReaderPlugin {
188199
paragEl.style.marginTop = `${newTop}px`;
189200
yAdded += newTop;
190201
textLayer.appendChild(paragEl);
202+
textLayer.appendChild(document.createTextNode('\n'));
191203
}
192204
$container.append(textLayer);
193205
this.textSelectionManager.stopPageFlip($container);
194206
this.br.trigger('textLayerRendered', {
195207
pageIndex,
196208
pageContainer,
197209
});
210+
211+
// Check if page is visible
212+
if ($container.hasClass('BRpage-visible')) {
213+
this.br.trigger('textLayerVisible', {pageContainerEl: $container[0]});
214+
}
198215
}
199216

200217
/**

src/plugins/url/UrlPlugin.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,15 @@ export class UrlPlugin {
161161
* If it was changeed, update the urlState
162162
*/
163163
listenForHashChanges() {
164-
this.oldLocationHash = window.location.hash.substr(1);
164+
this.oldLocationHash = this.getHash();
165165
if (this.urlLocationPollId) {
166166
clearInterval(this.urlLocationPollId);
167167
this.urlLocationPollId = null;
168168
}
169169

170170
// check if the URL changes
171171
const updateHash = () => {
172-
const newFragment = window.location.hash.substr(1);
172+
const newFragment = this.getHash();
173173
const hasFragmentChange = newFragment != this.oldLocationHash;
174174

175175
if (!hasFragmentChange) { return; }
@@ -182,10 +182,30 @@ export class UrlPlugin {
182182
/**
183183
* Will read either the hash or URL and return the bookreader fragment
184184
*/
185-
pullFromAddressBar (location = window.location) {
185+
pullFromAddressBar(location = window.location) {
186186
const path = this.urlMode === 'history'
187187
? (location.pathname.substr(this.urlHistoryBasePath.length) + location.search)
188188
: location.hash.substr(1);
189189
this.urlState = this.urlStringToUrlState(path);
190190
}
191+
192+
/**
193+
* Get the hash out of the current URL. Also augments it with the text
194+
* from the main part of the URL, since that is not readable by JS
195+
* from the actual hash
196+
* @returns
197+
*/
198+
getHash() {
199+
const text = this.retrieveTextFragment(window.location.search);
200+
const textFragment = text ? `:~:text=${text[0]}` : '';
201+
return `${window.location.hash.slice(1)}${textFragment}`;
202+
}
203+
204+
/**
205+
* @param {string} urlString
206+
* @returns {string}
207+
*/
208+
retrieveTextFragment(urlString) {
209+
return urlString.match(/(?<=[&?]?text=)[^&]*/);
210+
}
191211
}

0 commit comments

Comments
 (0)