Skip to content

Commit e2b2e2e

Browse files
authored
Merge pull request #38 from github/shadowdom
Use the Shadow DOM for required HTML and CSS.
2 parents 2d522d7 + 81344c3 commit e2b2e2e

File tree

5 files changed

+111
-99
lines changed

5 files changed

+111
-99
lines changed

examples/index.html

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
<html lang="en">
33
<head>
44
<style>
5-
.wrapper {
6-
margin: 30px auto;
7-
}
85
.loading {
96
background: #f0f0f0;
107
text-align: center;
@@ -13,13 +10,12 @@
1310
</style>
1411
<meta charset="utf-8">
1512
<meta name="viewport" content="width=device-width, initial-scale=1" />
16-
<link href="../src/index.css" rel="stylesheet">
1713
<!-- <script type="module" src="../dist/index.js"></script> -->
1814
<script type="module" src="https://unpkg.com/@github/image-crop-element@latest/dist/index.js"></script>
1915
<title>image-crop-element demo</title>
2016
</head>
2117
<body>
22-
<image-crop src="./ams.jpg" class="wrapper" tabindex="0">
18+
<image-crop src="./ams.jpg" tabindex="0">
2319
<div data-loading-slot class="loading">loading</div>
2420
<input type="text" data-image-crop-input="x" name="x" size="4" aria-label="x">
2521
<input type="text" data-image-crop-input="y" name="y" size="4" aria-label="y">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"clean": "rm -rf dist",
1414
"lint": "eslint src/*.ts",
1515
"prebuild": "npm run clean && npm run lint && mkdir dist",
16-
"build": "tsc && cp src/index.css dist/index.css",
16+
"build": "tsc",
1717
"pretest": "npm run build",
1818
"prepublishOnly": "npm run build",
1919
"test": "karma start test/karma.config.cjs",

readme.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ $ npm install --save @github/image-crop-element
1616
<image-crop src="/avatar.jpg"></image-crop>
1717
```
1818

19+
### Rounded crop area
20+
21+
```html
22+
<image-crop src="/avatar.jpg" rounded></image-crop>
23+
```
24+
1925
### With loading state
2026

2127
```html
2228
<image-crop src="/avatar.jpg">
23-
<img src="spinner.gif" alt="" data-loading-slot>
29+
<img src="spinner.gif" alt="" data-loading-slot />
2430
</image-crop>
2531
```
2632

@@ -49,6 +55,9 @@ document.addEventListener('image-crop-change', function (event){
4955
})
5056
```
5157

58+
## CSS encapsulation
59+
The elements HTML structure is initialized in a [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM), so it is impossible to apply CSS to it. If you need to change the element's default style for any reason, you should open up a new issue (or a pull request!), describing your use case, and we'll work with you on solving the problem.
60+
5261
## Browser support
5362

5463
Browsers without native [custom element support][support] require a [polyfill][]. Legacy browsers require various other polyfills. See [`examples/index.html`][example] for details.

src/index.css

Lines changed: 0 additions & 59 deletions
This file was deleted.

src/index.ts

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,3 @@
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-
171
const startPositions: WeakMap<ImageCropElement, {startX: number; startY: number}> = new WeakMap()
182
const dragStartPositions: WeakMap<ImageCropElement, {dragStartX: number; dragStartY: number}> = new WeakMap()
193
const constructedElements: WeakMap<ImageCropElement, {image: HTMLImageElement; box: HTMLElement}> = new WeakMap()
@@ -77,8 +61,9 @@ function updateCropArea(event: TouchEvent | MouseEvent | KeyboardEvent) {
7761
const target = event.target
7862
if (!(target instanceof HTMLElement)) return
7963

80-
const el = target.closest('image-crop')
64+
const el = getShadowHost(target)
8165
if (!(el instanceof ImageCropElement)) return
66+
8267
const {box} = constructedElements.get(el) || {}
8368
if (!box) return
8469

@@ -107,12 +92,19 @@ function updateCropArea(event: TouchEvent | MouseEvent | KeyboardEvent) {
10792
if (deltaX && deltaY) updateDimensions(el, deltaX, deltaY, !(event instanceof KeyboardEvent))
10893
}
10994

95+
function getShadowHost(el: HTMLElement) {
96+
const rootNode = el.getRootNode()
97+
if (!(rootNode instanceof ShadowRoot)) return el
98+
return rootNode.host
99+
}
100+
110101
function startUpdate(event: TouchEvent | MouseEvent) {
111102
const currentTarget = event.currentTarget
112103
if (!(currentTarget instanceof HTMLElement)) return
113104

114-
const el = currentTarget.closest('image-crop')
105+
const el = getShadowHost(currentTarget)
115106
if (!(el instanceof ImageCropElement)) return
107+
116108
const {box} = constructedElements.get(el) || {}
117109
if (!box) return
118110

@@ -162,17 +154,6 @@ function updateDimensions(target: ImageCropElement, deltaX: number, deltaY: numb
162154
fireChangeEvent(target, {x, y, width: newSide, height: newSide})
163155
}
164156

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-
176157
function setInitialPosition(el: ImageCropElement) {
177158
const {image} = constructedElements.get(el) || {}
178159
if (!image) return
@@ -221,14 +202,99 @@ function fireChangeEvent(target: ImageCropElement, result: Result) {
221202
class ImageCropElement extends HTMLElement {
222203
connectedCallback() {
223204
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]')
226288
if (!(box instanceof HTMLElement)) return
227-
const image = this.querySelector('img')
289+
const image = shadowRoot.querySelector('img')
228290
if (!(image instanceof HTMLImageElement)) return
229291
constructedElements.set(this, {box, image})
230292

231-
image.addEventListener('load', imageReady)
293+
image.addEventListener('load', () => {
294+
this.loaded = true
295+
setInitialPosition(this)
296+
})
297+
232298
this.addEventListener('mouseleave', stopUpdate)
233299
this.addEventListener('touchend', stopUpdate)
234300
this.addEventListener('mouseup', stopUpdate)

0 commit comments

Comments
 (0)