I'm working on the backend of a community radio station website, and fancied trying my hand at writing a drag and drop scheduler.
It's working, but I feel I've made a mess with the drag and drop maths. What's the best way to handle an elements position in JavaScript?
I've used a combination of offsets from .pageX
, .getBoundsClientRect
and .offsetLeft
etc. but I had a real struggle getting them to work together, I'm sure there must be some standard way to handle this stuff.
I'm not much cop with JavaScript so any style or structure advice would be appreciated and if someone could offer some advice on getting this into some sort of class that would be great too.
var gridHolder = document.getElementById("gridHolder"),
gridAnchor = document.getElementById("gridAnchor"),
grid = document.getElementById('grid'),
staging = document.getElementById("staging"),
showCount = 0,
currentTarget,
currentTargetRect,
origin = gridAnchor.getBoundingClientRect(),
dx, dy,
intervals = 12,
blockHeight = 44,
blockWidth = 144,
blockIntervalWidth = blockWidth / intervals,
intervalValue = 60 / intervals,
monday = new Date(),
gridDays = document.createElement('div'),
gridDaysWidth = 100,
gridTimes = document.createElement('div'),
gridTimesHeight = 24,
days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
gridHolder.addEventListener("mousedown", selectBlock);
document.getElementById("addShow").addEventListener("click", addShow);
monday.setHours(-((monday.getDay() - 1) * 24), 0, 0, 0);
gridDays.className = "grid-days";
gridDays.style.width = gridDaysWidth;
gridTimes.className = "grid-times";
gridTimes.style.height = gridTimesHeight;
grid.style.marginLeft = gridDaysWidth;
for (var i = 0; i < 7; i++) {
var dayDiv = document.createElement('div'),
day = new Date(monday);
dayDiv.id = "day-" + i + 1;
dayDiv.className = "grid-day";
dayDiv.style.height = blockHeight + "px";
day.setHours(i * 24);
dayDiv.innerHTML = "<p>" + days[day.getDay()];
gridDays.appendChild(dayDiv);
}
for (var i = 0; i < 24; i++) {
var timeDiv = document.createElement('div'),
time = new Date(monday);
timeDiv.id = "day-" + i + 1;
timeDiv.className = "grid-time";
timeDiv.style.width = blockWidth + "px";
time.setHours(i);
timeDiv.innerHTML = "<p>" + formattedTime(time);
gridTimes.appendChild(timeDiv);
}
gridHolder.insertBefore(gridTimes, gridAnchor);
gridHolder.insertBefore(gridDays, gridAnchor);
function addShow() {
var select = document.getElementById("radioShows"),
option = select.options[select.selectedIndex];
if (option.text == "Select Show") {
return;
}
showBar = createBlock(option.text, option.value);
staging.appendChild(showBar);
}
function createBlock(showTitle, showID) {
showCount++;
var id = "radioShow-" + showCount,
showBar = document.createElement("div"),
title = document.createElement('p'),
date = document.createElement('p');
showBar.setAttribute("id", id);
showBar.setAttribute("class", "radio-show color-one");
showBar.dataset.title = showTitle;
showBar.dataset.id = showID;
title.innerHTML = showTitle;
date.setAttribute('class', 'date meta');
showBar.appendChild(title);
showBar.appendChild(date);
return showBar;
}
function selectBlock(ev) {
if (!hasClass(ev.target, 'radio-show')) {
return;
}
currentTarget = ev.target;
currentTargetRect = currentTarget.getBoundingClientRect();
dx = ev.offsetX + origin.left;
dy = ev.offsetY + origin.top;
if (!inResizeArea(currentTargetRect.width, ev.offsetX)) {
document.addEventListener('onscroll', moveBlock);
document.addEventListener('mousemove', moveBlock);
document.addEventListener('mouseup', dropBlock);
} else {
document.addEventListener('mousemove', resizeBlock);
document.addEventListener('mouseup', setBlockSize);
}
}
function hasClass(element, className) {
return element.className.indexOf(className) != -1;
}
function inResizeArea(blockWidth, offsetX) {
return blockWidth - offsetX < blockIntervalWidth;
}
function moveBlock(ev) {
var x = ev.pageX,
y = ev.pageY;
currentTarget.style.left = (x - dx) - gridDaysWidth + "px";
currentTarget.style.top = (y - dy) - gridTimesHeight + "px";
}
function dropBlock() {
if (isInGridBounds(currentTarget)) {
grid.appendChild(currentTarget);
snapBlock(currentTarget);
blockBounds = currentTarget.getBoundingClientRect();
setBlockDateTime()
} else {
currentTarget.remove();
}
document.removeEventListener('onscroll', moveBlock);
document.removeEventListener('mousemove', moveBlock);
document.removeEventListener('mouseup', dropBlock);
}
function resizeBlock(ev) {
var x = ev.pageX;
currentTarget.style.width = (x - currentTargetRect.left - window.scrollX) + "px";
}
function setBlockSize() {
snapWidth(currentTarget);
setBlockDateTime();
document.removeEventListener('mousemove', resizeBlock);
document.removeEventListener('mouseup', setBlockSize);
}
function isInGridBounds(element) {
var bounds = grid.getBoundingClientRect();
currentTargetBounds = element.getBoundingClientRect();
return (currentTargetBounds.left > bounds.left && currentTargetBounds.top > bounds.top && currentTargetBounds.bottom < (bounds.bottom + blockHeight - 1) && currentTargetBounds.right < (bounds.right + blockWidth - 1));
}
function snapWidth(element) {
var left, right,
snapTo = blockIntervalWidth,
currentTargetBounds = element.getBoundingClientRect();
right = currentTargetBounds.right;
left = currentTargetBounds.left;
if ((right - left) % snapTo != 0) {
var snapPoint = right - left + (snapTo - Math.ceil((right - left) % snapTo));
element.style.width = snapPoint + "px";
}
}
function snapBlock(element) {
var left, top,
snapToLeft = blockIntervalWidth,
snapToTop = blockHeight;
currentTargetBounds = element.style.left;
left = parseInt(element.offsetLeft);
top = parseInt(element.offsetTop);
if (left % snapToLeft != 0) {
element.style.left = Math.floor(left - (left % snapToLeft)) + "px";
}
if (top % snapToTop != 0) {
element.style.top = Math.floor(top - (top % snapToTop)) + "px";
}
}
function setBlockDateTime() {
var left = parseInt(currentTarget.offsetLeft),
right = currentTarget.getBoundingClientRect().width + left,
top = parseInt(currentTarget.offsetTop);
startTime = pixelsToTime(left);
endTime = pixelsToTime(right);
date = pixelsToDate(top);
setTimeAndDateOnBlock(currentTarget, startTime, endTime, date)
}
function setTimeAndDateOnBlock(element, startTime, endTime, date) {
element.lastChild.innerHTML = startTime + '–' + endTime;
element.dataset.date = date;
}
function pixelsToTime(sidePosition) {
var minutes,
dateTime = new Date(monday);
minutes = Math.round(sidePosition / blockIntervalWidth) * intervalValue;
dateTime.setMinutes(minutes);
return formattedTime(dateTime);
}
function pixelsToDate(top) {
var day = blockHeight,
dateTime = new Date(monday);
day = Math.round(top / day);
hoursInDay = 24;
dateTime.setHours((day - 1) * hoursInDay);
return formattedDate(dateTime);
}
function formattedDate(date) {
return date.getFullYear() + "-" + padZeroes(date.getMonth() + 1) + "-" + padZeroes(date.getDate());
}
function formattedTime(date) {
return padZeroes(date.getHours()) + ':' + padZeroes(date.getMinutes());
}
function padZeroes(n) {
return (n < 10) ? ("0" + n) : n;
}
/**
* Load data from API
*/
// var selectData = jsonRequest("http://localhost:63342/snap-grid/data/select-data.json", "GET", setSelectData);
// var radioShowData = jsonRequest("http://localhost:63342/snap-grid/data/radio-show-data.json", "GET", setRadioShowData);
function setRadioShowData() {
// var data = JSON.parse(radioShowData.response);
var data = {
"radio_shows": [{
"id": 1,
"name": "Daves Show",
"profile_picture": "show.png",
"start_time": "2017-01-10T01:00:00+0000",
"end_time": "2017-01-10T03:00:00+0000"
}, {
"id": 2,
"name": "Daisys Show",
"profile_picture": "show.png",
"start_time": "2017-01-11T02:00:00+0000",
"end_time": "2017-01-11T03:00:00+0000"
}, {
"id": 3,
"name": "Ronalds Show",
"profile_picture": "show.png",
"start_time": "2017-01-10T03:00:00+0000",
"end_time": "2017-01-10T04:00:00+0000"
}, {
"id": 4,
"name": "Cynthias Show",
"profile_picture": "show.png",
"start_time": "2017-01-12T04:30:00+0000",
"end_time": "2017-01-12T07:35:00+0000"
}]
}
for (var i = 0; i < data.radio_shows.length; i++) {
var radioShow = data.radio_shows[i],
startTime = convertedDate(radioShow.start_time),
endTime = convertedDate(radioShow.end_time),
startTimeMinutes = startTime.getHours() * 60 + startTime.getMinutes(),
endTimeMinutes = endTime.getHours() * 60 + endTime.getMinutes(),
startTimePosition = (startTimeMinutes / 5) * intervals,
endTimePosition = (endTimeMinutes / 5) * intervals,
top = (startTime.getDay() - 1) * blockHeight;
showBar = createBlock(radioShow.name, radioShow.id);
setTimeAndDateOnBlock(showBar, formattedTime(startTime), formattedTime(endTime), formattedDate(startTime));
showBar.style.left = startTimePosition + "px";
showBar.style.top = top + "px";
showBar.style.width = endTimePosition - startTimePosition + "px";
grid.appendChild(showBar);
}
}
function setSelectData() {
// var data = JSON.parse(selectData.response),
var data = {
"radio_shows": [{
"id": "1",
"name": "Show One"
}, {
"id": "2",
"name": "Show Two"
}, {
"id": "3",
"name": "Show Three"
}, {
"id": "4",
"name": "Show Four"
}]
}
var select = document.getElementById("radioShows");
for (var i = 0; i < data.radio_shows.length; i++) {
var option = document.createElement("option"),
radioShow = data.radio_shows[i];
option.value = radioShow.id;
option.innerHTML = radioShow.name;
select.appendChild(option);
}
}
function convertedDate(dateTime) {
return new Date((dateTime).replace(/-/g, "/").replace(/[TZ]/g, " ").replace("/+0000/g", ""));
}
function jsonRequest(url, method, callback) {
var res = new XMLHttpRequest();
res.addEventListener('load', callback);
res.open(method, url, true);
res.send();
return res;
}
setRadioShowData();
setSelectData();
div {
box-sizing: border-box;
}
.wrapper {
padding: 10px;
}
.grid-holder {
display: inline-block;
width: 4000px;
}
.grid-anchor {
position: relative;
float: left;
}
.grid-times {
margin-left: 100px;
height: 24px;
}
.grid-days {
padding-top: 12px;
padding-right: 8px;
width: 100px;
float: left;
text-align: right;
}
.grid-days p,
.grid-times p {
margin: 0;
padding: 0;
}
.grid-time {
display: inline-block;
}
.grid {
width: 3456px;
height: 308px;
border: 1px solid slategrey;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAABYCAIAAACVn+k2AAAABmJLR0QA/wD/AP+gvaeTAAABU0lEQVR4nO3VQa3DUBDFUM8H8KiGQACGQIiEQAqhv1Kl1JK9vovZHM3c901Jmpm/t6PjOHSbbdt+6p5vbYBJmKiEyTYkzFXCZBsS5iphsg0Jc5Uw2YaEuUqYbEPCXCVMtiFhrhIm25AwVwmTbUiYq4TJNiTMVcJkGxLmKmGyDQlzlTDZhoS5SphsQ8JcJUy2IWGuEibbkDBXCZNtSJirhMk2JMxVwmQbEuYqYbINCXOVMNmGhLmamadPqA+b67qevqH+21rr/Q87z1O32ff9p+751oaEuUqYbEPCXCVMtiFhrhIm25AwVwmTbUiYq4TJNiTMVcJkGxLmKmGyDQlzlTDZhoS5SphsQ8JcJUy2IWGuEibbkDBXCZNtSJirhMk2JMxVwmQbEuYqYbINCXOVMNmGhLlKmGxDwlwlTLYhYa4SJtuQMFcJk21ImKu11tMn1Ie9AMKvtvAicf0dAAAAAElFTkSuQmCC");
}
.radio-show {
position: absolute;
width: 144px;
height: 44px;
padding: 6px 8px 8px 44px;
color: slategrey;
z-index: 1;
background: whitesmoke url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAIAAACR5s1WAAAABmJLR0QA/wD/AP+gvaeTAAADaklEQVRYhe2XT0/iQBjGBzC2lVrRRKj0hsWDeIWEHkwA5eJn8Ct69NIU46VNekU8oYZESuXU1sEMprZ7aHh3ttTK7sH1wHOad/68/bUz88w0E4Yh+t/K/m8AhNYQv7WGWGgjsVZV1dFoFARBPp/vdrv7+/t0q+/7pmk+Pj4ihCqVSqPR2Nj4Iw/G2DCM8XjMcVytVjs5OUmHyCxvUVVVn56eIMzlcpeXl5ubm1CjadrDwwOEsiy3220a8erqynEcqFEUJZ0jYTpGoxEdfnx89Pt9CAkhNAFCaDgcEkIgtCyLJkAIDQaDFIJkiOVvM5vNaIjlIfP5PKVD4pAvILa2tmI1R0dHUN7Z2eF5nm7leV4QBAhFUcxm/0grSdJfQ5ydneVyOQir1aooihBmMplWq8WybBSyLNtutzOZDHQQBEFRFODY3d1tNpvpEAkLEyH0/v7e7/ff3t5iBCBCiGVZCKFyuQxAtDzPe3l5YRhGkiT6lf4C4pv1I8zqZ0MQQhzHSZksjDHG+LPWMAwdx/lyc0ZKsG3f929vbyNH4nm+1WodHBzQHV5fXzVNm06nCKFisdjpdLa3t+kOk8mk1+tF7iLL8unpaczXY0r4EqZpgidijFVVjb0QECCEptOppml0KyFEVVXwt+FwaJpmCkEyRHQy0Umj3QhYQAAc9LyMx+MYdCzhShD/INqs6PJnNV9DVCoVOmRZtlwuQ8jzfLFYpDuUSqV8Pg/hsn3FEq4E0Wg0ZFmGR3a73VjSTqcDHKVSiT7HI+jz83M4X2RZrtfr6RCfOuZ8PieECILw2ceM1kHsMAOFYei6LsdxDMOkE6RBfKd+tmN+p5KNzPd9y7IIIaIo0hcW0CpHuW3bLMuucpQnQGCMr6+vo3tiNptVFOX4+JjuMJlMwEajvRDz9fv7e13XgyBACBUKhYuLC3oPLythOgzDgJtqEAS6rnueB61hGPZ6PfBEQsjNzQ29uj3PAwKEkOM4uq6nECRD0CYdcdi2DaHruvS9FyGEMXZdF0LbtoEgMeFKEMtzzHFcYjlxSPrwVSFqtRodFgoF2rYZhgE/jVStVukHS5K0t7eXknBZyWZ1d3c3GAwIIZIkNZvN2LKifwMPDw/r9XrsujCbzQzDeH5+/vffwO/XjzCrNcRCa4iF1hALrSEW+gWcEa9+YBXXcwAAAABJRU5ErkJggg==") top left no-repeat;
cursor: pointer;
}
.radio-show p {
margin: 0;
padding: 0;
font-size: 12px;
}
.radio-show:after {
content: "";
position: absolute;
top: 0;
right: -10px;
display: block;
height: 44px;
width: 10px;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAsCAIAAADTpb7BAAAABmJLR0QA/wD/AP+gvaeTAAAAHklEQVQ4jWP8//8/A27AhEduVHpUelR6VHpUGg0AANa+A1VYGRD1AAAAAElFTkSuQmCC") center right no-repeat;
cursor: col-resize;
z-index: 4;
}
.staging {} .staging .radioShow {
display: inline-block;
}
.front {
z-index: 3;
}
.color-one {
border: 2px solid deeppink;
border-right-width: 12px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Snap Grid</title>
<link href="css/styles.css" rel="stylesheet" />
</head>
<body id="body">
<div class="wrapper">
<h1>Radio Schedule</h1>
<div class="grid-holder" id="gridHolder">
<div class="grid-anchor" id="gridAnchor">
<div class="grid" id="grid"></div>
<div class="staging" id="staging">
<div class="button-holder">
<select id="radioShows">
<option>Select Show</option>
</select>
<button id="addShow">Add Radio Show</button>
</div>
</div>
</div>
</div>
</div>
<script src="snap-grid.js"></script>
<script src="load-data.js"></script>
</body>
</html>