Wednesday, July 26, 2023

Sharing Calendars

Now you have sorted out all the information you want and downloaded various calendars - and possibly created your own for inclusion - you want to share this with friends, family, coworkers, the internet. But how?

The simple answer is to create a JSON file with all your chosen settings in it, put it on the internet somewhere and send a link including that JSON file. Simples.

So, we need to add two more features: one to download the current set of settings as a JSON file and the other to parse a JSON file and configure the settings.

It's fairly simple (these days) to download a file generated on the client. I simply stole an example function from StackOverflow and put it in download.txt. I then added a button and connected the button to a function which calls another to compute the desired settings and then calls the download function.
function initSharing() {
}

function shareJson() {
  download("share-calendar.json", JSON.stringify(assemblePropertiesObject()));
}

function assemblePropertiesObject() {
  return {};
}
// this function is from @johnpyp on stackoverflow - https://stackoverflow.com/a/45831280
function download(filename, text) {
  var element = document.createElement('a');
  element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
  element.setAttribute('download', filename);

  element.style.display = 'none';
  document.body.appendChild(element);

  element.click();

  document.body.removeChild(element);
}
Of course, the flaw in this so far is that we haven't actually created the object to store. Fortunately, this is relatively simple: we just look at the controls and obtain the value of each one (I have opted not to save and restore the paper format controls because that usually ends badly).
function assemblePropertiesObject() {
  return {
    start: start.value,
    end: end.value,
    firstDay: first.value,
    calendars: Object.keys(calendars)
  };
}
Obviously, if you want to share this, you either need to give it to someone (by email, thumb drive, carrier pigeon or otherwise) or put it somewhere on the internet where they can find it. Files and URLs are treated differently in JavaScript for security reasons, so we will give them two ways of loading it:
  • From the UI, they can choose a local file and push a button to load it;
  • In the UI, they can paste the URL into a text box and push a button to load it;
  • They can pass it as an encoded URL as a query parameter, which makes it possible to share the whole URL directly.
In each case, of course, once we have the file contents, we will redirect to a single function to parse the JSON, update the controls and redraw the screen. This should, therefore, go more quickly than it may at first sound.

First off, let's load the JSON from a file. I think this is basically all vanilla JavaScript and HTML, so I think I'm going to let it pass without comment:
      <label for="sharing-file">Load JSON from:</label>
      <input type="file" id="sharing-file" onchange="loadJsonFromFile()">
var sharingFile;

function initSharing() {
  sharingFile = document.getElementById('sharing-file');
}
function loadJsonFromFile() {
  sharingFile.files[0].text().then(tx => configureWith(JSON.parse(tx)));
}

function configureWith(json) {
  start.value = json.start;
  end.value = json.end;
  first.value = json.firstDay;
  for (var i=0;i<json.calendars.length;i++) {
    loadCalendar(json.calendars[i]);
  }
}

function loadCalendar(url) {
  ajax(url, (status, response) => handleICS(url, status, response));
}
It isn't very different to load it from a URL:
      <input type="url" id="sharing-url" placeholder="https://calendar.com/sharing.json">
      <input type="button" value="Load shared" onclick="loadSharedJson()">
function loadSharedJson() {
  var from = sharingURL.value;
  ajax(from, handleConfig);
}

function handleConfig(status, response) {
  if (status / 100 == 2) {
    configureWith(JSON.parse(response));
  } else {
    console.log(status, response);
  }
}
And, again, it isn't very different to do the work from the URL itself. We want to do this during page load, so we want to attach it to the onload event. Given that we have already trapped that and called the initSharing method, that seems like the obvious place to attach the call to try and read the parameter.
function initSharing() {
  sharingFile = document.getElementById('sharing-file');
  sharingURL = document.getElementById('sharing-url');
  readConfigFromParameter();
}
We can use URLSearchParams to decode all the query information, which, in turn, comes from window.location.search. We need (obviously) to check that this parameter exists, and if so, we want to load it in exactly the same way that we did the explicitly provided URL.
function readConfigFromParameter() {
  var params = new URLSearchParams(window.location.search);
  if (params.has("config")) {
    ajax(params.get("config"), handleConfig);
  }
}
Everything is now checked in and tagged as PLANNING_CALENDAR_LOAD_SHARED_FROM_PARAMETER.

I'm going to carry on developing this, because, as I said, it's something I use, and you can see the latest on the head of the master branch of the repository.

It's also live at https://gmmapowell.com/calendar.

Saturday, July 22, 2023

Importing ICS calendars

ICS is a standard format for specifying calendar information over the web. It is possible to store and download such files using HTTP and, by extension, AJAX. We can use that to pull information from the internet and display it on the calendar in the appropriate place.

There is a lot of complexity here that I can't be bothered to document, but I think it is a little bit interesting to do the absolute minimum: pull a calendar from somewhere and extract the basic "start date" and "summary" information to show a time and summary in the appropriate box.

So, first off, we need to add an input box to provide the URL where the calendar is to be found, and a button to do the loading. When this is triggered, we want to fire an AJAX request to that location and load the calendar. I think this is all fairly straightforward.

Because my life revolves around the Oldham Athletic home schedule, I am using their 2023/2024 season calendar as my example here, and I've checked it into the repository under data/Oldham_Athletic.ics. So, if you've set up the server as described above, you can now access this as http://localhost:8080/data/Oldham_Athletic.ics. Note that you can't use AJAX if you are looking at the page directly from the file system.

So now we need to process this file and turn it into a sequence of calendar events. I'm not going to claim that I understand the ICS format fully - or suggest that you should. But it's a fairly simple blocked, key-value text format that you can figure out if you read it. Each block consists of a pair of BEGIN:type and END:type. The outermost block type is VCALENDAR, and then within that are a sequence of EVENTs. Within that are more fields and more blocks, particularly VALARM blocks. We are going to parse this into a structure which has fields and blocks, with each inner block being the same. The outermost structure corresponds to the VCALENDAR. We can then do later processing based on this.

I have four observations as I write this code. The first is that, unlike most of the code we've written so far, this code really feels like it could use TDD in its development. Most of it is text based and involves pushing and popping stacks and any number of possible bunnyholes and error cases (particularly in the shape of malformed data). I'm going to resist that temptation for simplicity of description of nothing else, and hope for the best.

The second has to do with those error cases themselves. Given that so much can go wrong (not least the AJAX call itself), we need a method of providing error feedback to the user. Because we haven't needed it so far, we don't have it, and that calls for careful thought and design and probably testing. I'm going to admit I'm lazy and probably come back and do that later when it starts biting me (and I know what kind of feedback I'd like) and when I start actively styling the controls pane.

The third observation is that this code is all kind of generic. Most obviously, making an ajax call, for which I ripped off an implementation (I hesitate to call it a library) which I wrote for another project several years ago and now just pull out whenever I want some AJAX stuff doing. It's not jquery, but it's not that big either. But all the rest of the code is just processing what is basically a fairly simple hierarchical key/value file format.

Which leads to the fourth observation that this code is so boring I can't be bothered to describe it. But I will include it here in all its glory (although not the AJAX implementation - you can look at ajax.js for that). This is all tagged PLANNING_CALENDAR_PARSE_ICAL.
function loadICS() {
  var urlEntry = document.getElementById("ics-url");
  var url = urlEntry.value;
  ajax(url, handleICS);
}

function handleICS(status, response) {
  console.log("ajax returned status", status);
  if (status / 100 == 2) {
    var lines = joinLongLines(response.split(/\r\n/));
    var blocked = makeHierarchical(lines);
  } else {
    // TODO: handle status error cases somehow (e.g. 401, 404, 500)
    console.log("error status", status);
    console.log(response);
  }
}

function joinLongLines(lines) {
  var i=0;
  var ret = [];
  while (i < lines.length) {
    var s = lines[i];
    while (++i < lines.length && lines[i].startsWith(" ")) {
      s += lines[i].substring(1);
    }
    ret.push(s);
  }
  return ret;
}

function makeHierarchical(lines) {
  var ret = { type: "VCALENDAR", blocks: [], fields: {} };
  if (lines[0] != "BEGIN:VCALENDAR") {
    console.log("not a calendar"); // errors
    return null;
  }
  var curr = ret;
  var stack = [ ];
  for (var i=1;i<lines.length && lines[i] != "END:VCALENDAR";i++) {
    var kv = lines[i].split(/:/);
    if (kv[0] == "BEGIN") {
      var next = { type: kv[1], blocks: [], fields: {} };
      curr.blocks.push(next);
      stack.push(curr);
      curr = next;
    } else if (kv[0] == "END") {
      if (kv[1] != curr.type) {
        console.log("line",i,"mismatched BEGIN",curr.type,"and END",kv[1]);
        return null;
      }
      curr = stack.pop();
    } else {
      curr.fields[kv[0]] = kv[1];
    }
  }
  return ret;
}
Now we have a "calendar" that probably contains a set of "events". But it's not really a calendar as such, because we expect a calendar to be a set of dates. The date is contained in each event (in the DTSTART field) and a brief description of the event is in the SUMMARY field. What we want to do is extract these and build a table which is indexed by date, where every date has a list of events that happen on that date.

This is, again, very generic code and I don't feel the need to comment on it other than to point out (for those at the back) that this is code which is about calendars and dates and times, and thus issues with timezones arise. I'm not convinced that there is a "correct" solution to this, but the best solution would be to allow our calendar app to have an interface in which you could choose which timezone should be used for each day (if you don't use your calendars to plan trips across and between continents, your life is easy in more ways than one), or at least per calendar, but I am just going with the tried-and-true use the timezone you have set right now. One of the implications of this is that events might end up on a different date to the one in which they are written (e.g. an event at 19:45 in England could start at 06:45 the next morning in New Zealand). For this reason, we read the dates (which should be in GMT according to the spec) and translate them into Date objects, and then format that as the date string we will use as the key.

This code is checked in as PLANNING_CALENDAR_TABULATE_ICS and the tabulation function is shown below. The toStandard method converts the "standard ISO" date format used by ICAL to the "standard ISO" format expected by Javascript - adding in - and : characters between elements.
function makeTabular(blocks) {
  // This assumes that blocks is a VCALENDAR containing VEVENT objects
  // It will ignore other events

  var ret = {};
  for (var i=0;i<blocks.blocks.length;i++) {
    var b = blocks.blocks[i];
    if (b.type != "VEVENT")
      continue;
    var starts = new Date(Date.parse(toStandard(b.fields["DTSTART"])));
    var date = starts.getFullYear() + "-" + (starts.getMonth()+1).toString().padStart(2, '0') + "-" + starts.getDate().toString().padStart(2, '0');
    var time = starts.getHours().toString().padStart(2, '0') + starts.getMinutes().toString().padStart(2, '0');
    if (!ret[date]) {
      ret[date] = [];
    }
    var entry = { time, summary: b.fields.SUMMARY, fields: b.fields };
    for (var j=0;j<ret[date].length;j++) {
      if (ret[date][j].time < entry.time) {
        ret[date].splice(j, 0, entry);
        entry = null;
        break;
      }
    }
    if (entry) { // was not spliced in, append
      ret[date].push(entry);
    }
  }
  return ret;
}

function toStandard(date) {
  return date.substring(0, 4) + "-" + date.substring(4,6) + "-" + date.substring(6,11) + ":" + date.substring(11,13) + ":" + date.substring(13);
}
Now all we have to do is to make sure that these entries appear in the calendar. This consists of four relatively simple steps:
  • First, maintain a central registry of all the calendars we have loaded; this is almost as simple as it sounds, but we need to be sure to replace rather than duplicate calendars if the same URL is loaded twice;
  • Secondly, we need to remember to call redraw() when this is done;
  • Thirdly, redraw() needs to consider for each date cell whether or not there are any events for that day and, if so, to add divs for them within the box for the relevant day;
  • Fourthly, we need to add styles for these, which may involve dynamic metrics.
The main questions that occur with the first couple of items are "who has the responsibility for storing this registry?" and "who calls redraw?". We don't actually have any classes to take responsibility for anything, so the first is basically, "where do I put the code?" and I don't have a good answer for that either, but in controls.js seems the place where it is closest to most of the things it is associated with. On the second point, I much prefer to have calls to methods like redraw() as close to the event handler as possible. In this case, that is at the end of handleICS.

Again, most of this code is uninteresting, but I might as well rattle through it.

When we load the calendar, we call the registration function, and then redraw():
function loadICS() {
  var urlEntry = document.getElementById("ics-url");
  var url = urlEntry.value;
  ajax(url, (status, response) => handleICS(url, status, response));
}

function handleICS(url, status, response) {
  console.log("ajax returned status", status);
  if (status / 100 == 2) {
    var lines = joinLongLines(response.split(/\r\n/));
    var blocked = makeHierarchical(lines);
    if (blocked) {
      var table = makeTabular(blocked);
      addCalendar(url, table);
      redraw();
The registration function itself is very simple and stores all the calendars in a global object calendars:
function addCalendar(url, cal) {
  calendars[url] = cal;
}
redraw() first compiles a list of events on each day it displays:
      var calDate = cellDate.getFullYear() + "-" + (cellDate.getMonth()+1).toString().padStart(2, '0') + "-" + cellDate.getDate().toString().padStart(2, '0');

      var toShow = [];
      for (var url in calendars) {
        var cal = calendars[url];
        var today = cal[calDate];
        if (!today)
          continue;
        for (var j=0;j<today.length;j++) {
          var next = today[j];
          for (var k=0;k<toShow.length;k++) {
            if (next.time < toShow[k].time) {
              toShow.splice(k, 0, next);
              next = null;
              break;
            }
          }
          if (next != null)
            toShow.push(next);
        }
      }
      console.log("calDate is", calDate, "have", toShow);
And then adds divs for these events:
      if (toShow.length > 0) {
        var events = document.createElement("div");
        events.className = "body-day-events-container";
        day.appendChild(events);

        for (var j=0;j<toShow.length;j++) {
          var event = document.createElement("div");
          event.className = "body-day-event";
          events.appendChild(event);
          var timeText = document.createTextNode(toShow[j].time);
          var timeSpan = document.createElement("span");
          timeSpan.className = "body-day-event-time";
          timeSpan.appendChild(timeText);
          event.appendChild(timeSpan);
          var eventText = document.createTextNode(" " + toShow[j].summary);
          var eventSpan = document.createElement("span");
          eventSpan.className = "body-day-event-text";
          eventSpan.appendChild(eventText);
          event.appendChild(eventSpan);
        }
The formatting is slightly more tricky. As seen above, we divide the text for each event into two spans (one for the time, one for the summary) and give each of these and the enclosing div a class name. Each of these can be separately styled. To begin with, we make sure the div has absolute positioning, and we make the time bold. With this, all that is left is the sizing and positioning.
.body-day-events-container {
  position: absolute;
}

.body-day-event {
  border: none;
}

.body-day-event-time {
  font-weight: bold;
}

.body-day-event-text {
  font-style: normal;
}
The sizing and positioning just builds upon what we have already done to show the date box. All the events for the day are contained in a single box so that is the only thing we need to position and can contain a font-size declaration which will then cascade to inner boxes. So, when we are building our custom stylesheet, we can simply add a rule for this box.
  var eventsContainerY = 2 * ypos;
  sheet.insertRule(".body-day-events-container { top: " + eventsContainerY + pageSize.unitIn + "; font-size: " + dateSize + pageSize.unitIn + " }");
  sheet.insertRule(".body-day-events-container { top: " + eventsContainerY + pageSize.unitIn + "; font-size: " + dateSize + pageSize.unitIn + " }");
And that's it for calendar and events handling. It's all checked in and tagged as PLANNING_CALENDAR_SIZE_EVENTS.

Thursday, July 20, 2023

Month and Year

For short calendars, just having the day number is enough. For longer calendars, it is also necessary to have some indication of which month (and possibly which year) is intended.

In the past, I have just put the month and date to the right of the calendar, starting as the month changes. This time, however, I am going to go for a "watermark" effect in which the month and year appear behind the calendar. This seems intuitively easy, but there are four separate aspects we need to consider:
  • Which month/year combinations we want to show;
  • Which and how many rows each should occupy;
  • Consequently how big the month name can/should be;
  • The actual content and styling of the watermark.
All of this will be spread across all the code we have so far, with each aspect being taken on in the appropriate place.

First, let's consider which month names we want to display. This is most easily handled in redraw, where we are scrolling through the weeks. As we consider each week, we consider if we consider it to be a first or last week of the month and thus build a table of rows and their respective month names.

If that sounds simple, you haven't considered the problem.

First off, what do we do with rows that have the end of one month and the start of the next month? I say we ignore them: it would be confusing to label them as either.

What do we do if we start halfway through a month? We just collect the one or two rows that are definitely in that month.

What if we start with the last few days of the month on the same row as another month? We ignore it as before.

And we need to do all of that while keeping track of what month and year we are in, along with how many months that is.

Right, let's get started.

We're going to make a start by upgrading the information we send between the functions to an object. This now has an element numRows which reflects the total number of rows, as before, and a new months array which will contain objects describing the desired month labels. We also create an (initially null) variable to track the "current" month.

We then use the leftDate to calculate the rightDate (six days later - the date in the right hand column). This enables us to easily check if the whole row contains the same month. If not, we just set the tracking variable to null.

The next case we consider is whether we are currently tracking a month and it is the same month as the left date. If so, this is just another row in that month and we can increment it.

The final case is when all of the week is in the same month, but we are not currently tracking it. In this case, we want to create a new object, start tracking it, and add it to our list of months.
  var thisMonth = null;
  do {
    var rightDate = new Date(leftDate);
    rightDate.setDate(rightDate.getDate() + 6);
    console.log(" showing week with", leftDate, "in the left column and", rightDate, "in the right column");

    // figure out if this is worthy of making a month
    if (leftDate.getMonth() != rightDate.getMonth()) {
      // we are not interested
      thisMonth = null;
    } else if (thisMonth && thisMonth.month == leftDate.getMonth()) { // if we are already recording this month, increment it
      thisMonth.numRows++;
    } else { // this week is all in this month and is a different month to what has gone before
      thisMonth = { month: leftDate.getMonth(), year: leftDate.getFullYear(), from: rowInfo.numRows, numRows: 1 };
      rowInfo.months.push(thisMonth);
    }
And that is checked in and tagged as PLANNING_CALENDAR_COLLECT_MONTHS.

That isn't quite all the work done, however. We also need to create divs in here that contain the text of the month names. We are going to style them in the background, but that doesn't alter the fact that we need to create divs containing the month name and year. And it makes sense to do this at the same time as creating the divs for the weeks themselves.

Each div that we create contains the appropriate text for the month and the class watermark to obtain the static styling for all watermarks and watermark-i for the individual positioning and measurement.
      var watermark = document.createElement("div");
      watermark.classList = 'watermark watermark-' + watermarkNo;
      watermarkNo++;
      var text = leftDate.toLocaleDateString(undefined, { month: 'long', year: 'numeric'});
      var wktext = document.createTextNode(text);
      watermark.appendChild(wktext);
      fbdiv.appendChild(watermark);
That is checked in and tagged as PLANNING_CALENDAR_WATERMARK_DIVS.

Now that we have the month names and know where we want to position them, we can go about trying to style them. The easy bit is the static styling, which we place in watermark.css. Simply put, we just need to choose a font and a color and then make it slip into the background by specifying a z-index and specifying absolute positioning (this also requires us to define the positioning of the feedback div as relative). We obviously don't want to specify the font size or position here, since that is the purpose of the indexed watermark type.
.watermark {
  font-family: sans-serif;
  font-weight: bold;
  color: lightgrey;
  position: absolute;
  z-index: -1;
}
Moving onto that, then. When we are defining the measurement stylesheet, we have the list of rows which we want to style, and separately we know how big each row is. We now need to figure out how much space there is in those rows (both horizontally and vertically) and then how we can scale it to make it fit. In order to do that, we need to know a reference size for the text we want to display. This, in turn, requires us to know what that text is. Looking back at what we did earlier, it's actually quite easy to rearrange the code to include that by putting the creation of thisMonth to the end of the else clause.

The recommended way to figure out the size of text is to use an HTML canvas object and use the TextMetrics object. Even though we don't want a canvas per se, we will thus add a hidden canvas and use that to do the measuring.
  <body onload="init()">
    <div id='controls' class="controls">
      <canvas id='canvas' style="display: none;"></canvas>
      <label for="start-date">Start date:</label>
var metricFontSize = 10;
  canvas = document.getElementById("canvas");
  sheet = new CSSStyleSheet({ media: "screen" });
  printSheet = new CSSStyleSheet({ media: "print" });
  document.adoptedStyleSheets = [sheet, printSheet];
  cx = canvas.getContext("2d");
  cx.font = metricFontSize + "px sans-serif";
It's fairly easy then to see how to measure the text using the canvas measureText function. This then gives us a width and height of the bounding box for the text at a known font size.

The available vertical space for the text is the number of rows multiplied by the row height plus the number of gaps (one less than the number of rows) multiplied by the y margin. The available horizontal space is likewise 7 (the number of columns) multiplied by the cell width plus 6 multiplied by the x margin.

Now, obviously, we don't want to use all of that space - it would look too cramped. So we'll take 75% of that and see what scale we could get away with in both horizontal and vertical dimensions, and then choose the smaller of those. It's then just maths to put away all the rest of the placement and scaling.

One final problem which crops up is the "phantom space" at the top of the div. This is just caused by CSS allowing extra space that is "never used" between lines. There doesn't seem to be an easy way to remove this (I would expect to be able to specify where I want the baseline relative to the "top" of the div, but I can't), but it isn't that far away, so I'll not worry about it.
  for (var i=0;i<rowInfo.months.length;i++) {
    handleWatermarks(sheet, i, rowInfo.months[i], xday, xmargin, yweek, ymargin);
  }
}

function handleWatermarks(sheet, idx, rowInfo, xcell, xmargin, ycell, ymargin) {
  var mx = cx.measureText(rowInfo.text);
  var width = mx.actualBoundingBoxRight + mx.actualBoundingBoxLeft;
  var height = mx.actualBoundingBoxDescent + mx.actualBoundingBoxAscent;

  var availx = 7 * xcell + 6 * xmargin * 2;
  var availy = rowInfo.numRows * ycell + (rowInfo.numRows-1) * ymargin * 2;
  
  var scalex = availx/width;
  var scaley = availy/height;
  var scale = Math.min(scalex, scaley) * .75; // .75 to leave some space around the edges

  var usedx = scale * width;
  var usedy = scale * height;
  var left = (availx - usedx) / 2;
  var top = (availy - usedy) / 2 + rowInfo.from * (ycell + ymargin * 2);

  sheet.insertRule(".watermark-" + idx + "{ font-size: " + scale*metricFontSize + "pt; left: " + left + "px; top: " + top + "px; }");
So that's the display version sorted out, but what we're really after is the printed version. And a moment's testing will indicate that the printed version does not. For a very simple reason, which is that all of this canvas calculation is done in pixels, and all our measurements are in millimetres or inches. And we are just dividing one by the other. So we need to do a conversion. Allegedly (good enough for me) there are 96px in an inch, which is the same as 3.78px in a mm. And then we need to make sure that our units are correctly specified in the stylesheet.
  for (var i=0;i<rowInfo.months.length;i++) {
    handleWatermarks(sheet, i, rowInfo.months[i], pageSize.unitIn, xday, xmargin, yweek, ymargin);
  }
}

function handleWatermarks(sheet, idx, rowInfo, unitIn, xcell, xmargin, ycell, ymargin) {
  var mx = cx.measureText(rowInfo.text);
  var width = mx.actualBoundingBoxRight + mx.actualBoundingBoxLeft;
  var height = mx.actualBoundingBoxDescent + mx.actualBoundingBoxAscent;
  switch (unitIn) {
    case "mm": {
      width /= 3.78;
      height /= 3.78;
      break;
    }
    case "in": {
      width /= 96;
      height /= 96;
      break;
    }
  }

  var availx = 7 * xcell + 6 * xmargin * 2;
  var availy = rowInfo.numRows * ycell + (rowInfo.numRows-1) * ymargin * 2;
  var left = (availx - usedx) / 2;
  var top = (availy - usedy) / 2 + rowInfo.from * (ycell + ymargin * 2);

  sheet.insertRule(".watermark-" + idx + "{ font-size: " + scale*metricFontSize + "pt; left: " + left + unitIn + "; top: " + top + unitIn + "; }");
And everything is checked in and tagged as PLANNING_CALENDAR_WATERMARKS_PRINTED.

Formatting for paper

Having decided on our approach, we need to implement it.

We will repeat our earlier approach of dividing the work into "static" and "measuring" stylesheets. So, we start by including a new stylesheet, printing.css, which we include with the media type print.

First off, we want to turn off all the irrelevant features, so we set the .controls div to have display: none and the feedback div to have no border.

To test this, it is possible to use the Chrome Developer Tools to select a "render emulation mode" of "print". Exactly how varies between versions; right now (v115) there is a kabob menu on the developer tools which has "More tools" one of which is "Rendering". In the pane that opens there is an item "Emulate CSS media type" for which one of the options is "print". By selecting that, you can see the effects of your print stylesheets directly in the browser window. You can also have the same page loaded in two separate windows, so that you can see the effects of screen and print media side by side.

So far, so good. Now onto the hard part.

We have defined all our measurements for the screen in a stylesheet. We want to keep that, so we need to add one in parallel that defines the measurements for printing on our chosen paper size (my plan had been to have one for each paper size and have the right one be chosen during printing, but this way is less work for me). So, first, we can go back and make the measuring sheet we have already created "screen only". (Unlike the base spreadsheet, there is nothing we want to reuse from this - it is all measurements that will be different between the two media). This just involves adding an object {media: "screen"} to the constructor of the CSSStyleSheet. Doing this reduces our print window to gibberish again as all the measurements have disappeared.

We next create a second sheet with the print media type, and ensure that both of them are adopted by the document.

This is all checked in as PLANNING_CALENDAR_MINIMAL_PRINT_STYLESHEET.

Now we need to do a refactoring. The method fitToPageSize is called from redraw whenever anything changes. This includes the start and end dates, the first day of the week, the size of the screen and the paper options. Some of these affect the screen, some the printed page, some both, some neither. But rather than figure that out, we are just going to relayout both screen and printed page every time. And it is basically exactly the same logic, just with different options. So my strategy is to tease this apart and to have this method call a new pageLayout method that does the laying out given the number of rows, the stylesheet to populate, and a set of options about size and the like. For now, I'm not going to do anything with the new print stylesheet, just refactor the code we have currently.

This involves removing a couple of the globals (borderX, borderY), moving the call to calculateSizeOfFeedbackDiv to the invocation of the new pageLayout method, and enhancing the result from calculateSizeOfFeedbackDiv to include the size of the border and the measurement unit (the screen will use px and the paper either mm or in).

That is then tagged as PLANNING_CALENDAR_EXTRACT_PAGELAYOUT.

This, of course, has done nothing for printing, except that now we can write one line that does handle the print case:
  pageLayout(printSheet, rows, calculatePaperSize());
This does depend on the calculatePaperSize method, however, but for now we are just going to hack that in:
function calculatePaperSize() {
  var borderX = 1, borderY = 1;
  return { x : 210, y : 297, unitIn: "mm", borderX, borderY };
}
And we can tag that as PLANNING_CALENDAR_PRINT_LAYOUT.

Of course, nothing is ever that simple, and we soon find that although this "fits" on a single piece of paper, it takes three. There is a very simple reason for this: paper has margins and we are using the whole of the piece of paper and thus overwriting the margins. I don't know how to find out what margins it is allowing than I do what sized paper it is using, but I do know how to overwrite the margins: an @page directive allows us to set the margins. We can choose any margin we want and then do our calculations based on having that much less space. (There is also an issue of duplicate margins, which can be addressed by recognizing the top and bottom rows as special, along with the left and right columns, and ensuring that the appropriate margin setting is 0 in this case; I can't be bothered right now, although I may come back to it; pull requests welcome).

OK, checked in and tagged as PLANNING_CALENDAR_PRINT_MARGINS.

So, finally for the basic calendar, we need to handle the various page sizes (and landscape) that we offered in the dropdowns. This is just some simple comparison of the currently selected values and returning the right hardcoded objects in calculatePaperSize. The landscape option is handled by taking the hardcoded object and then exchanging the X and Y values.

So that, apart from a ludicrous amount of testing, is that for the basic calendar. And is checked in as PLANNING_CALENDAR_BASIC_COMPLETE.

So what is left? Well, there are thousands of features that could be added, but I'm going to add three:
  • Annotations of the current month/year and other styling (such as weekends);
  • Ability to automatically populate with ICS calendars loaded over the web;
  • The ability to define a calendar template for easier sharing.
I may add other features over time, including the ability to split a calendar over multiple pages, but that is more likely to go straight into the live version than be described here. And, as I say, pull requests are always welcome.

Wednesday, July 19, 2023

Handling paper sizes

It's now time to turn our attention to the other really important part of this process: actually printing something. I have lived in both the UK and the US, and so I've had to deal with the whole A4/Letter thing on a personal basis. And, from time to time, I've also wanted to print calendars on A3 and tabloid paper (mainly to fit more on). So I definitely want to be able to handle this, because it's a terrible waste of paper and space if you have to assume you have paper 11" x 210mm (mismatched units intentional).

So, how do we tackle this? Well, I think the theory is that you use @media queries to handle it. Although most of the standards bodies seem more concerned in your ability to specify what page size you (as a document designer) wants than handling the page size the user has chosen to print on. (See the @page size descriptor for more details, including a (to me) humorous comparison between A4 and letter paper).

So there doesn't seem to be a nice simple
@media  print  and(paper-size:a4)  {  …  }
query, but rather we need to use something like:
@media  print  and  (min-width:  208mm)  and  (max-width:  212mm)  {  …  }
which is incredibly unclear and quite hacky. The idea here is that you can examine the width of the paper and if it is in the suitable range you can choose to use the settings for (in this case) A4.

Except, as far as I can tell, in Chrome at least, it doesn't work. I created a separate test case in pagesize.html and pagesize.css to test out these things, and I found I needed to test whether the paper size was 519mm, at which point it seemed to work the same for all different paper sizes.

Googling suggested that this is in fact a widespread problem, and thus I don't want to go near it (even though I do). But not supporting different paper sizes is not an option, so we are going to have to ask the user what their paper size is and then generate the relevant values based on that. I know for myself I will get this wrong more than once. Oh, and we also have to ask if they want calendars printed portrait or landscape (in my experience, for calendars of less than about four weeks, landscape is preferable; over four weeks generally demands a portrait layout, but it probably depends on what your application is). Sadly, this also limits the range of paper sizes we can offer (although we could offer a "custom" option; pull requests are always welcome).

In conclusion then, we are just going to create a second set of stylesheets which simply use @media print, and then use our own dropdown to determine the appropriate values. All of this work is tagged PLANNING_CALENDAR_PAPER_SIZES.
      <label for="page-size">Page Size:</label>
      <select id="page-size" onchange="redraw()">
        <option value="a4">A4</option>
        <option value="letter">Letter</option>
        <option value="a3">A3</option>
        <option value="tabloid">Tabloid</option>
      </select>
      <input type="checkbox" id="landscape" onchange="redraw()">
      <label for="landscape">Landscape</label>

CSS from JavaScript

Right, now we come to the meat of this whole project. In the basic CSS presented in the last episode, I opted for fixed cell measurements and completely flexible page measurements. This is exactly the opposite of what we actually want: the paper size for the calendar is fixed (even if we offer multiple options) and we want the cell size to vary to fill the available space.

Rather than deal with the complexities of the printer right up front (I hope I don't regret this decision…), I'm going to spend this episode looking at doing that on the screen in the available space (where it is easy to debug everything) and then concern myself with the complexities of printing next time.

Before we go any further, yes, I am aware of calc in CSS, and use it frequently. However, I have convinced myself that what I want to do here goes beyond calc because of the multiple constraints that apply to any given element. If you can figure out how to do all of this just with calc, do let me know.

Contrariwise, I believe it is possible to do everything I want to do here with JavaScript and hacking at elements in the DOM. But that is painful and error-prone and, quite frankly, not as exciting as what I'm going to try and do here, which is to create a new stylesheet at run-time whose values are calculated dynamically. I'm then going to split the styling between true "styling" (which will stay in the static stylesheets) and the measuring (which will be calculated dynamically).

I've chosen to put this code in a separate JS file, mainly for clarity, but not to do all the work that I should do to make everything modular (also for clarity). So I've defined a separate initStyling method, and called that from the init method in controls.js, passing through the fbdiv which I'm then storing as styledDiv. The more important thing, though, is that I create a new CSSStyleSheet and assign it to the list of adoptedStyleSheets in the document. Note that this only "works" if you are in complete control of the set of sheets to be adopted; if you're not, you will need to do the work to merge your sheets with everyone else's.
    <link rel="stylesheet" href="css/controls.css">
    <link rel="stylesheet" href="css/feedback.css">
    <script src="js/controls.js"></script>
    <script src="js/styling.js"></script>
  </head>
  <body onload="init()">
    <div class="controls">
  start.valueAsDate = new Date();
  end.valueAsDate = new Date();

  initStyling(fbdiv);

  redraw();
var styledDiv;
var sheet;

function initStyling(fbdiv) {
  styledDiv = fbdiv;
  sheet = new CSSStyleSheet();
  document.adoptedStyleSheets = [sheet];
}
Setting the rules depends on figuring out the size of the area to be filled and then updating the rules. This is information which should be available in redraw, so we'll call the function to set the dimension rules from there. At the end of redraw, I've added a call to fitToPageSize, which is a method to be defined in styling.js.
  fitToPageSize();
}
function fitToPageSize() {
  while (sheet.cssRules.length > 0)
    sheet.deleteRule(0);
  sheet.insertRule(".body-day { width: 25mm; height: 25mm; margin: 2mm }");
  sheet.insertRule(".body-day-date { top: 3mm; left: 3mm; font-size: 5mm }");
}
Before we go any further, I'm going to remove the sizing properties from the static CSS, so that there is no conflict and it is clear whether the dynamic stylesheet is working. And my first pass in getting the dynamic sheet working is just to try and add the same (fixed) properties to it. If nothing changes, we have a working dynamic spreadsheet.

And thus we have what we wanted: a dynamic stylesheet that can be continually updated. We are just left with the task of calculating the correct values.

This is tagged PLANNING_CALENDAR_DYNAMIC_STYLESHEET.

This next bit is something I never like doing. I think the root of the problem is just that it completely violates everything that is innate to HTML which is that you have an infinite scroll of paper, and thus there is no such notion as the "height" of the page. So you have to take the window size, and then go around subtracting all sorts of random measurements (body margin, other divs, etc) until it seems to fit. And then you try it on a mobile device only to find …

So bear with me if this doesn't "quite" work for you, or if the numbers (such as 16, double the "default" body margin of 8) seem to have been plucked out of nowhere. Imagine, if you will, that this code is in fact just going to return the available size for the feedback div. To help you, I will bury it in a function with that name and put all the nastiness in that, and then delegate the layout work to another function which takes the result.

Now, having done all that, I have a size in pixels. Up until now, I've been using mm, but there's no real difference between them for what follows, as long as you are consistent about it.

Let's tackle the horizontal first. We know that we have exactly 7 days in a week, and we also have 8 "spaces" (one on each end and six in between). Because this is CSS, each of the spaces consists of one or two margins, so actually we have 7 left margins and 7 right margins. So, if we say these will be equal, we can consider that we have 14 margins. If we say that we want a ratio of 12:1 for the days to the spaces, then we have 84 "units" in days and 7 units in spaces, for a total of 91 units. Each unit is then the width divided by 91. And then I'll round down to make sure it fits. We then need to set the width of a "day" to 12 units and the margin to be 0.5 units.
var styledDiv;
var sheet;
var controlPane;

function initStyling(fbdiv) {
  styledDiv = fbdiv;
  controlPane = document.getElementById('controls');
  sheet = new CSSStyleSheet();
  document.adoptedStyleSheets = [sheet];
}

function fitToPageSize() {
  // delte the old rules
  while (sheet.cssRules.length > 0)
    sheet.deleteRule(0);

  // calculate the page size
  var pageSize = calculateSizeOfFeedbackDiv();
  var unitIn = "px";

  // calculate desired box sizes
  var xunit = Math.floor(pageSize.x / 91);
  var xday = xunit*12;
  var xmargin = xunit/2;



  // generate new rules
  sheet.insertRule(".feedback { border-width: 0.1mm; width: " + pageSize.x + "px; height: " + pageSize.y + "px; }");
  sheet.insertRule(".body-day { border-width: 0.1mm; width: " + xday + unitIn + "; height: 25mm; margin: " + xmargin + unitIn + " }");
  sheet.insertRule(".body-day-date { top: 3mm; left: 3mm; font-size: 5mm }");
}

function calculateSizeOfFeedbackDiv() {
  var viewx = window.innerWidth;
  var viewy = window.innerHeight;

  var fbx = viewx - 16 - 5; // 16 for double body margin // allow, say, 5 for border
  var fby = viewy - controlPane.clientHeight - 16 - 5;

  return { x : fbx, y : fby };
}
When I do this, I find that the line "wraps": it's longer than I have calculated. Inspecting the DOM in Chrome, I discover there is extra space between the elements. Ah, yes, I remember adding that now when we were all text based. So I removed it from the DOM.

This is tagged PLANNING_CALENDAR_X_CORRECT.

Now, for the vertical dimension, we need to figure out how many rows there are. In fact, we have already done this when drawing the rows, which is where this function is called from, so we'll get it to count them for us and hand the number over.
  if (leftDate > from) {
    leftDate.setDate(leftDate.getDate() - 7);
  }
  var numRows = 0;
  do {
    console.log(" showing week with", leftDate, "in the left column");


    // advance to next week
    leftDate.setDate(leftDate.getDate() + 7);
    numRows++;
  } while (leftDate <= to);

  fitToPageSize(numRows);
}
Then the calculation is basically the same: we have a given number of rows, with a margin above and below each one, and we want the ratio to be about 12:1. So we divide the page size by the number of rows multiplied by 13, and use that as the y unit size. Oh, and don't forget that CSS measurements for things like margins use the "compass points" notation so that y comes before x.

Finally, we want to calculate the position and size of the date number. This wants to be about a fifth of the way into the box and about an eighth of its size (a completely arbitrary and aesthetic judgment on my part, you understand). More importantly, the size wants to be the smaller of what x and y will allow (the positions are independent). However, there are a couple more constraints: it would be good if the number were legible, so I would like it to always be at least 8px, but it can't be more than the available space in the box, which implies a font size of about three quarters the height of the box.
function fitToPageSize(rows) {
  var xpos = xday / 5;
  var xsize = xday / 8;

  var yunit = Math.floor(pageSize.y / (rows * 13));
  var yweek = yunit*12;
  var ymargin = yunit/2;
  var ypos = yweek / 5;
  var ysize = Math.min(Math.max(yweek / 8, 8), yweek * 3/4);
  
  var dateSize = Math.min(xsize, ysize);

  // generate new rules
  sheet.insertRule(".feedback { border-width: 0.1mm; width: " + pageSize.x + "px; height: " + pageSize.y + "px; }");
  sheet.insertRule(".body-day { border-width: 0.1mm; width: " + xday + unitIn + "; height: " + yweek + unitIn + "; margin: " + ymargin + unitIn + " " + xmargin + unitIn + " }");
  sheet.insertRule(".body-day-date { top: " + ypos + unitIn + "; left: " + xpos + unitIn + "; font-size: " + dateSize + unitIn + " }");
}
So far, so good. Checked in and tagged as PLANNING_CALENDAR_XY_CORRECT.

Now, what's bugging me is that the rounding that I did to make sure that the calendar doesn't overflow is working perfectly in the horizontal direction, but is slightly too aggressive in the vertical dimension. Simply removing it causes the box to overflow, as I'd expect, as does rounding to a single decimal place. I hate figuring these things out, but I guess I have to.

The problem, "of course", is that I have been ignoring the effect of the border widths in these calculations and just thinking that it will cancel out. Which it kind of does, but it's too ugly.

So the only solution is actually to take that into account, calculate the correct offsets given the border width, and remove the Math.floor. Then it will all work correctly.

This is too dull and testy to show here, but you can see it in the repository; it's checked in and tagged as PLANNING_CALENDAR_BORDER_INCLUDED.

One last thing: we also want to redraw the calendar when the window size changes (e.g. going full screen or removing the developer tools). To handle this, we need to add a handler for the resize event. This is tagged PLANNING_CALENDAR_HANDLE_RESIZE.

  initStyling(fbdiv);

  addEventListener("resize", redraw);
  redraw();
}

Tuesday, July 18, 2023

Basic CSS

How much styling we want to add is always a difficult question. For me, it tends to be somewhat "creeping" - I'll start with "just enough" and then keep adding a bit at a time while I'm working on other features and using the code. On here, though, I tend to keep it minimal, especially since I'm trying to make it clear what is important and what isn't. So while I might usually do things such as styling the date pickers, all I'm going to do here is make it clear what's included in the "feedback" box and then style it to look like a calendar.

So, in this case, I add a border around the feedback area to make it clear what the "page" looks like, then a border around each "date" box, and then style the number in the date box. The only oddity (IMHO) is styling body-day as position: relative which has nothing to do with what it wants, but just to make it the frame of reference for the position: absolute in body-day-date.

Simples. You may also notice that I added a css class onto the feedback box, changed the id to a class for the controls box, and fixed a bug in where the date value was being added in controls.js.
.feedback {
  border: 0.1mm solid black;
}

.body-day {
  display: inline-block;
  border: 0.1mm solid black;
  margin: 2mm;
  width: 25mm;
  height: 25mm;
  position: relative;
}

.body-day-date {
  position: absolute;
  top: 3mm;
  left: 3mm;
  font-family: 'Courier New', Courier, monospace;
  font-weight: bold;
  font-size: 5mm;
}
This code is in PLANNING_CALENDAR_BASIC_CSS.

Wiring up the Basics

Now that we have the outline of the calendar, we want to actually generate the calendar that has been asked for. As you may have guessed, 90% of this blog is going to be about how to get the calendar to look the way you want; the actual generation is a few simple lines of JavaScript. Certainly I'm doing this more than anything else (well, I guess I'm actually interested in the end product, but as I already have a functioning desktop app…) to investigate building CSS stylesheets dynamically in JavaScript.

That being the case, I'm going to gloss over most of this with a couple of classic handwaves. As always, if you feel I've left out significant details, get back to me and I'll elaborate.

The outline has three ui elements, each of which controls what is seen in the feedback window, and when any of them change, we want to generate an appropriate calendar based on the current set of three values. So, we add a listener on the appropriate change event for each control, all of which invoke the same method to redraw the calendar.

While I'm about setting that up, I have also added an init method which initializes the default start and end dates to be today and, for good measure, then generates a "default" feedback pane to be this week, starting on Monday. It's not a lot, but it gets you started.
    <script src="js/controls.js"></script>
  </head>
  <body onload="init()">
    <div id="controls">
      <label for="start-date">Start date:</label>
      <input type="date" id="start-date" onchange="redraw()">
      <label for="end-date">End date:</label>
      <input type="date" id="end-date" onchange="redraw()">
      <label for="first-day">First Day:</label>
      <select id="first-day" onchange="redraw()">
        <option value="1">Monday</option>
        <option value="2">Tuesday</option>
        <option value="3">Wednesday</option>
        <option value="4">Thursday</option>
        <option value="5">Friday</option>
        <option value="6">Saturday</option>
        <option value="0">Sunday</option>
      </select>
    </div>
What we want to draw is basically the same as what I put in the outline: a nested structure consisting of the whole calendar containing weeks, containing days, containing information about that day. redraw needs to clear out the existing feedback calendar and draw a new one starting the week of the start date from the first day until the end date is in the past.

A couple of notes on this code which is, otherwise, fairly straightforward:
  • The calculation of the offset for the start of the week works most of the time, but when the start date is a day of the week before the "left column day", the first week might be missed, and it is important to check for this and start the calendar a week earlier.
  • It's important to copy the value of from into leftDate (and leftDate into cellDate), otherwise, because dates in JavaScript are references, both leftDate and from will be updated, making this check pointless.
  • In order to make this work correctly, I changed the values of the options in the select statement from what I had initially used.
  • I have used a "do" loop to make sure at least one week comes out, but after that it is just a question of deciding whether the end date is in the future or the past. This automatically handles the case where the end date is before the start date in a satisfactory way.
  • There are comments logged to the console to clarify what is happening.
Obviously, we have not provided any styling yet. Because of this, by default, all the dates "run together" on a line to make a mess. This did not happen in the outline because of the white space inherent in the layout of the document. To replicate this, I have added a white space character between successive date boxes which will be removed when we add the borders.
var start, end, first, fbdiv;

function init() {
  start = document.getElementById('start-date');
  end = document.getElementById('end-date');
  first = document.getElementById('first-day');
  fbdiv = document.getElementById('feedback');

  start.valueAsDate = new Date();
  end.valueAsDate = new Date();

  redraw();
}

function redraw() {
  var from = new Date(start.value);
  var to = new Date(end.value);
  var leftColumn = parseInt(first.value);
  console.log("Generating calendar from", from, "to", to, "based on", leftColumn);

  fbdiv.innerHTML = '';
  var leftDate = new Date(from);
  leftDate.setDate(leftDate.getDate() - leftDate.getDay() + leftColumn);
  if (leftDate > from) {
    leftDate.setDate(leftDate.getDate() - 7);
  }
  do {
    console.log(" showing week with", leftDate, "in the left column");

    // create a div for the whole week
    var week = document.createElement("div");
    week.className = "body-week";
    fbdiv.appendChild(week);
    for (var i=0;i<7;i++) {
      var cellDate = new Date(leftDate);
      cellDate.setDate(cellDate.getDate() + i);
      console.log(" cell", i, "has date", cellDate);

      // create a div for each day, to contain all the aspects we will have
      var day = document.createElement("div");
      day.className = "body-day";
      week.appendChild(day);

      // the first aspect is the date
      var date = document.createElement("div");
      date.className = "body-day-date";
      day.appendChild(date);

      // and set the date text in here
      var dateValue = document.createTextNode(cellDate.getDate());
      day.appendChild(dateValue);

      // add a space between days until we style this
      var space = document.createTextNode(" ");
      week.appendChild(space);
    }

    // advance to next week
    leftDate.setDate(leftDate.getDate() + 7);
  } while (leftDate <= to);
}
The code is under the tag PLANNING_CALENDAR_BASIC_WIRING.

Monday, July 17, 2023

Planning Calendars

I am, by nature, a planner.

Many who meet me through the agile community are surprised by this, but, in fact, the nature of my planning fits well with agile: it is small scale, and can be repeated endlessly as new information emerges.

As such, I am always looking for tools to help me achieve this or communicate this. And because the things I do are generally small scale and "cheap", the tools I am looking for are generally not high priced enterprise things.

One such tool is a calendar. As I always like to say when planning projects, "you can always get your money back; but once the time is gone, it's gone". In my experience, most people don't have a good grasp of time in general, and, in particular, that only one thing fits in a given spot.

The origins of this tool go back to a "Filofax replacement" project I was building on NeWS (if you're under 50, Google is your friend here) back in the late 80s. Not much came of that, but the PostScript header file is the basis of the subsequent calendar programs I've written over the past 30 years. The plan here is to replace all of those with one that doesn't use that PostScript header file because it is 100% web based (HTML and CSS with the appropriate print media definitions).

The key to this tool is threefold:
  • Most projects I have worked on do not run on a nice, comfortable "exactly one calendar month" basis for which a standard calendar is a good fit;
  • Most people I know work on a Monday-Friday schedule, and then enjoy a weekend, whereas most calendars I can buy seem to think the week runs from Sunday to Saturday;
  • The month breaks on standard calendars are extremely distracting and often make people count the same week twice.
So, what I want is a calendar whose first week is the first week of the project, whose last week is the last week of the project and exactly fills one sheet of A4 (or "letter paper" if you happen to live in the dark ages). Sounds simple? OK, well, in that case, for good measure, I'd like to be able to choose which day of the week it starts on (although the default should be Monday) and whether you arrange it in landscape or portrait orientation (obviously, this is just on the print menu, but I want the calendar to re-layout when I do). And obviously, I want a lot of controls and visual feedback on the screen, but just the calendar on the output.

Oh, and over the past 30 years I have acquired a few new requirements that would be good to include:
  • The option to display the calendar in different "styles", in particular, how and where the month/year names are displayed, colours and shading for dates, weeks and months, and so on;
  • Sometimes I do want "long range plans": not plans as such, but if I have certain fixed engagements over the next few months, I'd like to have a calendar that has those on it so that I can see what chunks of work I might be able to commit to in that span. Experience has taught me that 13 weeks (i.e. a quarter of a year) on a calendar works, but that 26 (half a year) does not. So I'd like to be able to have multiple pages with (approximately) the same number of weeks on each.
  • There are a lot of calendars out there in ICS form that I'd like to include so that I can have them show up in order to avoid conflicts and to know when holidays are; and I'd like to be able to configure how they show up.
All good? Let's get started.

The basics

I believe in keeping things simple. So what I basically want is a single page that has a control area and a feedback area. The control area allows you to select a start date (which will be in the first row of the calendar) and an end date (which should be later than the start date, in which case it will be in the end row; otherwise you will just get a one-week calendar with the start date) and which day of the week is going to appear in the left-hand column.

The feedback area shows you approximately how the calendar will display, but without laying it out "on paper", or applying any fancy styling.

And then the "print" button does all the hard work through the application of a print media stylesheet.

Let's get started. We won't get everything we want done today, but let's at least get that outline in place.

The code for this can be found tagged PLANNING_CALENDAR_OUTLINE in the directory calendar. We're just getting started here, so nothing really works as such, but hopefully it's a useful guide to you as to where we're going with this - I know it is to me.

<html>
  <head>
    <title>Planning Calendar</title>
    <link rel="stylesheet" href="css/controls.css">
    <link rel="stylesheet" href="css/feedback.css">
  </head>
  <body>
    <div id="controls">
      <label for="start-date">Start date:</label>
      <input type="date" id="start-date">
      <label for="end-date">End date:</label>
      <input type="date" id="end-date">
      <label for="first-day">First Day:</label>
      <select id="first-day">
        <option value="0">Monday</option>
        <option value="1">Tuesday</option>
        <option value="2">Wednesday</option>
        <option value="3">Thursday</option>
        <option value="4">Friday</option>
        <option value="5">Saturday</option>
        <option value="6">Sunday</option>
      </select>
    </div>
    <div id="feedback">
      <div class="body-week">
        <div class="body-day">
          <div class="body-day-date">17</div>
        </div>
        <div class="body-day">
          <div class="body-day-date">18</div>
        </div>
        <div class="body-day">
          <div class="body-day-date">19</div>
        </div>
        <div class="body-day">
          <div class="body-day-date">20</div>
        </div>
        <div class="body-day">
          <div class="body-day-date">21</div>
        </div>
        <div class="body-day">
          <div class="body-day-date">22</div>
        </div>
        <div class="body-day">
          <div class="body-day-date">23</div>
        </div>
      </div>
    </div>
  </body>
</html>
At the moment, this website is 100% static, so you can just open index.html in your browser, and everything will just work fine. This is going to be true for a while, until we start opening ICS calendars using AJAX. Then we'll need to have it running on/in a server. So that I'm prepared, I just run this command in the web directory:
python  -m  SimpleHTTPServer  8080
And then visit localhost:8080 in my browser. I know there are 2,000 ways of doing this, so feel free to use your own. I have just got into this habit, and it keeps working for me.