@inizioevoke/astro-core 2.0.0 → 2.0.2
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/package.json +1 -1
- package/src/components/ISI/Right/RightISI.astro +1 -1
- package/src/components/PdfViewer/PdfViewer.astro +20 -9
- package/src/components/PdfViewer/PdfViewer.css +58 -51
- package/src/components/PdfViewer/PdfViewer.ts +299 -91
- package/src/components/ScrollContainer/ScrollContainer.astro +28 -24
- package/src/components/ScrollContainer/ScrollContainer.css +66 -21
- package/src/components/ScrollContainer/ScrollContainer.ts +148 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
-
import ScrollContainer, { type Props as ScrollContainerProps } from '
|
|
3
|
+
import ScrollContainer, { type Props as ScrollContainerProps } from '../../ScrollContainer/ScrollContainer.astro';
|
|
4
4
|
import './RightISI.css';
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<'div'> {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import ScrollContainer from '../ScrollContainer/ScrollContainer.astro';
|
|
3
|
+
import type { Layout } from './PdfViewer';
|
|
4
|
+
import type { Scrolling } from '../ScrollContainer/ScrollContainer';
|
|
3
5
|
import './PdfViewer.css';
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
@@ -11,8 +13,9 @@ interface Props {
|
|
|
11
13
|
zoom?: 50 | 60 | 70 | 80 | 90 | 100 | 110 | 120 | 130 | 140 | 150;
|
|
12
14
|
page?: number;
|
|
13
15
|
pages?: string;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
layout?: Layout;
|
|
17
|
+
fit?: 'width' | 'height' | `${string}%` | number;
|
|
18
|
+
quality?: number;
|
|
16
19
|
toolbar?: {
|
|
17
20
|
nav?: boolean;
|
|
18
21
|
page?: boolean;
|
|
@@ -21,7 +24,7 @@ interface Props {
|
|
|
21
24
|
placeholder?: string;
|
|
22
25
|
items: Record<string, number>;
|
|
23
26
|
}
|
|
24
|
-
}
|
|
27
|
+
} | false;
|
|
25
28
|
}
|
|
26
29
|
const {
|
|
27
30
|
id,
|
|
@@ -31,6 +34,9 @@ const {
|
|
|
31
34
|
pdf,
|
|
32
35
|
zoom,
|
|
33
36
|
page = 1,
|
|
37
|
+
layout = 'vertical',
|
|
38
|
+
fit = '100%',
|
|
39
|
+
quality = 2,
|
|
34
40
|
toolbar,
|
|
35
41
|
...rest
|
|
36
42
|
} = Astro.props;
|
|
@@ -39,13 +45,18 @@ const attrs: Record<string, string> = {...rest};
|
|
|
39
45
|
if (height) {
|
|
40
46
|
attrs.style = `${attrs.style ? `${attrs.style}${attrs.style.endsWith(';') ? '' : ';'}` : ''}--evo-pdf-viewer-height:${typeof height == 'number' ? `${height}px` : height};`;
|
|
41
47
|
}
|
|
48
|
+
|
|
49
|
+
const scrollContainerScrolling: Scrolling = toolbar === undefined || (toolbar && toolbar?.zoom !== false) ? 'both' : layout as Scrolling;
|
|
42
50
|
---
|
|
43
51
|
<evo-pdf-viewer
|
|
44
52
|
id={id}
|
|
45
|
-
class:list={['evo-pdf-viewer', cssClass, theme ? `evo-pdf-viewer-theme-${theme}` : null]}
|
|
53
|
+
class:list={['evo-pdf-viewer', `evo-pdf-viewer-layout-${layout}`, cssClass, theme ? `evo-pdf-viewer-theme-${theme}` : null, toolbar === false ? 'evo-pdf-viewer-toolbar-hidden' : null]}
|
|
46
54
|
data-pdf={pdf}
|
|
47
55
|
data-page={page}
|
|
48
56
|
data-zoom={zoom}
|
|
57
|
+
data-layout={layout}
|
|
58
|
+
data-fit={fit}
|
|
59
|
+
data-quality={quality}
|
|
49
60
|
{...attrs}
|
|
50
61
|
>
|
|
51
62
|
<div class="evo-pdf-viewer-toolbar">
|
|
@@ -53,7 +64,7 @@ if (height) {
|
|
|
53
64
|
<slot name="toolbar-start" />
|
|
54
65
|
</div>
|
|
55
66
|
<div class="evo-pdf-viewer-toolbar-controls">
|
|
56
|
-
<div class:list={["evo-pdf-viewer-toolbar-arrows", toolbar
|
|
67
|
+
<div class:list={["evo-pdf-viewer-toolbar-arrows", toolbar && toolbar.nav === false ? 'evo-hidden' : null]}>
|
|
57
68
|
<button class="evo-pdf-viewer-toolbar-prev" title="Previous">
|
|
58
69
|
<slot name="toolbar-prev">
|
|
59
70
|
<svg class="evo-pdf-viewer-toolbar-icon" width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -69,10 +80,10 @@ if (height) {
|
|
|
69
80
|
</slot>
|
|
70
81
|
</button>
|
|
71
82
|
</div>
|
|
72
|
-
<div class:list={['evo-pdf-viewer-nav-page', toolbar
|
|
83
|
+
<div class:list={['evo-pdf-viewer-nav-page', toolbar && toolbar.page === false ? 'evo-hidden' : null]}>
|
|
73
84
|
<select><option value="1">1</option></select> of <span class="evo-pdf-viewer-nav-page-count">1</span>
|
|
74
85
|
</div>
|
|
75
|
-
<div class:list={['evo-pdf-viewer-toolbar-zoom', toolbar
|
|
86
|
+
<div class:list={['evo-pdf-viewer-toolbar-zoom', toolbar && toolbar.zoom === false ? 'evo-hidden' : null]}>
|
|
76
87
|
<button class="evo-pdf-viewer-toolbar-zoom-out" title="Zoom out">
|
|
77
88
|
<slot name="toolbar-zoom-out">
|
|
78
89
|
<svg class="evo-pdf-viewer-toolbar-icon" width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -94,7 +105,7 @@ if (height) {
|
|
|
94
105
|
</slot>
|
|
95
106
|
</button>
|
|
96
107
|
</div>
|
|
97
|
-
{toolbar
|
|
108
|
+
{toolbar && toolbar.bookmarks && Object.keys(toolbar.bookmarks.items).length > 0 &&
|
|
98
109
|
<div class="evo-pdf-viewer-toolbar-bookmarks">
|
|
99
110
|
<select>
|
|
100
111
|
<option value="" disabled selected hidden>{toolbar.bookmarks.placeholder ?? ''}</option>
|
|
@@ -109,7 +120,7 @@ if (height) {
|
|
|
109
120
|
</div>
|
|
110
121
|
</div>
|
|
111
122
|
<div class="evo-pdf-viewer-container">
|
|
112
|
-
<ScrollContainer>
|
|
123
|
+
<ScrollContainer scrolling={scrollContainerScrolling}>
|
|
113
124
|
<!-- <div class="evo-pdf-viewer-page">
|
|
114
125
|
|
|
115
126
|
</div>
|
|
@@ -3,10 +3,15 @@ evo-pdf-viewer {
|
|
|
3
3
|
--evo-pdf-viewer-toolbar-height: 44px;
|
|
4
4
|
--evo-pdf-viewer-toolbar-align: center;
|
|
5
5
|
--evo-pdf-viewer-scale: 1;
|
|
6
|
+
--evo-pdf-viewer-padding-top: 0px;
|
|
7
|
+
--evo-pdf-viewer-padding-right: 8px;
|
|
8
|
+
--evo-pdf-viewer-padding-bottom: 8px;
|
|
9
|
+
--evo-pdf-viewer-padding-left: calc(8px + var(--evo-scroll-container-track-width) + var(--evo-scroll-container-width-offset));
|
|
6
10
|
|
|
7
11
|
position: relative;
|
|
8
12
|
display: block;
|
|
9
13
|
height: var(--evo-pdf-viewer-height);
|
|
14
|
+
padding: var(--evo-pdf-viewer-padding-top) var(--evo-pdf-viewer-padding-right) var(--evo-pdf-viewer-padding-bottom) var(--evo-pdf-viewer-padding-left);
|
|
10
15
|
background-color: #f0f0f0;
|
|
11
16
|
box-sizing: border-box;
|
|
12
17
|
|
|
@@ -105,57 +110,27 @@ evo-pdf-viewer {
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
|
-
/*.evo-pdf-viewer-nav {
|
|
109
|
-
position: absolute;
|
|
110
|
-
left: calc(50% - 80px);
|
|
111
|
-
top: var(--evo-pdf-viewer-toolbar-height);
|
|
112
|
-
display: flex;
|
|
113
|
-
justify-content: space-between;
|
|
114
|
-
width: 160px;
|
|
115
|
-
padding: 6px 12px;
|
|
116
|
-
border-radius: 0 0 12px 12px;
|
|
117
|
-
background-color: rgba(51, 51, 51, 0.9);
|
|
118
|
-
backdrop-filter: blur(2px);
|
|
119
|
-
z-index: 1;
|
|
120
|
-
box-sizing: border-box;
|
|
121
|
-
|
|
122
|
-
button {
|
|
123
|
-
all: unset;
|
|
124
|
-
display: block;
|
|
125
|
-
cursor: pointer;
|
|
126
|
-
&.evo-pdf-viewer-nav-arrow {
|
|
127
|
-
width: 24px;
|
|
128
|
-
height: 24px;
|
|
129
|
-
}
|
|
130
|
-
&.evo-pdf-viewer-nav-page {
|
|
131
|
-
color: white;
|
|
132
|
-
font-family: sans-serif;
|
|
133
|
-
font-size: 18px;
|
|
134
|
-
line-height: 18px;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}*/
|
|
138
113
|
.evo-pdf-viewer-container {
|
|
139
|
-
height: calc(var(--evo-pdf-viewer-height) - var(--evo-pdf-viewer-toolbar-height));
|
|
140
|
-
|
|
114
|
+
--evo-pdf-viewer-container-height: calc(var(--evo-pdf-viewer-height) - var(--evo-pdf-viewer-padding-top) - var(--evo-pdf-viewer-toolbar-height) - var(--evo-pdf-viewer-padding-bottom));
|
|
115
|
+
height: var(--evo-pdf-viewer-container-height);
|
|
141
116
|
box-sizing: border-box;
|
|
142
117
|
|
|
143
118
|
evo-scroll-container {
|
|
144
119
|
--evo-scroll-container-track-color: #cccccc;
|
|
145
|
-
height:
|
|
120
|
+
/* height: 100%; */
|
|
121
|
+
height: var(--evo-pdf-viewer-container-height);
|
|
146
122
|
.evo-scroll-content {
|
|
147
|
-
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
align-items: flex-start;
|
|
126
|
+
gap: 16px;
|
|
127
|
+
|
|
148
128
|
.evo-pdf-viewer-page-wrapper {
|
|
149
129
|
display: block;
|
|
150
|
-
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
margin-left: auto;
|
|
132
|
+
margin-right: auto;
|
|
151
133
|
background-color: white;
|
|
152
|
-
|
|
153
|
-
&:first-child {
|
|
154
|
-
margin-top: 0;
|
|
155
|
-
}
|
|
156
|
-
&:last-child {
|
|
157
|
-
margin-bottom: 0;
|
|
158
|
-
}
|
|
159
134
|
}
|
|
160
135
|
|
|
161
136
|
canvas {
|
|
@@ -166,7 +141,7 @@ evo-pdf-viewer {
|
|
|
166
141
|
|
|
167
142
|
.evo-pdf-viewer-link-overlay {
|
|
168
143
|
pointer-events: auto;
|
|
169
|
-
z-index:
|
|
144
|
+
z-index: 0;
|
|
170
145
|
-webkit-tap-highlight-color: transparent;
|
|
171
146
|
user-select: none;
|
|
172
147
|
touch-action: manipulation;
|
|
@@ -179,18 +154,50 @@ evo-pdf-viewer {
|
|
|
179
154
|
}
|
|
180
155
|
}
|
|
181
156
|
|
|
182
|
-
|
|
157
|
+
&.evo-pdf-viewer-toolbar-hidden {
|
|
183
158
|
.evo-pdf-viewer-toolbar {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
161
|
+
.evo-pdf-viewer-container {
|
|
162
|
+
--evo-pdf-viewer-container-height: calc(var(--evo-pdf-viewer-height) - var(--evo-pdf-viewer-padding-top) - var(--evo-pdf-viewer-padding-bottom));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&.evo-pdf-viewer-layout-horizontal {
|
|
166
|
+
--evo-pdf-viewer-padding-left: var(--evo-pdf-viewer-padding-right);
|
|
167
|
+
}
|
|
168
|
+
&.evo-pdf-viewer-layout-vertical {
|
|
169
|
+
--evo-pdf-viewer-padding-top: 8px
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
&.evo-pdf-viewer-layout-horizontal {
|
|
174
|
+
.evo-pdf-viewer-toolbar {
|
|
175
|
+
.evo-pdf-viewer-toolbar-arrows {
|
|
176
|
+
.evo-pdf-viewer-toolbar-prev {
|
|
177
|
+
transform: rotate(-90deg);
|
|
178
|
+
}
|
|
179
|
+
.evo-pdf-viewer-toolbar-next {
|
|
180
|
+
transform: rotate(-90deg);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
.evo-pdf-viewer-container {
|
|
185
|
+
evo-scroll-container {
|
|
186
|
+
.evo-scroll-content {
|
|
187
|
+
display: flex;
|
|
188
|
+
flex-direction: row;
|
|
189
|
+
align-items: flex-start;
|
|
190
|
+
|
|
191
|
+
.evo-pdf-viewer-page-wrapper {
|
|
192
|
+
display: block;
|
|
193
|
+
margin-top: auto;
|
|
194
|
+
margin-bottom: auto;
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
}
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
}
|
|
193
|
-
}
|
|
200
|
+
}
|
|
194
201
|
&.evo-pdf-viewer-theme-dark {
|
|
195
202
|
background-color: #555555;
|
|
196
203
|
.evo-pdf-viewer-toolbar {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
import * as pdfjs from './pdf.min.mjs';
|
|
3
3
|
import workerSrc from './pdf.worker.min.mjs?url';
|
|
4
|
-
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
|
5
|
-
import { EvoScrollContainerElement } from '../ScrollContainer/ScrollContainer';
|
|
4
|
+
import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist';
|
|
5
|
+
import { EvoScrollContainerElement, type Scrolling } from '../ScrollContainer/ScrollContainer';
|
|
6
6
|
|
|
7
7
|
const TAG_NAME = 'evo-pdf-viewer';
|
|
8
|
+
const MIN_ZOOM = 1; // 10% — stored as tenths
|
|
9
|
+
const MAX_ZOOM = 80; // 800%
|
|
10
|
+
const RERENDER_DEBOUNCE_MS = 300;
|
|
11
|
+
const MAX_CANVAS_DIMENSION = 16384;
|
|
8
12
|
|
|
9
13
|
export interface EvoPdfViewerEventDetail {
|
|
10
14
|
type: string;
|
|
@@ -18,9 +22,11 @@ declare global {
|
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
export type Layout = Omit<Scrolling, 'both'>
|
|
26
|
+
|
|
21
27
|
export interface IEvoPdfViewerElement {
|
|
22
28
|
scrollToPage: (page: number | string) => void;
|
|
23
|
-
zoom: (z:
|
|
29
|
+
zoom: (z: -1 | 0 | 1) => void;
|
|
24
30
|
}
|
|
25
31
|
export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerElement {
|
|
26
32
|
#toolbar!: HTMLElement;
|
|
@@ -30,6 +36,19 @@ export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerEle
|
|
|
30
36
|
#toolbarHeight = 0;
|
|
31
37
|
#defaultZoom = 100;
|
|
32
38
|
#pageCount = 0;
|
|
39
|
+
#layout: Layout = 'vertical';
|
|
40
|
+
#fit: 'width' | 'height' | `${string}%` | number = 'width';
|
|
41
|
+
|
|
42
|
+
#pdf!: PDFDocumentProxy;
|
|
43
|
+
#outputScale = 1;
|
|
44
|
+
#renderQuality = 2;
|
|
45
|
+
#targetQuality = 2;
|
|
46
|
+
#pageLayouts = new Map<number, { viewportScale: number; cssWidth: string; cssHeight: string }>();
|
|
47
|
+
#renderedPages = new Map<number, { effectiveQuality: number; task?: RenderTask }>();
|
|
48
|
+
#visiblePages = new Set<number>();
|
|
49
|
+
#lazyObserver!: IntersectionObserver;
|
|
50
|
+
#boundUpdateCurrentPage = () => this.#updateCurrentPage();
|
|
51
|
+
#rerenderTimer: number | null = null;
|
|
33
52
|
|
|
34
53
|
constructor() {
|
|
35
54
|
super();
|
|
@@ -43,6 +62,18 @@ export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerEle
|
|
|
43
62
|
|
|
44
63
|
this.#toolbarHeight = parseFloat(getComputedStyle(this.#toolbar).height);
|
|
45
64
|
this.#defaultZoom = parseInt(this.getAttribute('data-zoom') ?? '100', 10);
|
|
65
|
+
this.#layout = this.dataset.layout as Layout;
|
|
66
|
+
|
|
67
|
+
const fit = this.dataset.fit;
|
|
68
|
+
if (fit) {
|
|
69
|
+
if (/^\d+%$/.test(fit)){
|
|
70
|
+
this.#fit = fit as `${string}%`;
|
|
71
|
+
} else if (/^\d+$/.test(fit)) {
|
|
72
|
+
this.#fit = parseInt(fit, 10);
|
|
73
|
+
} else if (fit === 'height' || fit === 'width') {
|
|
74
|
+
this.#fit = fit;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
46
77
|
|
|
47
78
|
// *** nav
|
|
48
79
|
this.querySelector<HTMLButtonElement>('button.evo-pdf-viewer-toolbar-prev')?.addEventListener('click', () => {
|
|
@@ -87,11 +118,12 @@ export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerEle
|
|
|
87
118
|
});
|
|
88
119
|
}
|
|
89
120
|
|
|
90
|
-
this.#scrollContainer.addEventListener('scroll', this.#
|
|
121
|
+
this.#scrollContainer.addEventListener('scroll', this.#boundUpdateCurrentPage, { passive: true });
|
|
91
122
|
|
|
92
123
|
this.#renderPDF()
|
|
93
124
|
.then(() => {
|
|
94
125
|
this.zoom(0);
|
|
126
|
+
this.#scrollContainer.refresh();
|
|
95
127
|
this.#updateCurrentPage();
|
|
96
128
|
})
|
|
97
129
|
.catch((err) => {
|
|
@@ -104,42 +136,108 @@ export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerEle
|
|
|
104
136
|
if (typeof page == 'string') {
|
|
105
137
|
page = parseInt(page, 10);
|
|
106
138
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
139
|
+
const wrapper = this.#scrollContainer.querySelector<HTMLElement>(`.evo-pdf-viewer-page-wrapper[data-page="${page}"]`);
|
|
140
|
+
if (wrapper) {
|
|
141
|
+
if (this.#layout === 'horizontal') {
|
|
142
|
+
this.#scrollContainer.scroll({
|
|
143
|
+
left: wrapper.getBoundingClientRect().left + this.#scrollContainer.scrollLeft - this.#scrollContainer.getBoundingClientRect().left,
|
|
144
|
+
behavior: 'smooth'
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
this.#scrollContainer.scroll({
|
|
148
|
+
top: wrapper.getBoundingClientRect().top + this.#scrollContainer.scrollTop - this.#toolbarHeight,
|
|
149
|
+
behavior: 'smooth'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
113
152
|
this.#pageSelect.selectedIndex = page - 1;
|
|
114
153
|
}
|
|
115
|
-
// }
|
|
116
154
|
}
|
|
117
155
|
|
|
118
|
-
zoom(z:
|
|
156
|
+
zoom(z: -1 | 0 | 1) {
|
|
119
157
|
const style = getComputedStyle(this);
|
|
120
158
|
const current = Math.floor(parseFloat(style.getPropertyValue('--evo-pdf-viewer-scale')) * 10);
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
159
|
+
const zoomIn = z === 1;
|
|
160
|
+
if (z !== 0) {
|
|
161
|
+
|
|
162
|
+
if (current >= 12) {
|
|
163
|
+
zoomIn ? z += 1 : z -= 1;
|
|
164
|
+
if (current >= 20) {
|
|
165
|
+
zoomIn ? z += 1 : z -= 1;
|
|
166
|
+
if (current >= 40) {
|
|
167
|
+
zoomIn ? z += 1 : z -= 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const target = z === 0 ? this.#defaultZoom / 10 : current + z;
|
|
173
|
+
const scale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, target));
|
|
174
|
+
if (scale === current) return;
|
|
175
|
+
|
|
176
|
+
const scroll = {
|
|
177
|
+
left: this.#scrollContainer.scrollLeft,
|
|
178
|
+
top: this.#scrollContainer.scrollTop
|
|
179
|
+
};
|
|
180
|
+
this.style.setProperty('--evo-pdf-viewer-scale', (scale / 10).toString());
|
|
181
|
+
this.#scrollContainer.scroll({
|
|
182
|
+
left: scroll.left * scale / current,
|
|
183
|
+
top: scroll.top * scale / current,
|
|
184
|
+
behavior: 'instant'
|
|
185
|
+
});
|
|
186
|
+
this.#scrollContainer.refresh();
|
|
187
|
+
|
|
188
|
+
// Update the target quality immediately so any page entering view via the
|
|
189
|
+
// lazy observer renders at the new scale; only the visible pages are
|
|
190
|
+
// re-rendered eagerly (debounced) — others get sharpened lazily on scroll.
|
|
191
|
+
this.#targetQuality = Math.max(this.#renderQuality, this.#renderQuality * (scale / 10));
|
|
192
|
+
this.#scheduleVisibleRerender();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#scheduleVisibleRerender() {
|
|
196
|
+
if (this.#rerenderTimer !== null) {
|
|
197
|
+
clearTimeout(this.#rerenderTimer);
|
|
133
198
|
}
|
|
199
|
+
this.#rerenderTimer = window.setTimeout(() => {
|
|
200
|
+
this.#rerenderTimer = null;
|
|
201
|
+
for (const pageNum of this.#visiblePages) {
|
|
202
|
+
const wrapper = this.#scrollContainer.content.querySelector<HTMLElement>(
|
|
203
|
+
`.evo-pdf-viewer-page-wrapper[data-page="${pageNum}"]`
|
|
204
|
+
);
|
|
205
|
+
if (wrapper) this.#considerRender(pageNum, wrapper);
|
|
206
|
+
}
|
|
207
|
+
}, RERENDER_DEBOUNCE_MS);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Decide whether to (re-)render a page at the current target quality.
|
|
211
|
+
// Used by both the lazy observer and the post-zoom sweep so the gating
|
|
212
|
+
// logic lives in one place.
|
|
213
|
+
#considerRender(pageNum: number, wrapper: HTMLElement) {
|
|
214
|
+
const info = this.#renderedPages.get(pageNum);
|
|
215
|
+
if (!info) {
|
|
216
|
+
this.#renderPage(pageNum, wrapper, this.#targetQuality);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Skip when an in-flight render at the same target is already running.
|
|
220
|
+
if (info.task && info.effectiveQuality >= this.#targetQuality * 0.95) return;
|
|
221
|
+
|
|
222
|
+
const ratio = info.effectiveQuality / this.#targetQuality;
|
|
223
|
+
// Zoom-in: re-render when canvas is noticeably below target sharpness.
|
|
224
|
+
// Zoom-out: re-render only on big drops (>25%) to reclaim memory without
|
|
225
|
+
// constant churn on tiny steps.
|
|
226
|
+
if (ratio >= 0.95 && ratio <= 1.25) return;
|
|
227
|
+
|
|
228
|
+
info.task?.cancel();
|
|
229
|
+
this.#renderPage(pageNum, wrapper, this.#targetQuality);
|
|
134
230
|
}
|
|
135
231
|
|
|
136
232
|
async #renderPDF() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
233
|
+
this.#outputScale = window.devicePixelRatio || 1;
|
|
234
|
+
this.#renderQuality = parseFloat(this.getAttribute('data-quality') ?? '2');
|
|
235
|
+
this.#targetQuality = this.#renderQuality;
|
|
236
|
+
this.#pdf = await pdfjs.getDocument(this.getAttribute('data-pdf') as string).promise;
|
|
237
|
+
|
|
238
|
+
const pages = new Array(this.#pdf.numPages).fill(1).map((_, i) => i + 1);
|
|
141
239
|
this.#pageCount = pages.length;
|
|
142
|
-
|
|
240
|
+
|
|
143
241
|
this.querySelector<HTMLElement>('.evo-pdf-viewer-nav-page-count')!.innerText = pages.length.toString();
|
|
144
242
|
|
|
145
243
|
this.#pageSelect.options.remove(0);
|
|
@@ -150,77 +248,195 @@ export class EvoPdfViewerElement extends HTMLElement implements IEvoPdfViewerEle
|
|
|
150
248
|
this.#pageSelect.options.add(option);
|
|
151
249
|
}
|
|
152
250
|
|
|
251
|
+
const containerStyle = getComputedStyle(this.#scrollContainer.content);
|
|
252
|
+
const trackWidth = parseInt(containerStyle.getPropertyValue('--evo-scroll-container-track-width'));
|
|
253
|
+
const containerWidth = parseFloat(containerStyle.width);
|
|
254
|
+
const containerHeight = parseFloat(containerStyle.height);
|
|
255
|
+
const gap = 16;
|
|
256
|
+
|
|
257
|
+
// Pass 1: create sized placeholder wrappers for all pages so the scroll
|
|
258
|
+
// container knows its total dimensions before any canvas is rendered.
|
|
153
259
|
for (const pageNum of pages) {
|
|
260
|
+
const pdfPage = await this.#pdf.getPage(pageNum);
|
|
261
|
+
const baseVp = pdfPage.getViewport({ scale: 1 });
|
|
262
|
+
|
|
263
|
+
let viewportScale: number;
|
|
264
|
+
let cssProp: 'width' | 'height';
|
|
265
|
+
let cssRenderedSize: number;
|
|
266
|
+
|
|
267
|
+
if (this.#layout === 'horizontal') {
|
|
268
|
+
if (this.#fit === 'height') {
|
|
269
|
+
viewportScale = (containerHeight - trackWidth * 2) / baseVp.height;
|
|
270
|
+
cssRenderedSize = Math.floor(baseVp.height * viewportScale) - (trackWidth * 2);
|
|
271
|
+
cssProp = 'height';
|
|
272
|
+
} else if (this.#fit === 'width') {
|
|
273
|
+
viewportScale = containerWidth / baseVp.width;
|
|
274
|
+
cssRenderedSize = Math.floor(baseVp.width * viewportScale);
|
|
275
|
+
cssProp = 'width';
|
|
276
|
+
} else if (/^\d+%$/.test(this.#fit.toString())) { // replacing 'actual' with a percentage
|
|
277
|
+
viewportScale = parseInt(this.#fit.toString(), 10) / 100;
|
|
278
|
+
cssRenderedSize = Math.floor(baseVp.height * viewportScale) - (trackWidth * 2);
|
|
279
|
+
cssProp = 'height';
|
|
280
|
+
} else {
|
|
281
|
+
const n = this.#fit as number;
|
|
282
|
+
viewportScale = (containerWidth - (n - 1) * gap) / (n * baseVp.width);
|
|
283
|
+
cssRenderedSize = Math.floor(baseVp.width * viewportScale);
|
|
284
|
+
cssProp = 'width';
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
if (this.#fit === 'width') {
|
|
288
|
+
viewportScale = containerWidth / baseVp.width;
|
|
289
|
+
cssRenderedSize = Math.floor(baseVp.width * viewportScale) - (trackWidth * 2);
|
|
290
|
+
cssProp = 'width';
|
|
291
|
+
} else if (this.#fit === 'height') {
|
|
292
|
+
viewportScale = containerHeight / baseVp.height;
|
|
293
|
+
cssRenderedSize = Math.floor(baseVp.height * viewportScale);
|
|
294
|
+
cssProp = 'height';
|
|
295
|
+
} else if (/^\d+%$/.test(this.#fit.toString())) { // replacing 'actual' with a percentage
|
|
296
|
+
viewportScale = parseInt(this.#fit.toString(), 10) / 100;
|
|
297
|
+
cssRenderedSize = Math.floor(baseVp.width * viewportScale) - (trackWidth * 2);
|
|
298
|
+
cssProp = 'width';
|
|
299
|
+
} else {
|
|
300
|
+
const n = this.#fit as number;
|
|
301
|
+
viewportScale = (containerHeight - (n - 1) * gap) / (n * baseVp.height);
|
|
302
|
+
cssRenderedSize = Math.floor(baseVp.height * viewportScale);
|
|
303
|
+
cssProp = 'height';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const layoutVp = pdfPage.getViewport({ scale: viewportScale });
|
|
308
|
+
const cssOtherSize = Math.round(cssProp === 'width'
|
|
309
|
+
? cssRenderedSize * layoutVp.height / layoutVp.width
|
|
310
|
+
: cssRenderedSize * layoutVp.width / layoutVp.height);
|
|
311
|
+
const cssWidth = `calc(${cssProp === 'width' ? cssRenderedSize : cssOtherSize}px * var(--evo-pdf-viewer-scale))`;
|
|
312
|
+
const cssHeight = `calc(${cssProp === 'height' ? cssRenderedSize : cssOtherSize}px * var(--evo-pdf-viewer-scale))`;
|
|
313
|
+
|
|
314
|
+
this.#pageLayouts.set(pageNum, { viewportScale, cssWidth, cssHeight });
|
|
154
315
|
|
|
155
|
-
const pdfPage = await pdf.getPage(pageNum);
|
|
156
|
-
const pdfPageWidth = pdfPage.getViewport({ scale: 1}).width;
|
|
157
|
-
const containerStyle = getComputedStyle(this.#scrollContainer.content);
|
|
158
|
-
const trackWidth = parseInt(containerStyle.getPropertyValue('--evo-scroll-container-track-width'));
|
|
159
|
-
const viewport = pdfPage.getViewport({ scale: parseFloat(containerStyle.width) / pdfPageWidth });
|
|
160
|
-
|
|
161
|
-
const canvas = document.createElement('canvas');
|
|
162
|
-
canvas.setAttribute('data-page', pageNum.toString());
|
|
163
|
-
const width = Math.floor(viewport.width * outputScale);
|
|
164
|
-
const height = Math.floor(viewport.height * outputScale);
|
|
165
|
-
canvas.width = width;
|
|
166
|
-
canvas.height = height;
|
|
167
|
-
canvas.style.aspectRatio = `${width}/${height}`;
|
|
168
|
-
canvas.style.width = `calc(${Math.floor(viewport.width) - (trackWidth * 2)}px * var(--evo-pdf-viewer-scale))`;
|
|
169
|
-
// canvas.style.height = `${Math.floor(viewport.height)}px`;
|
|
170
|
-
|
|
171
|
-
// Create wrapper for canvas and overlay
|
|
172
316
|
const pageWrapper = document.createElement('div');
|
|
173
317
|
pageWrapper.className = 'evo-pdf-viewer-page-wrapper';
|
|
318
|
+
pageWrapper.setAttribute('data-page', pageNum.toString());
|
|
174
319
|
pageWrapper.style.position = 'relative';
|
|
175
|
-
pageWrapper.style.width =
|
|
176
|
-
pageWrapper.style.
|
|
177
|
-
pageWrapper.appendChild(canvas);
|
|
178
|
-
|
|
320
|
+
pageWrapper.style.width = cssWidth;
|
|
321
|
+
pageWrapper.style.height = cssHeight;
|
|
179
322
|
this.#scrollContainer.content.append(pageWrapper);
|
|
323
|
+
this.#scrollContainer.refresh();
|
|
324
|
+
}
|
|
180
325
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
326
|
+
// Pass 2: observe wrappers. The observer is the single gate for both
|
|
327
|
+
// initial render and post-zoom re-render — pages outside its rootMargin
|
|
328
|
+
// keep their stale-quality canvas until they scroll back into view.
|
|
329
|
+
this.#lazyObserver = new IntersectionObserver(
|
|
330
|
+
(entries) => {
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const wrapper = entry.target as HTMLElement;
|
|
333
|
+
const pageNum = parseInt(wrapper.getAttribute('data-page')!, 10);
|
|
334
|
+
if (entry.isIntersecting) {
|
|
335
|
+
this.#visiblePages.add(pageNum);
|
|
336
|
+
this.#considerRender(pageNum, wrapper);
|
|
337
|
+
} else {
|
|
338
|
+
this.#visiblePages.delete(pageNum);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
root: this.#scrollContainer.content,
|
|
344
|
+
// Render one full container-length ahead in the scroll direction.
|
|
345
|
+
rootMargin: this.#layout === 'horizontal'
|
|
346
|
+
? `0px ${containerWidth}px 0px ${containerWidth}px`
|
|
347
|
+
: `${containerHeight}px 0px ${containerHeight}px 0px`,
|
|
348
|
+
threshold: 0
|
|
193
349
|
}
|
|
194
|
-
|
|
195
|
-
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
for (const wrapper of this.#scrollContainer.content.querySelectorAll<HTMLElement>('.evo-pdf-viewer-page-wrapper')) {
|
|
353
|
+
this.#lazyObserver.observe(wrapper);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async #renderPage(pageNum: number, wrapper: HTMLElement, quality = this.#renderQuality) {
|
|
358
|
+
const layout = this.#pageLayouts.get(pageNum)!;
|
|
359
|
+
const pdfPage = await this.#pdf.getPage(pageNum);
|
|
360
|
+
|
|
361
|
+
// Cap effective quality so the canvas never exceeds the browser's max texture
|
|
362
|
+
// dimension on any axis.
|
|
363
|
+
const baseVp = pdfPage.getViewport({ scale: layout.viewportScale });
|
|
364
|
+
const maxByWidth = MAX_CANVAS_DIMENSION / (baseVp.width * this.#outputScale);
|
|
365
|
+
const maxByHeight = MAX_CANVAS_DIMENSION / (baseVp.height * this.#outputScale);
|
|
366
|
+
const effectiveQuality = Math.min(quality, maxByWidth, maxByHeight);
|
|
367
|
+
|
|
368
|
+
const viewport = pdfPage.getViewport({ scale: layout.viewportScale * effectiveQuality });
|
|
369
|
+
const layoutVp = baseVp;
|
|
370
|
+
|
|
371
|
+
const canvas = document.createElement('canvas');
|
|
372
|
+
canvas.setAttribute('data-page', pageNum.toString());
|
|
373
|
+
canvas.width = Math.floor(viewport.width * this.#outputScale);
|
|
374
|
+
canvas.height = Math.floor(viewport.height * this.#outputScale);
|
|
375
|
+
canvas.style.width = layout.cssWidth;
|
|
376
|
+
canvas.style.height = layout.cssHeight;
|
|
377
|
+
|
|
378
|
+
const task = pdfPage.render({
|
|
379
|
+
canvas,
|
|
380
|
+
transform: this.#outputScale !== 1 ? [this.#outputScale, 0, 0, this.#outputScale, 0, 0] : undefined,
|
|
381
|
+
viewport
|
|
382
|
+
});
|
|
383
|
+
this.#renderedPages.set(pageNum, { effectiveQuality, task });
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await task.promise;
|
|
387
|
+
} catch (err: any) {
|
|
388
|
+
if (err?.name === 'RenderingCancelledException') return;
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Swap the new canvas in only after a successful render so we never flash
|
|
393
|
+
// an empty canvas during a re-render.
|
|
394
|
+
const oldCanvas = wrapper.querySelector('canvas');
|
|
395
|
+
const oldOverlay = wrapper.querySelector('.evo-pdf-viewer-link-overlay');
|
|
396
|
+
if (oldCanvas) oldCanvas.remove();
|
|
397
|
+
if (oldOverlay) oldOverlay.remove();
|
|
398
|
+
wrapper.appendChild(canvas);
|
|
399
|
+
this.#renderedPages.set(pageNum, { effectiveQuality });
|
|
400
|
+
|
|
401
|
+
const links = await getPageLinks(pdfPage);
|
|
402
|
+
if (links.length > 0) {
|
|
403
|
+
const linkOverlay = createLinkOverlay(canvas, links, layoutVp, pageNum, this, this.scrollToPage.bind(this), this.#pdf);
|
|
404
|
+
wrapper.appendChild(linkOverlay);
|
|
196
405
|
}
|
|
197
406
|
}
|
|
198
407
|
|
|
199
408
|
#updateCurrentPage() {
|
|
200
|
-
const
|
|
201
|
-
let
|
|
409
|
+
const wrappers = this.#scrollContainer.content.querySelectorAll<HTMLElement>('.evo-pdf-viewer-page-wrapper');
|
|
410
|
+
let visibleWrapper: HTMLElement | undefined;
|
|
202
411
|
let maxVisibility = 0;
|
|
203
412
|
|
|
204
|
-
|
|
205
|
-
const rect =
|
|
413
|
+
wrappers.forEach((wrapper) => {
|
|
414
|
+
const rect = wrapper.getBoundingClientRect();
|
|
206
415
|
const containerRect = this.#scrollContainer.getBoundingClientRect();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
416
|
+
|
|
417
|
+
let intersection: number;
|
|
418
|
+
let dimension: number;
|
|
419
|
+
if (this.#layout === 'horizontal') {
|
|
420
|
+
const intersectLeft = Math.max(rect.left, containerRect.left);
|
|
421
|
+
const intersectRight = Math.min(rect.right, containerRect.right);
|
|
422
|
+
intersection = Math.max(0, intersectRight - intersectLeft);
|
|
423
|
+
dimension = rect.width || 1;
|
|
424
|
+
} else {
|
|
425
|
+
const intersectTop = Math.max(rect.top, containerRect.top);
|
|
426
|
+
const intersectBottom = Math.min(rect.bottom, containerRect.bottom);
|
|
427
|
+
intersection = Math.max(0, intersectBottom - intersectTop);
|
|
428
|
+
dimension = rect.height || 1;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const visibility = intersection / dimension;
|
|
216
432
|
if (visibility > maxVisibility) {
|
|
217
433
|
maxVisibility = visibility;
|
|
218
|
-
|
|
434
|
+
visibleWrapper = wrapper;
|
|
219
435
|
}
|
|
220
436
|
});
|
|
221
437
|
|
|
222
|
-
if (
|
|
223
|
-
const pageNum =
|
|
438
|
+
if (visibleWrapper && maxVisibility >= 0.1) {
|
|
439
|
+
const pageNum = visibleWrapper.getAttribute('data-page');
|
|
224
440
|
if (pageNum) {
|
|
225
441
|
this.#pageSelect.selectedIndex = parseInt(pageNum, 10) - 1;
|
|
226
442
|
}
|
|
@@ -307,24 +523,16 @@ function createLinkOverlay(
|
|
|
307
523
|
gotoPage: (page: number | string) => void,
|
|
308
524
|
pdf: PDFDocumentProxy
|
|
309
525
|
): SVGSVGElement {
|
|
310
|
-
const trackWidth = parseInt(
|
|
311
|
-
getComputedStyle(canvas.parentElement!).getPropertyValue('--evo-scroll-container-track-width')
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
const canvasWidth = Math.floor(viewport.width) - trackWidth * 2;
|
|
315
|
-
const canvasHeight = Math.floor(viewport.height);
|
|
316
|
-
|
|
317
|
-
// Create SVG overlay
|
|
318
526
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
319
527
|
svg.setAttribute('class', 'evo-pdf-viewer-link-overlay');
|
|
320
528
|
svg.setAttribute('data-page', pageNum.toString());
|
|
321
529
|
svg.setAttribute('viewBox', `0 0 ${viewport.width} ${viewport.height}`);
|
|
322
|
-
svg.setAttribute('width',
|
|
323
|
-
svg.setAttribute('height',
|
|
530
|
+
svg.setAttribute('width', canvas.style.width);
|
|
531
|
+
svg.setAttribute('height', canvas.style.height);
|
|
324
532
|
svg.style.position = 'absolute';
|
|
325
533
|
svg.style.top = '0';
|
|
326
534
|
svg.style.left = '0';
|
|
327
|
-
svg.style.cursor = 'pointer';
|
|
535
|
+
// svg.style.cursor = 'pointer';
|
|
328
536
|
|
|
329
537
|
// Add link rectangles
|
|
330
538
|
for (const link of links) {
|
|
@@ -1,51 +1,55 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
import type { Scrolling } from './ScrollContainer';
|
|
3
4
|
import './ScrollContainer.css';
|
|
4
5
|
|
|
5
6
|
export interface Props extends HTMLAttributes<'div'> {
|
|
6
|
-
|
|
7
|
-
innerWidth?: string | number;
|
|
7
|
+
width?: string | number;
|
|
8
8
|
height?: string | number;
|
|
9
|
-
|
|
10
|
-
horizontal?: boolean;
|
|
9
|
+
scrolling?: Scrolling;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
const {
|
|
14
|
-
|
|
15
|
-
innerWidth,
|
|
13
|
+
width,
|
|
16
14
|
height,
|
|
17
|
-
|
|
18
|
-
horizontal = false,
|
|
15
|
+
scrolling = 'vertical',
|
|
19
16
|
...rest
|
|
20
17
|
} = Astro.props;
|
|
21
18
|
|
|
22
|
-
const containerAttrs = {...rest};
|
|
23
|
-
const
|
|
24
|
-
['class']
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
const containerAttrs = {...rest, 'data-scrolling': scrolling};
|
|
20
|
+
const cssClassList = ['evo-scroll-container', containerAttrs['class']];
|
|
21
|
+
delete containerAttrs['class'];
|
|
22
|
+
|
|
23
|
+
switch (scrolling) {
|
|
24
|
+
case 'horizontal':
|
|
25
|
+
cssClassList.push('evo-scroll-horz');
|
|
26
|
+
break;
|
|
27
|
+
case 'vertical':
|
|
28
|
+
cssClassList.push('evo-scroll-vert')
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
cssClassList.push('evo-scroll-both')
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
27
34
|
|
|
28
35
|
containerAttrs.style = typeof containerAttrs.style == 'string' ? `${containerAttrs.style}${containerAttrs.style.endsWith(';') ? '' : ';'}` : '';
|
|
29
|
-
if (
|
|
30
|
-
containerAttrs.style = `${containerAttrs.style}width:${typeof
|
|
36
|
+
if (width) {
|
|
37
|
+
containerAttrs.style = `${containerAttrs.style}width:${typeof width == 'number' || /^\d+$/.test(width) ? `${width}px` : width};`;
|
|
31
38
|
}
|
|
32
39
|
if (height) {
|
|
33
40
|
containerAttrs.style = `${containerAttrs.style}height:${typeof height == 'number' || /^\d+$/.test(height) ? `${height}px` : height};`;
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
const contentAttrs: Record<string, string> = {};
|
|
37
|
-
if (innerWidth) {
|
|
38
|
-
contentAttrs.style = `width:${typeof innerWidth == 'number' || /^\d+$/.test(innerWidth) ? `${innerWidth}px` : innerWidth};`
|
|
39
|
-
}
|
|
40
42
|
---
|
|
41
|
-
<evo-scroll-container class:list={
|
|
42
|
-
<div class="evo-scroll-content"
|
|
43
|
+
<evo-scroll-container class:list={cssClassList} {...containerAttrs}>
|
|
44
|
+
<div class="evo-scroll-content">
|
|
43
45
|
<slot />
|
|
44
46
|
</div>
|
|
45
|
-
{
|
|
47
|
+
{scrolling !== 'horizontal' &&
|
|
48
|
+
<div class="evo-scroll-track evo-scroll-track-vert">
|
|
46
49
|
<div class="evo-scroll-thumb"></div>
|
|
47
50
|
</div>}
|
|
48
|
-
{
|
|
51
|
+
{scrolling !== 'vertical' &&
|
|
52
|
+
<div class="evo-scroll-track evo-scroll-track-horz">
|
|
49
53
|
<div class="evo-scroll-thumb"></div>
|
|
50
54
|
</div>}
|
|
51
55
|
</evo-scroll-container>
|
|
@@ -1,68 +1,113 @@
|
|
|
1
1
|
@layer inizioevoke-astro-components {
|
|
2
2
|
:root {
|
|
3
|
-
--evo-scroll-container-border-radius: 4px;
|
|
4
3
|
--evo-scroll-container-track-width: 8px;
|
|
5
|
-
--evo-scroll-container-
|
|
6
|
-
--evo-scroll-container-
|
|
7
|
-
--evo-scroll-container-
|
|
4
|
+
--evo-scroll-container-width-offset: var(--evo-scroll-container-track-width);
|
|
5
|
+
--evo-scroll-container-height-offset: var(--evo-scroll-container-track-width);
|
|
6
|
+
--evo-scroll-container-border-radius: calc(var(--evo-scroll-container-track-width) / 2);
|
|
7
|
+
--evo-scroll-container-track-vert-top: 0px;
|
|
8
|
+
--evo-scroll-container-track-vert-bottom: 0px;
|
|
9
|
+
--evo-scroll-container-track-vert-right: 0px;
|
|
10
|
+
--evo-scroll-container-track-horz-bottom: 0px;
|
|
11
|
+
--evo-scroll-container-track-horz-left: 0px;
|
|
12
|
+
--evo-scroll-container-track-horz-right: 0px;
|
|
13
|
+
/* --evo-scroll-container-track-padding: calc(1rem + var(--evo-scroll-container-track-width)); */
|
|
8
14
|
--evo-scroll-container-track-color: #dddddd;
|
|
9
15
|
--evo-scroll-container-thumb-color: #666666;
|
|
10
16
|
--evo-scroll-container-thumb-border-width: 0px;
|
|
11
17
|
--evo-scroll-container-thumb-border-color: var(--evo-scroll-container-track-color);
|
|
12
|
-
--evo-scroll-container-content-padding-right: calc(1rem + var(--evo-scroll-container-track-width));
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
evo-scroll-container,
|
|
16
21
|
.evo-scroll-container {
|
|
22
|
+
--evo-scroll-container-scrollbar-offset: 0px;
|
|
17
23
|
position: relative;
|
|
18
24
|
display: block;
|
|
19
25
|
width: 100%;
|
|
20
26
|
height: 100%;
|
|
21
27
|
overflow: hidden;
|
|
22
28
|
box-sizing: border-box;
|
|
29
|
+
|
|
30
|
+
&.evo-scroll-both {
|
|
31
|
+
--evo-scroll-container-scrollbar-offset: var(--evo-scroll-container-track-width);
|
|
32
|
+
}
|
|
23
33
|
|
|
24
34
|
* {
|
|
25
35
|
box-sizing: border-box;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
.evo-scroll-content {
|
|
29
|
-
height: 100%;
|
|
30
|
-
overflow-y: scroll;
|
|
31
|
-
padding-right: var(--evo-scroll-container-content-padding-right);
|
|
32
39
|
-ms-overflow-style: none;
|
|
33
40
|
scrollbar-width: none;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
width: 90%;
|
|
34
43
|
&::-webkit-scrollbar {
|
|
35
44
|
display: none;
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
*:first-child {
|
|
39
|
-
margin-top: 0 !important;
|
|
40
|
-
}
|
|
41
|
-
*:last-child {
|
|
42
|
-
margin-bottom: 0 !important;
|
|
43
|
-
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
.evo-scroll-track {
|
|
47
49
|
position: absolute;
|
|
48
|
-
top: var(--evo-scroll-container-track-top);
|
|
49
|
-
right: var(--evo-scroll-container-track-right);
|
|
50
|
-
width: var(--evo-scroll-container-track-width);
|
|
51
|
-
height: calc(100% - var(--evo-scroll-container-track-top) - var(--evo-scroll-container-track-bottom));
|
|
52
50
|
background: var(--evo-scroll-container-track-color);
|
|
53
51
|
border-radius: var(--evo-scroll-container-border-radius);
|
|
54
52
|
|
|
55
53
|
.evo-scroll-thumb {
|
|
56
54
|
box-sizing: border-box;
|
|
57
55
|
position: absolute;
|
|
58
|
-
width: 100%;
|
|
59
56
|
background-color: var(--evo-scroll-container-thumb-color);
|
|
60
57
|
border: var(--evo-scroll-container-thumb-border-width) solid var(--evo-scroll-container-thumb-border-color);
|
|
61
58
|
border-radius: var(--evo-scroll-container-border-radius);
|
|
62
|
-
height: 50px;
|
|
63
59
|
cursor: grab;
|
|
64
60
|
font-size: 0;
|
|
65
61
|
}
|
|
62
|
+
|
|
63
|
+
&.evo-scroll-track-vert {
|
|
64
|
+
top: var(--evo-scroll-container-track-vert-top);
|
|
65
|
+
right: var(--evo-scroll-container-track-vert-right);
|
|
66
|
+
width: var(--evo-scroll-container-track-width);
|
|
67
|
+
height: calc(100% - var(--evo-scroll-container-track-vert-top) - var(--evo-scroll-container-track-vert-bottom) - var(--evo-scroll-container-scrollbar-offset));
|
|
68
|
+
|
|
69
|
+
.evo-scroll-thumb {
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 50px;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
&.evo-scroll-track-horz {
|
|
75
|
+
left: var(--evo-scroll-container-track-horz-left);
|
|
76
|
+
bottom: var(--evo-scroll-container-track-horz-bottom);
|
|
77
|
+
width: calc(100% - var(--evo-scroll-container-track-horz-left) - var(--evo-scroll-container-track-horz-right) - var(--evo-scroll-container-scrollbar-offset));
|
|
78
|
+
height: var(--evo-scroll-container-track-width);
|
|
79
|
+
|
|
80
|
+
.evo-scroll-thumb {
|
|
81
|
+
width: 50px;
|
|
82
|
+
height: 100%;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&.evo-scroll-horz,
|
|
88
|
+
&.evo-scroll-both {
|
|
89
|
+
.evo-scroll-content {
|
|
90
|
+
height: calc(100% - var(--evo-scroll-container-track-width) - var(--evo-scroll-container-track-horz-bottom) - var(--evo-scroll-container-height-offset));
|
|
91
|
+
overflow-x: scroll;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
&.evo-scroll-horz {
|
|
95
|
+
.evo-scroll-content {
|
|
96
|
+
width: 100%;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&.evo-scroll-vert,
|
|
101
|
+
&.evo-scroll-both {
|
|
102
|
+
.evo-scroll-content {
|
|
103
|
+
width: calc(100% - var(--evo-scroll-container-track-width) - var(--evo-scroll-container-track-vert-right) - var(--evo-scroll-container-width-offset));
|
|
104
|
+
overflow-y: scroll;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
&.evo-scroll-vert {
|
|
108
|
+
.evo-scroll-content {
|
|
109
|
+
height: 100%;
|
|
110
|
+
}
|
|
66
111
|
}
|
|
67
112
|
}
|
|
68
113
|
}
|
|
@@ -24,6 +24,8 @@ const mutationObserver = new MutationObserver((mutations) => {
|
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
export type Scrolling = 'horizontal' | 'vertical' | 'both';
|
|
28
|
+
|
|
27
29
|
export interface IEvoScrollContainerElement {
|
|
28
30
|
content: HTMLElement;
|
|
29
31
|
scrollLeft: number;
|
|
@@ -48,15 +50,23 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
#content!: HTMLElement;
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
53
|
+
#trackHorz: HTMLElement | null = null;
|
|
54
|
+
#thumbHorz: HTMLElement | null = null;
|
|
55
|
+
#trackVert: HTMLElement | null = null;
|
|
56
|
+
#thumbVert: HTMLElement | null = null;
|
|
57
|
+
#isDraggingVert: boolean = false;
|
|
54
58
|
#startY: number = 0;
|
|
55
59
|
#startScrollTop: number = 0;
|
|
56
|
-
|
|
60
|
+
#isDraggingHorz: boolean = false;
|
|
61
|
+
#startX: number = 0;
|
|
62
|
+
#startScrollLeft: number = 0;
|
|
63
|
+
#scrolling: Scrolling = 'vertical';
|
|
64
|
+
|
|
57
65
|
#scrollbarSettings: ScrollbarSettings = {};
|
|
58
|
-
#
|
|
59
|
-
#
|
|
66
|
+
#mouseupHandlerVert: any = undefined;
|
|
67
|
+
#mousemoveHandlerVert: any = undefined;
|
|
68
|
+
#mouseupHandlerHorz: any = undefined;
|
|
69
|
+
#mousemoveHandlerHorz: any = undefined;
|
|
60
70
|
#boundResizeHandler: () => void;
|
|
61
71
|
|
|
62
72
|
constructor() {
|
|
@@ -66,24 +76,32 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
66
76
|
|
|
67
77
|
connectedCallback() {
|
|
68
78
|
this.#content = this.querySelector('.evo-scroll-content') as HTMLElement;
|
|
69
|
-
this.#
|
|
70
|
-
this.#
|
|
71
|
-
this.#
|
|
79
|
+
this.#trackHorz = this.querySelector<HTMLElement>('.evo-scroll-track-horz');
|
|
80
|
+
this.#thumbHorz = this.querySelector<HTMLElement>('.evo-scroll-track-horz .evo-scroll-thumb');
|
|
81
|
+
this.#trackVert = this.querySelector<HTMLElement>('.evo-scroll-track-vert');
|
|
82
|
+
this.#thumbVert = this.querySelector<HTMLElement>('.evo-scroll-track-vert .evo-scroll-thumb');
|
|
83
|
+
this.#isDraggingVert = false;
|
|
72
84
|
this.#startY = 0;
|
|
73
85
|
this.#startScrollTop = 0;
|
|
74
86
|
|
|
75
|
-
this.#
|
|
87
|
+
this.#thumbVert?.addEventListener('mousedown', this.#thumbMouseDownVert.bind(this));
|
|
88
|
+
this.#thumbHorz?.addEventListener('mousedown', this.#thumbMouseDownHorz.bind(this));
|
|
76
89
|
this.#content.addEventListener('scroll', this.#scrollContent.bind(this), { passive: true });
|
|
90
|
+
this.#scrolling = (this.dataset.scrolling as Scrolling) ?? 'vertical';
|
|
77
91
|
|
|
78
92
|
intersectionObserver.observe(this);
|
|
79
93
|
mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
|
|
80
94
|
|
|
81
95
|
window.addEventListener('resize', this.#boundResizeHandler, { passive: true });
|
|
96
|
+
if (this.#scrolling === 'horizontal') {
|
|
97
|
+
this.addEventListener('wheel', this.#wheelHorz, { passive: true });
|
|
98
|
+
}
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
disconnectedCallback() {
|
|
85
102
|
intersectionObserver.unobserve(this);
|
|
86
|
-
window.removeEventListener('resize', this.#boundResizeHandler);
|
|
103
|
+
window.removeEventListener('resize', this.#boundResizeHandler);
|
|
104
|
+
this.removeEventListener('wheel', this.#wheelHorz);
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
get content() {
|
|
@@ -153,79 +171,138 @@ export class EvoScrollContainerElement extends HTMLElement implements IEvoScroll
|
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
#updateThumbSize() {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
if (this.#thumbVert && this.#trackVert) {
|
|
175
|
+
const scrollHeight = this.#content.scrollHeight - getPaddingY(this.#content);
|
|
176
|
+
const containerHeight = this.clientHeight - getPaddingY(this);
|
|
177
|
+
const trackHeight = this.#trackVert.clientHeight;
|
|
178
|
+
|
|
179
|
+
const visibleRatioVert = containerHeight / scrollHeight;
|
|
180
|
+
const thumbHeight = visibleRatioVert * trackHeight;
|
|
181
|
+
if (this.#scrollbarSettings.thumbHeight != undefined) {
|
|
182
|
+
this.#thumbVert.style.height = `${Math.max(0, Math.min(this.#scrollbarSettings.thumbHeight, trackHeight))}px`;
|
|
183
|
+
} else {
|
|
184
|
+
this.#thumbVert.style.height = `${Math.min(thumbHeight > 20 ? thumbHeight : 20, trackHeight)}px`;
|
|
185
|
+
}
|
|
186
|
+
if (this.#scrollbarSettings.trackVisible !== undefined) {
|
|
187
|
+
this.#trackVert.style.display = this.#scrollbarSettings.trackVisible === true ? 'block' : 'none';
|
|
188
|
+
} else {
|
|
189
|
+
this.#trackVert.style.display = visibleRatioVert < 1 ? 'block' : 'none';
|
|
190
|
+
}
|
|
171
191
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
192
|
+
|
|
193
|
+
if (this.#thumbHorz && this.#trackHorz) {
|
|
194
|
+
const scrollWidth = this.#content.scrollWidth - getPaddingX(this.#content);
|
|
195
|
+
const containerWidth = this.clientWidth - getPaddingX(this);
|
|
196
|
+
const trackWidth = this.#trackHorz.clientWidth;
|
|
197
|
+
|
|
198
|
+
const visibleRatioHorz = containerWidth / scrollWidth;
|
|
199
|
+
const thumbWidth = visibleRatioHorz * trackWidth;
|
|
200
|
+
this.#thumbHorz.style.width = `${Math.min(thumbWidth > 20 ? thumbWidth : 20, trackWidth)}px`;
|
|
201
|
+
if (this.#scrollbarSettings.trackVisible !== undefined) {
|
|
202
|
+
this.#trackHorz.style.display = this.#scrollbarSettings.trackVisible === true ? 'block' : 'none';
|
|
203
|
+
} else {
|
|
204
|
+
this.#trackHorz.style.display = visibleRatioHorz < 1 ? 'block' : 'none';
|
|
205
|
+
}
|
|
176
206
|
}
|
|
177
|
-
|
|
178
207
|
}
|
|
179
208
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
this.#
|
|
183
|
-
|
|
184
|
-
document.addEventListener('
|
|
185
|
-
document.addEventListener('
|
|
186
|
-
this.#
|
|
209
|
+
#thumbMouseDownVert(e: MouseEvent) {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
this.#mouseupHandlerVert = this.#mouseDetachVert.bind(this);
|
|
212
|
+
this.#mousemoveHandlerVert = this.#thumbMouseMoveVert.bind(this);
|
|
213
|
+
document.addEventListener('mouseup', this.#mouseupHandlerVert, { passive: true });
|
|
214
|
+
document.addEventListener('dragend', this.#mouseupHandlerVert, { passive: true });
|
|
215
|
+
document.addEventListener('mousemove', this.#mousemoveHandlerVert, { passive: true });
|
|
216
|
+
this.#isDraggingVert = true;
|
|
187
217
|
this.#startY = e.clientY;
|
|
188
218
|
this.#startScrollTop = this.#content.scrollTop;
|
|
189
|
-
this.#
|
|
219
|
+
this.#thumbVert!.style.cursor = 'grabbing';
|
|
190
220
|
}
|
|
191
221
|
|
|
192
|
-
#
|
|
193
|
-
if (this.#
|
|
222
|
+
#thumbMouseMoveVert(e: MouseEvent) {
|
|
223
|
+
if (this.#isDraggingVert) {
|
|
194
224
|
const deltaY = e.clientY - this.#startY;
|
|
195
|
-
const thumbTrackHeight = this.clientHeight - this.#
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const scrollRatio =
|
|
202
|
-
(this.#content.scrollHeight - this.clientHeight) / thumbTrackHeight;
|
|
203
|
-
|
|
225
|
+
const thumbTrackHeight = this.clientHeight - this.#thumbVert!.clientHeight;
|
|
226
|
+
if (thumbTrackHeight <= 0) return;
|
|
227
|
+
const scrollRatio = (this.#content.scrollHeight - this.clientHeight) / thumbTrackHeight;
|
|
204
228
|
this.#content.scrollTop = this.#startScrollTop + deltaY * scrollRatio;
|
|
205
229
|
} else {
|
|
206
|
-
this.#
|
|
230
|
+
this.#mouseDetachVert();
|
|
207
231
|
}
|
|
208
232
|
}
|
|
209
233
|
|
|
210
|
-
#
|
|
211
|
-
this.#
|
|
212
|
-
this.#
|
|
213
|
-
document.removeEventListener('mouseup', this.#
|
|
214
|
-
document.removeEventListener('dragend', this.#
|
|
215
|
-
document.removeEventListener('mousemove', this.#
|
|
234
|
+
#mouseDetachVert() {
|
|
235
|
+
this.#isDraggingVert = false;
|
|
236
|
+
this.#thumbVert!.style.cursor = 'grab';
|
|
237
|
+
document.removeEventListener('mouseup', this.#mouseupHandlerVert);
|
|
238
|
+
document.removeEventListener('dragend', this.#mouseupHandlerVert);
|
|
239
|
+
document.removeEventListener('mousemove', this.#mousemoveHandlerVert);
|
|
216
240
|
}
|
|
217
241
|
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
#thumbMouseDownHorz(e: MouseEvent) {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
this.#mouseupHandlerHorz = this.#mouseDetachHorz.bind(this);
|
|
245
|
+
this.#mousemoveHandlerHorz = this.#thumbMouseMoveHorz.bind(this);
|
|
246
|
+
document.addEventListener('mouseup', this.#mouseupHandlerHorz, { passive: true });
|
|
247
|
+
document.addEventListener('dragend', this.#mouseupHandlerHorz, { passive: true });
|
|
248
|
+
document.addEventListener('mousemove', this.#mousemoveHandlerHorz, { passive: true });
|
|
249
|
+
this.#isDraggingHorz = true;
|
|
250
|
+
this.#startX = e.clientX;
|
|
251
|
+
this.#startScrollLeft = this.#content.scrollLeft;
|
|
252
|
+
this.#thumbHorz!.style.cursor = 'grabbing';
|
|
253
|
+
}
|
|
222
254
|
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
#thumbMouseMoveHorz(e: MouseEvent) {
|
|
256
|
+
if (this.#isDraggingHorz) {
|
|
257
|
+
const deltaX = e.clientX - this.#startX;
|
|
258
|
+
const thumbTrackWidth = this.clientWidth - this.#thumbHorz!.clientWidth;
|
|
259
|
+
if (thumbTrackWidth <= 0) return;
|
|
260
|
+
const scrollRatio = (this.#content.scrollWidth - this.clientWidth) / thumbTrackWidth;
|
|
261
|
+
this.#content.scrollLeft = this.#startScrollLeft + deltaX * scrollRatio;
|
|
225
262
|
} else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
263
|
+
this.#mouseDetachHorz();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#mouseDetachHorz() {
|
|
268
|
+
this.#isDraggingHorz = false;
|
|
269
|
+
this.#thumbHorz!.style.cursor = 'grab';
|
|
270
|
+
document.removeEventListener('mouseup', this.#mouseupHandlerHorz);
|
|
271
|
+
document.removeEventListener('dragend', this.#mouseupHandlerHorz);
|
|
272
|
+
document.removeEventListener('mousemove', this.#mousemoveHandlerHorz);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#wheelHorz(e: WheelEvent) {
|
|
276
|
+
this.scrollLeft += e.deltaY;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#scrollContent() {
|
|
280
|
+
if (this.#thumbVert && this.#trackVert) {
|
|
281
|
+
const scrollHeight = this.#content.scrollHeight - getPaddingY(this.#content);
|
|
282
|
+
const containerHeight = this.clientHeight - getPaddingY(this);
|
|
283
|
+
const scrollableHeight = scrollHeight - containerHeight;
|
|
284
|
+
if (scrollableHeight <= 0) {
|
|
285
|
+
this.#thumbVert.style.top = '0px';
|
|
286
|
+
} else {
|
|
287
|
+
const scrollRatio = this.#content.scrollTop / scrollableHeight;
|
|
288
|
+
const maxTop = this.#trackVert.clientHeight - this.#thumbVert.clientHeight;
|
|
289
|
+
const top = Math.max(0, Math.min(scrollRatio * maxTop, maxTop));
|
|
290
|
+
this.#thumbVert.style.top = `${top}px`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (this.#thumbHorz && this.#trackHorz) {
|
|
295
|
+
const scrollWidth = this.#content.scrollWidth - getPaddingX(this.#content);
|
|
296
|
+
const containerWidth = this.clientWidth - getPaddingX(this);
|
|
297
|
+
const scrollableWidth = scrollWidth - containerWidth;
|
|
298
|
+
if (scrollableWidth <= 0) {
|
|
299
|
+
this.#thumbHorz.style.left = '0px';
|
|
300
|
+
} else {
|
|
301
|
+
const scrollRatio = this.#content.scrollLeft / scrollableWidth;
|
|
302
|
+
const maxLeft = this.#trackHorz.clientWidth - this.#thumbHorz.clientWidth;
|
|
303
|
+
const left = Math.max(0, Math.min(scrollRatio * maxLeft, maxLeft));
|
|
304
|
+
this.#thumbHorz.style.left = `${left}px`;
|
|
305
|
+
}
|
|
229
306
|
}
|
|
230
307
|
}
|
|
231
308
|
}
|
|
@@ -291,3 +368,8 @@ function getPaddingY(content: HTMLElement): number {
|
|
|
291
368
|
const style = getComputedStyle(content);
|
|
292
369
|
return parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
|
|
293
370
|
}
|
|
371
|
+
|
|
372
|
+
function getPaddingX(content: HTMLElement): number {
|
|
373
|
+
const style = getComputedStyle(content);
|
|
374
|
+
return parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
375
|
+
}
|