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)) +}