@operato/mini-map 2.0.0-alpha.57 → 2.0.0-beta.10

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.
@@ -0,0 +1,256 @@
1
+ import { css, LitElement, PropertyValues } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+
4
+ // Utility function to get the point from MouseEvent or TouchEvent
5
+ function getPoint(e: MouseEvent | TouchEvent): { x: number; y: number } | undefined {
6
+ if (e instanceof MouseEvent && e.button === 0) {
7
+ return { x: e.clientX, y: e.clientY }
8
+ } else if (e instanceof TouchEvent && e.touches.length === 1) {
9
+ const touch = e.touches[0]
10
+ return { x: touch.clientX, y: touch.clientY }
11
+ }
12
+ return undefined
13
+ }
14
+
15
+ /**
16
+ * Example usage:
17
+ *
18
+ * <html>
19
+ * <head>
20
+ * <script type="module" src="path/to/ox-zoomable-image.js"></script>
21
+ * <style>
22
+ * body {
23
+ * margin: 0;
24
+ * overflow: hidden;
25
+ * display: flex;
26
+ * justify-content: center;
27
+ * align-items: center;
28
+ * height: 100vh;
29
+ * }
30
+ * ox-zoomable-image {
31
+ * width: 80%;
32
+ * height: 80%;
33
+ * }
34
+ * </style>
35
+ * </head>
36
+ * <body>
37
+ * <ox-zoomable-image src="path/to/your/image.jpg"></ox-zoomable-image>
38
+ * </body>
39
+ * </html>
40
+ */
41
+
42
+ @customElement('ox-zoomable-image')
43
+ export class OxZoomableImage extends LitElement {
44
+ // Define CSS styles for the component
45
+ static styles = css`
46
+ :host {
47
+ display: block;
48
+ position: relative;
49
+ width: 100%;
50
+ height: 100%;
51
+ overflow: hidden;
52
+ }
53
+
54
+ :host::before {
55
+ content: '';
56
+ display: block;
57
+ position: absolute;
58
+ top: 0;
59
+ right: 0;
60
+ width: 100%;
61
+ height: 100%;
62
+ background-image: var(
63
+ --background-image,
64
+ linear-gradient(var(--primary-color, lightgray), var(--primary-color, lightgray))
65
+ );
66
+ transform: var(--image-transform, '');
67
+ background-repeat: no-repeat;
68
+ background-position: center;
69
+ background-size: contain;
70
+ }
71
+ `
72
+
73
+ @property({ type: String }) src?: string // Source URL for the image
74
+ @state() private zoom = 1 // Current zoom level
75
+ @state() private left = 0 // Horizontal position
76
+ @state() private top = 0 // Vertical position
77
+ @state() private minZoom = 1 // Minimum zoom level
78
+ @state() private imageWidth = 0
79
+ @state() private imageHeight = 0
80
+
81
+ private dragStart?: { x: number; y: number } // Starting point of drag
82
+
83
+ // Event listeners bound to the component
84
+ private readonly dragEndHandler = this.onDragEnd.bind(this) as EventListener
85
+ private readonly dragMoveHandler = this.onDragMove.bind(this) as EventListener
86
+ private readonly dragStartHandler = this.onDragStart.bind(this) as EventListener
87
+ private readonly wheelEventHandler = this.onWheelEvent.bind(this) as EventListener
88
+ private readonly resetHandler = this.onReset.bind(this) as EventListener
89
+
90
+ connectedCallback(): void {
91
+ super.connectedCallback()
92
+
93
+ // Register event listeners for zooming and dragging
94
+ this.addEventListener('wheel', this.wheelEventHandler)
95
+ this.addEventListener('mousedown', this.dragStartHandler)
96
+ this.addEventListener('touchstart', this.dragStartHandler)
97
+ this.addEventListener('dblclick', this.resetHandler)
98
+
99
+ document.addEventListener('mouseup', this.dragEndHandler)
100
+ document.addEventListener('touchend', this.dragEndHandler)
101
+ document.addEventListener('touchcancel', this.dragEndHandler)
102
+ document.addEventListener('mousemove', this.dragMoveHandler)
103
+ document.addEventListener('touchmove', this.dragMoveHandler)
104
+
105
+ this.calculateMinZoom() // Calculate the minimum zoom level based on the image and container size
106
+ }
107
+
108
+ disconnectedCallback(): void {
109
+ // Remove event listeners when the component is disconnected
110
+ this.removeEventListener('wheel', this.wheelEventHandler)
111
+ this.removeEventListener('mousedown', this.dragStartHandler)
112
+ this.removeEventListener('touchstart', this.dragStartHandler)
113
+ this.removeEventListener('dblclick', this.resetHandler)
114
+
115
+ document.removeEventListener('mouseup', this.dragEndHandler)
116
+ document.removeEventListener('touchend', this.dragEndHandler)
117
+ document.removeEventListener('touchcancel', this.dragEndHandler)
118
+ document.removeEventListener('mousemove', this.dragMoveHandler)
119
+ document.removeEventListener('touchmove', this.dragMoveHandler)
120
+
121
+ super.disconnectedCallback()
122
+ }
123
+
124
+ updated(changes: PropertyValues<this>): void {
125
+ // Update the background image and transform style when properties change
126
+ if (changes.has('src') && this.src) {
127
+ this.style.setProperty('--background-image', `url('${this.src}')`)
128
+ this.calculateMinZoom() // Recalculate the minimum zoom level when the source changes
129
+ }
130
+ this.updateTransform() // Apply the current zoom and position
131
+ }
132
+
133
+ private calculateMinZoom() {
134
+ // Calculate the minimum zoom level to ensure the image always fills the container
135
+ const img = new Image()
136
+ img.src = this.src || ''
137
+ img.onload = () => {
138
+ this.imageWidth = img.width
139
+ this.imageHeight = img.height
140
+
141
+ const aspectRatio = img.width / img.height
142
+ const hostAspectRatio = this.offsetWidth / this.offsetHeight
143
+ if (aspectRatio < hostAspectRatio) {
144
+ this.minZoom = this.offsetHeight / img.height
145
+ } else {
146
+ this.minZoom = this.offsetWidth / img.width
147
+ }
148
+ this.zoom = 1 // this.minZoom
149
+ this.left = 0 // Center the image
150
+ this.top = 0 // Center the image
151
+ this.updateTransform()
152
+ }
153
+ }
154
+
155
+ private onWheelEvent(e: WheelEvent): void {
156
+ // Handle zoom in and out based on mouse wheel events
157
+ e.preventDefault()
158
+ const delta = Math.sign(e.deltaY)
159
+ const newZoom = Math.max(1, this.zoom * (1 - delta / 20))
160
+ const zoomFactor = newZoom / this.zoom
161
+
162
+ const rect = this.getBoundingClientRect()
163
+ const offsetX = e.clientX - rect.left - rect.width / 2
164
+ const offsetY = e.clientY - rect.top - rect.height / 2
165
+
166
+ // Adjust the position to zoom in/out relative to the center of the client rect
167
+ this.left -= offsetX * (zoomFactor - 1)
168
+ this.top -= offsetY * (zoomFactor - 1)
169
+
170
+ this.zoom = newZoom
171
+ this.applyBounds() // Ensure the image stays within bounds
172
+ this.updateTransform() // Apply the new zoom and position
173
+ }
174
+
175
+ private onDragStart(e: MouseEvent | TouchEvent): void {
176
+ // Start dragging the image
177
+ const point = getPoint(e)
178
+ if (point) {
179
+ this.dragStart = point
180
+ e.stopPropagation()
181
+ }
182
+ }
183
+
184
+ private onDragMove(e: MouseEvent | TouchEvent): void {
185
+ // Move the image based on drag events
186
+ if (!this.dragStart) return
187
+
188
+ const point = getPoint(e)
189
+ if (!point) return
190
+
191
+ e.stopPropagation()
192
+ e.preventDefault()
193
+
194
+ const { x, y } = point
195
+ this.left += x - this.dragStart.x
196
+ this.top += y - this.dragStart.y
197
+
198
+ this.dragStart = point
199
+ this.applyBounds() // Ensure the image stays within bounds
200
+ this.updateTransform() // Apply the new position
201
+ }
202
+
203
+ private onDragEnd(e: Event): void {
204
+ // End the drag operation
205
+ if (this.dragStart) {
206
+ e.stopPropagation()
207
+ e.preventDefault()
208
+ this.dragStart = undefined
209
+ }
210
+ }
211
+
212
+ private onReset(e: Event): void {
213
+ // Reset zoom and position to initial state
214
+ this.zoom = 1
215
+ this.left = 0
216
+ this.top = 0
217
+ this.updateTransform()
218
+ }
219
+
220
+ private updateTransform() {
221
+ // Apply the current zoom and position to the image
222
+ this.style.setProperty('--image-transform', `scale(${this.zoom}) translate(${this.left}px, ${this.top}px)`)
223
+ }
224
+
225
+ private applyBounds() {
226
+ // Ensure the image stays within the bounds of the container
227
+ const hostWidth = this.offsetWidth
228
+ const hostHeight = this.offsetHeight
229
+
230
+ const aspectRatio = this.imageHeight / this.imageWidth
231
+ const hostAspectRatio = hostHeight / hostWidth
232
+
233
+ if (aspectRatio < hostAspectRatio) {
234
+ /* image ratio is wider than host */
235
+ var minX = Math.min(0, (-hostWidth * (this.zoom - 1)) / 2 / this.zoom)
236
+ var maxX = -minX
237
+
238
+ var minY = (hostHeight - hostWidth * aspectRatio * this.zoom) / 2 / this.zoom
239
+ minY = Math.min(minY, -minY)
240
+ var maxY = -minY
241
+
242
+ this.left = Math.max(minX, Math.min(this.left, maxX))
243
+ this.top = Math.max(minY, Math.min(this.top, maxY))
244
+ } else {
245
+ var minX = (hostWidth - (hostHeight / aspectRatio) * this.zoom) / 2 / this.zoom
246
+ minX = Math.min(minX, -minX)
247
+ var maxX = -minX
248
+
249
+ var minY = Math.min(0, (-hostHeight * (this.zoom - 1)) / 2 / this.zoom)
250
+ var maxY = -minY
251
+
252
+ this.left = Math.max(minX, Math.min(this.left, maxX))
253
+ this.top = Math.max(minY, Math.min(this.top, maxY))
254
+ }
255
+ }
256
+ }
@@ -0,0 +1,54 @@
1
+ import { html, TemplateResult } from 'lit'
2
+ import '../src/ox-mini-map.js'
3
+
4
+ export default {
5
+ title: 'MiniMap',
6
+ component: 'ox-mini-map',
7
+ argTypes: {}
8
+ }
9
+
10
+ interface Story<T> {
11
+ (args: T): TemplateResult
12
+ args?: Partial<T>
13
+ argTypes?: Record<string, unknown>
14
+ }
15
+
16
+ interface ArgTypes {}
17
+
18
+ const Template: Story<ArgTypes> = ({}: ArgTypes) => html`
19
+ <style>
20
+ #parent {
21
+ padding: 0;
22
+ margin: 0;
23
+ width: 600px;
24
+ height: 400px;
25
+ background-color: red;
26
+ position: relative; /* Make the parent element the reference for the mini map */
27
+ overflow: auto;
28
+ }
29
+
30
+ #content {
31
+ flex: 1;
32
+ width: 1200px;
33
+ height: 1000px;
34
+ background-image: url('https://via.placeholder.com/800x600.png'); /* Fix the background image URL */
35
+ background-size: cover;
36
+ }
37
+
38
+ ox-mini-map {
39
+ position: fixed; /* Fix to the bottom right of the parent element */
40
+ width: 160px;
41
+ height: 120px;
42
+ bottom: 10px;
43
+ right: 10px;
44
+ }
45
+ </style>
46
+
47
+ <div id="parent">
48
+ <div id="content"></div>
49
+ <ox-mini-map .target=${document.getElementById('parent')}></ox-mini-map>
50
+ </div>
51
+ `
52
+
53
+ export const Regular = Template.bind({})
54
+ Regular.args = {}
@@ -0,0 +1,58 @@
1
+ import { html, TemplateResult } from 'lit-html'
2
+ import '../src/ox-zoomable-image' // 경로를 실제 경로로 변경하세요.
3
+
4
+ export default {
5
+ title: 'OxZoomableImage',
6
+ component: 'ox-zoomable-image',
7
+ argTypes: {
8
+ src: { control: 'text' },
9
+ width: { control: 'number' },
10
+ height: { control: 'number' }
11
+ }
12
+ }
13
+
14
+ interface Story<T> {
15
+ (args: T): TemplateResult
16
+ args?: Partial<T>
17
+ argTypes?: Record<string, unknown>
18
+ }
19
+
20
+ interface OxZoomableImageProps {
21
+ src: string
22
+ width: number
23
+ height: number
24
+ }
25
+
26
+ const Template: Story<OxZoomableImageProps> = ({ src, width, height }: OxZoomableImageProps) => html`
27
+ <style>
28
+ ox-zoomable-image {
29
+ width: ${width}px;
30
+ height: ${height}px;
31
+ background-color: red;
32
+ padding: 0;
33
+ margin: 100px;
34
+ }
35
+ </style>
36
+ <ox-zoomable-image src=${src}></ox-zoomable-image>
37
+ `
38
+
39
+ export const Default = Template.bind({})
40
+ Default.args = {
41
+ src: 'https://via.placeholder.com/800x600.png', // 기본 이미지 URL을 실제 경로로 변경하세요.
42
+ width: 400,
43
+ height: 600
44
+ }
45
+
46
+ export const Wide = Template.bind({})
47
+ Wide.args = {
48
+ src: 'https://via.placeholder.com/800x600.png', // 기본 이미지 URL을 실제 경로로 변경하세요.
49
+ width: 600,
50
+ height: 300
51
+ }
52
+
53
+ export const CustomImage = Template.bind({})
54
+ CustomImage.args = {
55
+ src: 'https://via.placeholder.com/1200x800.png', // 다른 테스트 이미지 URL을 실제 경로로 변경하세요.
56
+ width: 400,
57
+ height: 600
58
+ }
@@ -1,16 +0,0 @@
1
- import { html } from 'lit';
2
- import '../src/ox-mini-map.js';
3
- export default {
4
- title: 'MiniMap',
5
- component: 'mini-map',
6
- argTypes: {}
7
- };
8
- const Template = ({}) => html `
9
- <div id="parent" style="overflow: auto; width: 100%; height: 500px;">
10
- <div id="content" style="width: 1200px; height: 1000px"></div>
11
- <ox-mini-map style="width: 100px; height: 100px; position: absolute; bottom: 10px; right: 10px;"> </ox-mini-map>
12
- </div>
13
- `;
14
- export const Regular = Template.bind({});
15
- Regular.args = {};
16
- //# sourceMappingURL=index.stories.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.stories.js","sourceRoot":"","sources":["../../stories/index.stories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAkB,MAAM,KAAK,CAAA;AAC1C,OAAO,uBAAuB,CAAA;AAE9B,eAAe;IACb,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,UAAU;IACrB,QAAQ,EAAE,EAAE;CACb,CAAA;AAUD,MAAM,QAAQ,GAAoB,CAAC,EAAY,EAAE,EAAE,CAAC,IAAI,CAAA;;;;;CAKvD,CAAA;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxC,OAAO,CAAC,IAAI,GAAG,EAAE,CAAA","sourcesContent":["import { html, TemplateResult } from 'lit'\nimport '../src/ox-mini-map.js'\n\nexport default {\n title: 'MiniMap',\n component: 'mini-map',\n argTypes: {}\n}\n\ninterface Story<T> {\n (args: T): TemplateResult\n args?: Partial<T>\n argTypes?: Record<string, unknown>\n}\n\ninterface ArgTypes {}\n\nconst Template: Story<ArgTypes> = ({}: ArgTypes) => html`\n <div id=\"parent\" style=\"overflow: auto; width: 100%; height: 500px;\">\n <div id=\"content\" style=\"width: 1200px; height: 1000px\"></div>\n <ox-mini-map style=\"width: 100px; height: 100px; position: absolute; bottom: 10px; right: 10px;\"> </ox-mini-map>\n </div>\n`\n\nexport const Regular = Template.bind({})\nRegular.args = {}\n"]}
@@ -1,26 +0,0 @@
1
- import { html, TemplateResult } from 'lit'
2
- import '../src/ox-mini-map.js'
3
-
4
- export default {
5
- title: 'MiniMap',
6
- component: 'mini-map',
7
- argTypes: {}
8
- }
9
-
10
- interface Story<T> {
11
- (args: T): TemplateResult
12
- args?: Partial<T>
13
- argTypes?: Record<string, unknown>
14
- }
15
-
16
- interface ArgTypes {}
17
-
18
- const Template: Story<ArgTypes> = ({}: ArgTypes) => html`
19
- <div id="parent" style="overflow: auto; width: 100%; height: 500px;">
20
- <div id="content" style="width: 1200px; height: 1000px"></div>
21
- <ox-mini-map style="width: 100px; height: 100px; position: absolute; bottom: 10px; right: 10px;"> </ox-mini-map>
22
- </div>
23
- `
24
-
25
- export const Regular = Template.bind({})
26
- Regular.args = {}