Skip to content

Commit 317ab7d

Browse files
authored
add support for resizing elements (#569)
* Add UI * Implement resizing 🎉 * Improve readability * Fix user-select * Fix formatting (semicolon and whitespace) * Move clamp to utilities folder Co-authored-by: Mayank <[email protected]>
1 parent eb62c95 commit 317ab7d

11 files changed

Lines changed: 297 additions & 24 deletions

File tree

app/components/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { Handles } from './selection/handles.element'
2+
export { Handle } from './selection/handle.element'
23
export { Hover } from './selection/hover.element'
34
export { Label } from './selection/label.element'
45
export { Gridlines } from './selection/gridlines.element'

app/components/selection/corners.element.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Handles } from './handles.element'
2-
import { HandleStyles, CornerStyles } from '../styles.store'
2+
import { HandlesStyles, CornerStyles } from '../styles.store'
33

44
export class Corners extends Handles {
55

66
constructor() {
77
super()
8-
this.styles = [HandleStyles, CornerStyles]
8+
this.styles = [HandlesStyles, CornerStyles]
99
}
1010

1111
render({ width, height, top, left }) {

app/components/selection/grip.element.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Handles } from './handles.element'
2-
import { HandleStyles, GripStyles } from '../styles.store'
2+
import { HandlesStyles, GripStyles } from '../styles.store'
33
import { isFixed } from '../../utilities/';
44

55
export class Grip extends Handles {
66

77
constructor() {
88
super()
9-
this.styles = [HandleStyles, GripStyles]
9+
this.styles = [HandlesStyles, GripStyles]
1010
}
1111

1212
toggleHovering({hovering}) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
@import "../_variables.css";
2+
3+
:host {
4+
display: grid;
5+
grid-area: 1 / -1;
6+
place-self: var(--align-self, center) var(--justify-self, center);
7+
transform: translate(var(--translate-x, 0), var(--translate-y, 0));
8+
}
9+
10+
:host([hidden]) {
11+
display: none;
12+
}
13+
14+
:host > button {
15+
pointer-events: auto;
16+
background-color: white;
17+
border: 1px solid hotpink;
18+
padding: 0;
19+
width: 0.5rem;
20+
height: 0.5rem;
21+
border-radius: 50%;
22+
position: relative;
23+
cursor: var(--cursor);
24+
25+
/* increase tap target size */
26+
&::before {
27+
content: '';
28+
position: absolute;
29+
inset: -0.5rem;
30+
}
31+
}
32+
33+
:host([placement^="top"]) {
34+
--align-self: start;
35+
--translate-y: -50%;
36+
}
37+
38+
:host([placement^="bottom"]) {
39+
--align-self: end;
40+
--translate-y: 50%;
41+
}
42+
43+
:host([placement$="start"]) {
44+
--justify-self: start;
45+
--translate-x: -50%;
46+
}
47+
48+
:host([placement$="end"]) {
49+
--justify-self: end;
50+
--translate-x: 50%;
51+
}
52+
53+
:host([placement^="top"]),
54+
:host([placement^="bottom"]) {
55+
--cursor: ns-resize;
56+
}
57+
58+
:host([placement$="start"]),
59+
:host([placement$="end"]) {
60+
--cursor: ew-resize;
61+
}
62+
63+
:host([placement="top-start"]) {
64+
--cursor: nw-resize;
65+
}
66+
67+
:host([placement="top-end"]) {
68+
--cursor: ne-resize;
69+
}
70+
71+
:host([placement="bottom-start"]) {
72+
--cursor: sw-resize;
73+
}
74+
75+
:host([placement="bottom-end"]) {
76+
--cursor: se-resize;
77+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import $ from 'blingblingjs'
2+
import { HandleStyles } from '../styles.store'
3+
import { clamp } from '../../utilities/numbers'
4+
5+
export class Handle extends HTMLElement {
6+
7+
constructor() {
8+
super()
9+
this.$shadow = this.attachShadow({mode: 'closed'})
10+
this.styles = [HandleStyles]
11+
}
12+
13+
connectedCallback() {
14+
this.$shadow.adoptedStyleSheets = this.styles
15+
this.$shadow.innerHTML = this.render()
16+
17+
this.button = this.$shadow.querySelector('button')
18+
this.button.addEventListener('pointerdown', this.on_element_resize_start.bind(this))
19+
20+
this.placement = this.getAttribute('placement')
21+
}
22+
23+
static get observedAttributes() {
24+
return ['placement']
25+
}
26+
27+
attributeChangedCallback(name, oldValue, newValue) {
28+
if (name === 'placement') {
29+
this.placement = newValue
30+
}
31+
}
32+
33+
on_element_resize_start(e) {
34+
e.preventDefault()
35+
e.stopPropagation()
36+
37+
if (e.button !== 0) return
38+
39+
const placement = this.placement
40+
const handlesEl = e.path.find(el => el.tagName === 'VISBUG-HANDLES')
41+
const nodeLabelId = handlesEl.getAttribute('data-label-id')
42+
const [sourceEl] = $(`[data-label-id="${nodeLabelId}"]`)
43+
44+
if (!sourceEl) return
45+
46+
const { x: initialX, y: initialY } = e
47+
const initialStyle = getComputedStyle(sourceEl)
48+
const initialWidth = parseFloat(initialStyle.width)
49+
const initialHeight = parseFloat(initialStyle.height)
50+
const initialTransform = new DOMMatrix(initialStyle.transform)
51+
52+
const originalElTransition = sourceEl.style.transition
53+
const originalDocumentCursor = document.body.style.cursor
54+
const originalDocumentUserSelect = document.body.style.userSelect
55+
sourceEl.style.transition = 'none'
56+
document.body.style.cursor = getComputedStyle(this).getPropertyValue('--cursor')
57+
document.body.style.userSelect = 'none'
58+
59+
document.addEventListener('pointermove', on_element_resize_move)
60+
61+
function on_element_resize_move(e) {
62+
e.preventDefault()
63+
e.stopPropagation()
64+
65+
const newX = clamp(0, e.clientX, document.documentElement.clientWidth)
66+
const newY = clamp(0, e.clientY, document.documentElement.clientHeight)
67+
68+
const diffX = newX - initialX
69+
const diffY = newY - initialY
70+
71+
switch (placement) {
72+
case 'top-start': {
73+
const newWidth = initialWidth - diffX
74+
const newHeight = initialHeight - diffY
75+
const newTranslate = initialTransform.translate(diffX, diffY).transformPoint()
76+
77+
requestAnimationFrame(() => {
78+
sourceEl.style.width = `${newWidth}px`
79+
sourceEl.style.height = `${newHeight}px`
80+
sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
81+
})
82+
break
83+
}
84+
case 'top-center': {
85+
const newHeight = initialHeight - diffY
86+
const newTranslate = initialTransform.translate(0, diffY).transformPoint()
87+
88+
requestAnimationFrame(() => {
89+
sourceEl.style.height = `${newHeight}px`
90+
sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
91+
})
92+
break
93+
}
94+
case 'top-end': {
95+
const newWidth = initialWidth + diffX
96+
const newHeight = initialHeight - diffY
97+
const newTranslate = initialTransform.translate(0, diffY).transformPoint()
98+
99+
requestAnimationFrame(() => {
100+
sourceEl.style.width = `${newWidth}px`
101+
sourceEl.style.height = `${newHeight}px`
102+
sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
103+
})
104+
break
105+
}
106+
case 'middle-start': {
107+
const newWidth = initialWidth - diffX
108+
const newTranslate = initialTransform.translate(diffX).transformPoint()
109+
110+
requestAnimationFrame(() => {
111+
sourceEl.style.width = `${newWidth}px`
112+
sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
113+
})
114+
break
115+
}
116+
case 'middle-end': {
117+
const newWidth = initialWidth + diffX
118+
119+
requestAnimationFrame(() => {
120+
sourceEl.style.width = `${newWidth}px`
121+
})
122+
break
123+
}
124+
case 'bottom-start': {
125+
const newWidth = initialWidth - diffX
126+
const newHeight = initialHeight + diffY
127+
const newTranslate = initialTransform.translate(diffX, 0).transformPoint()
128+
129+
requestAnimationFrame(() => {
130+
sourceEl.style.width = `${newWidth}px`
131+
sourceEl.style.height = `${newHeight}px`
132+
sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
133+
})
134+
break
135+
}
136+
case 'bottom-center': {
137+
const newHeight = initialHeight + diffY
138+
139+
requestAnimationFrame(() => {
140+
sourceEl.style.height = `${newHeight}px`
141+
})
142+
break
143+
}
144+
case 'bottom-end': {
145+
const newWidth = initialWidth + diffX
146+
const newHeight = initialHeight + diffY
147+
148+
requestAnimationFrame(() => {
149+
sourceEl.style.width = `${newWidth}px`
150+
sourceEl.style.height = `${newHeight}px`
151+
})
152+
break
153+
}
154+
}
155+
}
156+
157+
document.addEventListener('pointerup', on_element_resize_end, { once: true })
158+
document.addEventListener('mouseleave', on_element_resize_end, { once: true })
159+
160+
function on_element_resize_end() {
161+
document.removeEventListener('pointermove', on_element_resize_move)
162+
document.body.style.cursor = originalDocumentCursor
163+
document.body.style.userSelect = originalDocumentUserSelect
164+
sourceEl.style.transition = originalElTransition
165+
}
166+
}
167+
168+
disconnectedCallback() {
169+
this.button.removeEventListener('pointerdown', this.on_element_resize_start.bind(this))
170+
}
171+
172+
render() {
173+
return `
174+
<button type="button" aria-label="Resize"></button>
175+
`
176+
}
177+
}
178+
179+
customElements.define('visbug-handle', Handle)
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
@import "../_variables.css";
22

3-
:host > svg {
3+
:host {
44
position: var(--position, 'absolute');
55
top: var(--top);
66
left: var(--left);
77
overflow: visible;
88
pointer-events: none;
99
z-index: var(--layer-3);
10+
width: var(--width);
11+
height: var(--height);
12+
display: grid;
13+
isolation: isolate;
14+
}
15+
16+
:host > svg {
17+
position: absolute;
1018
}

app/components/selection/handles.element.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import $ from 'blingblingjs'
2-
import { HandleStyles } from '../styles.store'
2+
import { HandlesStyles } from '../styles.store'
33
import { isFixed } from '../../utilities/';
44

55
export class Handles extends HTMLElement {
66

77
constructor() {
88
super()
99
this.$shadow = this.attachShadow({mode: 'closed'})
10-
this.styles = [HandleStyles]
11-
this.on_resize = this.on_resize.bind(this)
10+
this.styles = [HandlesStyles]
11+
this.on_resize = this.on_window_resize.bind(this)
1212
}
1313

1414
connectedCallback() {
1515
this.$shadow.adoptedStyleSheets = this.styles
16-
window.addEventListener('resize', this.on_resize)
16+
window.addEventListener('resize', this.on_window_resize)
1717
}
1818

1919
disconnectedCallback() {
20-
window.removeEventListener('resize', this.on_resize)
20+
window.removeEventListener('resize', this.on_window_resize)
2121
}
2222

23-
on_resize() {
23+
on_window_resize() {
2424
window.requestAnimationFrame(() => {
2525
const node_label_id = this.$shadow.host.getAttribute('data-label-id')
2626
const [source_el] = $(`[data-label-id="${node_label_id}"]`)
@@ -62,6 +62,8 @@ export class Handles extends HTMLElement {
6262
this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
6363
this.style.setProperty('--left', `${left}px`)
6464
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
65+
this.style.setProperty('--width', `${width}px`)
66+
this.style.setProperty('--height', `${height}px`)
6567

6668
return `
6769
<svg
@@ -71,15 +73,15 @@ export class Handles extends HTMLElement {
7173
version="1.1" xmlns="http://www.w3.org/2000/svg"
7274
>
7375
<rect stroke="hotpink" fill="none" width="100%" height="100%"></rect>
74-
<circle stroke="hotpink" fill="white" cx="0" cy="0" r="2"></circle>
75-
<circle stroke="hotpink" fill="white" cx="100%" cy="0" r="2"></circle>
76-
<circle stroke="hotpink" fill="white" cx="100%" cy="100%" r="2"></circle>
77-
<circle stroke="hotpink" fill="white" cx="0" cy="100%" r="2"></circle>
78-
<circle fill="hotpink" cx="${width/2}" cy="0" r="2"></circle>
79-
<circle fill="hotpink" cx="0" cy="${height/2}" r="2"></circle>
80-
<circle fill="hotpink" cx="${width/2}" cy="${height}" r="2"></circle>
81-
<circle fill="hotpink" cx="${width}" cy="${height/2}" r="2"></circle>
8276
</svg>
77+
<visbug-handle placement="top-start"></visbug-handle>
78+
<visbug-handle placement="top-center"></visbug-handle>
79+
<visbug-handle placement="top-end"></visbug-handle>
80+
<visbug-handle placement="middle-start"></visbug-handle>
81+
<visbug-handle placement="middle-end"></visbug-handle>
82+
<visbug-handle placement="bottom-start"></visbug-handle>
83+
<visbug-handle placement="bottom-center"></visbug-handle>
84+
<visbug-handle placement="bottom-end"></visbug-handle>
8385
`
8486
}
8587
}

app/components/selection/hover.element.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Handles } from './handles.element'
2-
import { HandleStyles, HoverStyles } from '../styles.store'
2+
import { HandlesStyles, HoverStyles } from '../styles.store'
33

44
export class Hover extends Handles {
55

66
constructor() {
77
super()
8-
this.styles = [HandleStyles, HoverStyles]
8+
this.styles = [HandlesStyles, HoverStyles]
99
}
1010

1111
render({ width, height, top, left }, node_label_id, isFixed) {

0 commit comments

Comments
 (0)