@internetarchive/bookreader 5.0.0-116 → 5.0.0-117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BookReader/BookReader.css +44 -18
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +74 -74
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.chapters.js +2 -2
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.experiments.js +1 -1
- package/BookReader/plugins/plugin.experiments.js.map +1 -1
- package/BookReader/plugins/plugin.iframe.js +1 -1
- package/BookReader/plugins/plugin.iframe.js.map +1 -1
- package/BookReader/plugins/plugin.iiif.js +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.search.js +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +22 -2
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.translate.js +23 -3
- package/BookReader/plugins/plugin.translate.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +1 -1
- package/README.md +3 -1
- package/package.json +33 -29
- package/src/BookReader.js +16 -0
- package/src/css/_TextSelection.scss +51 -18
- package/src/plugins/plugin.chapters.js +8 -4
- package/src/plugins/plugin.iframe.js +36 -37
- package/src/plugins/plugin.text_selection.js +36 -11
- package/src/plugins/search/plugin.search.js +2 -8
- package/src/plugins/tts/plugin.tts.js +3 -8
- package/src/util/TextSelectionManager.js +53 -8
- package/src/util/dom.js +88 -0
|
@@ -6,6 +6,7 @@ import { Cache } from '../util/cache.js';
|
|
|
6
6
|
import { toISO6391 } from './tts/utils.js';
|
|
7
7
|
import { BookReaderTextFragment, renderHighlight, TextSelectionManager } from '../util/TextSelectionManager.js';
|
|
8
8
|
import { genMap, lookAroundWindow, zip } from '../util/generators.js';
|
|
9
|
+
import textSelectionCss from '../css/_TextSelection.scss';
|
|
9
10
|
/** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
|
|
10
11
|
/** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
|
|
11
12
|
|
|
@@ -39,6 +40,13 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
39
40
|
|
|
40
41
|
_jumpedToHighlight = false;
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Isolated document/layout used to performantly measure OCR text-layer
|
|
45
|
+
* elements.
|
|
46
|
+
* @type {Document}
|
|
47
|
+
*/
|
|
48
|
+
_measurementDocument;
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* @param {import('../BookReader.js').default} br
|
|
44
52
|
*/
|
|
@@ -56,6 +64,19 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
56
64
|
init() {
|
|
57
65
|
if (!this.options.enabled) return;
|
|
58
66
|
|
|
67
|
+
// Setup measurement iframe for OCR
|
|
68
|
+
const measurementIframe = document.createElement('iframe');
|
|
69
|
+
measurementIframe.setAttribute('aria-hidden', 'true');
|
|
70
|
+
measurementIframe.tabIndex = -1;
|
|
71
|
+
measurementIframe.style.cssText = 'position:fixed; top:-99999px; left:-99999px; width:2000px; height:4000px; border:0; visibility:hidden;';
|
|
72
|
+
document.body.appendChild(measurementIframe);
|
|
73
|
+
this._measurementDocument = measurementIframe.contentDocument;
|
|
74
|
+
// Injects _TextSelection.scss so measurements match the real
|
|
75
|
+
// rendering
|
|
76
|
+
const style = this._measurementDocument.createElement('style');
|
|
77
|
+
style.textContent = textSelectionCss;
|
|
78
|
+
this._measurementDocument.head.appendChild(style);
|
|
79
|
+
|
|
59
80
|
this.br.on('pageVisible', (_, {pageContainerEl}) => {
|
|
60
81
|
const textLayer = pageContainerEl.querySelector('.BRtextLayer');
|
|
61
82
|
if (textLayer) {
|
|
@@ -78,7 +99,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
78
99
|
const markEls = renderHighlight(textLayer, targetTextFragment, 'BRhighlight--target-text');
|
|
79
100
|
// Only jump once; presumably on first page load.
|
|
80
101
|
if (!this._jumpedToHighlight) {
|
|
81
|
-
markEls[0]
|
|
102
|
+
this.br.scrollIntoView(markEls[0], {behavior: 'smooth', block: 'center'});
|
|
82
103
|
this._jumpedToHighlight = true;
|
|
83
104
|
}
|
|
84
105
|
}
|
|
@@ -94,7 +115,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
94
115
|
_configurePageContainer(pageContainer) {
|
|
95
116
|
// Disable if thumb mode; it's too janky
|
|
96
117
|
// .page can be null for "pre-cover" region
|
|
97
|
-
if (this.br.mode !== this.br.constModeThumb && pageContainer.page?.isViewable) {
|
|
118
|
+
if (this.options.enabled && this.br.mode !== this.br.constModeThumb && pageContainer.page?.isViewable) {
|
|
98
119
|
this.createTextLayer(pageContainer);
|
|
99
120
|
}
|
|
100
121
|
return pageContainer;
|
|
@@ -204,7 +225,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
204
225
|
});
|
|
205
226
|
|
|
206
227
|
// Fix up paragraph positions
|
|
207
|
-
const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement');
|
|
228
|
+
const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement', this._measurementDocument);
|
|
208
229
|
let yAdded = 0;
|
|
209
230
|
for (const [ocrParagraph, paragEl] of zip(ocrParagraphs, paragEls)) {
|
|
210
231
|
const ocrParagBounds = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
|
|
@@ -312,7 +333,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
312
333
|
paragEl.style.fontSize = `${paragWordHeight}px`;
|
|
313
334
|
|
|
314
335
|
// Fix up sizes - stretch/crush words as necessary using letter spacing
|
|
315
|
-
let wordRects = determineRealRects(paragEl, '.BRwordElement');
|
|
336
|
+
let wordRects = determineRealRects(paragEl, '.BRwordElement', this._measurementDocument);
|
|
316
337
|
const ocrWords = $(ocrParagraph).find("WORD").toArray();
|
|
317
338
|
const wordEls = paragEl.querySelectorAll('.BRwordElement');
|
|
318
339
|
for (const [ocrWord, wordEl] of zip(ocrWords, wordEls)) {
|
|
@@ -333,8 +354,8 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
333
354
|
|
|
334
355
|
// Stretch/crush lines as necessary using line spacing
|
|
335
356
|
// Recompute rects after letter spacing
|
|
336
|
-
wordRects = determineRealRects(paragEl, '.BRwordElement');
|
|
337
|
-
const spaceRects = determineRealRects(paragEl, '.BRspace');
|
|
357
|
+
wordRects = determineRealRects(paragEl, '.BRwordElement', this._measurementDocument);
|
|
358
|
+
const spaceRects = determineRealRects(paragEl, '.BRspace', this._measurementDocument);
|
|
338
359
|
|
|
339
360
|
const ocrLines = $(ocrParagraph).find("LINE[coords]").toArray();
|
|
340
361
|
const lineEls = Array.from(paragEl.querySelectorAll('.BRlineElement'));
|
|
@@ -384,9 +405,11 @@ BookReader?.registerPlugin('textSelection', TextSelectionPlugin);
|
|
|
384
405
|
/**
|
|
385
406
|
* @param {HTMLElement} parentEl
|
|
386
407
|
* @param {string} selector
|
|
408
|
+
* @param {Document} measurementDocument Isolated document to measure within
|
|
409
|
+
* (see TextSelectionPlugin#_measurementDocument for why).
|
|
387
410
|
* @returns {Map<Element, Rect>}
|
|
388
411
|
*/
|
|
389
|
-
function determineRealRects(parentEl, selector) {
|
|
412
|
+
function determineRealRects(parentEl, selector, measurementDocument) {
|
|
390
413
|
const initals = {
|
|
391
414
|
position: parentEl.style.position,
|
|
392
415
|
visibility: parentEl.style.visibility,
|
|
@@ -399,21 +422,23 @@ function determineRealRects(parentEl, selector) {
|
|
|
399
422
|
parentEl.style.top = '0';
|
|
400
423
|
parentEl.style.left = '0';
|
|
401
424
|
parentEl.style.transform = 'none';
|
|
402
|
-
|
|
425
|
+
measurementDocument.body.appendChild(parentEl);
|
|
403
426
|
const rects = new Map(
|
|
404
427
|
Array.from(parentEl.querySelectorAll(selector))
|
|
405
428
|
.map(wordEl => {
|
|
406
429
|
const origRect = wordEl.getBoundingClientRect();
|
|
407
430
|
return [wordEl, new Rect(
|
|
408
|
-
origRect.left +
|
|
409
|
-
origRect.top +
|
|
431
|
+
origRect.left + measurementDocument.defaultView.scrollX,
|
|
432
|
+
origRect.top + measurementDocument.defaultView.scrollY,
|
|
410
433
|
origRect.width,
|
|
411
434
|
origRect.height,
|
|
412
435
|
)];
|
|
413
436
|
}),
|
|
414
437
|
);
|
|
415
|
-
|
|
438
|
+
measurementDocument.body.removeChild(parentEl);
|
|
416
439
|
Object.assign(parentEl.style, initals);
|
|
440
|
+
// Need to restore the document to the main window document
|
|
441
|
+
document.adoptNode(parentEl);
|
|
417
442
|
return rects;
|
|
418
443
|
}
|
|
419
444
|
|
|
@@ -383,14 +383,8 @@ export class SearchPlugin extends BookReaderPlugin {
|
|
|
383
383
|
const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
|
|
384
384
|
if ($boxes.length) {
|
|
385
385
|
$boxes.css('animation', 'none');
|
|
386
|
-
$boxes[0]
|
|
387
|
-
|
|
388
|
-
// 2up, if we're not fullscreen, the whole body gets scrolled around to try to
|
|
389
|
-
// center the highlight 🙄 See:
|
|
390
|
-
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
|
|
391
|
-
// Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
|
|
392
|
-
// full-width, and covers up the last line of the highlight.
|
|
393
|
-
block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
|
|
386
|
+
this.br.scrollIntoView($boxes[0], {
|
|
387
|
+
block: 'center',
|
|
394
388
|
inline: 'center',
|
|
395
389
|
behavior: onNearbyPage ? 'smooth' : 'auto',
|
|
396
390
|
});
|
|
@@ -350,14 +350,9 @@ export class TtsPlugin extends BookReaderPlugin {
|
|
|
350
350
|
// It behaves weird if used in thumb mode
|
|
351
351
|
if (this.br.constModeThumb == this.br.mode) return;
|
|
352
352
|
|
|
353
|
-
$(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0]
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// center the highlight 🙄 See:
|
|
357
|
-
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
|
|
358
|
-
// Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
|
|
359
|
-
// full-width, and covers up the last line of the highlight.
|
|
360
|
-
block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
|
|
353
|
+
const highlightRect = $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0];
|
|
354
|
+
this.br.scrollIntoView(highlightRect, {
|
|
355
|
+
block: 'center',
|
|
361
356
|
inline: 'center',
|
|
362
357
|
behavior: 'smooth',
|
|
363
358
|
});
|
|
@@ -7,6 +7,7 @@ import { customElement, property, query } from 'lit/decorators.js';
|
|
|
7
7
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
8
8
|
import '@internetarchive/icon-share';
|
|
9
9
|
import '@internetarchive/icon-edit-pencil/icon-edit-pencil.js';
|
|
10
|
+
import '@internetarchive/icon-ellipses';
|
|
10
11
|
import { isIOS, isAndroid } from './browserSniffing.js';
|
|
11
12
|
import { genAt, genFilter } from './generators.js';
|
|
12
13
|
|
|
@@ -412,6 +413,9 @@ export class BRSelectMenuOption extends LitElement {
|
|
|
412
413
|
if (this.icon === 'edit-pencil') {
|
|
413
414
|
return html`<ia-icon-edit-pencil class="br-select-menu__icon" aria-hidden="true"></ia-icon-edit-pencil>`;
|
|
414
415
|
}
|
|
416
|
+
if (this.icon === 'ellipses') {
|
|
417
|
+
return html`<ia-icon-ellipses class="br-select-menu__icon" aria-hidden="true"></ia-icon-ellipses>`;
|
|
418
|
+
}
|
|
415
419
|
return '';
|
|
416
420
|
}
|
|
417
421
|
|
|
@@ -433,7 +437,9 @@ export class BRSelectMenuOption extends LitElement {
|
|
|
433
437
|
aria-label=${ifDefined(accessibleLabel)}
|
|
434
438
|
>
|
|
435
439
|
${this.renderIcon()}
|
|
436
|
-
${
|
|
440
|
+
${
|
|
441
|
+
!hasTemporaryText && !baseLabel ? '' :
|
|
442
|
+
hasTemporaryText ? html`
|
|
437
443
|
<span class="br-select-menu__label-wrap" style="display: inline-flex; position: relative; align-items: center;">
|
|
438
444
|
<span
|
|
439
445
|
class="br-select-menu__label"
|
|
@@ -459,8 +465,12 @@ class BRSelectMenu extends LitElement {
|
|
|
459
465
|
/** @type {import('../BookReader.js').default} */
|
|
460
466
|
br;
|
|
461
467
|
|
|
468
|
+
/** @type {boolean} */
|
|
469
|
+
@property({type: Boolean, reflect: true})
|
|
470
|
+
showExtended = false;
|
|
471
|
+
|
|
462
472
|
/** @type {BRSelectMenuOption | null} */
|
|
463
|
-
@query('#copy-link-option')
|
|
473
|
+
@query('#br-select-copy-link-option')
|
|
464
474
|
copyLinkOption;
|
|
465
475
|
|
|
466
476
|
@property({type: Boolean, reflect: true})
|
|
@@ -521,7 +531,7 @@ class BRSelectMenu extends LitElement {
|
|
|
521
531
|
// Mousedown needed to prevent selection from being cleared on iOS
|
|
522
532
|
return html`
|
|
523
533
|
<br-menu-option
|
|
524
|
-
id="copy-link-option"
|
|
534
|
+
id="br-select-copy-link-option"
|
|
525
535
|
@mousedown=${/** @param {MouseEvent} e */ (e) => e.preventDefault()}
|
|
526
536
|
@click=${this.handleCopyLinkToHighlight}
|
|
527
537
|
icon="share"
|
|
@@ -560,6 +570,19 @@ class BRSelectMenu extends LitElement {
|
|
|
560
570
|
></br-menu-option>
|
|
561
571
|
`;
|
|
562
572
|
}
|
|
573
|
+
|
|
574
|
+
renderShowMoreOption() {
|
|
575
|
+
return html`
|
|
576
|
+
<br-menu-option
|
|
577
|
+
id="br-select-more"
|
|
578
|
+
@mousedown=${/** @param {MouseEvent} e */ (e) => e.preventDefault()}
|
|
579
|
+
@click=${this.toggleExtendedMenu}
|
|
580
|
+
icon="ellipses"
|
|
581
|
+
aria-label="Show more options"
|
|
582
|
+
></br-menu-option>
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
|
|
563
586
|
renderLocalStorageOptions() {
|
|
564
587
|
return html`
|
|
565
588
|
<br-menu-option
|
|
@@ -574,16 +597,30 @@ class BRSelectMenu extends LitElement {
|
|
|
574
597
|
></br-menu-option>`;
|
|
575
598
|
}
|
|
576
599
|
|
|
577
|
-
|
|
578
|
-
// TODO change the second button to use a different icon
|
|
600
|
+
renderDefaultOptions() {
|
|
579
601
|
return html`
|
|
580
602
|
${this.copyLinkToHighlightEnabled ? this.renderCopyLinkToHighlightOption() : ''}
|
|
581
603
|
${this.highlightAnnotationEnabled && !this.nodesForRemoval ? this.renderHighlightOption() : ''}
|
|
582
|
-
${this.highlightAnnotationEnabled ? this.renderLocalStorageOptions() : ''}
|
|
583
604
|
${this.nodesForRemoval ? this.renderRemoveOption() : ''}
|
|
584
605
|
`;
|
|
585
606
|
}
|
|
586
607
|
|
|
608
|
+
renderExtendedOptions() {
|
|
609
|
+
return html`
|
|
610
|
+
${this.renderDefaultOptions()}
|
|
611
|
+
${this.renderLocalStorageOptions()}
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
render() {
|
|
616
|
+
const hasMoreOptions = this.br.plugins.experiments?.isEnabled('annotateHighlight');
|
|
617
|
+
// TODO change the second button to use a different icon
|
|
618
|
+
return html`
|
|
619
|
+
${this.showExtended ? this.renderExtendedOptions() : this.renderDefaultOptions()}
|
|
620
|
+
${!this.showExtended && hasMoreOptions ? this.renderShowMoreOption() : ""}
|
|
621
|
+
`;
|
|
622
|
+
}
|
|
623
|
+
|
|
587
624
|
/**
|
|
588
625
|
* @param {MouseEvent} e
|
|
589
626
|
*/
|
|
@@ -615,6 +652,14 @@ class BRSelectMenu extends LitElement {
|
|
|
615
652
|
this.copyLinkOption?.showTemporaryText('Copied!');
|
|
616
653
|
}
|
|
617
654
|
|
|
655
|
+
/**
|
|
656
|
+
* @param {MouseEvent} e
|
|
657
|
+
*/
|
|
658
|
+
toggleExtendedMenu(e) {
|
|
659
|
+
e.preventDefault();
|
|
660
|
+
this.showExtended = !this.showExtended;
|
|
661
|
+
}
|
|
662
|
+
|
|
618
663
|
/**
|
|
619
664
|
* Returns the closest BRtextLayer element on the page that contains the target node
|
|
620
665
|
* @param {Node} node
|
|
@@ -761,12 +806,12 @@ class BRSelectMenu extends LitElement {
|
|
|
761
806
|
this.style.left = `${left}px`;
|
|
762
807
|
}
|
|
763
808
|
|
|
809
|
+
// Will always show the simplified menu when rendered after hiding
|
|
764
810
|
async show() {
|
|
765
811
|
if (this.br.plugins.translate?.userToggleTranslate) return;
|
|
766
812
|
|
|
767
813
|
this.style.zIndex = '1';
|
|
768
814
|
this.style.position = 'absolute';
|
|
769
|
-
this.style.display = 'block';
|
|
770
815
|
this.open = true;
|
|
771
816
|
this.classList.remove('br-select-menu__root--scrolling');
|
|
772
817
|
window.removeEventListener('scroll', this._onScroll, { capture: true });
|
|
@@ -778,7 +823,7 @@ class BRSelectMenu extends LitElement {
|
|
|
778
823
|
|
|
779
824
|
hide() {
|
|
780
825
|
if (!this.open) return;
|
|
781
|
-
this.
|
|
826
|
+
this.showExtended = false;
|
|
782
827
|
this.open = false;
|
|
783
828
|
window.removeEventListener('scroll', this._onScroll, { capture: true });
|
|
784
829
|
this.clearNodesForRemoval();
|
package/src/util/dom.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { clamp } from '../BookReader/utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walks up the DOM tree starting at (but not including) the given element,
|
|
6
|
+
* returning the nearest ancestor that can actually scroll its overflow.
|
|
7
|
+
* @param {Element} el
|
|
8
|
+
* @return {Element | null}
|
|
9
|
+
*/
|
|
10
|
+
export function findScrollableAncestor(el) {
|
|
11
|
+
let current = el.parentElement;
|
|
12
|
+
while (current) {
|
|
13
|
+
const style = getComputedStyle(current);
|
|
14
|
+
const canScrollY = (style.overflowY === 'auto' || style.overflowY === 'scroll') && current.scrollHeight > current.clientHeight;
|
|
15
|
+
const canScrollX = (style.overflowX === 'auto' || style.overflowX === 'scroll') && current.scrollWidth > current.clientWidth;
|
|
16
|
+
if (canScrollY || canScrollX) return current;
|
|
17
|
+
current = current.parentElement;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Computes the scroll offset for a single axis needed to align an element
|
|
24
|
+
* within a container, per the same semantics as scrollIntoView's block/inline.
|
|
25
|
+
* @param {number} elStart
|
|
26
|
+
* @param {number} elEnd
|
|
27
|
+
* @param {number} containerStart
|
|
28
|
+
* @param {number} containerEnd
|
|
29
|
+
* @param {number} currentScroll
|
|
30
|
+
* @param {'start' | 'center' | 'end' | 'nearest'} align
|
|
31
|
+
* @return {number}
|
|
32
|
+
*/
|
|
33
|
+
function computeAxisScroll(elStart, elEnd, containerStart, containerEnd, currentScroll, align) {
|
|
34
|
+
let delta = 0;
|
|
35
|
+
switch (align) {
|
|
36
|
+
case 'start':
|
|
37
|
+
delta = elStart - containerStart;
|
|
38
|
+
break;
|
|
39
|
+
case 'end':
|
|
40
|
+
delta = elEnd - containerEnd;
|
|
41
|
+
break;
|
|
42
|
+
case 'center':
|
|
43
|
+
delta = (elStart + elEnd) / 2 - (containerStart + containerEnd) / 2;
|
|
44
|
+
break;
|
|
45
|
+
case 'nearest':
|
|
46
|
+
default:
|
|
47
|
+
if (elStart < containerStart) delta = elStart - containerStart;
|
|
48
|
+
else if (elEnd > containerEnd) delta = elEnd - containerEnd;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
return currentScroll + delta;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Scrolls the given element into view within a single scroll container.
|
|
56
|
+
*
|
|
57
|
+
* Unlike the native `Element.scrollIntoView`, this never walks up and scrolls
|
|
58
|
+
* every scrollable ancestor in turn (which can otherwise unexpectedly move
|
|
59
|
+
* the whole page) — it only ever adjusts the scroll position of one
|
|
60
|
+
* container: either the one explicitly passed in, or the nearest scrollable
|
|
61
|
+
* ancestor found via {@link findScrollableAncestor}.
|
|
62
|
+
* @param {Element} el
|
|
63
|
+
* @param {object} [options]
|
|
64
|
+
* @param {'auto' | 'smooth'} [options.behavior]
|
|
65
|
+
* @param {'start' | 'center' | 'end' | 'nearest'} [options.block]
|
|
66
|
+
* @param {'start' | 'center' | 'end' | 'nearest'} [options.inline]
|
|
67
|
+
* @param {Element} [options.scrollContainer] The scrollable element whose
|
|
68
|
+
* scroll position should be adjusted. Defaults to `findScrollableAncestor(el)`.
|
|
69
|
+
*/
|
|
70
|
+
export function singleScrollIntoView(el, options) {
|
|
71
|
+
const { block = 'start', inline = 'nearest', behavior = 'auto', scrollContainer = findScrollableAncestor(el) } = options || {};
|
|
72
|
+
if (!scrollContainer) {
|
|
73
|
+
el.scrollIntoView({ block, inline, behavior });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
|
78
|
+
const elRect = el.getBoundingClientRect();
|
|
79
|
+
|
|
80
|
+
const top = computeAxisScroll(elRect.top, elRect.bottom, containerRect.top, containerRect.bottom, scrollContainer.scrollTop, block);
|
|
81
|
+
const left = computeAxisScroll(elRect.left, elRect.right, containerRect.left, containerRect.right, scrollContainer.scrollLeft, inline);
|
|
82
|
+
|
|
83
|
+
scrollContainer.scrollTo({
|
|
84
|
+
top: clamp(top, 0, scrollContainer.scrollHeight - scrollContainer.clientHeight),
|
|
85
|
+
left: clamp(left, 0, scrollContainer.scrollWidth - scrollContainer.clientWidth),
|
|
86
|
+
behavior,
|
|
87
|
+
});
|
|
88
|
+
}
|