diff --git a/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.html b/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.html
index 7209f8b0..60d53d7e 100644
--- a/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.html
+++ b/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.html
@@ -16,7 +16,7 @@
edit view display...
deactivate view display...
add to my own profile...
- refresh now
+ refresh now
show nanopub
diff --git a/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.java b/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.java
index f2dff824..6615363f 100644
--- a/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.java
+++ b/src/main/java/com/knowledgepixels/nanodash/component/menu/ViewDisplayMenu.java
@@ -53,7 +53,9 @@ public class ViewDisplayMenu extends BaseDisplayMenu {
* @param viewDisplay the view display this menu acts on (must have a non-null nanopub)
* @param queryRef the query reference used by this view display, or null for a
* query-less view (a header view), which hides the
- * query-dependent entries (show query, full screen, refresh)
+ * query-dependent entries (show query, full screen). "refresh now"
+ * stays: since issue #654 it brings the view definition up to date
+ * too, which is the whole of what a header view has to refresh.
* @param pageResource the page-level resource used to determine whether "adjust" is visible
* @param viewActions the view-level actions to show as top entries (may be empty)
*/
@@ -209,7 +211,9 @@ protected void onConfigure() {
AjaxFallbackLink refreshLink = new AjaxFallbackLink<>("refreshNow") {
@Override
public void onClick(Optional target) {
- ApiCache.clearCache(queryRef, 0);
+ // A header view has no query, and so nothing to mark outdated here — its
+ // whole content comes from the view definition re-checked just below.
+ if (queryRef != null) ApiCache.clearCache(queryRef, 0);
// Bringing a view up to date is not only a matter of re-running its query:
// the view definition itself can have been superseded since this page was
// built, and neither the memoized resolution nor the version the page's
@@ -233,11 +237,12 @@ public void onClick(Optional target) {
if (view == null && target.isPresent()) {
// Not every view display puts results in the page. A query-form view
// shows a form, and the results it leads to live on the page it submits
- // to, so there is nothing here to bring up to date: the query has just
- // been marked outdated for that next submit, and the view definition has
- // been re-checked above. Re-rendering the page on top of that would
- // repaint everything for no visible change — which is what made these
- // views, alone among the display types, flicker on every refresh.
+ // to; a header view has no query at all. Either way there is nothing
+ // here to bring up to date once the view definition has been re-checked
+ // above (and, for the form, its query marked outdated for the next
+ // submit). Re-rendering the page on top of that would repaint everything
+ // for no visible change — which is what made query-form views, alone
+ // among the display types then offering a refresh, flicker on every one.
return;
}
// A view is not always what stands in the page: while it waits for its first
@@ -269,7 +274,10 @@ public void onClick(Optional target) {
}
};
- refreshLink.setVisible(session.getUserIri() != null && queryRef != null);
+ // Offered wherever there is something to bring up to date. That used to mean a query,
+ // but since issue #654 the refresh re-resolves the view definition as well, which is
+ // all a query-less header view consists of — so it is offered there too.
+ refreshLink.setVisible(session.getUserIri() != null && (queryRef != null || shownViewId != null));
addEntry("refreshNow", refreshLink);
BookmarkablePageLink viewDeclarationLink = new BookmarkablePageLink<>("viewDeclaration", ExplorePage.class,
diff --git a/src/main/java/com/knowledgepixels/nanodash/script/nanodash.js b/src/main/java/com/knowledgepixels/nanodash/script/nanodash.js
index f872715b..9211bc6e 100644
--- a/src/main/java/com/knowledgepixels/nanodash/script/nanodash.js
+++ b/src/main/java/com/knowledgepixels/nanodash/script/nanodash.js
@@ -178,12 +178,35 @@ function makeSpinner() {
var UPDATE_SPINNER_DELAY_MS = 250;
/* Backstop for a call that never reports completion, so a spinner cannot get stuck. */
var UPDATE_SPINNER_MAX_MS = 30000;
+/* An update the user explicitly asked for — a view's "refresh now" — is the exception to
+ the delay above: it shows at once and stays long enough to be read. The delay is there so
+ that updates happening *incidentally*, as a side effect of typing or paging, do not
+ flicker; a refresh someone clicked for is the opposite case, and answering it with
+ nothing visible reads as a click that did nothing. Most of these round trips finish in
+ about a tenth of a second, which is why the delay alone left them silent. */
+var REQUESTED_SPINNER_MIN_MS = 600;
var updatingPanels = new Map();
function isVisible(el) {
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
}
+/* The title rows a spinner can go in: a regular view panel's, and a header view's, which
+ is built differently (an h3 under a rule rather than an h4) but sits in the same place
+ and means the same thing. */
+var TITLE_ROW_SELECTOR = ".paneltitlerow, .view-header-titlerow";
+var TITLE_SELECTOR = "h4, h3";
+
+/* Whether an Ajax call came from a control the user clicked to ask for a refresh, as
+ opposed to one where updating is a side effect (a filter field, a paging link). The
+ markup says so: "refresh now" carries the class. */
+function isRequestedRefresh(attributes) {
+ var id = attributes && attributes.c;
+ if (!id || typeof id !== "string") return false;
+ var el = document.getElementById(id);
+ return !!(el && el.classList.contains("refresh-request"));
+}
+
/* The view panel an Ajax call was triggered from, or null for calls that belong to no
single panel — the page-wide lazy-load and refresh-poll timers among them, which is
why they never light up every panel on the page. */
@@ -195,7 +218,7 @@ function findUpdatingPanel(attributes) {
var panel = el.closest('[class*="col-"]');
// A view panel is a column with a title row; anything else (a page-level column, a
// form) is left alone, since the gutter position is meaningless there.
- return panel && panel.querySelector(".paneltitlerow") ? panel : null;
+ return panel && panel.querySelector(TITLE_ROW_SELECTOR) ? panel : null;
}
function showUpdateSpinner(panel) {
@@ -207,9 +230,9 @@ function showUpdateSpinner(panel) {
if (existing && isVisible(existing)) return;
// Right after the title, where the view's own spinner goes; the title row's layout keeps
// it clear of the title icon and of the filter and menu on the right.
- var titleRow = panel.querySelector(".paneltitlerow");
- var title = titleRow ? titleRow.querySelector("h4") : null;
+ var titleRow = panel.querySelector(TITLE_ROW_SELECTOR);
if (!titleRow) return;
+ var title = titleRow.querySelector(TITLE_SELECTOR);
var spinner = makeSpinner();
spinner.title = "Updating...";
panel.classList.add("view-refreshing");
@@ -219,8 +242,18 @@ function showUpdateSpinner(panel) {
function hideUpdateSpinner(panel) {
var state = updatingPanels.get(panel);
- updatingPanels.delete(panel);
if (!state) return;
+ // An explicitly requested refresh keeps its spinner until it has been visible long
+ // enough to register, however quickly the server answered.
+ if (state.minUntil) {
+ var left = state.minUntil - performance.now();
+ if (left > 0) {
+ state.minUntil = null;
+ setTimeout(function () { hideUpdateSpinner(panel); }, left);
+ return;
+ }
+ }
+ updatingPanels.delete(panel);
if (state.showTimer) clearTimeout(state.showTimer);
if (state.maxTimer) clearTimeout(state.maxTimer);
if (!state.spinner) return;
@@ -232,16 +265,21 @@ function hideUpdateSpinner(panel) {
}
}
-function onUpdateStart(panel) {
+function onUpdateStart(panel, requested) {
var state = updatingPanels.get(panel);
if (state) {
state.count++;
return;
}
- state = {count: 1, spinner: null, showTimer: null, maxTimer: null};
+ state = {count: 1, spinner: null, showTimer: null, maxTimer: null, minUntil: null};
updatingPanels.set(panel, state);
- state.showTimer = setTimeout(function () { showUpdateSpinner(panel); }, UPDATE_SPINNER_DELAY_MS);
state.maxTimer = setTimeout(function () { hideUpdateSpinner(panel); }, UPDATE_SPINNER_MAX_MS);
+ if (requested) {
+ state.minUntil = performance.now() + REQUESTED_SPINNER_MIN_MS;
+ showUpdateSpinner(panel);
+ } else {
+ state.showTimer = setTimeout(function () { showUpdateSpinner(panel); }, UPDATE_SPINNER_DELAY_MS);
+ }
}
function onUpdateEnd(panel) {
@@ -255,7 +293,7 @@ function trackAjaxUpdates() {
if (typeof Wicket === "undefined" || !Wicket.Event) return;
Wicket.Event.subscribe("/ajax/call/before", function (jqEvent, attributes) {
var panel = findUpdatingPanel(attributes);
- if (panel) onUpdateStart(panel);
+ if (panel) onUpdateStart(panel, isRequestedRefresh(attributes));
});
Wicket.Event.subscribe("/ajax/call/complete", function (jqEvent, attributes) {
var panel = findUpdatingPanel(attributes);
diff --git a/src/main/webapp/style.css b/src/main/webapp/style.css
index 0f183866..2b92b5cd 100644
--- a/src/main/webapp/style.css
+++ b/src/main/webapp/style.css
@@ -2515,6 +2515,32 @@ p.waiting {
margin-left: 0;
}
+/* A header view's title row is built differently — an h3 above a rule, baseline-aligned,
+ with no filter field — but the spinner means the same thing and goes to the same place.
+ It rides the row's baseline: an empty inline-block has no line box of its own, so its
+ baseline is its bottom edge, which lands the circle on the title's baseline like a
+ capital letter. Centring it in the row instead would sit it 7px high, since the row is
+ taller than the text by the heading's top margin. */
+.view-header-titlerow > .refresh-spinner {
+ width: 16px;
+ height: 16px;
+ border-width: 2px;
+ margin-left: 10px;
+ margin-right: auto;
+ align-self: baseline;
+ flex: none;
+}
+
+/* As in a panel title row: the title stops filling the row so the spinner takes that space
+ instead, and the buttons' auto left margin gives way to the spinner's auto right one. */
+[class*="col-"].section-header .view-header-titlerow:has(> .refresh-spinner) h3 {
+ flex: 0 1 auto;
+}
+
+.view-header-titlerow:has(> .refresh-spinner) > span.buttons {
+ margin-left: 0;
+}
+
/* Standing on its own — a whole page section or panel body with nothing in it yet — the
same spinner is drawn larger: there is no title or message beside it to carry the eye,
and none of the gutter's width limit applies. */