Skip to content

Instantly share code, notes, and snippets.

@paitonic
Created November 26, 2022 09:13
Show Gist options
  • Save paitonic/f15dade75c1b10f88047c42992ea2248 to your computer and use it in GitHub Desktop.
Save paitonic/f15dade75c1b10f88047c42992ea2248 to your computer and use it in GitHub Desktop.
resizeable
<!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>
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
*/
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);
});
});
<!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