Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
1 change: 1 addition & 0 deletions app/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { Overlay } from './selection/overlay.element'
export { BoxModel } from './selection/box-model.element'
export { Corners } from './selection/corners.element'
export { Grip } from './selection/grip.element'
export { Rotation } from './selection/rotation.element'

export { Metatip } from './metatip/metatip.element'
export { Ally } from './metatip/ally.element'
Expand Down
34 changes: 34 additions & 0 deletions app/components/selection/delete.element.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import "../_variables.css";

:host {
position: var(--position, absolute);
top: var(--top, 0);
left: var(--left, 0);
z-index: var(--layer-top);
}

:host button {
background: var(--neon-pink);
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
padding: 0;
transition: transform 0.2s ease;
}

:host button:hover {
transform: scale(1.1);
}

:host svg {
width: 16px;
height: 16px;
fill: currentColor;
}
69 changes: 69 additions & 0 deletions app/components/selection/delete.element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { DeleteStyles } from '../styles.store'

export class Delete extends HTMLElement {
constructor() {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
this.styles = [DeleteStyles]
this.observers = []
}

addObservers(observers) {
this.observers = observers
}

set linkedElementsToDeleteToo(elements) {
this._linkedElementsToDeleteToo = elements
}

connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
this.$shadow.innerHTML = this.render()

const deleteBtn = this.$shadow.querySelector('button')
deleteBtn.addEventListener('click', this.deleteElement.bind(this))
}

set position({el}) {
this.targetElement = el
const {top, right} = el.getBoundingClientRect()
const isFixed = getComputedStyle(el).position === 'fixed'

this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
this.style.setProperty('--left', `${right + 10}px`)
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
}

deleteElement(e) {
e.preventDefault()
e.stopPropagation()

if (!this.targetElement) {
console.warn('No target element found to delete')
return
}

this.observers.forEach(observer => observer.disconnect())
this._linkedElementsToDeleteToo?.forEach(el => el.remove())
this.targetElement.remove()

const labelId = this.targetElement.getAttribute('data-label-id')
if (labelId) {
document.querySelectorAll(`[data-label-id="${labelId}"]`).forEach(el => el.remove())
}

this.remove()
}

render() {
return `
<button type="button" aria-label="Delete element">
<svg viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
`
}
}

customElements.define('visbug-delete', Delete)
56 changes: 56 additions & 0 deletions app/components/selection/rotation.element.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@import "../_variables.css";

:host {
position: var(--position);
top: var(--top);
left: var(--left);
pointer-events: none;
z-index: var(--layer-3);
}

:host .rotation-line {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}

:host .rotation-line.active {
display: block;
}

:host .rotation-line line {
stroke: var(--neon-pink);
stroke-width: 1;
}

:host .rotation-handle {
position: absolute;
width: 24px;
height: 24px;
top: -30px;
left: calc(var(--width) / 2 - 12px);
cursor: grab;
pointer-events: all;
background: var(--neon-pink);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
-webkit-user-select: none;
}

:host .rotation-handle:active {
cursor: grabbing;
}

:host .rotation-icon {
width: 16px;
height: 16px;
fill: white;
pointer-events: none;
}
128 changes: 128 additions & 0 deletions app/components/selection/rotation.element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { HandlesStyles, RotationStyles } from '../styles.store'

export class Rotation extends HTMLElement {
constructor() {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
this.styles = [HandlesStyles, RotationStyles]
this.totalAngle = 0
}

connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
}

set position({el}) {
this.targetElement = el
const {left, top, width, height} = el.getBoundingClientRect()
const isFixed = getComputedStyle(el).position === 'fixed'

this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
this.style.setProperty('--left', `${left}px`)
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
this.style.setProperty('--width', `${width}px`)

this.$shadow.innerHTML = this.render()
this.setupRotationHandlers()
}

setupRotationHandlers() {
const handle = this.$shadow.querySelector('.rotation-handle')
const line = this.$shadow.querySelector('.rotation-line')

const onMouseDown = e => {
e.preventDefault()
const {left, top, width, height} = this.targetElement.getBoundingClientRect()
this.originalCenter = {
x: left + width / 2,
y: top + height / 2
}
this.lastAngle = Math.atan2(
e.clientY - this.originalCenter.y,
e.clientX - this.originalCenter.x
)

this.handleRadius = Math.sqrt(
Math.pow(e.clientX - this.originalCenter.x, 2) +
Math.pow(e.clientY - this.originalCenter.y, 2)
)

line.classList.add('active')

document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}

const onMouseMove = e => {
const currentAngle = Math.atan2(
e.clientY - this.originalCenter.y,
e.clientX - this.originalCenter.x
)

// track accumulated rotation to allow multiple revolutions
if (!this.lastAngle) this.lastAngle = currentAngle
let delta = currentAngle - this.lastAngle
// normalize the delta to avoid "flipping" at boundary crossing
if (delta > Math.PI) {
delta -= 2 * Math.PI
} else if (delta < -Math.PI) {
delta += 2 * Math.PI
}

this.totalAngle += delta
this.lastAngle = currentAngle

const rotationDegrees = this.totalAngle * (180 / Math.PI)
this.targetElement.style.transform = `rotate(${rotationDegrees}deg)`

const handleX = e.clientX
const handleY = e.clientY

const hostRect = this.getBoundingClientRect()
const handleRect = handle.getBoundingClientRect()
const handleSize = handleRect.width

// position handle centered on cursor
handle.style.left = `${handleX - hostRect.left - handleSize/2}px`
handle.style.top = `${handleY - hostRect.top - handleSize/2}px`

const lineSvg = line.querySelector('line')
lineSvg.setAttribute('x1', this.originalCenter.x)
lineSvg.setAttribute('y1', this.originalCenter.y)
lineSvg.setAttribute('x2', handleX)
lineSvg.setAttribute('y2', handleY)
}
Comment on lines +66 to +98
Copy link
Contributor

Choose a reason for hiding this comment

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

love this so much

Copy link
Contributor Author

Choose a reason for hiding this comment

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

human + ai teamwork for the win! i remember cleaning up the comments and code and making suggestions and asking for eli5 explanations of why this weird math works haha


const onMouseUp = () => {
this.lastAngle = 0
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
line.classList.remove('active')
}

handle.addEventListener('mousedown', onMouseDown)

this.cleanup = () => {
handle.removeEventListener('mousedown', onMouseDown)
}
}

disconnectedCallback() {
this.cleanup && this.cleanup()
}

render() {
return `
<svg class="rotation-line">
<line/>
</svg>
<div class="rotation-handle">
<svg class="rotation-icon" viewBox="0 0 24 24">
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
</svg>
</div>
`
}
}

customElements.define('visbug-rotation', Rotation)
4 changes: 4 additions & 0 deletions app/components/styles.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { default as boxmodel_css } from './selection/box-model.element.css'
import { default as metatip_css } from './metatip/metatip.element.css'
import { default as hotkeymap_css } from './hotkey-map/base.element.css'
import { default as grip_css } from './selection/grip.element.css'
import { default as rotation_css } from './selection/rotation.element.css'
import { default as delete_css } from './selection/delete.element.css'

import { default as light_css } from './_variables_light.css'
import { default as visbug_light_css } from './vis-bug/vis-bug.element_light.css'
Expand Down Expand Up @@ -44,6 +46,8 @@ export const OverlayStyles = constructStylesheet(overlay_css)
export const BoxModelStyles = constructStylesheet(boxmodel_css)
export const HotkeymapStyles = constructStylesheet(hotkeymap_css)
export const GripStyles = constructStylesheet(grip_css)
export const RotationStyles = constructStylesheet(rotation_css)
export const DeleteStyles = constructStylesheet(delete_css)

export const LightTheme = constructStylesheet(light_css)
export const VisBugLightStyles = constructStylesheet(visbug_light_css)
Expand Down
2 changes: 1 addition & 1 deletion app/components/vis-bug/vis-bug.element.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import hotkeys from 'hotkeys-js'

import {
Handles, Handle, Label, Overlay, Gridlines, Corners,
Hotkeys, Metatip, Ally, Distance, BoxModel, Grip
Hotkeys, Metatip, Ally, Distance, BoxModel, Grip, Rotation
} from '../'

import {
Expand Down
25 changes: 21 additions & 4 deletions app/features/selectable.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
getTextShadowValues, isFixed, onRemove
} from '../utilities/'

import '../components/selection/delete.element.js'

export function Selectable(visbug) {
const page = document.body
let selected = []
Expand Down Expand Up @@ -427,7 +429,7 @@ export function Selectable(visbug) {
if (tool === 'guides') {
handles.forEach(handle => {
handle.hidePopover && handle.hidePopover()
handle.showPopover && handle.showPopover()
if (handle.isConnected && handle.showPopover) handle.showPopover()
})
}
}
Expand All @@ -454,7 +456,7 @@ export function Selectable(visbug) {

$('visbug-metatip, visbug-ally').forEach(tip => {
tip.hidePopover && tip.hidePopover()
tip.showPopover && tip.showPopover()
if (tip.isConnected && tip.showPopover) tip.showPopover()
})

selected.unshift(el)
Expand Down Expand Up @@ -483,6 +485,8 @@ export function Selectable(visbug) {
...$('visbug-label'),
...$('visbug-hover'),
...$('visbug-distance'),
...$('visbug-delete'),
...$('visbug-rotation'),
]).forEach(el =>
el.remove())

Expand Down Expand Up @@ -576,11 +580,24 @@ export function Selectable(visbug) {
template: handleLabelText(el, visbug.activeTool)
})

const deleteBtn = document.createElement('visbug-delete')
const rotationBtn = document.createElement('visbug-rotation')
rotationBtn.position = {el}
rotationBtn.setAttribute('data-label-id', id)

deleteBtn.position = {el}
deleteBtn.linkedElementsToDeleteToo = [rotationBtn]

let observer = createObserver(el, {handle,label})
let parentObserver = createObserver(el, {handle,label})

observer.observe(el, { attributes: true })
parentObserver.observe(el.parentNode, { childList:true, subtree:true })
if (el) observer.observe(el, { attributes: true })
if (el.parentNode) parentObserver.observe(el.parentNode, { childList:true, subtree:true })

deleteBtn.addObservers([observer, parentObserver])

document.body.appendChild(deleteBtn)
document.body.appendChild(rotationBtn)

if (label !== null) {
onRemove(label, () => {
Expand Down
2 changes: 2 additions & 0 deletions app/utilities/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export const isOffBounds = node =>
|| node.closest('visbug-corners')
|| node.closest('visbug-grip')
|| node.closest('visbug-gridlines')
|| node.closest('visbug-rotation')
|| node.closest('visbug-delete')
)

export const isSelectorValid = (qs => (
Expand Down
Loading