Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 62 additions & 63 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ tmpl.innerHTML = `
</div>
`

const startPositions: WeakMap<ImageCropElement, {startX: number, startY: number}> = new WeakMap()
const dragStartPositions: WeakMap<ImageCropElement, {dragStartX: number, dragStartY: number}> = new WeakMap()
const constructedElements: WeakMap<ImageCropElement, {image: HTMLImageElement, box: HTMLElement}> = new WeakMap()

function moveCropArea(event: MouseEvent | KeyboardEvent) {
const el = event.currentTarget
if (!(el instanceof ImageCropElement)) return
const {box, image} = constructedElements.get(el) || {}

let deltaX = 0
let deltaY = 0
Expand All @@ -32,23 +37,27 @@ function moveCropArea(event: MouseEvent | KeyboardEvent) {
} else if (event.key === 'ArrowRight') {
deltaX = 1
}
} else if (el.dragStartX && el.dragStartY && event instanceof MouseEvent) {
deltaX = event.pageX - el.dragStartX
deltaY = event.pageY - el.dragStartY
} else if (dragStartPositions.has(el) && event instanceof MouseEvent) {
const pos = dragStartPositions.get(el)
if (!pos) return
deltaX = event.pageX - pos.dragStartX
deltaY = event.pageY - pos.dragStartY
}

if (deltaX !== 0 || deltaY !== 0) {
const x = Math.min(Math.max(0, el.box.offsetLeft + deltaX), el.image.width - el.box.offsetWidth)
const y = Math.min(Math.max(0, el.box.offsetTop + deltaY), el.image.height - el.box.offsetHeight)
el.box.style.left = `${x}px`
el.box.style.top = `${y}px`
const x = Math.min(Math.max(0, box.offsetLeft + deltaX), image.width - box.offsetWidth)
const y = Math.min(Math.max(0, box.offsetTop + deltaY), image.height - box.offsetHeight)
box.style.left = `${x}px`
box.style.top = `${y}px`

fireChangeEvent(el, {x, y, width: el.box.offsetWidth, height: el.box.offsetHeight})
fireChangeEvent(el, {x, y, width: box.offsetWidth, height: box.offsetHeight})
}

if (event instanceof MouseEvent) {
el.dragStartX = event.pageX
el.dragStartY = event.pageY
dragStartPositions.set(el, {
dragStartX: event.pageX,
dragStartY: event.pageY
})
}
}

Expand All @@ -58,6 +67,7 @@ function updateCropArea(event: MouseEvent | KeyboardEvent) {

const el = target.closest('image-crop')
if (!(el instanceof ImageCropElement)) return
const {box} = constructedElements.get(el) || {}

const rect = el.getBoundingClientRect()
let deltaX, deltaY, delta
Expand All @@ -66,13 +76,14 @@ function updateCropArea(event: MouseEvent | KeyboardEvent) {
if (event.key === '-') delta = -10
if (event.key === '=') delta = +10
if (!delta) return
deltaX = el.box.offsetWidth + delta
deltaY = el.box.offsetHeight + delta
el.startX = el.box.offsetLeft
el.startY = el.box.offsetTop
deltaX = box.offsetWidth + delta
deltaY = box.offsetHeight + delta
startPositions.set(el, {startX: box.offsetLeft, startY: box.offsetTop})
} else if (event instanceof MouseEvent) {
deltaX = event.pageX - el.startX - rect.left - window.pageXOffset
deltaY = event.pageY - el.startY - rect.top - window.pageYOffset
const pos = startPositions.get(el)
if (!pos) return
deltaX = event.pageX - pos.startX - rect.left - window.pageXOffset
deltaY = event.pageY - pos.startY - rect.top - window.pageYOffset
}

if (deltaX && deltaY) updateDimensions(el, deltaX, deltaY, !(event instanceof KeyboardEvent))
Expand All @@ -84,6 +95,7 @@ function startUpdate(event: MouseEvent) {

const el = currentTarget.closest('image-crop')
if (!(el instanceof ImageCropElement)) return
const {box} = constructedElements.get(el) || {}

const target = event.target
if (!(target instanceof HTMLElement)) return
Expand All @@ -94,8 +106,10 @@ function startUpdate(event: MouseEvent) {
el.addEventListener('mousemove', updateCropArea)
if (['nw', 'se'].indexOf(direction) >= 0) el.classList.add('nwse')
if (['ne', 'sw'].indexOf(direction) >= 0) el.classList.add('nesw')
el.startX = el.box.offsetLeft + (['se', 'ne'].indexOf(direction) >= 0 ? 0 : el.box.offsetWidth)
el.startY = el.box.offsetTop + (['se', 'sw'].indexOf(direction) >= 0 ? 0 : el.box.offsetHeight)
startPositions.set(el, {
startX: box.offsetLeft + (['se', 'ne'].indexOf(direction) >= 0 ? 0 : box.offsetWidth),
startY: box.offsetTop + (['se', 'sw'].indexOf(direction) >= 0 ? 0 : box.offsetHeight)
})
updateCropArea(event)
} else {
// Move crop area
Expand All @@ -104,25 +118,24 @@ function startUpdate(event: MouseEvent) {
}

function updateDimensions(target, deltaX, deltaY, reposition = true) {
let newSide = Math.max(Math.abs(deltaX), Math.abs(deltaY), target.minWidth)
let newSide = Math.max(Math.abs(deltaX), Math.abs(deltaY), 10)
const pos = startPositions.get(target)
if (!pos) return
const {box, image} = constructedElements.get(target) || {}
newSide = Math.min(
newSide,
deltaY > 0 ? target.image.height - target.startY : target.startY,
deltaX > 0 ? target.image.width - target.startX : target.startX
deltaY > 0 ? image.height - pos.startY : pos.startY,
deltaX > 0 ? image.width - pos.startX : pos.startX
)

const x = reposition
? Math.round(Math.max(0, deltaX > 0 ? target.startX : target.startX - newSide))
: target.box.offsetLeft
const y = reposition
? Math.round(Math.max(0, deltaY > 0 ? target.startY : target.startY - newSide))
: target.box.offsetTop
const x = reposition ? Math.round(Math.max(0, deltaX > 0 ? pos.startX : pos.startX - newSide)) : box.offsetLeft
const y = reposition ? Math.round(Math.max(0, deltaY > 0 ? pos.startY : pos.startY - newSide)) : box.offsetTop

target.box.style.left = `${x}px`
target.box.style.top = `${y}px`
box.style.left = `${x}px`
box.style.top = `${y}px`

target.box.style.width = `${newSide}px`
target.box.style.height = `${newSide}px`
box.style.width = `${newSide}px`
box.style.height = `${newSide}px`
fireChangeEvent(target, {x, y, width: newSide, height: newSide})
}

Expand All @@ -138,25 +151,28 @@ function imageReady(event: Event) {
}

function setInitialPosition(el) {
const image = el.image
const {image} = constructedElements.get(el) || {}
const side = Math.round(image.clientWidth > image.clientHeight ? image.clientHeight : image.clientWidth)
el.startX = (image.clientWidth - side) / 2
el.startY = (image.clientHeight - side) / 2
startPositions.set(el, {
startX: (image.clientWidth - side) / 2,
startY: (image.clientHeight - side) / 2
})
updateDimensions(el, side, side)
}

function stopUpdate(event: MouseEvent) {
const el = event.currentTarget
if (!(el instanceof ImageCropElement)) return

el.dragStartX = el.dragStartY = null
dragStartPositions.delete(el)
el.classList.remove('nwse', 'nesw')
el.removeEventListener('mousemove', updateCropArea)
el.removeEventListener('mousemove', moveCropArea)
}

function fireChangeEvent(target: ImageCropElement, result: {x: number, y: number, width: number, height: number}) {
const ratio = target.image.naturalWidth / target.image.width
const {image} = constructedElements.get(target) || {}
const ratio = image.naturalWidth / image.width
for (const key in result) {
const value = Math.round(result[key] * ratio)
result[key] = value
Expand All @@ -167,42 +183,24 @@ function fireChangeEvent(target: ImageCropElement, result: {x: number, y: number
target.dispatchEvent(new CustomEvent('image-crop-change', {bubbles: true, detail: result}))
}

export class ImageCropElement extends HTMLElement {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the a reason the class is no longer exported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's exported as default at the end of this file so I removed this named export.

image: HTMLImageElement
box: HTMLElement
constructed: boolean
minWidth: number
dragStartX: ?number
dragStartY: ?number
startX: number
startY: number

constructor() {
super()
this.minWidth = 10
}

class ImageCropElement extends HTMLElement {
connectedCallback() {
if (this.constructed) return
this.constructed = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@muan I just removed this, not sure what this was doing 😬

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was preventing the template from being appended every time connectedCallback is triggered since we can't add nodes to the element in constructor.

This can potentially check for existence of .crop-wrapper within the node instead. However we should probably also convert these classes to data attributes and update the CSS accordingly since these classes could conflict with the host app.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was preventing the template from being appended every time connectedCallback is triggered since we can't add nodes to the element in constructor.

Cool! I just pushed a commit that checks a WeakMap for the element and returns if it has added the template already.

This can potentially check for existence of .crop-wrapper within the node instead. However we should probably also convert these classes to data attributes and update the CSS accordingly since these classes could conflict with the host app.

Would it be cool if I did this in a follow up?


if (constructedElements.has(this)) return
this.appendChild(document.importNode(tmpl.content, true))
const image = this.querySelector('img')
if (!(image instanceof HTMLImageElement)) return
this.image = image

const box = this.querySelector('[data-crop-box]')
if (!(box instanceof HTMLElement)) return
this.box = box
const image = this.querySelector('img')
if (!(image instanceof HTMLImageElement)) return
constructedElements.set(this, {box, image})

this.image.addEventListener('load', imageReady)
image.addEventListener('load', imageReady)
this.addEventListener('mouseleave', stopUpdate)
this.addEventListener('mouseup', stopUpdate)
this.box.addEventListener('mousedown', startUpdate)
box.addEventListener('mousedown', startUpdate)
this.addEventListener('keydown', moveCropArea)
this.addEventListener('keydown', updateCropArea)

if (this.src) this.image.src = this.src
if (this.src) image.src = this.src
}

static get observedAttributes() {
Expand Down Expand Up @@ -234,9 +232,10 @@ export class ImageCropElement extends HTMLElement {
}

attributeChangedCallback(attribute: string, oldValue: string, newValue: string) {
const {image} = constructedElements.get(this) || {}
if (attribute === 'src') {
this.loaded = false
if (this.image) this.image.src = newValue
if (image) image.src = newValue
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions index.js.flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* @flow strict */

declare module '@github/image-crop-element' {
declare export default class ImageCropElement extends HTMLElement {
get src(): ?string;
set src(val: ?string): void;
get loaded(): boolean;
set loaded(val: boolean): void;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"prebuild": "npm run clean && npm run lint && mkdir dist",
"build-umd": "BABEL_ENV=umd babel index.js -o dist/index.umd.js",
"build-esm": "BABEL_ENV=esm babel index.js -o dist/index.esm.js",
"build": "npm run build-umd && npm run build-esm",
"build": "npm run build-umd && npm run build-esm && cp index.js.flow dist/index.umd.js.flow && cp index.js.flow dist/index.esm.js.flow",
"pretest": "npm run build",
"prepublishOnly": "npm run build",
"test": "karma start test/karma.config.js"
Expand Down