|
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