Created
November 26, 2022 09:13
-
-
Save paitonic/f15dade75c1b10f88047c42992ea2248 to your computer and use it in GitHub Desktop.
resizeable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Document</title> | |
<link rel="stylesheet" href="styles.css"> | |
<style> | |
body { | |
margin: 200px 200px; | |
} | |
#div-1 { | |
height: 150px; | |
width: 150px; | |
background-color: pink; | |
} | |
.resizer { | |
height: 50px; | |
width: 50px; | |
position: absolute; | |
border: 1px dashed black; | |
} | |
.resizer__handle { | |
position: absolute; | |
height: 15px; | |
width: 15px; | |
background-color: black; | |
} | |
.resizer__top-left { | |
top: -5px; | |
left: -5px; | |
background-color: red; | |
} | |
.resizer__top-left:before { | |
content: ""; | |
color: #FFF; | |
width: 10px; | |
height: 10px; | |
position: relative; | |
transform: translateX(-50%); | |
display: inline-block; | |
} | |
.resizer__top-right { | |
top: -5px; | |
right: -5px; | |
background-color: purple; | |
} | |
.resizer__top-right:before { | |
content: ""; | |
color: #FFF; | |
width: 10px; | |
height: 10px; | |
position: relative; | |
transform: translateX(-50%); | |
display: inline-block; | |
} | |
.resizer__bottom-right { | |
bottom: -5px; | |
right: -5px; | |
background-color: green; | |
} | |
.resizer__bottom-right:before { | |
content: ""; | |
color: #FFF; | |
width: 10px; | |
height: 10px; | |
position: relative; | |
transform: translateX(-50%); | |
display: inline-block; | |
} | |
.resizer__bottom-left { | |
bottom: -5px; | |
left: -5px; | |
background-color: blue; | |
} | |
.resizer__bottom-left:before { | |
content: ""; | |
color: #FFF; | |
width: 10px; | |
height: 10px; | |
position: relative; | |
transform: translateX(-50%); | |
display: inline-block; | |
} | |
.some-container { | |
position: relative; | |
padding: 20px; | |
border: 1px solid black; | |
height: 400px; | |
} | |
.edge { | |
height: 1px; | |
position: absolute; | |
width: 100%; | |
background-color: red; | |
left: 0; | |
top: 0; | |
} | |
#output { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
} | |
/*#bottom {*/ | |
/* position: absolute;*/ | |
/* background-color: red;*/ | |
/* top: 150px;*/ | |
/* height: 1px;*/ | |
/* width: 100%;*/ | |
/*}*/ | |
</style> | |
</head> | |
<body> | |
<!-- <textarea id="name" style="margin-top: 20px; margin-left: 20px;"></textarea>--> | |
<div id="div-1"></div> | |
<div id="output"></div> | |
<!-- <div id="bottom"></div>--> | |
<!-- <div class="edge"></div>--> | |
<div class="resizer"> | |
<div class="resizer__handle resizer__top-left"></div> | |
<div class="resizer__handle resizer__top-right"></div> | |
<div class="resizer__handle resizer__bottom-right"></div> | |
<div class="resizer__handle resizer__bottom-left"></div> | |
</div> | |
<!-- <div class="some-container">--> | |
<!-- some container--> | |
<!-- </div>--> | |
<script type="module"> | |
import {resizable} from "./resizable.mjs"; | |
const resizer = document.querySelector('.resizer'); | |
const element = document.getElementById('div-1'); | |
const output = document.getElementById('output'); | |
resizable(resizer, element, () => { | |
output.innerText = ` | |
element.style.left = ${element.style.left}; | |
element.style.top = ${element.style.top}; | |
element.style.width = ${element.style.width}; | |
element.style.height = ${element.style.height}; | |
mousePosition.top = ${window.scrollY + event.clientY} | |
mousePosition.left = ${window.scrollX + event.clientX} | |
`; | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function topBoundary(mousePosition, elementDimensions) { | |
const horizontalBoundary = elementDimensions.top; | |
const isAboveHorizontalBoundary = mousePosition.top < horizontalBoundary; | |
const top = isAboveHorizontalBoundary ? mousePosition.top : elementDimensions.top; | |
const height = isAboveHorizontalBoundary ? | |
elementDimensions.top - mousePosition.top : | |
mousePosition.top - elementDimensions.top; | |
return {top, height}; | |
} | |
export function bottomBoundary(mousePosition, elementDimensions) { | |
const horizontalBoundary = elementDimensions.top + elementDimensions.height; | |
const isUnderHorizontalBoundary = mousePosition.top > horizontalBoundary; | |
const top = isUnderHorizontalBoundary ? horizontalBoundary : mousePosition.top; | |
const height = isUnderHorizontalBoundary ? | |
mousePosition.top - top : | |
elementDimensions.height + (elementDimensions.top - mousePosition.top); | |
return {top, height}; | |
} | |
export function leftBoundary(mousePosition, elementDimensions) { | |
const verticalBoundary = elementDimensions.left; | |
const isOverBoundary = mousePosition.left < verticalBoundary; | |
const left = isOverBoundary ? mousePosition.left : elementDimensions.left; | |
const width = isOverBoundary ? | |
elementDimensions.left - mousePosition.left : | |
mousePosition.left - elementDimensions.left; | |
return {left, width}; | |
} | |
export function rightBoundary(mousePosition, elementDimensions) { | |
const verticalBoundary = elementDimensions.left + elementDimensions.width; | |
const isOverBoundary = mousePosition.left > verticalBoundary; | |
const left = isOverBoundary ? elementDimensions.left + elementDimensions.width : mousePosition.left; | |
const width = isOverBoundary ? | |
mousePosition.left - left : | |
elementDimensions.width + (elementDimensions.left - mousePosition.left); | |
return {left, width}; | |
} | |
const handleClassName = { | |
TOP_LEFT: 'resizer__top-left', | |
TOP_RIGHT: 'resizer__top-right', | |
BOTTOM_RIGHT: 'resizer__bottom-right', | |
BOTTOM_LEFT: 'resizer__bottom-left' | |
}; | |
const boundariesFn = { | |
[handleClassName.TOP_LEFT]: [bottomBoundary, rightBoundary], | |
[handleClassName.TOP_RIGHT]: [bottomBoundary, leftBoundary], | |
[handleClassName.BOTTOM_RIGHT]: [topBoundary, leftBoundary], | |
[handleClassName.BOTTOM_LEFT]: [topBoundary, rightBoundary] | |
}; | |
function applyBoundaries(mouseEvent, elementDimensions, boundaries) { | |
return boundaries.reduce((resized, boundaryFn) => { | |
return Object.assign(resized, boundaryFn(mouseEvent, elementDimensions)); | |
}, {}); | |
} | |
export function resizable(resizer, element, onMove) { | |
let dimensions = {}; | |
function cloneStyles(from, to) { | |
const dimensions = from.getBoundingClientRect(); | |
to.style.left = `${dimensions.left + window.scrollX}px`; | |
to.style.top = `${dimensions.top + window.scrollY}px`; | |
to.style.width = `${dimensions.width}px`; | |
to.style.height = `${dimensions.height}px`; | |
} | |
element.style.position = 'absolute'; | |
cloneStyles(element, resizer); | |
let currentHandle; | |
function onMouseDown(event) { | |
const targetDimensions = element.getBoundingClientRect(); | |
dimensions = { | |
width: targetDimensions.width, | |
height: targetDimensions.height, | |
left: window.scrollX + targetDimensions.left, | |
top: window.scrollY + targetDimensions.top | |
}; | |
currentHandle = event.currentTarget; | |
document.addEventListener('mousemove', onMouseMove); | |
document.addEventListener('mouseup', onMouseUp); | |
} | |
function onMouseMove(event) { | |
const mousePosition = { | |
left: window.scrollX + event.clientX, | |
top: window.scrollY + event.clientY | |
}; | |
const calculateFn = applyBoundaries.bind(null, mousePosition, dimensions); | |
let resized = {}; | |
if (currentHandle.classList.contains(handleClassName.TOP_LEFT)) { | |
resized = calculateFn(boundariesFn[handleClassName.TOP_LEFT]); | |
} else if (currentHandle.classList.contains(handleClassName.TOP_RIGHT)) { | |
resized = calculateFn(boundariesFn[handleClassName.TOP_RIGHT]); | |
} else if (currentHandle.classList.contains(handleClassName.BOTTOM_LEFT)) { | |
resized = calculateFn(boundariesFn[handleClassName.BOTTOM_LEFT]); | |
} else if (currentHandle.classList.contains(handleClassName.BOTTOM_RIGHT)) { | |
resized = calculateFn(boundariesFn[handleClassName.BOTTOM_RIGHT]); | |
} | |
// Object.assign(dimensions, resized); | |
const left = resized.left + 'px'; | |
const top = resized.top + 'px'; | |
const width = resized.width + 'px'; | |
const height = resized.height + 'px'; | |
element.style.left = left; | |
element.style.top = top; | |
element.style.width = width; | |
element.style.height = height; | |
resizer.style.left = left; | |
resizer.style.top = top; | |
resizer.style.width = width; | |
resizer.style.height = height; | |
onMove(event); | |
} | |
function onMouseUp() { | |
document.removeEventListener('mouseup', onMouseUp); | |
document.removeEventListener('mousemove', onMouseMove); | |
} | |
const topLeftHandle = document.querySelector('.resizer .resizer__top-left'); | |
topLeftHandle.addEventListener('mousedown', onMouseDown); | |
const bottomLeftHandle = document.querySelector('.resizer .resizer__bottom-left'); | |
bottomLeftHandle.addEventListener('mousedown', onMouseDown); | |
const topRightHandle = document.querySelector('.resizer .resizer__top-right'); | |
topRightHandle.addEventListener('mousedown', onMouseDown); | |
const bottomRightHandle = document.querySelector('.resizer .resizer__bottom-right'); | |
bottomRightHandle.addEventListener('mousedown', onMouseDown); | |
} | |
// resizable(resizer); | |
/* | |
offsetTop - relative to the parent element | |
clientX, clientY - relative to the viewport | |
pageX, pageY - relative to the entire document | |
offsetX, offsetY - relative to the element | |
window.scrollY + element.getBoundingClientRects().top = absolute element position | |
Input | |
element_height | |
element_width | |
mouse_position | |
Output | |
element.style.left = ? | |
element.style.width = ? | |
element.style.height = ? | |
element.style.top = ? | |
*/ | |
/* | |
left right | |
<- v v -> | |
+---------+ < top | |
| | | |
| | | |
+---------+ < bottom | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {bottomBoundary, leftBoundary, rightBoundary, topBoundary} from "./resizable.mjs"; | |
describe("bottomBoundary", () => { | |
/* vertical boundary | |
v handle v | |
+---------+ | |
| | | |
| | | |
+---------+ < horizontal boundary | |
*/ | |
it('should increase height when mouse moved toward element top', () => { | |
const {height, top} = bottomBoundary( | |
{left: 0, top: 0}, | |
{ | |
width: 100, | |
height: 100, | |
left: 0, | |
top: 20 | |
} | |
); | |
expect(height).toBe(120); | |
expect(top).toBe(0); | |
}); | |
it('should decrease height when mouse moved toward element bottom', () => { | |
const {height, top} = bottomBoundary( | |
{left: 0, top: 40}, | |
{ | |
width: 100, | |
height: 100, | |
left: 0, | |
top: 20 | |
} | |
); | |
expect(height).toBe(80); | |
expect(top).toBe(40); | |
}); | |
it('should return zero height and correct top when at the element bottom', () => { | |
const {height, top} = bottomBoundary( | |
{left: 0, top: 120}, | |
{ | |
width: 100, | |
height: 100, | |
left: 0, | |
top: 20 | |
} | |
); | |
expect(height).toBe(0); | |
expect(top).toBe(20 + 100 /* top + height */) | |
}); | |
it('should increase height when mouse moves below the element bottom', () => { | |
const {height, top} = bottomBoundary( | |
{left: 0, top: 121}, | |
{ | |
width: 100, | |
height: 100, | |
left: 0, | |
top: 20 | |
} | |
); | |
expect(height).toBe(1); | |
expect(top).toBe(120 /* top + height */) | |
}); | |
// | |
// | |
// it('should be able to increase height and width simultaneously', () => { | |
// const width = 100, left = 20, height = 100, top = 20; | |
// const mouseMove = {left: left - 1, top: top - 1}; // up and to the left by 1px | |
// | |
// const resized = bottomBoundary( | |
// {left: mouseMove.left, top: mouseMove.top}, | |
// {width, height, left, top} | |
// ); | |
// | |
// expect(resized.height).toBe(height + 1); | |
// expect(resized.top).toBe(top - 1); | |
// | |
// expect(resized.width).toBe(width + 1); | |
// expect(resized.left).toBe(left - 1); | |
// }); | |
// | |
// it('should be able to decrease height and width simultaneously', () => { | |
// const width = 100, left = 20, height = 100, top = 20; | |
// const mouseMove = {left: left + 1, top: top + 1}; // down and to the right by 1px | |
// | |
// const resized = bottomBoundary( | |
// {left: mouseMove.left, top: mouseMove.top}, | |
// {width, height, left, top} | |
// ); | |
// | |
// expect(resized.height).toBe(height - 1); | |
// expect(resized.top).toBe(top + 1); | |
// | |
// expect(resized.width).toBe(width - 1); | |
// expect(resized.left).toBe(left + 1); | |
// }); | |
}); | |
describe("topBoundary", () => { | |
/* | |
v vertical boundary | |
+---------+ < horizontal boundary | |
| | | |
| | | |
+---------+ | |
^ handle | |
*/ | |
it('should decrease height when mouse moved toward element top (before the horizontal boundary)', () => { | |
const height = 100, top = 1, mouseTop = 100; | |
const resized = topBoundary( | |
{left: 0, top: mouseTop}, | |
{width: 100, height: height, left: 0, top: top} | |
); | |
expect(resized.height).toBe(mouseTop - top); | |
expect(resized.top).toBe(top); | |
}); | |
it('should increase height when mouse moves down', () => { | |
const height = 100, top = 10, mouseTop = height + top + 1; | |
const resized = topBoundary( | |
{left: 0, top: mouseTop}, | |
{width: 100, height: height, left: 0, top: top} | |
); | |
expect(resized.height).toBe(mouseTop - top); | |
expect(resized.top).toBe(top); | |
}); | |
it('should return zero height and correct top when at the horizontal boundary', () => { | |
const height = 100, top = 0, mouseTop = 0; | |
const resized = topBoundary( | |
{left: 0, top: mouseTop}, | |
{width: 100, height: height, left: 0, top: top} | |
); | |
expect(resized.height).toBe(mouseTop - top); | |
expect(resized.top).toBe(top); | |
}); | |
it('should increase height when mouse moves above horizontal boundary', () => { | |
const height = 100, top = 10, mouseTop = 0; | |
const resized = topBoundary( | |
{left: 0, top: mouseTop}, | |
{width: 100, height: height, left: 0, top: top} | |
); | |
expect(resized.height).toBe(top - mouseTop); | |
expect(resized.top).toBe(mouseTop); | |
}); | |
}); | |
describe("leftBoundary", () => { | |
/* | |
left boundary | |
v | |
+---------+ | |
| | | |
| | | |
+---------+ | |
*/ | |
it('should increase width when mouse moves to the right', () => { | |
const width = 100, left = 10, mouseLeft = 120; | |
const resized = leftBoundary( | |
{left: mouseLeft, top: 0}, | |
{width: width, height: 100, left: left, top: 0} | |
); | |
expect(resized.width).toBe(mouseLeft - left); | |
expect(resized.left).toBe(left); | |
}); | |
it('should decrease width when mouse moves to the left', () => { | |
const width = 100, left = 10, mouseLeft = 90; | |
const resized = leftBoundary( | |
{left: mouseLeft, top: 0}, | |
{width: width, height: 100, left: left, top: 0} | |
); | |
expect(resized.width).toBe(mouseLeft - left); | |
expect(resized.left).toBe(left); | |
}); | |
it('should return zero width when mouse is on the vertical boundary', () => { | |
const width = 100, left = 10, mouseLeft = 10; | |
const resized = leftBoundary( | |
{left: mouseLeft, top: 0}, | |
{width: width, height: 100, left: left, top: 0} | |
); | |
expect(resized.width).toBe(0); | |
expect(resized.left).toBe(left); | |
}); | |
it('should increase width when mouse moves to the left beyond the vertical boundary', () => { | |
const width = 100, left = 10, mouseLeft = 0; | |
const resized = leftBoundary( | |
{left: mouseLeft, top: 0}, | |
{width: width, height: 100, left: left, top: 0} | |
); | |
expect(resized.width).toBe(left - mouseLeft); | |
expect(resized.left).toBe(mouseLeft); | |
}); | |
}); | |
describe("rightBoundary", () => { | |
/* | |
v right boundary | |
+---------+ | |
| | | |
| | | |
+---------+ | |
*/ | |
it('should increase width when mouse moves to the left', () => { | |
const {width, left} = rightBoundary( | |
{left: 0, top: 0}, | |
{ | |
width: 100, | |
height: 100, | |
left: 20, | |
top: 0 | |
} | |
); | |
expect(width).toBe(120); | |
expect(left).toBe(0); | |
}); | |
it('should decrease width when mouse moves to the right', () => { | |
const {width, left} = rightBoundary( | |
{left: 20, top: 0}, | |
{ | |
width: 100, | |
height: 100, | |
left: 0, | |
top: 0 | |
} | |
); | |
expect(width).toBe(80 /* height - top */); | |
expect(left).toBe(20); | |
}); | |
it('should return zero width and correct left when at the element left side', () => { | |
const width = 100, left = 20, mouseLeft = left + width; | |
const resized = rightBoundary( | |
{left: mouseLeft, top: 0}, | |
{ | |
width: width, | |
height: 100, | |
left: left, | |
top: 20 | |
} | |
); | |
expect(resized.width).toBe(left + width - mouseLeft); | |
expect(resized.left).toBe(left + width); | |
}); | |
it('should increase width when mouse moves beyond the element left side', () => { | |
const width = 100, left = 20, mouseLeft = width + left + 1 /*extra pixel to the right*/; | |
const resized = rightBoundary( | |
{left: mouseLeft, top: 0}, | |
{ | |
width: width, | |
height: 100, | |
left: left, | |
top: 20 | |
} | |
); | |
expect(resized.width).toBe(mouseLeft - (left + width)); | |
expect(resized.left).toBe(left + width); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Title</title> | |
</head> | |
<body> | |
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/jasmine.min.css"> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/jasmine.js"></script> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/jasmine-html.js"></script> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/3.4.0/boot.min.js"></script> | |
<script type="module" src="resizable.mjs"></script> | |
<script type="module" src="resizable.spec.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment