@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inizioevoke/astro-core",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
- import ScrollContainer, { type Props as ScrollContainerProps } from '../ScrollContainer/ScrollContainer.astro';
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
- // bookmarks?: Record<string, number>;
15
- // bookmarkText?: string;
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?.nav === false ? 'evo-hidden' : null]}>
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?.page === false ? 'evo-hidden' : null]}>
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?.zoom === false ? 'evo-hidden' : null]}>
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?.bookmarks && Object.keys(toolbar.bookmarks.items).length > 0 &&
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
- padding: 0 9px 9px var(--evo-scroll-container-content-padding-right);
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: calc(var(--evo-pdf-viewer-height) - var(--evo-pdf-viewer-toolbar-height));
120
+ /* height: 100%; */
121
+ height: var(--evo-pdf-viewer-container-height);
146
122
  .evo-scroll-content {
147
- overflow-x: hidden;
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
- margin: 18px auto;
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: 1;
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
- /* &.evo-pdf-viewer-theme-modern {
157
+ &.evo-pdf-viewer-toolbar-hidden {
183
158
  .evo-pdf-viewer-toolbar {
184
- button {
185
- background-color: transparent;
186
- border: none;
187
- border-radius: 0;
188
- &:hover {
189
- background-color: rgba(0,0,0,0.05);
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: number) => void;
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.#updateCurrentPage, { passive: true });
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
- // requestAnimationFrame(() => {
108
- const canvas = this.#scrollContainer.querySelector<HTMLElement>(`canvas[data-page="${page}"]`);
109
- if (canvas) {
110
- this.#scrollContainer.scroll({
111
- top: canvas.getBoundingClientRect().top + this.#scrollContainer.scrollTop - this.#toolbarHeight
112
- }); // , behavior: 'smooth' });
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: number) {
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 scale = z === 0 ? this.#defaultZoom / 10 : current + z;
122
- if (scale >= 1) {
123
- const scroll = {
124
- left: this.#scrollContainer.scrollLeft,
125
- top: this.#scrollContainer.scrollTop
126
- };
127
- this.style.setProperty('--evo-pdf-viewer-scale', (scale/10).toString());
128
- this.#scrollContainer.scroll({
129
- left: scroll.left * scale / current,
130
- top: scroll.top * scale / current
131
- });
132
- this.#scrollContainer.refresh();
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
- const outputScale = window.devicePixelRatio || 1;
138
- const pdf: PDFDocumentProxy = await pdfjs.getDocument(this.getAttribute('data-pdf') as string).promise;
139
-
140
- const pages = new Array(pdf.numPages).fill(1).map((v, i) => { return i + 1; });
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 = canvas.style.width;
176
- pageWrapper.style.aspectRatio = canvas.style.aspectRatio;
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
- pdfPage.render({
182
- canvas,
183
- // canvasContext: canvas.getContext('2d') as CanvasRenderingContext2D,
184
- transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : undefined,
185
- viewport
186
- });
187
-
188
- // Extract and render links
189
- const links = await getPageLinks(pdfPage);
190
- if (links.length > 0) {
191
- const linkOverlay = createLinkOverlay(canvas, links, viewport, pageNum, this, this.scrollToPage.bind(this), pdf);
192
- pageWrapper.appendChild(linkOverlay);
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
- this.#scrollContainer.refresh();
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 canvases = this.#scrollContainer.content.querySelectorAll<HTMLCanvasElement>('canvas');
201
- let visiblePage: HTMLCanvasElement | undefined;
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
- canvases.forEach((canvas) => {
205
- const rect = canvas.getBoundingClientRect();
413
+ wrappers.forEach((wrapper) => {
414
+ const rect = wrapper.getBoundingClientRect();
206
415
  const containerRect = this.#scrollContainer.getBoundingClientRect();
207
-
208
- // Calculate intersection height
209
- const intersectTop = Math.max(rect.top, containerRect.top);
210
- const intersectBottom = Math.min(rect.bottom, containerRect.bottom);
211
- const intersectionHeight = Math.max(0, intersectBottom - intersectTop);
212
-
213
- // Calculate visibility ratio
214
- const visibility = intersectionHeight / (rect.height || 1);
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
- visiblePage = canvas;
434
+ visibleWrapper = wrapper;
219
435
  }
220
436
  });
221
437
 
222
- if (visiblePage && maxVisibility >= 0.1) {
223
- const pageNum = visiblePage.getAttribute('data-page');
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', `calc(${canvasWidth}px * var(--evo-pdf-viewer-scale))`);
323
- svg.setAttribute('height', `calc(${canvasHeight}px * var(--evo-pdf-viewer-scale))`);
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
- outerWidth?: string | number;
7
- innerWidth?: string | number;
7
+ width?: string | number;
8
8
  height?: string | number;
9
- vertical?: boolean;
10
- horizontal?: boolean;
9
+ scrolling?: Scrolling;
11
10
  }
12
11
 
13
12
  const {
14
- outerWidth,
15
- innerWidth,
13
+ width,
16
14
  height,
17
- vertical = true,
18
- horizontal = false,
15
+ scrolling = 'vertical',
19
16
  ...rest
20
17
  } = Astro.props;
21
18
 
22
- const containerAttrs = {...rest};
23
- const cssClass = containerAttrs['class'] ?? '';
24
- ['class'].forEach((a) => {
25
- delete containerAttrs[a as keyof typeof containerAttrs];
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 (outerWidth) {
30
- containerAttrs.style = `${containerAttrs.style}width:${typeof outerWidth == 'number' || /^\d+$/.test(outerWidth) ? `${outerWidth}px` : outerWidth};`;
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={['evo-scroll-container', cssClass]} {...containerAttrs}>
42
- <div class="evo-scroll-content" {...contentAttrs}>
43
+ <evo-scroll-container class:list={cssClassList} {...containerAttrs}>
44
+ <div class="evo-scroll-content">
43
45
  <slot />
44
46
  </div>
45
- {vertical && <div class="evo-scroll-track evo-scroll-track-y">
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
- {horizontal && <div class="evo-scroll-track evo-scroll-track-x">
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-track-top: 0px;
6
- --evo-scroll-container-track-bottom: 0px;
7
- --evo-scroll-container-track-right: 0px;
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
- #track!: HTMLElement;
52
- #thumb!: HTMLElement;
53
- #isDragging: boolean = false;
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
- #mouseupHandler: any = undefined;
59
- #mousemoveHandler: any = undefined;
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.#track = this.querySelector('.evo-scroll-track') as HTMLElement;
70
- this.#thumb = this.querySelector('.evo-scroll-thumb') as HTMLElement;
71
- this.#isDragging = false;
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.#thumb.addEventListener('mousedown', this.#thumbMouseDown.bind(this), { passive: true });
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
- const scrollHeight = this.#content.scrollHeight - getPaddingY(this.#content);
157
- const containerHeight = this.clientHeight - getPaddingY(this);
158
-
159
- if (scrollHeight <= 0) {
160
- this.#thumb.style.height = '20px';
161
- // this.#track.style.display = 'none';
162
- return;
163
- }
164
-
165
- const visibleRatio = containerHeight / scrollHeight;
166
- const thumbHeight = visibleRatio * containerHeight;
167
- if (this.#scrollbarSettings.thumbHeight != undefined) {
168
- this.#thumb.style.height = `${this.#scrollbarSettings.thumbHeight < 0 ? 0 : this.#scrollbarSettings.thumbHeight > containerHeight ? containerHeight : this.#scrollbarSettings.thumbHeight}px`;
169
- } else {
170
- this.#thumb.style.height = `${thumbHeight > 20 ? thumbHeight : 20}px`;
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
- if (this.#scrollbarSettings.trackVisible !== undefined) {
173
- this.#track.style.display = this.#scrollbarSettings.trackVisible === true ? 'block' : 'none';
174
- } else {
175
- this.#track.style.display = visibleRatio < 1 ? 'block' : 'none';
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
- #thumbMouseDown(e: MouseEvent) {
181
- this.#mouseupHandler = this.#mouseDetach.bind(this);
182
- this.#mousemoveHandler = this.#thumbMouseMove.bind(this);
183
- document.addEventListener('mouseup', this.#mouseupHandler, { passive: true });
184
- document.addEventListener('dragend', this.#mouseupHandler, { passive: true });
185
- document.addEventListener('mousemove', this.#mousemoveHandler, { passive: true });
186
- this.#isDragging = true;
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.#thumb.style.cursor = 'grabbing';
219
+ this.#thumbVert!.style.cursor = 'grabbing';
190
220
  }
191
221
 
192
- #thumbMouseMove(e: MouseEvent) {
193
- if (this.#isDragging) {
222
+ #thumbMouseMoveVert(e: MouseEvent) {
223
+ if (this.#isDraggingVert) {
194
224
  const deltaY = e.clientY - this.#startY;
195
- const thumbTrackHeight = this.clientHeight - this.#thumb.clientHeight;
196
-
197
- if (thumbTrackHeight <= 0) {
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.#mouseDetach();
230
+ this.#mouseDetachVert();
207
231
  }
208
232
  }
209
233
 
210
- #mouseDetach() {
211
- this.#isDragging = false;
212
- this.#thumb.style.cursor = 'grab';
213
- document.removeEventListener('mouseup', this.#mouseupHandler);
214
- document.removeEventListener('dragend', this.#mouseupHandler);
215
- document.removeEventListener('mousemove', this.#mousemoveHandler);
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
- #scrollContent() {
219
- const scrollHeight = this.#content.scrollHeight - getPaddingY(this.#content);
220
- const containerHeight = this.clientHeight - getPaddingY(this);
221
- const scrollableHeight = scrollHeight - containerHeight;
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
- if (scrollableHeight <= 0) {
224
- this.#thumb.style.top = '0px';
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
- const scrollRatio = this.#content.scrollTop / scrollableHeight;
227
- const top = scrollRatio * (this.#track.clientHeight - this.#thumb.clientHeight);
228
- this.#thumb.style.top = `${top}px`;
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
+ }