|
1 |
| -const tmpl = document.createElement('template') |
2 |
| -tmpl.innerHTML = ` |
3 |
| - <div class="crop-wrapper"> |
4 |
| - <img width="100%" class="crop-image" alt=""> |
5 |
| - <div class="crop-container"> |
6 |
| - <div data-crop-box class="crop-box"> |
7 |
| - <div class="crop-outline"></div> |
8 |
| - <div data-direction="nw" class="handle nw"></div> |
9 |
| - <div data-direction="ne" class="handle ne"></div> |
10 |
| - <div data-direction="sw" class="handle sw"></div> |
11 |
| - <div data-direction="se" class="handle se"></div> |
12 |
| - </div> |
13 |
| - </div> |
14 |
| - </div> |
15 |
| -` |
16 |
| - |
17 | 1 | const startPositions: WeakMap<ImageCropElement, {startX: number; startY: number}> = new WeakMap()
|
18 | 2 | const dragStartPositions: WeakMap<ImageCropElement, {dragStartX: number; dragStartY: number}> = new WeakMap()
|
19 | 3 | const constructedElements: WeakMap<ImageCropElement, {image: HTMLImageElement; box: HTMLElement}> = new WeakMap()
|
@@ -77,8 +61,9 @@ function updateCropArea(event: TouchEvent | MouseEvent | KeyboardEvent) {
|
77 | 61 | const target = event.target
|
78 | 62 | if (!(target instanceof HTMLElement)) return
|
79 | 63 |
|
80 |
| - const el = target.closest('image-crop') |
| 64 | + const el = getShadowHost(target) |
81 | 65 | if (!(el instanceof ImageCropElement)) return
|
| 66 | + |
82 | 67 | const {box} = constructedElements.get(el) || {}
|
83 | 68 | if (!box) return
|
84 | 69 |
|
@@ -107,12 +92,19 @@ function updateCropArea(event: TouchEvent | MouseEvent | KeyboardEvent) {
|
107 | 92 | if (deltaX && deltaY) updateDimensions(el, deltaX, deltaY, !(event instanceof KeyboardEvent))
|
108 | 93 | }
|
109 | 94 |
|
| 95 | +function getShadowHost(el: HTMLElement) { |
| 96 | + const rootNode = el.getRootNode() |
| 97 | + if (!(rootNode instanceof ShadowRoot)) return el |
| 98 | + return rootNode.host |
| 99 | +} |
| 100 | + |
110 | 101 | function startUpdate(event: TouchEvent | MouseEvent) {
|
111 | 102 | const currentTarget = event.currentTarget
|
112 | 103 | if (!(currentTarget instanceof HTMLElement)) return
|
113 | 104 |
|
114 |
| - const el = currentTarget.closest('image-crop') |
| 105 | + const el = getShadowHost(currentTarget) |
115 | 106 | if (!(el instanceof ImageCropElement)) return
|
| 107 | + |
116 | 108 | const {box} = constructedElements.get(el) || {}
|
117 | 109 | if (!box) return
|
118 | 110 |
|
@@ -162,17 +154,6 @@ function updateDimensions(target: ImageCropElement, deltaX: number, deltaY: numb
|
162 | 154 | fireChangeEvent(target, {x, y, width: newSide, height: newSide})
|
163 | 155 | }
|
164 | 156 |
|
165 |
| -function imageReady(event: Event) { |
166 |
| - const currentTarget = event.currentTarget |
167 |
| - if (!(currentTarget instanceof HTMLElement)) return |
168 |
| - |
169 |
| - const el = currentTarget.closest('image-crop') |
170 |
| - if (!(el instanceof ImageCropElement)) return |
171 |
| - |
172 |
| - el.loaded = true |
173 |
| - setInitialPosition(el) |
174 |
| -} |
175 |
| - |
176 | 157 | function setInitialPosition(el: ImageCropElement) {
|
177 | 158 | const {image} = constructedElements.get(el) || {}
|
178 | 159 | if (!image) return
|
@@ -221,14 +202,99 @@ function fireChangeEvent(target: ImageCropElement, result: Result) {
|
221 | 202 | class ImageCropElement extends HTMLElement {
|
222 | 203 | connectedCallback() {
|
223 | 204 | if (constructedElements.has(this)) return
|
224 |
| - this.appendChild(document.importNode(tmpl.content, true)) |
225 |
| - const box = this.querySelector('[data-crop-box]') |
| 205 | + |
| 206 | + const shadowRoot = this.attachShadow({mode: 'open'}) |
| 207 | + shadowRoot.innerHTML = ` |
| 208 | +<style> |
| 209 | + :host { touch-action: none; display: block; } |
| 210 | + :host(.nesw) { cursor: nesw-resize; } |
| 211 | + :host(.nwse) { cursor: nwse-resize; } |
| 212 | + :host(.nesw) .crop-box, :host(.nwse) .crop-box { cursor: inherit; } |
| 213 | + :host([loaded]) .crop-image { display: block; } |
| 214 | + :host([loaded]) ::slotted([data-loading-slot]), .crop-image { display: none; } |
| 215 | +
|
| 216 | + .crop-wrapper { |
| 217 | + position: relative; |
| 218 | + font-size: 0; |
| 219 | + } |
| 220 | + .crop-container { |
| 221 | + user-select: none; |
| 222 | + -ms-user-select: none; |
| 223 | + -moz-user-select: none; |
| 224 | + -webkit-user-select: none; |
| 225 | + position: absolute; |
| 226 | + overflow: hidden; |
| 227 | + z-index: 1; |
| 228 | + top: 0; |
| 229 | + width: 100%; |
| 230 | + height: 100%; |
| 231 | + } |
| 232 | +
|
| 233 | + :host([rounded]) .crop-box { |
| 234 | + border-radius: 50%; |
| 235 | + box-shadow: 0 0 0 4000px rgba(0, 0, 0, 0.3); |
| 236 | + } |
| 237 | + .crop-box { |
| 238 | + position: absolute; |
| 239 | + border: 1px dashed #fff; |
| 240 | + box-sizing: border-box; |
| 241 | + cursor: move; |
| 242 | + } |
| 243 | +
|
| 244 | + :host([rounded]) .crop-outline { |
| 245 | + outline: none; |
| 246 | + } |
| 247 | + .crop-outline { |
| 248 | + position: absolute; |
| 249 | + top: 0; |
| 250 | + bottom: 0; |
| 251 | + left: 0; |
| 252 | + right: 0; |
| 253 | + outline: 4000px solid rgba(0, 0, 0, .3); |
| 254 | + } |
| 255 | +
|
| 256 | + .handle { position: absolute; } |
| 257 | + :host([rounded]) .handle::before { border-radius: 50%; } |
| 258 | + .handle:before { |
| 259 | + position: absolute; |
| 260 | + display: block; |
| 261 | + padding: 4px; |
| 262 | + transform: translate(-50%, -50%); |
| 263 | + content: ' '; |
| 264 | + background: #fff; |
| 265 | + border: 1px solid #767676; |
| 266 | + } |
| 267 | + .ne { top: 0; right: 0; cursor: nesw-resize; } |
| 268 | + .nw { top: 0; left: 0; cursor: nwse-resize; } |
| 269 | + .se { bottom: 0; right: 0; cursor: nwse-resize; } |
| 270 | + .sw { bottom: 0; left: 0; cursor: nesw-resize; } |
| 271 | +</style> |
| 272 | +<slot></slot> |
| 273 | +<div class="crop-wrapper"> |
| 274 | + <img width="100%" class="crop-image" alt=""> |
| 275 | + <div class="crop-container"> |
| 276 | + <div data-crop-box class="crop-box"> |
| 277 | + <div class="crop-outline"></div> |
| 278 | + <div data-direction="nw" class="handle nw"></div> |
| 279 | + <div data-direction="ne" class="handle ne"></div> |
| 280 | + <div data-direction="sw" class="handle sw"></div> |
| 281 | + <div data-direction="se" class="handle se"></div> |
| 282 | + </div> |
| 283 | + </div> |
| 284 | +</div> |
| 285 | +` |
| 286 | + |
| 287 | + const box = shadowRoot.querySelector('[data-crop-box]') |
226 | 288 | if (!(box instanceof HTMLElement)) return
|
227 |
| - const image = this.querySelector('img') |
| 289 | + const image = shadowRoot.querySelector('img') |
228 | 290 | if (!(image instanceof HTMLImageElement)) return
|
229 | 291 | constructedElements.set(this, {box, image})
|
230 | 292 |
|
231 |
| - image.addEventListener('load', imageReady) |
| 293 | + image.addEventListener('load', () => { |
| 294 | + this.loaded = true |
| 295 | + setInitialPosition(this) |
| 296 | + }) |
| 297 | + |
232 | 298 | this.addEventListener('mouseleave', stopUpdate)
|
233 | 299 | this.addEventListener('touchend', stopUpdate)
|
234 | 300 | this.addEventListener('mouseup', stopUpdate)
|
|
0 commit comments