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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<a wicket:id="adjust"><span class="actionmenu-icon">✎</span>edit view display...</a>
<a wicket:id="deactivate"><span class="actionmenu-icon">⊘</span>deactivate view display...</a>
<a wicket:id="addToOwn"><span class="actionmenu-icon">⊕</span>add to my own profile...</a>
<a wicket:id="refreshNow"><span class="actionmenu-icon">↻</span>refresh now</a>
<a wicket:id="refreshNow" class="refresh-request"><span class="actionmenu-icon">↻</span>refresh now</a>
<a wicket:id="viewDeclaration">show nanopub</a>
</wicket:extend>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down Expand Up @@ -209,7 +211,9 @@ protected void onConfigure() {
AjaxFallbackLink<Void> refreshLink = new AjaxFallbackLink<>("refreshNow") {
@Override
public void onClick(Optional<AjaxRequestTarget> 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
Expand All @@ -233,11 +237,12 @@ public void onClick(Optional<AjaxRequestTarget> 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
Expand Down Expand Up @@ -269,7 +274,10 @@ public void onClick(Optional<AjaxRequestTarget> 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<Void> viewDeclarationLink = new BookmarkablePageLink<>("viewDeclaration", ExplorePage.class,
Expand Down
54 changes: 46 additions & 8 deletions src/main/java/com/knowledgepixels/nanodash/script/nanodash.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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) {
Expand All @@ -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");
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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);
Expand Down
26 changes: 26 additions & 0 deletions src/main/webapp/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down