diff --git a/app/components/index.js b/app/components/index.js
index f63833ea..f6aeb776 100644
--- a/app/components/index.js
+++ b/app/components/index.js
@@ -1,4 +1,5 @@
export { Handles } from './selection/handles.element'
+export { Handle } from './selection/handle.element'
export { Hover } from './selection/hover.element'
export { Label } from './selection/label.element'
export { Gridlines } from './selection/gridlines.element'
diff --git a/app/components/selection/corners.element.js b/app/components/selection/corners.element.js
index 30fbe4b3..cc6556ef 100644
--- a/app/components/selection/corners.element.js
+++ b/app/components/selection/corners.element.js
@@ -1,11 +1,11 @@
import { Handles } from './handles.element'
-import { HandleStyles, CornerStyles } from '../styles.store'
+import { HandlesStyles, CornerStyles } from '../styles.store'
export class Corners extends Handles {
constructor() {
super()
- this.styles = [HandleStyles, CornerStyles]
+ this.styles = [HandlesStyles, CornerStyles]
}
render({ width, height, top, left }) {
diff --git a/app/components/selection/grip.element.js b/app/components/selection/grip.element.js
index aed5a0f1..c68b4320 100644
--- a/app/components/selection/grip.element.js
+++ b/app/components/selection/grip.element.js
@@ -1,12 +1,12 @@
import { Handles } from './handles.element'
-import { HandleStyles, GripStyles } from '../styles.store'
+import { HandlesStyles, GripStyles } from '../styles.store'
import { isFixed } from '../../utilities/';
export class Grip extends Handles {
constructor() {
super()
- this.styles = [HandleStyles, GripStyles]
+ this.styles = [HandlesStyles, GripStyles]
}
toggleHovering({hovering}) {
diff --git a/app/components/selection/handle.element.css b/app/components/selection/handle.element.css
new file mode 100644
index 00000000..0e3a9679
--- /dev/null
+++ b/app/components/selection/handle.element.css
@@ -0,0 +1,77 @@
+@import "../_variables.css";
+
+:host {
+ display: grid;
+ grid-area: 1 / -1;
+ place-self: var(--align-self, center) var(--justify-self, center);
+ transform: translate(var(--translate-x, 0), var(--translate-y, 0));
+}
+
+:host([hidden]) {
+ display: none;
+}
+
+:host > button {
+ pointer-events: auto;
+ background-color: white;
+ border: 1px solid hotpink;
+ padding: 0;
+ width: 0.5rem;
+ height: 0.5rem;
+ border-radius: 50%;
+ position: relative;
+ cursor: var(--cursor);
+
+ /* increase tap target size */
+ &::before {
+ content: '';
+ position: absolute;
+ inset: -0.5rem;
+ }
+}
+
+:host([placement^="top"]) {
+ --align-self: start;
+ --translate-y: -50%;
+}
+
+:host([placement^="bottom"]) {
+ --align-self: end;
+ --translate-y: 50%;
+}
+
+:host([placement$="start"]) {
+ --justify-self: start;
+ --translate-x: -50%;
+}
+
+:host([placement$="end"]) {
+ --justify-self: end;
+ --translate-x: 50%;
+}
+
+:host([placement^="top"]),
+:host([placement^="bottom"]) {
+ --cursor: ns-resize;
+}
+
+:host([placement$="start"]),
+:host([placement$="end"]) {
+ --cursor: ew-resize;
+}
+
+:host([placement="top-start"]) {
+ --cursor: nw-resize;
+}
+
+:host([placement="top-end"]) {
+ --cursor: ne-resize;
+}
+
+:host([placement="bottom-start"]) {
+ --cursor: sw-resize;
+}
+
+:host([placement="bottom-end"]) {
+ --cursor: se-resize;
+}
diff --git a/app/components/selection/handle.element.js b/app/components/selection/handle.element.js
new file mode 100644
index 00000000..dc399ba1
--- /dev/null
+++ b/app/components/selection/handle.element.js
@@ -0,0 +1,179 @@
+import $ from 'blingblingjs'
+import { HandleStyles } from '../styles.store'
+import { clamp } from '../../utilities/numbers'
+
+export class Handle extends HTMLElement {
+
+ constructor() {
+ super()
+ this.$shadow = this.attachShadow({mode: 'closed'})
+ this.styles = [HandleStyles]
+ }
+
+ connectedCallback() {
+ this.$shadow.adoptedStyleSheets = this.styles
+ this.$shadow.innerHTML = this.render()
+
+ this.button = this.$shadow.querySelector('button')
+ this.button.addEventListener('pointerdown', this.on_element_resize_start.bind(this))
+
+ this.placement = this.getAttribute('placement')
+ }
+
+ static get observedAttributes() {
+ return ['placement']
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (name === 'placement') {
+ this.placement = newValue
+ }
+ }
+
+ on_element_resize_start(e) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ if (e.button !== 0) return
+
+ const placement = this.placement
+ const handlesEl = e.path.find(el => el.tagName === 'VISBUG-HANDLES')
+ const nodeLabelId = handlesEl.getAttribute('data-label-id')
+ const [sourceEl] = $(`[data-label-id="${nodeLabelId}"]`)
+
+ if (!sourceEl) return
+
+ const { x: initialX, y: initialY } = e
+ const initialStyle = getComputedStyle(sourceEl)
+ const initialWidth = parseFloat(initialStyle.width)
+ const initialHeight = parseFloat(initialStyle.height)
+ const initialTransform = new DOMMatrix(initialStyle.transform)
+
+ const originalElTransition = sourceEl.style.transition
+ const originalDocumentCursor = document.body.style.cursor
+ const originalDocumentUserSelect = document.body.style.userSelect
+ sourceEl.style.transition = 'none'
+ document.body.style.cursor = getComputedStyle(this).getPropertyValue('--cursor')
+ document.body.style.userSelect = 'none'
+
+ document.addEventListener('pointermove', on_element_resize_move)
+
+ function on_element_resize_move(e) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ const newX = clamp(0, e.clientX, document.documentElement.clientWidth)
+ const newY = clamp(0, e.clientY, document.documentElement.clientHeight)
+
+ const diffX = newX - initialX
+ const diffY = newY - initialY
+
+ switch (placement) {
+ case 'top-start': {
+ const newWidth = initialWidth - diffX
+ const newHeight = initialHeight - diffY
+ const newTranslate = initialTransform.translate(diffX, diffY).transformPoint()
+
+ requestAnimationFrame(() => {
+ sourceEl.style.width = `${newWidth}px`
+ sourceEl.style.height = `${newHeight}px`
+ sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ })
+ break
+ }
+ case 'top-center': {
+ const newHeight = initialHeight - diffY
+ const newTranslate = initialTransform.translate(0, diffY).transformPoint()
+
+ requestAnimationFrame(() => {
+ sourceEl.style.height = `${newHeight}px`
+ sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ })
+ break
+ }
+ case 'top-end': {
+ const newWidth = initialWidth + diffX
+ const newHeight = initialHeight - diffY
+ const newTranslate = initialTransform.translate(0, diffY).transformPoint()
+
+ requestAnimationFrame(() => {
+ sourceEl.style.width = `${newWidth}px`
+ sourceEl.style.height = `${newHeight}px`
+ sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ })
+ break
+ }
+ case 'middle-start': {
+ const newWidth = initialWidth - diffX
+ const newTranslate = initialTransform.translate(diffX).transformPoint()
+
+ requestAnimationFrame(() => {
+ sourceEl.style.width = `${newWidth}px`
+ sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ })
+ break
+ }
+ case 'middle-end': {
+ const newWidth = initialWidth + diffX
+
+ requestAnimationFrame(() => {
+ sourceEl.style.width = `${newWidth}px`
+ })
+ break
+ }
+ case 'bottom-start': {
+ const newWidth = initialWidth - diffX
+ const newHeight = initialHeight + diffY
+ const newTranslate = initialTransform.translate(diffX, 0).transformPoint()
+
+ requestAnimationFrame(() => {
+ sourceEl.style.width = `${newWidth}px`
+ sourceEl.style.height = `${newHeight}px`
+ sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ })
+ break
+ }
+ case 'bottom-center': {
+ const newHeight = initialHeight + diffY
+
+ requestAnimationFrame(() => {
+ sourceEl.style.height = `${newHeight}px`
+ })
+ break
+ }
+ case 'bottom-end': {
+ const newWidth = initialWidth + diffX
+ const newHeight = initialHeight + diffY
+
+ requestAnimationFrame(() => {
+ sourceEl.style.width = `${newWidth}px`
+ sourceEl.style.height = `${newHeight}px`
+ })
+ break
+ }
+ }
+ }
+
+ document.addEventListener('pointerup', on_element_resize_end, { once: true })
+ document.addEventListener('mouseleave', on_element_resize_end, { once: true })
+
+ function on_element_resize_end() {
+ document.removeEventListener('pointermove', on_element_resize_move)
+ document.body.style.cursor = originalDocumentCursor
+ document.body.style.userSelect = originalDocumentUserSelect
+ sourceEl.style.transition = originalElTransition
+ }
+ }
+
+ disconnectedCallback() {
+ this.button.removeEventListener('pointerdown', this.on_element_resize_start.bind(this))
+ }
+
+ render() {
+ return `
+
+ `
+ }
+}
+
+customElements.define('visbug-handle', Handle)
diff --git a/app/components/selection/handles.element.css b/app/components/selection/handles.element.css
index 52fbb490..f7ae7ab1 100644
--- a/app/components/selection/handles.element.css
+++ b/app/components/selection/handles.element.css
@@ -1,10 +1,18 @@
@import "../_variables.css";
-:host > svg {
+:host {
position: var(--position, 'absolute');
top: var(--top);
left: var(--left);
overflow: visible;
pointer-events: none;
z-index: var(--layer-3);
+ width: var(--width);
+ height: var(--height);
+ display: grid;
+ isolation: isolate;
+}
+
+:host > svg {
+ position: absolute;
}
diff --git a/app/components/selection/handles.element.js b/app/components/selection/handles.element.js
index c78a1237..a9d69c9e 100644
--- a/app/components/selection/handles.element.js
+++ b/app/components/selection/handles.element.js
@@ -1,5 +1,5 @@
import $ from 'blingblingjs'
-import { HandleStyles } from '../styles.store'
+import { HandlesStyles } from '../styles.store'
import { isFixed } from '../../utilities/';
export class Handles extends HTMLElement {
@@ -7,20 +7,20 @@ export class Handles extends HTMLElement {
constructor() {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
- this.styles = [HandleStyles]
- this.on_resize = this.on_resize.bind(this)
+ this.styles = [HandlesStyles]
+ this.on_resize = this.on_window_resize.bind(this)
}
connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
- window.addEventListener('resize', this.on_resize)
+ window.addEventListener('resize', this.on_window_resize)
}
disconnectedCallback() {
- window.removeEventListener('resize', this.on_resize)
+ window.removeEventListener('resize', this.on_window_resize)
}
- on_resize() {
+ on_window_resize() {
window.requestAnimationFrame(() => {
const node_label_id = this.$shadow.host.getAttribute('data-label-id')
const [source_el] = $(`[data-label-id="${node_label_id}"]`)
@@ -62,6 +62,8 @@ export class Handles extends HTMLElement {
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.style.setProperty('--height', `${height}px`)
return `
+
+
+
+
+
+
+
+
`
}
}
diff --git a/app/components/selection/hover.element.js b/app/components/selection/hover.element.js
index 5786517b..a32e585a 100644
--- a/app/components/selection/hover.element.js
+++ b/app/components/selection/hover.element.js
@@ -1,11 +1,11 @@
import { Handles } from './handles.element'
-import { HandleStyles, HoverStyles } from '../styles.store'
+import { HandlesStyles, HoverStyles } from '../styles.store'
export class Hover extends Handles {
constructor() {
super()
- this.styles = [HandleStyles, HoverStyles]
+ this.styles = [HandlesStyles, HoverStyles]
}
render({ width, height, top, left }, node_label_id, isFixed) {
diff --git a/app/components/styles.store.js b/app/components/styles.store.js
index 0e595f18..89c5f07c 100644
--- a/app/components/styles.store.js
+++ b/app/components/styles.store.js
@@ -1,7 +1,8 @@
import 'construct-style-sheets-polyfill'
import { default as visbug_css } from './vis-bug/vis-bug.element.css'
-import { default as handle_css } from './selection/handles.element.css'
+import { default as handles_css } from './selection/handles.element.css'
+import { default as handle_css } from './selection/handle.element.css'
import { default as hover_css } from './selection/hover.element.css'
import { default as corners_css } from './selection/corners.element.css'
import { default as distance_css } from './selection/distance.element.css'
@@ -30,6 +31,7 @@ const constructStylesheet = (styles, stylesheet = new CSSStyleSheet()) => {
}
export const VisBugStyles = constructStylesheet(visbug_css)
+export const HandlesStyles = constructStylesheet(handles_css)
export const HandleStyles = constructStylesheet(handle_css)
export const HoverStyles = constructStylesheet(hover_css)
export const CornerStyles = constructStylesheet(corners_css)
diff --git a/app/components/vis-bug/vis-bug.element.js b/app/components/vis-bug/vis-bug.element.js
index 26b8a795..039c693b 100644
--- a/app/components/vis-bug/vis-bug.element.js
+++ b/app/components/vis-bug/vis-bug.element.js
@@ -2,7 +2,7 @@ import $ from 'blingblingjs'
import hotkeys from 'hotkeys-js'
import {
- Handles, Label, Overlay, Gridlines, Corners,
+ Handles, Handle, Label, Overlay, Gridlines, Corners,
Hotkeys, Metatip, Ally, Distance, BoxModel, Grip
} from '../'
diff --git a/app/utilities/numbers.js b/app/utilities/numbers.js
index cbf461d5..142589ce 100644
--- a/app/utilities/numbers.js
+++ b/app/utilities/numbers.js
@@ -1,3 +1,7 @@
export function numberBetween(min, max) {
return Math.floor(min + (Math.random() * (max - min)))
-}
\ No newline at end of file
+}
+
+export function clamp(min, val, max) {
+ return Math.max(min, Math.min(val, max))
+}