// Fire when a tab is selected/clicked.
$(document).on('shown.bs.tab', '[data-toggle="tab"]', function(event) {
- // When we switch tabs, remove "active" from any refreshable panes within
- // the previous tab content so they don't continue to refresh unnecessarily, and
- // add "active" to any refreshable panes under the newly shown tab content.
-
- var tgt = $($(event.relatedTarget).attr('href'));
- $(".pane-anchor", tgt).each(function (i, e) {
- var a = $($(e).attr('href'));
- a.removeClass("active");
- });
-
- tgt = $($(event.target).attr('href'));
- $(".pane-anchor", tgt).each(function (i, e) {
- var a = $($(e).attr('href'));
- a.addClass("active");
- });
-
- if (!$(event.target).hasClass("pane-loaded")) {
- // pane needs to be loaded
- $(event.target).trigger('arv:pane:reload');
- }
+ // reload the pane (unless it's already loaded)
+ $($(event.target).attr('href')).
+ not('.pane-loaded').
+ trigger('arv:pane:reload');
});
// Ask a refreshable pane to reload via ajax.
//
-// Target of this event is the anchor element that manages the pane. A reload
-// consists of an AJAX call to load the "data-pane-content-url" and replace the
-// contents of the DOM node pointed to by "href".
+// Target of this event is the DOM element to be updated. A reload
+// consists of an AJAX call to load the "data-pane-content-url" and
+// replace the content of the target element with the retrieved HTML.
//
-// There are four CSS classes set on the object to indicate its state:
+// There are four CSS classes set on the element to indicate its state:
// pane-loading, pane-stale, pane-loaded, pane-reload-pending
//
// There are five states based on the presence or absence of css classes:
//
-// 1. no pane-* states means the pane must be loaded when the pane becomes active
+// 1. Absence of any pane-* states means the pane is empty, and should
+// be loaded as soon as it becomes visible.
//
-// 2. "pane-loading" means an AJAX call has been made to reload the pane and we are
-// waiting on a result
+// 2. "pane-loading" means an AJAX call has been made to reload the
+// pane and we are waiting on a result.
//
-// 3. "pane-loading pane-stale" indicates a pane that is already loading has
-// been invalidated and should schedule a reload immediately when the current
-// load completes. (This happens when there is a cluster of events, where the
-// reload is triggered by the first event, but we want ensure that we
-// eventually load the final quiescent state).
+// 3. "pane-loading pane-stale" means the pane is loading, but has
+// already been invalidated and should schedule a reload as soon as
+// possible after the current load completes. (This happens when there
+// is a cluster of events, where the reload is triggered by the first
+// event, but we want ensure that we eventually load the final
+// quiescent state).
//
-// 4. "pane-loaded" means the pane is up to date
+// 4. "pane-loaded" means the pane is up to date.
//
-// 5. "pane-loaded pane-reload-pending" indicates a reload is scheduled (but has
-// not started yet), suppressing scheduling of any further reloads.
+// 5. "pane-loaded pane-reload-pending" means a reload is needed, and
+// has been scheduled, but has not started because the pane's
+// minimum-time-between-reloads throttle has not yet been reached.
//
-$(document).on('arv:pane:reload', function(e) {
- e.stopPropagation();
+$(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) {
+ if (this != e.target) {
+ // An arv:pane:reload event was sent to an element (e.target)
+ // which happens to have an ancestor (this) matching the above
+ // '[data-pane-content-url]' selector. This happens because
+ // events bubble up the DOM on their way to document. However,
+ // here we only care about events delivered directly to _this_
+ // selected element (i.e., this==e.target), not ones delivered
+ // to its children. The event "e" is uninteresting here.
+ return;
+ }
- // '$anchor' is the event target, which is a .pane-anchor or a bootstrap
- // tab anchor. This is the element that stores the state of the pane. The
- // actual element that will contain the content is pointed to in the 'href'
- // attribute of etarget.
- var $anchor = $(e.target);
+ // $pane, the event target, is an element whose content is to be
+ // replaced. Pseudoclasses on $pane (pane-loading, etc) encode the
+ // current loading state.
+ var $pane = $(this);
- if ($anchor.hasClass('pane-loading')) {
+ if ($pane.hasClass('pane-loading')) {
// Already loading, mark stale to schedule a reload after this one.
- $anchor.addClass('pane-stale');
+ $pane.addClass('pane-stale');
return;
}
- var throttle = $anchor.attr('data-load-throttle');
- if (!throttle) {
- throttle = 15000;
- }
+ // The default throttle (mininum milliseconds between refreshes)
+ // can be overridden by an .arv-log-refresh-control element inside
+ // the pane -- or, failing that, the pane element itself -- with a
+ // data-load-throttle attribute. This allows the server to adjust
+ // the throttle depending on the pane content.
+ var throttle =
+ $pane.find('.arv-log-refresh-control').attr('data-load-throttle') ||
+ $pane.attr('data-load-throttle') ||
+ 15000;
var now = (new Date()).getTime();
- var loaded_at = $anchor.attr('data-loaded-at');
+ var loaded_at = $pane.attr('data-loaded-at');
var since_last_load = now - loaded_at;
if (loaded_at && (since_last_load < throttle)) {
- if (!$anchor.hasClass('pane-reload-pending')) {
- $anchor.addClass('pane-reload-pending');
+ if (!$pane.hasClass('pane-reload-pending')) {
+ $pane.addClass('pane-reload-pending');
setTimeout((function() {
- $anchor.trigger('arv:pane:reload');
+ $pane.trigger('arv:pane:reload');
}), throttle - since_last_load);
}
return;
}
// We know this doesn't have 'pane-loading' because we tested for it above
- $anchor.removeClass('pane-reload-pending');
- $anchor.removeClass('pane-loaded');
- $anchor.removeClass('pane-stale');
+ $pane.removeClass('pane-reload-pending');
+ $pane.removeClass('pane-loaded');
+ $pane.removeClass('pane-stale');
- // $pane is the actual content area that is going to be updated.
- var $pane = $($anchor.attr('href'));
- if ($pane.hasClass('active')) {
- $anchor.addClass('pane-loading');
+ if (!$pane.hasClass('active') &&
+ $pane.parent().hasClass('tab-content')) {
+ // $pane is one of the content areas in a bootstrap tabs
+ // widget, and it isn't the currently selected tab. If and
+ // when the user does select the corresponding tab, it will
+ // get a shown.bs.tab event, which will invoke this reload
+ // function again (see handler above). For now, we just insert
+ // a spinner, which will be displayed while the new content is
+ // loading.
+ $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
+ return;
+ }
- var content_url = $anchor.attr('data-pane-content-url');
- $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}).
- done(function(data, status, jqxhr) {
- // Preserve collapsed state
- var collapsable = {};
- $(".collapse", this).each(function(i, c) {
- collapsable[c.id] = $(c).hasClass('in');
- });
- var tmp = $(data);
- $(".collapse", tmp).each(function(i, c) {
- if (collapsable[c.id]) {
- $(c).addClass('in');
- } else {
- $(c).removeClass('in');
- }
- });
- this.html(tmp);
- $anchor.removeClass('pane-loading');
- $anchor.addClass('pane-loaded');
- $anchor.attr('data-loaded-at', (new Date()).getTime());
- this.trigger('arv:pane:loaded');
+ $pane.addClass('pane-loading');
- if ($anchor.hasClass('pane-stale')) {
- $anchor.trigger('arv:pane:reload');
+ var content_url = $pane.attr('data-pane-content-url');
+ $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}).
+ done(function(data, status, jqxhr) {
+ var $pane = this;
+ // Preserve collapsed state
+ var collapsable = {};
+ $(".collapse", this).each(function(i, c) {
+ collapsable[c.id] = $(c).hasClass('in');
+ });
+ var tmp = $(data);
+ $(".collapse", tmp).each(function(i, c) {
+ if (collapsable[c.id]) {
+ $(c).addClass('in');
+ } else {
+ $(c).removeClass('in');
+ }
+ });
+ $pane.html(tmp);
+ $pane.removeClass('pane-loading');
+ $pane.addClass('pane-loaded');
+ $pane.attr('data-loaded-at', (new Date()).getTime());
+ $pane.trigger('arv:pane:loaded', [$pane]);
+
+ if ($pane.hasClass('pane-stale')) {
+ $pane.trigger('arv:pane:reload');
+ }
+ }).fail(function(jqxhr, status, error) {
+ var $pane = this;
+ var errhtml;
+ var contentType = jqxhr.getResponseHeader('Content-Type');
+ if (jqxhr.readyState == 0 || jqxhr.status == 0) {
+ if ($pane.attr('data-loaded-at') > 0) {
+ // Stale content is already present. Leave it
+ // there while loading the next page.
+ $pane.removeClass('pane-loading');
+ $pane.addClass('pane-loaded');
+ // ...but schedule another refresh (after a
+ // throttle delay) in case the act of navigating
+ // away gets cancelled itself, leaving this page
+ // with content that we know is stale.
+ $pane.addClass('pane-stale');
+ $pane.attr('data-loaded-at', (new Date()).getTime());
+ $pane.trigger('arv:pane:reload');
+ return;
}
- }).fail(function(jqxhr, status, error) {
- var errhtml;
- if (jqxhr.getResponseHeader('Content-Type').match(/\btext\/html\b/)) {
- var $response = $(jqxhr.responseText);
- var $wrapper = $('div#page-wrapper', $response);
- if ($wrapper.length) {
- errhtml = $wrapper.html();
- } else {
- errhtml = jqxhr.responseText;
- }
+ errhtml = "Cancelled.";
+ } else if (contentType && contentType.match(/\btext\/html\b/)) {
+ var $response = $(jqxhr.responseText);
+ var $wrapper = $('div#page-wrapper', $response);
+ if ($wrapper.length) {
+ errhtml = $wrapper.html();
} else {
- errhtml = ("An error occurred: " +
- (jqxhr.responseText || status)).
- replace(/&/g, '&').
- replace(/</g, '<').
- replace(/>/g, '>');
+ errhtml = jqxhr.responseText;
}
- this.html('<div><p>' +
- '<a href="#" class="btn btn-primary tab_reload">' +
- '<i class="fa fa-fw fa-refresh"></i> ' +
- 'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
- $('.tab_reload', this).click(function() {
- this.html('<div class="spinner spinner-32px spinner-h-center"></div>');
- $anchor.trigger('arv:pane:reload');
- });
- // We want to render the error in an iframe, in order to
- // avoid conflicts with the main page's element ids, etc.
- // In order to do that dynamically, we have to set a
- // timeout on the iframe window to load our HTML *after*
- // the default source (e.g., about:blank) has loaded.
- var iframe = $('iframe', this)[0];
- iframe.contentWindow.setTimeout(function() {
- $('body', iframe.contentDocument).html(errhtml);
- iframe.height = iframe.contentDocument.body.scrollHeight + "px";
- }, 1);
- $anchor.removeClass('pane-loading');
- $anchor.addClass('pane-loaded');
+ } else {
+ errhtml = ("An error occurred: " +
+ (jqxhr.responseText || status)).
+ replace(/&/g, '&').
+ replace(/</g, '<').
+ replace(/>/g, '>');
+ }
+ $pane.html('<div class="pane-error-display"><p>' +
+ '<a href="#" class="btn btn-primary tab_reload">' +
+ '<i class="fa fa-fw fa-refresh"></i> ' +
+ 'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
+ $('.tab_reload', $pane).click(function() {
+ $(this).
+ html('<div class="spinner spinner-32px spinner-h-center"></div>').
+ closest('.pane-loaded').
+ attr('data-loaded-at', 0).
+ trigger('arv:pane:reload');
});
- } else {
- // When the user selects e.target tab, show a spinner instead of
- // old content while loading.
- $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
- }
+ // We want to render the error in an iframe, in order to
+ // avoid conflicts with the main page's element ids, etc.
+ // In order to do that dynamically, we have to set a
+ // timeout on the iframe window to load our HTML *after*
+ // the default source (e.g., about:blank) has loaded.
+ var iframe = $('iframe', $pane)[0];
+ iframe.contentWindow.setTimeout(function() {
+ $('body', iframe.contentDocument).html(errhtml);
+ iframe.height = iframe.contentDocument.body.scrollHeight + "px";
+ }, 1);
+ $pane.removeClass('pane-loading');
+ $pane.addClass('pane-loaded');
+ });
});
// Mark all panes as stale/dirty. Refresh any 'active' panes.
$(document).on('arv:pane:reload:all', function() {
- $('.pane-anchor').trigger('arv:pane:reload');
+ $('[data-pane-content-url]').trigger('arv:pane:reload');
});
-$(document).on('ready ajax:complete', function() {
+$(document).on('arv-log-event', '.arv-refresh-on-log-event', function(event) {
+ if (this != event.target) {
+ // Not interested in events sent to child nodes.
+ return;
+ }
// Panes marked arv-refresh-on-log-event should be refreshed
- $('.pane-anchor.arv-refresh-on-log-event').on('arv-log-event', function(e) {
- $(e.target).trigger('arv:pane:reload');
- });
+ $(event.target).trigger('arv:pane:reload');
});
// If there is a 'tab counts url' in the nav-tabs element then use it to get some javascript that will update them