8784: Fix test for latest firefox.
[arvados.git] / apps / workbench / app / assets / javascripts / tab_panes.js
1 // Load tab panes on demand. See app/views/application/_content.html.erb
2
3 // Fire when a tab is selected/clicked.
4 $(document).on('shown.bs.tab', '[data-toggle="tab"]', function(event) {
5     // reload the pane (unless it's already loaded)
6     $($(event.target).attr('href')).
7         not('.pane-loaded').
8         trigger('arv:pane:reload');
9 });
10
11 // Ask a refreshable pane to reload via ajax.
12 //
13 // Target of this event is the DOM element to be updated. A reload
14 // consists of an AJAX call to load the "data-pane-content-url" and
15 // replace the content of the target element with the retrieved HTML.
16 //
17 // There are four CSS classes set on the element to indicate its state:
18 // pane-loading, pane-stale, pane-loaded, pane-reload-pending
19 //
20 // There are five states based on the presence or absence of css classes:
21 //
22 // 1. Absence of any pane-* states means the pane is empty, and should
23 // be loaded as soon as it becomes visible.
24 //
25 // 2. "pane-loading" means an AJAX call has been made to reload the
26 // pane and we are waiting on a result.
27 //
28 // 3. "pane-loading pane-stale" means the pane is loading, but has
29 // already been invalidated and should schedule a reload as soon as
30 // possible after the current load completes. (This happens when there
31 // is a cluster of events, where the reload is triggered by the first
32 // event, but we want ensure that we eventually load the final
33 // quiescent state).
34 //
35 // 4. "pane-loaded" means the pane is up to date.
36 //
37 // 5. "pane-loaded pane-reload-pending" means a reload is needed, and
38 // has been scheduled, but has not started because the pane's
39 // minimum-time-between-reloads throttle has not yet been reached.
40 //
41 $(document).on('arv:pane:reload', '[data-pane-content-url]', function(e) {
42     if (this != e.target) {
43         // An arv:pane:reload event was sent to an element (e.target)
44         // which happens to have an ancestor (this) matching the above
45         // '[data-pane-content-url]' selector. This happens because
46         // events bubble up the DOM on their way to document. However,
47         // here we only care about events delivered directly to _this_
48         // selected element (i.e., this==e.target), not ones delivered
49         // to its children. The event "e" is uninteresting here.
50         return;
51     }
52
53     // $pane, the event target, is an element whose content is to be
54     // replaced. Pseudoclasses on $pane (pane-loading, etc) encode the
55     // current loading state.
56     var $pane = $(this);
57
58     if ($pane.hasClass('pane-loading')) {
59         // Already loading, mark stale to schedule a reload after this one.
60         $pane.addClass('pane-stale');
61         return;
62     }
63
64     // The default throttle (mininum milliseconds between refreshes)
65     // can be overridden by an .arv-log-refresh-control element inside
66     // the pane -- or, failing that, the pane element itself -- with a
67     // data-load-throttle attribute. This allows the server to adjust
68     // the throttle depending on the pane content.
69     var throttle =
70         $pane.find('.arv-log-refresh-control').attr('data-load-throttle') ||
71         $pane.attr('data-load-throttle') ||
72         15000;
73     var now = (new Date()).getTime();
74     var loaded_at = $pane.attr('data-loaded-at');
75     var since_last_load = now - loaded_at;
76     if (loaded_at && (since_last_load < throttle)) {
77         if (!$pane.hasClass('pane-reload-pending')) {
78             $pane.addClass('pane-reload-pending');
79             setTimeout((function() {
80                 $pane.trigger('arv:pane:reload');
81             }), throttle - since_last_load);
82         }
83         return;
84     }
85
86     // We know this doesn't have 'pane-loading' because we tested for it above
87     $pane.removeClass('pane-reload-pending');
88     $pane.removeClass('pane-loaded');
89     $pane.removeClass('pane-stale');
90
91     if (!$pane.hasClass('active') &&
92         $pane.parent().hasClass('tab-content')) {
93         // $pane is one of the content areas in a bootstrap tabs
94         // widget, and it isn't the currently selected tab. If and
95         // when the user does select the corresponding tab, it will
96         // get a shown.bs.tab event, which will invoke this reload
97         // function again (see handler above). For now, we just insert
98         // a spinner, which will be displayed while the new content is
99         // loading.
100         $pane.html('<div class="spinner spinner-32px spinner-h-center"></div>');
101         return;
102     }
103
104     $pane.addClass('pane-loading');
105
106     var content_url = $pane.attr('data-pane-content-url');
107     $.ajax(content_url, {dataType: 'html', type: 'GET', context: $pane}).
108         done(function(data, status, jqxhr) {
109             var $pane = this;
110             // Preserve collapsed state
111             var collapsable = {};
112             $(".collapse", this).each(function(i, c) {
113                 collapsable[c.id] = $(c).hasClass('in');
114             });
115             var tmp = $(data);
116             $(".collapse", tmp).each(function(i, c) {
117                 if (collapsable[c.id]) {
118                     $(c).addClass('in');
119                 } else {
120                     $(c).removeClass('in');
121                 }
122             });
123             $pane.html(tmp);
124             $pane.removeClass('pane-loading');
125             $pane.addClass('pane-loaded');
126             $pane.attr('data-loaded-at', (new Date()).getTime());
127             $pane.trigger('arv:pane:loaded', [$pane]);
128
129             if ($pane.hasClass('pane-stale')) {
130                 $pane.trigger('arv:pane:reload');
131             }
132         }).fail(function(jqxhr, status, error) {
133             var $pane = this;
134             var errhtml;
135             var contentType = jqxhr.getResponseHeader('Content-Type');
136             if (jqxhr.readyState == 0 || jqxhr.status == 0) {
137                 if ($pane.attr('data-loaded-at') > 0) {
138                     // Stale content is already present. Leave it
139                     // there while loading the next page.
140                     $pane.removeClass('pane-loading');
141                     $pane.addClass('pane-loaded');
142                     // ...but schedule another refresh (after a
143                     // throttle delay) in case the act of navigating
144                     // away gets cancelled itself, leaving this page
145                     // with content that we know is stale.
146                     $pane.addClass('pane-stale');
147                     $pane.attr('data-loaded-at', (new Date()).getTime());
148                     $pane.trigger('arv:pane:reload');
149                     return;
150                 }
151                 errhtml = "Cancelled.";
152             } else if (contentType && contentType.match(/\btext\/html\b/)) {
153                 var $response = $(jqxhr.responseText);
154                 var $wrapper = $('div#page-wrapper', $response);
155                 if ($wrapper.length) {
156                     errhtml = $wrapper.html();
157                 } else {
158                     errhtml = jqxhr.responseText;
159                 }
160             } else {
161                 errhtml = ("An error occurred: " +
162                            (jqxhr.responseText || status)).
163                     replace(/&/g, '&amp;').
164                     replace(/</g, '&lt;').
165                     replace(/>/g, '&gt;');
166             }
167             $pane.html('<div class="pane-error-display"><p>' +
168                       '<a href="#" class="btn btn-primary tab_reload">' +
169                       '<i class="fa fa-fw fa-refresh"></i> ' +
170                       'Reload tab</a></p><iframe style="width: 100%"></iframe></div>');
171             $('.tab_reload', $pane).click(function() {
172                 $(this).
173                     html('<div class="spinner spinner-32px spinner-h-center"></div>').
174                     closest('.pane-loaded').
175                     attr('data-loaded-at', 0).
176                     trigger('arv:pane:reload');
177             });
178             // We want to render the error in an iframe, in order to
179             // avoid conflicts with the main page's element ids, etc.
180             // In order to do that dynamically, we have to set a
181             // timeout on the iframe window to load our HTML *after*
182             // the default source (e.g., about:blank) has loaded.
183             var iframe = $('iframe', $pane)[0];
184             iframe.contentWindow.setTimeout(function() {
185                 $('body', iframe.contentDocument).html(errhtml);
186                 iframe.height = iframe.contentDocument.body.scrollHeight + "px";
187             }, 1);
188             $pane.removeClass('pane-loading');
189             $pane.addClass('pane-loaded');
190         });
191 });
192
193 // Mark all panes as stale/dirty. Refresh any 'active' panes.
194 $(document).on('arv:pane:reload:all', function() {
195     $('[data-pane-content-url]').trigger('arv:pane:reload');
196 });
197
198 $(document).on('arv-log-event', '.arv-refresh-on-log-event', function(event) {
199     if (this != event.target) {
200         // Not interested in events sent to child nodes.
201         return;
202     }
203     // Panes marked arv-refresh-on-log-event should be refreshed
204     $(event.target).trigger('arv:pane:reload');
205 });
206
207 // If there is a 'tab counts url' in the nav-tabs element then use it to get some javascript that will update them
208 $(document).on('ready count-change', function() {
209     var tabCountsUrl = $('ul.nav-tabs').data('tab-counts-url');
210     if( tabCountsUrl && tabCountsUrl.length ) {
211         $.get( tabCountsUrl );
212     }
213 });