@web-atoms/web-controls 2.4.41 → 2.4.43
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/dist/basic/PinchZoomView.d.ts.map +1 -1
- package/dist/basic/PinchZoomView.js +8 -4
- package/dist/basic/PinchZoomView.js.map +1 -1
- package/dist/basic/ZoomView.d.ts +21 -0
- package/dist/basic/ZoomView.d.ts.map +1 -0
- package/dist/basic/ZoomView.js +241 -0
- package/dist/basic/ZoomView.js.map +1 -0
- package/dist/basic/styles/pinch-zoom-view-style.js +9 -1
- package/dist/basic/styles/pinch-zoom-view-style.js.map +1 -1
- package/dist/basic/styles/zoom-view-style.d.ts +2 -0
- package/dist/basic/styles/zoom-view-style.d.ts.map +1 -0
- package/dist/basic/styles/zoom-view-style.js +82 -0
- package/dist/basic/styles/zoom-view-style.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/basic/PinchZoomView.tsx +15 -3
- package/src/basic/ZoomView.tsx +270 -0
- package/src/basic/styles/pinch-zoom-view-style.ts +11 -1
- package/src/basic/styles/zoom-view-style.ts +73 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import Bind from "@web-atoms/core/dist/core/Bind";
|
|
2
|
+
import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty";
|
|
3
|
+
import XNode, { isTemplateSymbol } from "@web-atoms/core/dist/core/XNode";
|
|
4
|
+
import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
|
|
5
|
+
|
|
6
|
+
import "./styles/zoom-view-style";
|
|
7
|
+
|
|
8
|
+
const center = (ev: TouchEvent) => {
|
|
9
|
+
const touch = ev.touches[0];
|
|
10
|
+
if (touch) {
|
|
11
|
+
return {
|
|
12
|
+
x: touch.clientX,
|
|
13
|
+
y: touch.clientY
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
x: 0,
|
|
18
|
+
y: 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const distance = (first: Touch, second: Touch) => {
|
|
23
|
+
return Math.hypot(first.pageX - second.pageX, first.pageY - second.pageY);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface IZoom {
|
|
27
|
+
anchorX: number;
|
|
28
|
+
anchorY: number;
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
scale: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default class ZoomView extends AtomControl {
|
|
35
|
+
|
|
36
|
+
@BindableProperty
|
|
37
|
+
public zoom: IZoom;
|
|
38
|
+
|
|
39
|
+
@BindableProperty
|
|
40
|
+
public source: string;
|
|
41
|
+
|
|
42
|
+
@BindableProperty
|
|
43
|
+
private loading: boolean;
|
|
44
|
+
|
|
45
|
+
private image: HTMLImageElement;
|
|
46
|
+
|
|
47
|
+
private scrollDiv: HTMLDivElement;
|
|
48
|
+
|
|
49
|
+
private imageContainer: HTMLDivElement;
|
|
50
|
+
|
|
51
|
+
protected preCreate() {
|
|
52
|
+
|
|
53
|
+
this.element.title = "Use mouse wheel to zoom";
|
|
54
|
+
|
|
55
|
+
this.loading = false;
|
|
56
|
+
this.zoom = {
|
|
57
|
+
scale: 0,
|
|
58
|
+
anchorX: 0,
|
|
59
|
+
anchorY: 0,
|
|
60
|
+
x: 0,
|
|
61
|
+
y: 0
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.element.setAttribute("data-zoom-view", "zoom-view");
|
|
65
|
+
|
|
66
|
+
this.element.draggable = false;
|
|
67
|
+
|
|
68
|
+
this.render(<div>
|
|
69
|
+
<div class="scroll">
|
|
70
|
+
<img
|
|
71
|
+
class="image-container"
|
|
72
|
+
src={Bind.oneWay(() => this.getSource(this.source))}
|
|
73
|
+
style-opacity={Bind.oneWay(() => this.loading ? "0.3" : "1")}
|
|
74
|
+
event-load={() => {
|
|
75
|
+
this.loading = false;
|
|
76
|
+
this.updateZoom(this.zoom);
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
<i class={Bind.oneWay(() => this.loading ? "spinner fa-duotone fa-spinner fa-spin" : "hide")}/>
|
|
81
|
+
<i
|
|
82
|
+
event-click={() => this.updateZoom()}
|
|
83
|
+
class={Bind.oneWay(() => this.zoom.scale ? "scale" : "hide")}
|
|
84
|
+
title="Display entire image"/>
|
|
85
|
+
</div>);
|
|
86
|
+
this.scrollDiv = this.element.firstElementChild as HTMLDivElement;
|
|
87
|
+
this.imageContainer = this.scrollDiv.firstElementChild as HTMLDivElement;
|
|
88
|
+
this.image = this.imageContainer as HTMLImageElement;
|
|
89
|
+
|
|
90
|
+
const scrollView = this.element;
|
|
91
|
+
|
|
92
|
+
let previous: {x: number, y: number};
|
|
93
|
+
|
|
94
|
+
let touchMoveDisposable;
|
|
95
|
+
let touchEndDisposable;
|
|
96
|
+
|
|
97
|
+
this.bindEvent(scrollView, "touchstart", (evs: TouchEvent) => {
|
|
98
|
+
|
|
99
|
+
previous = center(evs);
|
|
100
|
+
let previousDistance = undefined;
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
touchMoveDisposable ??= this.bindEvent(scrollView, "touchmove", (ev: TouchEvent) => {
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
let { x, y, anchorX, anchorY, scale } = this.zoom;
|
|
107
|
+
|
|
108
|
+
if (ev.touches.length === 2) {
|
|
109
|
+
|
|
110
|
+
ev.preventDefault();
|
|
111
|
+
ev.stopImmediatePropagation();
|
|
112
|
+
|
|
113
|
+
const rect = this.element.getBoundingClientRect();
|
|
114
|
+
const first = ev.touches[0];
|
|
115
|
+
const second = ev.touches[1];
|
|
116
|
+
anchorX = ((first.clientX + second.clientX) / 2) - rect.left;
|
|
117
|
+
anchorY = ((first.clientY + second.clientY) / 2) - rect.top;
|
|
118
|
+
const newScale = distance(first, second);
|
|
119
|
+
if (previousDistance === void 0) {
|
|
120
|
+
previousDistance = newScale;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (previousDistance === newScale) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
scale += newScale - previousDistance;
|
|
128
|
+
previousDistance = newScale;
|
|
129
|
+
this.updateZoom({
|
|
130
|
+
anchorX,
|
|
131
|
+
anchorY,
|
|
132
|
+
x,
|
|
133
|
+
y,
|
|
134
|
+
scale
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
touchEndDisposable ??= this.bindEvent(scrollView, "touchend", (ev: TouchEvent) => {
|
|
143
|
+
// ev.preventDefault();
|
|
144
|
+
// ev.stopImmediatePropagation?.();
|
|
145
|
+
|
|
146
|
+
touchMoveDisposable?.dispose();
|
|
147
|
+
touchEndDisposable?.dispose();
|
|
148
|
+
touchMoveDisposable = undefined;
|
|
149
|
+
touchEndDisposable = undefined;
|
|
150
|
+
previousDistance = undefined;
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
let mouseMoveDisposable;
|
|
155
|
+
let mouseUpDisposable;
|
|
156
|
+
|
|
157
|
+
this.bindEvent(scrollView, "dragstart", (ev: DragEvent) => {
|
|
158
|
+
ev.preventDefault();
|
|
159
|
+
ev.stopImmediatePropagation();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.bindEvent(scrollView, "mousedown", (ev: MouseEvent) => {
|
|
163
|
+
this.element.dataset.state = "grabbing";
|
|
164
|
+
previous = {
|
|
165
|
+
x: ev.clientX,
|
|
166
|
+
y: ev.clientY
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
mouseMoveDisposable ??= this.bindEvent(scrollView, "mousemove", (e: MouseEvent) => {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
e.stopImmediatePropagation?.();
|
|
172
|
+
const cp = { x: e.clientX, y: e.clientY };
|
|
173
|
+
const diffX = previous.x - cp.x;
|
|
174
|
+
const diffY = previous.y - cp.y;
|
|
175
|
+
previous = cp;
|
|
176
|
+
this.scrollDiv.scrollBy(diffX, diffY);
|
|
177
|
+
});
|
|
178
|
+
mouseUpDisposable ??= this.bindEvent(scrollView, "mouseup", (e: MouseEvent) => {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
e.stopImmediatePropagation?.();
|
|
181
|
+
|
|
182
|
+
this.element.dataset.state = "";
|
|
183
|
+
previous = null;
|
|
184
|
+
mouseMoveDisposable.dispose();
|
|
185
|
+
mouseUpDisposable.dispose();
|
|
186
|
+
mouseMoveDisposable = undefined;
|
|
187
|
+
mouseUpDisposable = undefined;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
this.bindEvent(scrollView, "wheel", (ev: WheelEvent) => {
|
|
193
|
+
|
|
194
|
+
ev.preventDefault();
|
|
195
|
+
ev.stopImmediatePropagation?.();
|
|
196
|
+
|
|
197
|
+
const newScale = this.zoom.scale - (ev.deltaY < 0 ? -50 : 50);
|
|
198
|
+
|
|
199
|
+
const anchorX = ev.offsetX;
|
|
200
|
+
const anchorY = ev.offsetY;
|
|
201
|
+
const { x, y } = this.zoom;
|
|
202
|
+
this.updateZoom({
|
|
203
|
+
anchorX,
|
|
204
|
+
anchorY,
|
|
205
|
+
x,
|
|
206
|
+
y,
|
|
207
|
+
scale: newScale < 0 ? 0 : newScale
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
}, undefined, {
|
|
211
|
+
passive: false
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private getSource(text: string) {
|
|
216
|
+
if (text) {
|
|
217
|
+
this.loading = true;
|
|
218
|
+
}
|
|
219
|
+
return text;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private updateZoom(zoom: IZoom = {
|
|
223
|
+
x: 0,
|
|
224
|
+
y: 0,
|
|
225
|
+
anchorX: 0,
|
|
226
|
+
anchorY: 0,
|
|
227
|
+
scale: 0
|
|
228
|
+
}) {
|
|
229
|
+
const { anchorX, anchorY, x, y } = zoom;
|
|
230
|
+
let { scale } = zoom;
|
|
231
|
+
// console.log(zoom);
|
|
232
|
+
this.zoom = zoom;
|
|
233
|
+
const image = this.image;
|
|
234
|
+
if (!image.naturalHeight) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const maxHeight = this.element.clientWidth > this.element.clientHeight;
|
|
238
|
+
const s = maxHeight
|
|
239
|
+
? this.element.clientWidth / image.naturalWidth
|
|
240
|
+
: this.element.clientHeight / image.naturalHeight ;
|
|
241
|
+
|
|
242
|
+
if (scale <= 0) {
|
|
243
|
+
scale = 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { clientWidth, clientHeight } = this.element;
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if (scale <= 0) {
|
|
250
|
+
this.element.scrollTo(0,0);
|
|
251
|
+
this.imageContainer.style.transform = ``;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const scaleFactor = (clientWidth + scale) / clientWidth;
|
|
256
|
+
|
|
257
|
+
this.imageContainer.style.transformOrigin = `${anchorX}px ${anchorY}px`;
|
|
258
|
+
this.imageContainer.style.transform = `scale(${scaleFactor})`;
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
const e = this.imageContainer.getBoundingClientRect();
|
|
262
|
+
// console.log([e.left, e.top]);
|
|
263
|
+
const left = -e.left;
|
|
264
|
+
const top = -e.top;
|
|
265
|
+
this.imageContainer.style.transformOrigin = "0 0";
|
|
266
|
+
const de = this.imageContainer.getBoundingClientRect();
|
|
267
|
+
// console.log([de.left, de.top]);
|
|
268
|
+
this.scrollDiv.scrollTo({ left: left + de.left, top: top + de.top , behavior: "instant" });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import styled, { svgAsCssDataUrl } from "@web-atoms/core/dist/style/styled";
|
|
3
|
+
|
|
4
|
+
const svg = svgAsCssDataUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M502.6 54.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L336 130.7 336 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-50.7 0L502.6 54.6zM80 272c-17.7 0-32 14.3-32 32s14.3 32 32 32l50.7 0L9.4 457.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L176 381.3l0 50.7c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32L80 272z"/></svg>`);
|
|
5
|
+
|
|
2
6
|
|
|
3
7
|
styled.css `
|
|
4
8
|
position: relative;
|
|
@@ -18,6 +22,12 @@ import styled from "@web-atoms/core/dist/style/styled";
|
|
|
18
22
|
background-color: white;
|
|
19
23
|
padding: 4px;
|
|
20
24
|
color: black;
|
|
25
|
+
width: 30px;
|
|
26
|
+
height: 30px;
|
|
27
|
+
background-image: ${svg};
|
|
28
|
+
background-position: 2px 2px;
|
|
29
|
+
background-size: 20px 20px;
|
|
30
|
+
background-repeat: no-repeat;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
& > .image-container {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
import styled, { svgAsCssDataUrl } from "@web-atoms/core/dist/style/styled";
|
|
3
|
+
|
|
4
|
+
const svg = svgAsCssDataUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M502.6 54.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L336 130.7 336 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32l128 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-50.7 0L502.6 54.6zM80 272c-17.7 0-32 14.3-32 32s14.3 32 32 32l50.7 0L9.4 457.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L176 381.3l0 50.7c0 17.7 14.3 32 32 32s32-14.3 32-32l0-128c0-17.7-14.3-32-32-32L80 272z"/></svg>`);
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
styled.css `
|
|
8
|
+
position: relative;
|
|
9
|
+
cursor: grab;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
|
|
12
|
+
&[data-state=grabbing] {
|
|
13
|
+
cursor: grabbing;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
& > div {
|
|
17
|
+
|
|
18
|
+
position: absolute;
|
|
19
|
+
left: 0;
|
|
20
|
+
top: 0;
|
|
21
|
+
right: 0;
|
|
22
|
+
bottom: 0;
|
|
23
|
+
scrollbar-width: none;
|
|
24
|
+
overflow: auto;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
& > .image-container {
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: 0;
|
|
30
|
+
top: 0;
|
|
31
|
+
right: 0;
|
|
32
|
+
bottom: 0;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
object-fit: contain;
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
& > .scale {
|
|
42
|
+
position: absolute;
|
|
43
|
+
right: 10px;
|
|
44
|
+
top: 10px;
|
|
45
|
+
border: solid 2px darkgray;
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
background-color: white;
|
|
48
|
+
padding: 4px;
|
|
49
|
+
color: black;
|
|
50
|
+
width: 30px;
|
|
51
|
+
height: 30px;
|
|
52
|
+
background-image: ${svg};
|
|
53
|
+
background-position: 2px 2px;
|
|
54
|
+
background-size: 20px 20px;
|
|
55
|
+
background-repeat: no-repeat;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
& > .spinner {
|
|
59
|
+
position: absolute;
|
|
60
|
+
left: 0;
|
|
61
|
+
top: 0;
|
|
62
|
+
right: 0;
|
|
63
|
+
bottom: 0;
|
|
64
|
+
display: inline-grid;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-items: center;
|
|
67
|
+
z-index: 10;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
& > .hide {
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
73
|
+
`.installGlobal("div[data-zoom-view]");
|