In a nutshell, you have (at least) two ways to achieve what you’re trying to get: either with the help of PHP (server-side) or JS (client-side). This topic lead me to choose the first option, which worked perfectly (all browsers/OSes). But after second thoughts, I went for the JS version (less compatibility but way cleaner code). Here’s the breakdown:
PHP-version
Once your structured field is set up as a collection, put something like this in your template (or better: controller):
<?php // Build ics export
$icsTitle = 't="' . $event->event_title()->html() . '"';
$icsLocation = 'l="' . $event->event_location()->html() . '"';
$icsStart = 's="' . $event->date('Ymd\THis', 'event_dtstart') . '"';
$icsEnd = 'e="' . $event->date('Ymd\THis', 'event_dtend') . '"';
$icsQuery = '/ics/' . rawurlencode($icsTitle . '&' . $icsLocation . '&' . $icsStart . '&' . $icsEnd);
?>
The resulting $icsQuery
is a concatenated string containing all relevant information regarding your event, that you can use like this:
<a href="<?= url() . $icsQuery ?>">my awesome event</a>
Thus, this link will essentially lead to some specific URL, which your router (see config.php) will interpret. This was how it was set up in my case:
// Generate ics file
c::set('routes',array(
array(
'pattern' => 'ics/(:all)',
'action' => function($allPlaceholder) {
// some regex magic
preg_match('/t%3D%22(.+?)%22%26l%3D%22(.+?)%22%26s%3D%22(.+?)%22%26e%3D%22(.+?)%22/', $allPlaceholder, $m);
header::download(['mime'=>'text/calendar', 'name' => html_entity_decode(rawurldecode($m[1])) . '.ics']);
return snippet('components/ics', ['event' => $m]);
}
)
));
You’ll see in this piece of code that it links to a snippet located in ‘components/ics’. Here is the relevant code:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//whatever you want
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART;TZID=Europe/Zurich:<?= rawurldecode($event[3]) . "\n"; ?>
DTEND;TZID=Europe/Zurich:<?= rawurldecode($event[4]) . "\n"; ?>
SUMMARY:arbitrary text : <?= html_entity_decode(rawurldecode($event[1])) . "\n"; ?>
LOCATION:<?= html_entity_decode(rawurldecode($event[2])) . "\n"; ?>
END:VEVENT
END:VCALENDAR
Should you need more info regarding the formatting of an ICS file, head over to here: https://www.kanzaki.com/docs/ical/
JS-version
With this approach you’ll add all relevant info regarding your event by using data-attributes.
Add this to your template file:
<article class="event"
data-title="<?= $event->event_title()->html() ?>"
data-description="<?= $event->event_description()->escape() ?>"
data-location="<?= $event->event_location()->html() ?>"
data-start="<?= $event->date('Ymd\THis', 'event_dtstart') ?>"
data-end="<?= $event->date('Ymd\THis', 'event_dtend') ?>"
data-uid="<?= $uid = uniqid() ?>">
Then, create the link to download your ics file (I’ve used span
, but it can be anything else):
<span class="link" onclick="ics(this)"><?= event_time($event, 'H\hi') ?></span>
Notice this bit: onclick="ics(this)"
? This calls the javascript function “ics” and adds this specific span node as parameter.
This “ics” function is then defined thanks to the following piece of code, which you’ll have to load in a .js file, or enclosed in a <script></script>
tag:
function ics(e){
// find parent node that matches the "event" class, since the span node doesn't contain any info https://stackoverflow.com/questions/22119673/find-the-closest-ancestor-element-that-has-a-specific-class
while ((e = e.parentElement) && !e.classList.contains('event'));
var e = {
title: e.dataset.title.replace(/<(.|\n)*?>/g, ''),
description: e.dataset.description.replace(/<(.|\n)*?>/g, ''),
location: e.dataset.location.replace(/<(.|\n)*?>/g, ''),
start: e.dataset.start,
end: e.dataset.end,
uid: e.dataset.uid
};
// helper functions
// iso date for ical formats
e._isofix = function(){
var
n = new Date(),
y = n.getFullYear(),
m = e._zp(n.getMonth()+1),
f = e._zp(n.getDate()),
h = e._zp(n.getHours()),
o = e._zp(n.getMinutes()),
s = e._zp(n.getSeconds());
return y+m+f+'T'+h+o+s;
}
//zero padding for data fixes
e._zp = function(s){return ("0"+s).slice(-2);}
e._save = function(fileURL){
// Blob
var blob = new Blob([fileURL], {type : 'text/calendar'});
var objectUrl = window.URL.createObjectURL(blob);
if (navigator.msSaveBlob) { // IE11+ : (has Blob, but not a[download])
navigator.msSaveBlob(blob, 'CIB2018 – '+e.title+'.ics');
} else if (navigator.msSaveOrOpenBlob) { // IE10+ : (has Blob, but not a[download])
navigator.msSaveOrOpenBlob(blob, 'CIB2018 – '+e.title+'.ics');
} else {
// A-download
var anchor = document.createElement('a');
anchor.setAttribute('href', objectUrl);
anchor.setAttribute('download', 'CIB2018 – '+e.title+'.ics');
// Firefox requires the link to be added to the DOM before it can be clicked.
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
setTimeout(function() { // Firefox needs a timeout
window.URL.revokeObjectURL(objectUrl);
}, 0);
}
var now = new Date();
var ics_lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//whatever,
"METHOD:PUBLISH",
"BEGIN:VEVENT",
"UID:"+e.uid,
"DTSTAMP:"+e._isofix(),
"DTSTART;TZID=Europe/Zurich:"+e.start,
"DTEND;TZID=Europe/Zurich:"+e.end,
"SUMMARY;LANGUAGE=fr-ch:"+e.title.replace(/[^ -ù]+/g, ''),
"DESCRIPTION;LANGUAGE=fr-ch:"+e.description.replace(/[^ -ù]+/g, ''),
"URL;VALUE=URI://yoururl,
"LAST-MODIFIED:"+e._isofix(),
"SEQUENCE:0",
"END:VEVENT",
"END:VCALENDAR"
];
var dlurl = ics_lines.join('\r\n');
try {
e._save(dlurl);
}catch(e){
console.log(e);
}
}
You’ll see the result here: https://cib2018.ch/#programme
Caveat: I still haven’t gotten the JS version to work under android…
Good luck!