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. */