Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/main/java/com/knowledgepixels/nanodash/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.string.Strings;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Statement;
Expand Down Expand Up @@ -410,6 +411,39 @@ public static boolean looksLikeHtml(String value) {
return LEADING_TAG.matcher(value).find();
}

/**
* Matches an xsd:dateTime-style literal with a time component, e.g.
* "2026-04-16T08:27:12.954Z" or "2026-04-16T08:27:12+02:00".
*/
private static final Pattern DATETIME_LITERAL =
Pattern.compile("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$");

/**
* Checks whether a (raw query-result) string looks like an ISO-8601 date-time literal.
*
* @param value the string to check
* @return true if the string parses as an xsd:dateTime-style value
*/
public static boolean isDateTimeLiteral(String value) {
return value != null && DATETIME_LITERAL.matcher(value).matches();
}

/**
* Renders a {@code <time>} element for an ISO-8601 date-time value. The machine-readable
* value goes in the {@code datetime} attribute; client-side script (nanodash.js) rewrites
* the visible text to a relative form ("10 minutes ago") in the viewer's local timezone and
* puts the absolute date-time in the tooltip. If script does not run, {@code fallbackText}
* remains visible.
*
* @param isoValue the ISO-8601 date-time string (machine-readable)
* @param fallbackText the human-readable text shown when script is unavailable
* @return an HTML {@code <time>} element string (caller must render with escaping disabled)
*/
public static String friendlyDateHtml(String isoValue, String fallbackText) {
return "<time class=\"friendly-date\" datetime=\"" + Strings.escapeMarkup(isoValue) + "\">"
+ Strings.escapeMarkup(fallbackText) + "</time>";
}

/**
* Converts PageParameters to a URL-encoded string representation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ protected void populateItem(Item<IRI> item) {
} else {
WebMarkupContainer footer = new WebMarkupContainer("footer");
if (n.getCreationTime() != null) {
footer.add(new Label("datetime", simpleDateTimeFormat.format(n.getCreationTime().getTime())));
// Friendly relative time client-side; server-formatted date is the no-script fallback.
String iso = n.getCreationTime().toInstant().toString();
String fallback = simpleDateTimeFormat.format(n.getCreationTime().getTime());
footer.add(new Label("datetime", Utils.friendlyDateHtml(iso, fallback)).setEscapeModelStrings(false));
} else {
footer.add(new Label("datetime", "(undated)"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ public void populateItem(Item<ICellPopulator<ApiResponseEntry>> cellItem, String
} else {
if (key.startsWith("pubkey")) {
cellItem.add(new Label(componentId, value).add(new AttributeAppender("style", "overflow-wrap: anywhere;")));
} else if (Utils.isDateTimeLiteral(value)) {
// Show a friendly relative time (client-side); raw ISO value stays as no-script fallback.
cellItem.add(new Label(componentId, Utils.friendlyDateHtml(value, value)).setEscapeModelStrings(false));
} else {
Label cellLabel;
if (Utils.looksLikeHtml(value)) {
Expand Down
41 changes: 40 additions & 1 deletion src/main/java/com/knowledgepixels/nanodash/script/nanodash.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,49 @@ function wrapLeadingEmoji() {
node.parentNode.insertBefore(span, node);
});
}
/* Friendly date rendering — turns <time class="friendly-date" datetime="..."> into a
relative form ("10 minutes ago") in the viewer's local timezone, with the absolute
date-time in the tooltip. Falls back silently to the server-rendered text if the value
does not parse. No jQuery dependency; safe to call repeatedly (idempotent). */
function friendlyRelative(date, absDateFallback) {
var diffSec = Math.round((date.getTime() - Date.now()) / 1000); // negative = past
if (Math.abs(diffSec) < 45) return "just now";
if (typeof Intl === "undefined" || !Intl.RelativeTimeFormat) return absDateFallback;
var rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
var min = Math.round(diffSec / 60);
if (Math.abs(min) < 60) return rtf.format(min, "minute");
var hr = Math.round(diffSec / 3600);
if (Math.abs(hr) < 24) return rtf.format(hr, "hour");
var day = Math.round(diffSec / 86400);
if (Math.abs(day) < 7) return rtf.format(day, "day");
return absDateFallback; // older than a week → absolute date
}

function renderFriendlyDates(root) {
var scope = root || document;
scope.querySelectorAll("time.friendly-date[datetime]").forEach(function (el) {
if (el.dataset.friendlyRendered === "1") return;
var d = new Date(el.getAttribute("datetime"));
if (isNaN(d.getTime())) return; // unparseable → leave server-rendered text as-is
el.dataset.friendlyRendered = "1";
// Full, pretty tooltip: weekday + full date + time with seconds and timezone,
// e.g. "Thursday, 16 April 2026 at 10:27:12 CEST".
var absFull = d.toLocaleString(undefined, { dateStyle: "full", timeStyle: "long" });
var absDate = d.toLocaleDateString(undefined, { dateStyle: "medium" });
el.setAttribute("title", absFull);
el.textContent = friendlyRelative(d, absDate);
});
}

document.addEventListener("DOMContentLoaded", function() {
wrapLeadingEmoji();
renderFriendlyDates();
// Re-run after Wicket AJAX calls complete (dynamically loaded content)
if (typeof Wicket !== "undefined" && Wicket.Event) {
Wicket.Event.subscribe("/ajax/call/complete", wrapLeadingEmoji);
Wicket.Event.subscribe("/ajax/call/complete", function() {
wrapLeadingEmoji();
renderFriendlyDates();
});
}
});

Expand All @@ -38,6 +76,7 @@ $(window).on('load', updateElements);

function updateElements() {
wrapLeadingEmoji();
renderFriendlyDates();
adjustValueWidths();
setCollapseOverflow();
collapseNanopubAssertions();
Expand Down