DWW DWW - 7 months ago 15
HTML Question

Check if two or more DOM elements overlap

Good day people!

I have run into an issue with my simple single day calendar script.

I've been tasked to create a single day calendar, which shows each block hour from 9am to 6pm. If an event overlaps another, they should equal the same width and not overlap. I have managed to achieve this for two events, however if more than two overlap, things go abit south, I need help figuring out a method to fix this where any number of events overlap, their widths will equal the same.

Events are rendered on the calendar using a global function:

renderDay([{start: 30, end: 120},{start: 60, end: 120}])

which takes an array of objects as an argument, where the integers are the number of minutes pasted from 9am. eg. 30 is 9:30am, 120 is 11am

here is the collision function I took from stackoverflow

// collision function to return boolean
// attribute: http://stackoverflow.com/questions/14012766/detecting-whether-two-divs-overlap
function collision($div1, $div2) {
let x1 = $div1.offset().left;
let y1 = $div1.offset().top;
let h1 = $div1.outerHeight(true);
let w1 = $div1.outerWidth(true);
let b1 = y1 + h1;
let r1 = x1 + w1;
let x2 = $div2.offset().left;
let y2 = $div2.offset().top;
let h2 = $div2.outerHeight(true);
let w2 = $div2.outerWidth(true);
let b2 = y2 + h2;
let r2 = x2 + w2;

if (b1 < y2 || y1 > b2 || r1 < x2 || x1 > r2) return false;
return true;
}


I run a loop on all the event divs which I want to check for overlaps

// JQuery on each, check if collision
$('.event').each(function(index, value) {

// statement to break out on final loop
if(index === $('.event').length - 1) return;

console.log('at index: ', index);

// if collison === true, halve width of both event divs, re-position
if(collision( $('#e-'+index) , $('#e-'+(index + 1)) )) {

$('#e-'+index).css('width', $('#e-'+index).width() / 2);
$('#e-'+(index+ 1)).css('width', $('#e-'+(index+ 1)).width() / 2).css('left', $('#e-'+(index + 1)).width());

if(collision)
}
})
}
})


Screenshots to help visualize :)

When two overlap, they have equal widths

When three or more overlap, things go wrong

Any help would be greatly appreciated!
DW

Answer

After looking at the code, it seems overly complex to check the rendered elements for collision when you can work this out from the start and end times.

The way I've done it, is to group events which collide in arrays like so:

let collisions = [
      // only 1 event in this array so no collisions
      [{
        start: 30,
        end: 120
      }],
      // 3 events in this array which have overlapping times
      [{
        start: 300,
        end: 330
      }, {
        start: 290,
        end: 330
      }, {
        start: 300,
        end: 330
      }]
    ];

Then we iterate through each group of collisions, and create the elements with the appropriate width and positioning.

for (var i = 0; i < collisions.length; i++) {
  var collision = collisions[i];

  for (var j = 0; j < collision.length; j++) {
    var event = collision[j];

    let height = event.end - event.start;
    let top = event.start + 50;

    // 360 = max width of event
    let width = 360 / collision.length;

    // a lot of this could be moved into a css class
    // I removed the "display: inline-block" code because these are absolutely positioned. Replaced it with "left: (j * width)px"
    let div = $(`<div id=${'e-'+ (i + j)}>`).css('position', 'absolute').css('top', top)
    .css('height', height).css('width', width).css('left', (j * width) + 'px')
    .css('backgroundColor', arrayOfColors.shift()).addClass('event')
    .text('New Event').css('fontWeight', 'bold');

    // append event div to parent container
    $('#events').append(div);
  }
}

//**********************************************************************
//
//  TITLE     - Thought Machine Coding Challenge, Single Day Calendar
//  AUTHOR    - DOUGLAS WISSETT WALKER
//  DATE      - 21/04/2016
//  VERSION   - 0.0.3
//  PREVIOUS  - 0.0.2
//
//**********************************************************************

let arr = [{
  start: 30,
  end: 120
}, {
  start: 70,
  end: 180
}, {
  start: 80,
  end: 190
}, {
  start: 300,
  end: 330
}, {
  start: 290,
  end: 330
}, {
  start: 220,
  end: 260
}, {
  start: 220,
  end: 260
}, {
  start: 220,
  end: 260
}, {
  start: 220,
  end: 260
}, {
  start: 400,
  end: 440
}, {
  start: 20,
  end: 200
}];

let renderDay;
$(document).ready(() => {

  renderDay = function(array) {
    $('.event').each(function(i, el) {
      $(el).remove();
    });

    // background colors for events
    let arrayOfColors = [
      'rgba(255, 153, 153, 0.75)',
      'rgba(255, 204, 153, 0.75)',
      'rgba(204, 255, 153, 0.75)',
      'rgba(153, 255, 255, 0.75)',
      'rgba(153, 153, 255, 0.75)',
      'rgba(255, 153, 255, 0.75)'
    ]

    let collisions = mapCollisions(array);

    let eventCount = 0; // used for unique id
    for (let i = 0; i < collisions.length; i++) {
      let collision = collisions[i];

      for (let j = 0; j < collision.length; j++) {
        let event = collision[j];

        let height = event.end - event.start;
        let top = event.start + 50;

        // 360 = max width of event
        let width = 360 / collision.length;

        // a lot of this could be moved into a css class
        // I removed the "display: inline-block" code because these are absolutely positioned
        // Replaced it with "left: (j * width)px"
        let div = $("<div id='e-" + eventCount + "'>").css('position', 'absolute').css('top', top)
          .css('height', height).css('width', width).css('left', (j * width) + 'px')
          .css('backgroundColor', arrayOfColors.shift()).addClass('event')
          .text('New Event').css('fontWeight', 'bold');

        eventCount++;
        // append event div to parent container
        $('#events').append(div);
      }
    }
  }

  renderDay(arr);
});

// Sorry this is pretty messy and I'm not familiar with ES6/Typescript or whatever you are using
function mapCollisions(array) {
  let collisions = [];

  for (let i = 0; i < array.length; i++) {
    let event = array[i];
    let collides = false;

    // for each group of colliding events, check if this event collides
    for (let j = 0; j < collisions.length; j++) {
      let collision = collisions[j];

      // for each event in a group of colliding events
      for (let k = 0; k < collision.length; k++) {
        let collidingEvent = collision[k]; // event which possibly collides

        // Not 100% sure if this will catch all collisions
        if (
          event.start >= collidingEvent.start && event.start < collidingEvent.end || event.end <= collidingEvent.end && event.end > collidingEvent.start || collidingEvent.start >= event.start && collidingEvent.start < event.end || collidingEvent.end <= event.end && collidingEvent.end > event.start) {
          collision.push(event);
          collides = true;
          break;
        }

      }

    }

    if (!collides) {
      collisions.push([event]);
    }
  }

  console.log(collisions);
  return collisions;
}
html,
body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
}
#container {
  height: 100%;
  width: 100%;
}
#header-title {
  text-align: center;
}
#calendar {
  width: 400px;
  height: 620px;
  margin-top: 70px;
}
#events {
  position: absolute;
  top: 80px;
  left: 100px;
  width: 800px;
  height: 620px;
}
.event {
  box-shadow: 0 0 20px black;
  border-radius: 5px;
}
.hr-block {
  border-top: 2px solid black;
  height: 58px;
  margin: 0;
  padding: 0;
  margin-left: 100px;
  min-width: 360px;
  opacity: .5;
}
.hr-header {
  position: relative;
  top: -33px;
  left: -68px;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="css/styles.css">
  <link rel="stylesheet" href="css/responsive.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

  <script charset="UTF-8" src="js/moment.js"></script>
  <script charset="UTF-8" src="js/script2.js"></script>
  <title>Thought Machine Code Challenge</title>
</head>

<body>

  <div id="container">
    <div class="header">
      <h1 id="header-title"></h1>
    </div>
    <div id="calendar">
      <div class="hr-block">
        <h2 class="hr-header">09:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">10:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">11:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">12:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">13:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">14:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">15:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">16:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">17:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">18:00</h2>
      </div>
    </div>
  </div>

  <div id="events">

  </div>

  <script>
    document.getElementById("header-title").innerHTML = moment().calendar();
  </script>
</body>

</html>