@@ -89,7 +66,7 @@ echo '
';
echo '' . $tPage['title'] . ' ';
-echo checkForEmbeds($tPage['content']);
+echo checkForEmbeds($tPage['content'], $settings);
echo '
diff --git a/js/main.js b/js/main.js
index fbfd403..1f6b5f7 100644
--- a/js/main.js
+++ b/js/main.js
@@ -14,12 +14,46 @@ document.addEventListener("DOMContentLoaded", function() {
let translations = {
"en" : {
+ "More" : "More",
+ "MoreAtMuseumDigital" : "museum-digital",
+ "ObjectAtMuseumDigital" : "Object entry at museum-digital",
+ "Collection" : "Collection",
+ "object_material_technique" : "Material / Technique",
+ "object_dimensions" : "Dimensions",
+ "Metadata" : "Metadata",
+ "LastUpdated" : "Last Updated",
+ "Licence" : "Licence",
+ "Tags" : "Tags",
+ "People" : "People",
+ "Places" : "Places",
+ "Times" : "Times",
+ "Search" : "Search",
+ "SearchingFor" : "Searching for",
+ "... who" : "... who",
+ "... where" : "... where",
+ "... when" : "... when",
+ "eventType1" : "Created",
},
"de" : {
-
- },
- "hu" : {
-
+ "More" : "Mehr",
+ "MoreAtMuseumDigital" : "museum-digital",
+ "ObjectAtMuseumDigital" : "Objekt bei museum-digital",
+ "Collection" : "Sammlung",
+ "object_material_technique" : "Material / Technik",
+ "object_dimensions" : "Ausmaße",
+ "Metadata" : "Metadaten",
+ "LastUpdated" : "Zuletzt geupdatet",
+ "Licence" : "Lizenz",
+ "Tags" : "Schlagworte",
+ "People" : "Personen",
+ "Places" : "Orte",
+ "Times" : "Zeiten",
+ "Search" : "Suche",
+ "SearchingFor" : "Suche nach",
+ "... who" : "... wer",
+ "... where" : "... wo",
+ "... when" : "... wann",
+ "eventType1" : "Hergestellt",
}
};
@@ -108,6 +142,13 @@ document.addEventListener("DOMContentLoaded", function() {
}
}
+ (function() {
+ let toTranslate = document.getElementsByClassName("toTranslate");
+ for (let i = 0, max = toTranslate.length; i < max; i++) {
+ console.log(toTranslate[i].getAttribute("data-content"));
+ toTranslate[i].textContent = getTranslation(translations, toTranslate[i].getAttribute("data-content"));
+ }
+ })();
});
diff --git a/js/mdCalendar.js b/js/mdCalendar.js
new file mode 100644
index 0000000..0a26a7a
--- /dev/null
+++ b/js/mdCalendar.js
@@ -0,0 +1,480 @@
+/**
+ * mdCalendar.js
+ */
+
+document.addEventListener("DOMContentLoaded", function() {
+
+ /**
+ * @var {array} translations Array of all translations.
+ */
+ let translations = {
+ "en" : {
+ "dow0" : "Sunday",
+ "dow1" : "Monday",
+ "dow2" : "Tuesday",
+ "dow3" : "Wednesday",
+ "dow4" : "Thursday",
+ "dow5" : "Friday",
+ "dow6" : "Saturday",
+ "mon0" : "January",
+ "mon1" : "February",
+ "mon2" : "March",
+ "mon3" : "April",
+ "mon4" : "May",
+ "mon5" : "June",
+ "mon6" : "July",
+ "mon7" : "August",
+ "mon8" : "September",
+ "mon9" : "October",
+ "mon10" : "November",
+ "mon11" : "December",
+ "Today" : "Today",
+ "Title" : "Title",
+ "Start" : "Start",
+ "End" : "End",
+ "Location" : "Location"
+ },
+ "de" : {
+ "dow0" : "Sonntag",
+ "dow1" : "Montag",
+ "dow2" : "Dienstag",
+ "dow3" : "Mittwoch",
+ "dow4" : "Donnerstag",
+ "dow5" : "Freitag",
+ "dow6" : "Samstag",
+ "mon0" : "Januar",
+ "mon1" : "Februar",
+ "mon2" : "März",
+ "mon3" : "April",
+ "mon4" : "Mai",
+ "mon5" : "Juni",
+ "mon6" : "Juli",
+ "mon7" : "August",
+ "mon8" : "September",
+ "mon9" : "Oktober",
+ "mon10" : "November",
+ "mon11" : "Dezember",
+ "Today" : "Jetzt",
+ "Title" : "Titel",
+ "Start" : "Beginn",
+ "End" : "Ende",
+ "Location" : "Ort"
+ },
+ "hu" : {
+ "dow0": "Vas\u00e1rnap",
+ "dow1": "H\u00e9tf\u0151",
+ "dow2": "Kedd",
+ "dow3": "Szerda",
+ "dow4": "Cs\u00fct\u00f6rt\u00f6k",
+ "dow5": "P\u00e9ntek",
+ "dow6": "Szombat",
+ "mon0": "Janu\u00e1r",
+ "mon1": "Febru\u00e1r",
+ "mon2": "M\u00e1rcius",
+ "mon3": "\u00c1prilis",
+ "mon4": "M\u00e1jus",
+ "mon5": "J\u00fanius",
+ "mon6": "J\u00falius",
+ "mon7": "Augusztus",
+ "mon8": "Szeptember",
+ "mon9": "Okt\u00f3ber",
+ "mon10": "November",
+ "mon11": "December",
+ "Today": "Ma",
+ "Title": "C\u00edm",
+ "Start": "Nyit",
+ "End": "Bez\u00e1r",
+ "Location": "Helysz\u00edn"
+ }
+ }
+
+ /**
+ * @var {boolean} Toggle debugging.
+ */
+ let debugging = false;
+
+ /**
+ * Function queryPage queries a web page and runs the specified function over the output.
+ *
+ * @param {string} url URL to query.
+ * @param {function} func Callback function to run on the request after loading.
+ * @param {boolean} debug Enable / disable debug mode.
+ *
+ * @return {boolean}
+ */
+ function queryPage(url, func, debug = false) {
+
+ let request = new XMLHttpRequest();
+ request.open('GET', url);
+ request.setRequestHeader("Cache-Control", "no-cache");
+ request.responseType = 'htm';
+ request.send();
+ request.onload = function() {
+
+ func(request, debug);
+ };
+
+ }
+
+ /**
+ * Removes all children of an element.
+ *
+ * @param {string} id ID of the element to tear down.
+ *
+ * @return {void}
+ */
+ function emptyElement(element) {
+ while (element.firstChild) {
+ emptyElement(element.firstChild);
+ }
+ element.parentNode.removeChild(element);
+ if (debugging === true) {
+ console.log("Removed element:");
+ console.log(element);
+ }
+ }
+
+ function tearDownById(id) {
+ let target = document.getElementById(id);
+ if (target !== null) emptyElement(target);
+ }
+
+ /**
+ * Returns a requested translation from an array in the currently used language.
+ *
+ * @param {mixed[]} list Translation variable.
+ * @param {string} specifier Specifies which translation to get.
+ *
+ * @return {string}
+ */
+ function getTranslation(list, specifier) {
+
+ let preferedLang = document.getElementsByTagName("html")[0].getAttribute("lang");
+
+ if (list[preferedLang] !== undefined && list[preferedLang][specifier] !== null) return list[preferedLang][specifier];
+ return list["en"][specifier];
+
+ }
+
+ /**
+ * Function to create calendar.
+ */
+ function createCalendarTable(target, events) {
+
+ let startOfWeek = 1; // The week starts on Monday.
+ let endOfWeek = 0; // The week ends on Sunday.
+
+ let calLocale = document.getElementsByTagName("html")[0].getAttribute("lang");
+ if (calLocale === undefined || calLocale === null) calLocale = "en";
+
+ function removeToolTips() {
+ let toolTips = document.getElementsByClassName("mdCToolTip");
+ for (let i = 0, max = toolTips.length; i < max; i++) {
+ emptyElement(toolTips[i]);
+ }
+ }
+
+ /**
+ * Gets all days to display per month.
+ * Adapted version of function described in https://stackoverflow.com/a/13146828.
+ *
+ * @param {int} The month number, 0 based
+ * @param {int} The year, not zero based, required to account for leap years
+ *
+ * @return {Date[]} List with date objects for each day of the month
+ */
+ function getDaysInMonth(month, year) {
+
+ var date = new Date(year, month, 1);
+ var days = [];
+
+ // If the weekday of the first of the month does not equal 1 (Monday),
+ // get days until the last monday before the month.
+ if (date.getDay() !== startOfWeek) {
+ while (date.getDay() !== startOfWeek) {
+ date.setDate(date.getDate() - 1);
+ days.unshift(new Date(date));
+ }
+ }
+
+ date = new Date(year, month, 1);
+ // Get days of the month
+ while (date.getMonth() === month) {
+ days.push(new Date(date));
+ date.setDate(date.getDate() + 1);
+ }
+
+ // If the weekday of the first of the month does not equal 1 (Monday),
+ // get days until the last monday before the month.
+ while (date.getDay() !== startOfWeek) {
+ days.push(new Date(date));
+ date.setDate(date.getDate() + 1);
+ }
+ return days;
+ }
+
+ events = (function() {
+
+ let colorSchemeLength = 29;
+ for (var i = 0, max = events.length; i < max; i++) {
+ let hash = 0;
+ for (var j = 0, maxj = events[i].name.length; j < maxj; j++) {
+ hash = events[i].name.charCodeAt(j) + hash;
+ }
+ hash = hash % (colorSchemeLength) - 1;
+
+ events[i].color = hash;
+ }
+ return events;
+
+ })();
+
+ let d = new Date();
+
+ let year;
+ if (target.getAttribute("data-year") !== null) year = parseInt(target.getAttribute("data-year"));
+ else year = d.getFullYear();
+
+ let month;
+ if (target.getAttribute("data-month") !== null) month = parseInt(target.getAttribute("data-month") - 1);
+ else month = d.getMonth();
+
+ // Create outer div
+ let mdCalDiv = document.createElement("div");
+ mdCalDiv.classList.add("mdCalendar");
+
+ /**
+ * Creater header line of the calendar.
+ */
+ (function() {
+
+ let mdCalTitleLine = document.createElement("header");
+ mdCalTitleLine.classList.add("mdCTitleLine");
+
+ let mdCalTitle = document.createElement("h3");
+ mdCalTitle.textContent = getTranslation(translations, "mon" + month.toString());
+ if (year != d.getFullYear()) mdCalTitle.textContent = mdCalTitle.textContent + " (" + year + ")";
+ mdCalTitleLine.appendChild(mdCalTitle);
+
+ let mdCalNav = document.createElement("div");
+ mdCalNav.classList.add("mdCNav");
+ mdCalTitleLine.appendChild(mdCalNav);
+
+ let mdCalNavPrev = document.createElement("a");
+ mdCalNavPrev.href = target.getAttribute("data-prev");
+ mdCalNavPrev.rel = "prev";
+ mdCalNavPrev.classList.add("mdCNavPrev");
+ mdCalNav.appendChild(mdCalNavPrev);
+
+ let mdCalNavNext = document.createElement("a");
+ mdCalNavNext.href = target.getAttribute("data-next");
+ mdCalNavNext.rel = "next";
+ mdCalNavNext.classList.add("mdCNavNext");
+ mdCalNav.appendChild(mdCalNavNext);
+
+ let mdCalNavToday = document.createElement("a");
+ mdCalNavToday.href = target.getAttribute("data-today");
+ mdCalNavToday.textContent = getTranslation(translations, "Today");
+ mdCalNav.appendChild(mdCalNavToday);
+
+ mdCalTitleLine.appendChild(mdCalNav);
+ mdCalDiv.appendChild(mdCalTitleLine);
+
+ })();
+
+ /**
+ * Create table.
+ */
+ let table = document.createElement("table");
+ table.classList.add("mdCTable");
+
+ let thead = document.createElement("thead");
+ let theadTr = document.createElement("tr");
+
+ for (let i = 1, max = 7; i < max; i++) {
+ let th = document.createElement("th");
+ th.textContent = getTranslation(translations, "dow" + i.toString());
+ theadTr.appendChild(th);
+ }
+ let th = document.createElement("th");
+ th.textContent = getTranslation(translations, "dow0");
+ theadTr.appendChild(th);
+
+ thead.appendChild(theadTr);
+ table.appendChild(thead);
+
+ let tbody = document.createElement("tbody");
+
+ let days = getDaysInMonth(month, year);
+ let tr;
+
+ let daysTDs = [];
+
+ function createSingleEventOverlay(e, parentElement, data) {
+
+ let toolTip = document.createElement("table");
+ toolTip.classList.add("mdCToolTip");
+
+ let tableData = [];
+ tableData.push(["Title", data["name"]]);
+ if (data["start"] !== undefined) tableData.push(["Start", data["start"]]);
+ if (data["end"] !== undefined) tableData.push(["End", data["end"]]);
+ if (data["place"] !== undefined) tableData.push(["Location", data["place"]]);
+
+ for (let i = 0, max= tableData.length; i < max; i++) {
+ let nameRow = document.createElement("tr");
+ let nameTh = document.createElement("th");
+ nameTh.textContent = getTranslation(translations, tableData[i][0]);
+ nameRow.appendChild(nameTh);
+ let nameTd = document.createElement("td");
+ nameTd.textContent = tableData[i][1];
+ nameRow.appendChild(nameTd);
+ toolTip.appendChild(nameRow);
+ }
+
+ parentElement.appendChild(toolTip);
+
+ }
+
+ for (let i = 0, max= days.length; i < max; i++) {
+
+ // Begin a new table row every Monday.
+ if (days[i].getDay() === startOfWeek) {
+ tr = document.createElement("tr");
+ tbody.appendChild(tr);
+ }
+
+ // Create new TD per day and add appropriate classes.
+ let td = document.createElement("td");
+ if (days[i].getMonth() !== month) td.classList.add("mdCOtherMonth");
+ if (days[i].getYear() == d.getYear() && days[i].getMonth() == d.getMonth() && days[i].getDate() == d.getDate()) td.classList.add("mdCToday");
+
+ tdTitle = document.createElement("time");
+ tdTitle.textContent = days[i].getDate();
+
+ td.appendChild(tdTitle);
+ tr.appendChild(td);
+
+ // Append dates.
+ let dayTime = days[i].getTime();
+ let dayTimeMinusDay = days[i].getTime() + 24 * 3600 * 1000;
+ let dayTimePlusDay = days[i].getTime() - 0 * 3600 * 1000;
+ for (let j = 0, maxj = events.length; j < maxj; j++) {
+ if (Date.parse(events[j].start) > dayTimeMinusDay || Date.parse(events[j].end) < dayTimePlusDay) continue;
+
+ let eventSignifier = document.createElement("a");
+ eventSignifier.classList.add("color" + events[j].color.toString());
+ if (events[j].link) eventSignifier.href = events[j].link;
+
+ let eventSignifierText = document.createElement("span");
+ eventSignifierText.textContent = events[j].name;
+ eventSignifier.appendChild(eventSignifierText);
+
+ eventSignifier.addEventListener('mouseover', function(e) {
+ createSingleEventOverlay(e, eventSignifier, events[j]);
+ });
+ eventSignifier.addEventListener('mouseout', function(e) {
+ removeToolTips();
+ });
+
+ td.appendChild(eventSignifier);
+
+ }
+
+ // Add posibility for overlay.
+ tdTitle.addEventListener('click', function(e) {
+
+ tearDownById("mdCOverlay");
+
+ let overlay = document.createElement("div");
+ overlay.id = "mdCOverlay";
+
+ let eventTitleBar = document.createElement("div");
+ eventTitleBar.classList.add("mdCOverlayTitleBar");
+ overlay.appendChild(eventTitleBar);
+
+ let title = document.createElement("span");
+ title.classList.add("mdCOverlayTitle");
+ title.textContent = days[i].toLocaleDateString(calLocale);
+ eventTitleBar.appendChild(title);
+
+ let closer = document.createElement("span");
+ closer.classList.add("mdCOverlayClose");
+ closer.addEventListener('click', function(e) {
+ if (debugging === true) console.log("Clicked close button: Tearing down daily agenda.");
+ tearDownById("mdCOverlay");
+ });
+ eventTitleBar.appendChild(closer);
+
+ let ul = document.createElement("ul");
+
+ // Append events.
+ for (let j = 0, maxj = events.length; j < maxj; j++) {
+
+ let start = new Date(events[j].start);
+ let end = new Date(events[j].end);
+
+ if (start.getTime() > dayTime || end.getTime() < dayTimePlusDay) continue;
+
+ let eventLi = document.createElement("li");
+
+ let eventSignifier = document.createElement("a");
+ eventSignifier.textContent = events[j].name;
+ if (events[j].link) eventSignifier.href = events[j].link;
+ eventLi.appendChild(eventSignifier);
+
+ let eventP = document.createElement("p");
+ eventP.textContent = start.toLocaleDateString(calLocale) + " - " + end.toLocaleDateString(calLocale);
+ if (events[j].place !== undefined) eventP.textContent = eventP.textContent + ", " + events[j].place;
+ eventLi.appendChild(eventP);
+
+ let eventDesc = document.createElement("p");
+ eventDesc.textContent = events[j].description;
+ eventLi.appendChild(eventDesc);
+
+ ul.appendChild(eventLi);
+
+ }
+
+ overlay.appendChild(ul);
+ document.getElementsByTagName("body")[0].appendChild(overlay);
+
+ });
+
+ if (td.childElementCount > 6) td.classList.add("mdCManyElements");
+ daysTDs[days[i].getFullYear() + "-" + days[i].getMonth() + "-" + days[i].getDate()] = td;
+
+ }
+
+ table.appendChild(tbody);
+ mdCalDiv.appendChild(table);
+
+ target.appendChild(mdCalDiv);
+
+ // Enable closing overlay by pressing escape.
+ document.addEventListener('keydown', function(e) {
+ if (e.keyCode !== 27) return;
+ if (debugging === true) console.log("Pressed escape: Tearing down daily agenda.");
+ tearDownById("mdCOverlay");
+ });
+
+ return daysTDs;
+ }
+
+ (function() {
+
+ let calendars = document.getElementsByClassName("mdCalendar");
+ for (let i = 0, max = calendars.length; i < max; i++) {
+
+ queryPage(
+ encodeURI(calendars[i].getAttribute("data-src")),
+ function (request) {
+ if (debugging === true) console.log("Loaded\n" + request.response);
+ let elements = JSON.parse(request.response);
+ let tCalendar = createCalendarTable(calendars[i], elements);
+ });
+ }
+ }
+ )();
+
+});
diff --git a/object.php b/object.php
new file mode 100644
index 0000000..10e2811
--- /dev/null
+++ b/object.php
@@ -0,0 +1,66 @@
+
+ */
+
+// Include functions and settings.
+
+require_once __DIR__ . "/inc/functions.php";
+
+// Check validity of request.
+
+if (!isset($_GET['id']) or !is_numeric($_GET['id'])) {
+ echo printErrorPage("Object does not exist.");
+ return;
+}
+
+// Ensure working environment for frontend.
+
+ensureEnvironment();
+$pages = loadPublicPages(); // Load overview of pages.
+
+$contents = json_decode(queryCachePage($settings['mdVersion'] . "?t=objekt&oges=" . urlencode($_GET['id']) . "&output=json", "object", $settings), true);
+
+if (!$contents || (isset($contents[0]) and $contents[0] == "There is no object with this ID yet.")) {
+ echo printErrorPage("Temporarily unavailable.");
+ return;
+}
+
+/*
+ * Output
+ */
+$addToHead = '
+
';
+
+echo printPublicHead($settings, $_GET['id'], $settings['pageTitle'] . " - " . $contents['object_name'], $settings['logo'], $addToHead);
+echo printPublicHeader($settings['pageTitle']);
+echo printStaticPagePart("banner", "header"); // Print aside (if need be)
+
+echo generatePublicNav($pages);
+
+echo '
+
+';
+
+// Print main content
+echo '
+ ';
+
+echo drawObjectDetails($contents, $settings);
+
+echo '
+
+';
+
+echo printStaticPagePart("aside", "aside"); // Print aside (if need be)
+
+echo '
+
';
+
+echo printStaticPagePart("footer", "footer"); // Print footer (if need be)
+
+echo printPublicEnd();
+
+?>
diff --git a/search.php b/search.php
new file mode 100644
index 0000000..88907d7
--- /dev/null
+++ b/search.php
@@ -0,0 +1,73 @@
+
+ */
+
+// Include functions and settings.
+
+require_once __DIR__ . "/inc/functions.php";
+require_once __DIR__ . "/inc/search.php";
+
+// Ensure working environment for frontend.
+
+ensureEnvironment();
+$pages = loadPublicPages(); // Load overview of pages.
+
+/*
+ * Load data.
+ */
+
+/*
+ * Output
+ */
+
+echo printPublicHead($settings, "search", $settings['pageTitle'], $settings['logo']);
+echo printPublicHeader($settings['pageTitle']);
+echo printStaticPagePart("banner", "header"); // Print aside (if need be)
+
+echo generatePublicNav($pages);
+
+echo '
+
+';
+
+// Print main content
+echo '
+ ';
+
+if (!isset($_GET['q'])) {
+
+ echo '
+
+
+ ';
+
+}
+else {
+
+ echo ' "' . $_GET['q'] . '" ';
+ $resultsInPages = searchInPages($_GET['q']);
+ print_r($resultsInPages);
+
+}
+
+echo '
+
+';
+
+echo printStaticPagePart("aside", "aside"); // Print aside (if need be)
+
+echo '
+
';
+
+echo printStaticPagePart("footer", "footer"); // Print footer (if need be)
+
+echo printPublicEnd();
+
+?>
diff --git a/themes/default/default.css b/themes/default/default.css
index f7ed95d..1c5ab52 100644
--- a/themes/default/default.css
+++ b/themes/default/default.css
@@ -2,6 +2,12 @@
* Default theme for the backend of md:cms.
*/
+/****************************
+ * Imports
+ */
+
+@import 'mdEmbeds.css';
+
/****************************
* Load fonts
*/
@@ -82,7 +88,7 @@ aside > * { display: block; padding: 1em; border: 1px solid #D6D6D6; }
body > header:nth-child(2) { margin: 0; padding: 0; }
body > header:nth-child(2) p { margin: 0; padding: 0; }
-body > header:nth-child(2) img { width: 100vw; height: 20vh; object-fit: cover; margin-bottom: -.4em; }
+body > header:nth-child(2) img { width: 100%; height: 20vh; object-fit: cover; margin-bottom: -.4em; }
/************
* Main wrapper
@@ -104,7 +110,7 @@ body > nav { display: block; background: #212121; color: #EEE;
border: 1px solid #D6D6D6; border-width: 0; padding: 0 5vw; }
body > nav ul { list-style: none; margin: 0; padding: 0 0; }
body > nav li { position: relative; }
-body > nav > ul { display: block; border-bottom: 1px solid #D6D6D6; }
+body > nav > ul { display: block; }
body > nav > ul > li { display: inline-block; }
body > nav > ul > li ul { position: absolute; left: 0; top: 100%;
background: #424242; color: #EEE; z-index: 2; animation: fade-in .4s; }
diff --git a/themes/default/mdEmbeds.css b/themes/default/mdEmbeds.css
new file mode 100644
index 0000000..664979e
--- /dev/null
+++ b/themes/default/mdEmbeds.css
@@ -0,0 +1,54 @@
+/**
+ * Default theme for the backend of md:cms:
+ * Styles for data embedded from museum-digital.
+ */
+
+/**********
+ * Simple tiles for displaying an object.
+ */
+
+div.objTile { display: inline-block; width: 200px; border: 1px solid #D6D6D6; text-align: center;
+ transition: background .4s, box-shadow .4s; }
+div.objTile > div { position: relative; padding: .5em .5em 1.5em .5em; }
+div.objTile h4 { margin: 0; padding: 0 .5em; }
+div.objTile img { max-width: 198px; }
+div.objTile:hover { background: #F2F2F2; box-shadow: 1px 1px 3px #D6D6D6, -1px -1px 3px #D6D6D6; }
+
+div.objTile > div > div { color: #888; font-size: .8em; }
+div.objTile > div > div > a { position: absolute; top: 100%; left: -1px;
+ display: none; max-width: 50%; padding: .3em;
+ background: #FFF; text-align: left; border: 1px solid #D6D6D6;
+ transition: background .4s, color .4s; }
+
+div.objTile:hover > div > div > a { display: block; animation: fade-in .4s; }
+div.objTile > div > div > a:last-child { left: initial; right: -1px; text-align: right; }
+div.objTile > div > div > a:hover { background: #EEE; color: #212121; }
+
+/**********
+ * Object details.
+ */
+
+div.objDetails { }
+div.objDetails dt { font-weight: bold; margin-top: .5em; }
+div.objDetails dd { margin-left: 0; padding-left: 0; }
+div.objDetails .objMainImg { max-width: 200px; margin: .5em 1em 1em 0; float: left;
+ border: 1px solid #D6D6D6; transition: max-width .4s; }
+div.objDetails .objMainImg:hover { max-width: 100vw; max-height: 80vh; float: initial; }
+
+.metadataLine { margin-top: 2.5em; color: #666; border-top: 1px solid #D6D6D6; }
+.metadataLine > span { margin-right: 2em; font-size: .95em; font-style: italic; }
+
+/*****
+ * Events on object detail page.
+ */
+
+.events { display: block; max-width: 100%; width: auto; margin: 0 0 1em 0; }
+.events > * { display: inline-block; margin: 0 1em 1em 0; padding: 0 1em; border: 1px solid #D6D6D6;
+ vertical-align: top; transition: box-shadow .4s; }
+.events h5 { margin: 0; padding: .5em 1em; font-size: .9em; background: #F2F2F2; color: #646464; }
+.events tr * { padding: .1em .5em; vertical-align: middle; font-size: .95em; }
+.events th { font-weight: normal; color: #646464; }
+.events table { margin: .5em 0; }
+
+.events > *:hover { box-shadow: 1px 1px 3px #D6D6D6, -1px -1px 3px #D6D6D6; }
+.events tr:hover { background: initial; }
diff --git a/themes/imports.css b/themes/imports.css
new file mode 100644
index 0000000..81ef84f
--- /dev/null
+++ b/themes/imports.css
@@ -0,0 +1 @@
+@import 'mdCalendar.css';
diff --git a/themes/mdCalendar.css b/themes/mdCalendar.css
new file mode 100644
index 0000000..fc1ba5c
--- /dev/null
+++ b/themes/mdCalendar.css
@@ -0,0 +1,151 @@
+/**
+ * CSS file for mdCalendar.
+ */
+
+.mdCalendar { display: block; }
+.mdCTable { width: 100%; table-layout: fixed; font-size: .9em; }
+
+/**
+ * Table
+ */
+
+.mdCTable thead tr { border: 1px solid #D6D6D6; }
+
+.mdCTable th { color: #646464; font-weight: normal; text-align: center; }
+.mdCTable td { padding: 0; height: 10em; background: #FFF; border: 1px solid #D6D6D6; vertical-align: top; }
+.mdCTable *:hover { z-index: 2; }
+
+.mdCTable time { display: block; color: #646464; border-bottom: 1px dashed #D6D6D6; font-weight: bold;
+ cursor: pointer; transition: background .4s, color .4s; }
+.mdCTable time:hover { background: #03A9F4; color: #FFF; }
+.mdCTable td > * { display: block; padding: .3em .5em; line-height: 1.2em; }
+
+.mdCTable td > a { position: relative; margin: .2em; padding-left: .7em;
+ background: rgba(240, 240, 240, .2); color: #424242; border: 1px solid #D6D6D6;
+ cursor: pointer; transition: background .4s, color .4s; }
+.mdCTable td > a:hover { background: #333; color: #FFF; z-index: 2; }
+
+.mdCTable td > a:before { content: " "; position: absolute; left: 0; top: 0; width: .5em; height: 100%; }
+
+.mdCTable .mdCToday { background: #FAFAFA; }
+.mdCTable .mdCOtherMonth { color: #646464; background: #EEE; }
+
+/**
+ * Table head.
+ */
+
+.mdCTitleLine { display: table; width: 100%; margin: .5em 0; padding: 0; }
+.mdCTitleLine > * { display: table-cell; vertical-align: middle; }
+
+.mdCTitleLine > h3 { font-weight: normal; color: #424242; }
+
+.mdCNav { width: 200px; color: #424242; text-align: right; }
+
+.mdCNav > * { transition: background .4s, color .4s; font-size: 1em; padding: .5em; }
+.mdCNav > *:hover { background: #F2F2F2; color: #000; }
+.mdCNavPrev:before { content: "<"; }
+.mdCNavNext:before { content: ">"; }
+
+/**
+ * Alternative display of events in case there are too many for a day.
+ */
+
+.mdCTable td.mdCManyElements > a { display: inline-block; height: 1.1em; width: 1.1em; }
+.mdCTable td.mdCManyElements > a:before { width: 100%; }
+.mdCTable td.mdCManyElements > a span { display: none; }
+
+@media screen and (max-width: 75em) {
+ .mdCTable td > a { display: inline-block; height: 1.1em; width: 1.1em; }
+ .mdCTable td > a span { display: none; }
+ .mdCTable td > a:before { width: 100%; }
+}
+
+/**
+ * Tooltips
+ */
+.mdCToolTip { position: absolute; top: calc(100% + 22px); left: 50%; transform: translate(-50%, 0);
+ width: 300px; padding: 1em 0;
+ background: #FFF; border: 1px solid #646464;
+ z-index: 1000; animation: fade-in-tooltip .4s; }
+
+.mdCToolTip:before,
+.mdCToolTip:after { content: " "; position: absolute; bottom: calc(100% - 1px); left: 50%; transform: translate(-50%, 0);
+ display: block; width: 0; height: 0;
+ border-left: 21px solid transparent; border-right: 21px solid transparent;
+ border-bottom: 21px solid #FFF; z-index: 1002; }
+.mdCToolTip:after { border-left: 22px solid transparent; border-right: 22px solid transparent;
+ border-bottom: 22px solid #646464; z-index: 1001; }
+.mdCToolTip tr * { height: auto; padding: .5em; vertical-align: top; text-align: left; color: #424242; border: 0; }
+.mdCToolTip tr:first-child * { padding-top: 1em; }
+.mdCToolTip tr:last-child * { padding-bottom: 1em; }
+
+/**
+ * Overlay
+ */
+
+#mdCOverlay { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
+ max-height: 80vh; width: 600px; max-width: 80vw; background: #FFF;
+ box-shadow: 2px 2px 4px #D6D6D6, -2px -2px 4px #D6D6D6;
+ overflow-y: auto; z-index: 1003; }
+
+.mdCOverlayTitleBar { display: table; width: 100%; }
+.mdCOverlayTitleBar > * { display: table-cell; padding: .5rem 1.5rem; background: #EEE; color: #000; }
+
+.mdCOverlayClose { width: 5em; text-align: right; }
+.mdCOverlayClose:before { content: "x"; display: inline-block; padding: .1em .7em .3em .7em;
+ background: #03A9F4; color: #FFF; text-align: center;
+ border-radius: 100%; cursor: pointer; transition: background .4s; color .4s; }
+.mdCOverlayClose:hover:before { background: #FFF; color: #03A9F4; }
+
+#mdCOverlay ul { list-style: none; margin: 1em 1.5rem; padding: 0; }
+#mdCOverlay li { margin: 0; padding: 1em 0; border-bottom: 1px solid #D6D6D6; }
+#mdCOverlay li > a:first-child { color: #424242; font-weight: bold; }
+
+/**
+ * @var {string[]} colorScheme Material design colors as taken from https://material.io/design/color/the-color-system.html#tools-for-picking-colors
+ */
+
+.color0:before { background: #F44336; }
+.color1:before { background: #E53935; }
+.color2:before { background: #D32F2F; }
+.color3:before { background: #C62828; }
+.color4:before { background: #B71C1C; }
+
+.color5:before { background: #3F51B5; }
+.color6:before { background: #3949AB; }
+.color7:before { background: #303F9F; }
+.color8:before { background: #283593; }
+.color9:before { background: #1A237E; }
+
+.color10:before { background: #2196F3; }
+.color11:before { background: #1E88E5; }
+.color12:before { background: #1976D2; }
+.color13:before { background: #1565C0; }
+.color14:before { background: #0D47A1; }
+
+.color15:before { background: #03A9F4; }
+.color16:before { background: #039BE5; }
+.color17:before { background: #0288D1; }
+.color18:before { background: #0277BD; }
+.color19:before { background: #01579B; }
+
+.color20:before { background: #00BCD4; }
+.color21:before { background: #00ACC1; }
+.color22:before { background: #0097A7; }
+.color23:before { background: #00838F; }
+.color24:before { background: #006064; }
+
+.color25:before { background: #4CAF50; }
+.color26:before { background: #43A047; }
+.color27:before { background: #388E3C; }
+.color28:before { background: #2E7D32; }
+.color29:before { background: #1B5E20; }
+
+/**
+ * Animations
+ */
+
+@keyframes fade-in-tooltip {
+ from { top: calc(100% + 19px); opacity: 0.1; }
+ to { top: calc(100% + 22px); opacity: 1.0; }
+}