Hook into the "dom:loaded" event instead of onLoad, to prevent needless flashing
[gitorious:yousource.git] / public / javascripts / application.js
1 /*
2 #--
3 #   Copyright (C) 2007-2009 Johan Sørensen <johan@johansorensen.com>
4 #   Copyright (C) 2009 Marius Mathiesen <marius.mathiesen@gmail.com>
5 #
6 #   This program is free software: you can redistribute it and/or modify
7 #   it under the terms of the GNU Affero General Public License as published by
8 #   the Free Software Foundation, either version 3 of the License, or
9 #   (at your option) any later version.
10 #
11 #   This program is distributed in the hope that it will be useful,
12 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #   GNU Affero General Public License for more details.
15 #
16 #   You should have received a copy of the GNU Affero General Public License
17 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #-- 
19 */
20
21 var ProjectSluggorizer = Class.create({
22   initialize: function(source, target) {
23    this.source = $(source);
24    this.target = $(target); 
25    new Form.Element.Observer(
26      this.source,
27      0.8,  
28      function(el, value){
29        this.target.value = this._lintedName(value);
30      }.bind(this)
31    )
32   },
33   
34   _lintedName: function(val) {
35     var linted = val.gsub(/\W+/, ' ')
36     linted = linted.gsub(/\ +/, '-')
37     linted = linted.toLowerCase();
38     linted = linted.gsub(/\-+$/, '')
39     return linted;
40   }
41 });
42
43 var Gitorious = {
44   LineHighlighter: Class.create({    
45     initialize: function(table) {
46       this.table = $(table);
47       this.currentHighlights = [];
48       this.highlightClassname = "marked";
49     },
50
51
52     update: function(line){
53       var lineno = line || this._getLineNumberFromUri();
54       if (lineno) {
55         this._clearExistingHighlights();
56         this._highlight(lineno);
57       }
58     },
59
60     _highlight: function(lineId) {
61       this.table.getElementsBySelector(lineId + " td").each(function(cell) {
62         cell.addClassName(this.highlightClassname);
63         this.currentHighlights.push(cell);
64       }.bind(this));
65     },
66
67     _getLineNumberFromUri: function() {
68       var uriHash = window.location.hash;
69       if (/^#line\d+$/.test(uriHash)) {
70         return uriHash;
71       }
72     },
73
74     _clearExistingHighlights: function() {
75       if (this.currentHighlights.length > 0) {
76         this.currentHighlights.each(function(hl) {
77           hl.removeClassName(this.highlightClassname);
78         }.bind(this));
79         this.currentHighlights = [];
80       }
81     }
82   }),
83   
84   Wordwrapper: {
85     wrap: function(elements) {
86       elements.each(function(e) {
87         //e.addClassName("softwrapped");
88         e.removeClassName("unwrapped");
89       });
90     },
91     
92     unwrap: function(elements) {
93       elements.each(function(e) {
94         //e.removeClassName("softwrapped");
95         e.addClassName("unwrapped");
96       });
97     },
98     
99     toggle: function(elements) {
100       if (/unwrapped/.test(elements.first().className)) {
101         Gitorious.Wordwrapper.wrap(elements);
102       } else {
103         Gitorious.Wordwrapper.unwrap(elements);
104       }
105     }
106   },
107   
108   DownloadChecker: {
109     checkURL: function(url, container) {
110       var element = $(container);
111       element.absolutize();
112       var sourceLink = element.previous();
113       // Position the box
114       if (sourceLink) {
115         element.clonePosition(sourceLink);
116         element.setStyle({
117           top: parseInt(element.style.top) - (element.getHeight()+10) + "px",
118           width: "175px",
119           height: "70px"
120         });
121       }
122       
123       element.show();
124       element.innerHTML = '<p class="spin"><img src="/images/spinner.gif" /></p>'
125       // load the status
126       new Ajax.Request(url, {
127         onSuccess: function(transport) {
128           $(container).update(transport.responseText);
129         }
130       });
131       return false;
132     }
133   }
134 };
135
136 Event.observe(window, "dom:loaded", function(e){
137   var blobTable = $("codeblob")
138   if (blobTable) {
139     var highlighter = new Gitorious.LineHighlighter(blobTable);
140     highlighter.update();
141     
142     blobTable.getElementsBySelector("td.line-numbers a").each(function(link) {
143       var lineno = link.href.split("#").last();
144       link.observe("click", function(event) {
145         highlighter.update("#" + lineno);
146       });
147     });
148   }
149   
150   $$("a.link_noop").each(function(element) {
151     element.observe("click", function(e){ 
152       Event.stop(e); 
153       return false;
154     })
155   })
156
157   // Unobtrusively hooking the regular/OpenID login box stuff, so that it works
158   // in a semi-sensible way with javascript disabled.
159   var loginBox = $("big_header_login");
160   if (loginBox) {
161     var openIdLoginBox  = $("big_header_login_box_openid");
162     var regularLoginBox   = $("big_header_login_box_regular");
163     
164     // Only showing the regular login
165     
166     // Toggle between the two
167     var loginBoxToggler = function(e){
168       Effect.toggle(openIdLoginBox, "appear", {duration: 0.3 });
169       Effect.toggle(regularLoginBox, "appear", {duration: 0.3 });
170       Event.stop(e);
171       return false;
172     }
173     
174     $("big_header_login_box_to_openid").observe("click", loginBoxToggler);
175     $("big_header_login_box_to_regular").observe("click", loginBoxToggler);
176   }
177   
178   var headerSearchForm = $("main_menu_search_form")
179   if (headerSearchForm) {
180     // The "Search..." label
181     var labelText = "Search..."
182     var searchInput = $("main_menu_search_form_query");
183     searchInput.value = labelText;
184     searchInput.observe("focus", function(){
185       if (searchInput.value == labelText) {
186         searchInput.value = "";
187         searchInput.removeClassName("unfocused")
188       }
189     });
190     
191     searchInput.observe("blur", function(){
192       if (searchInput.value == "") {
193         searchInput.value = labelText;
194         searchInput.addClassName("unfocused")
195       }
196     });
197     
198     // Hide the regular native submit button
199     var nativeSubmitButton = headerSearchForm.getElementsBySelector("input[type=submit]")[0];
200     nativeSubmitButton.hide();
201     
202     // Create our own awesome submit button.
203     var awesomeSubmitButton = $(document.createElement("a"));
204     awesomeSubmitButton.writeAttribute("id", "main_menu_search_form_graphic_submit");
205     awesomeSubmitButton.writeAttribute("href", "#");
206     awesomeSubmitButton.observe("click", function(e){
207       headerSearchForm.submit();
208       Event.stop(e);
209       return false;
210     });
211     
212     nativeSubmitButton.insert({after: awesomeSubmitButton});
213   }
214   
215   var recentActivitiesTarget = $("recent_activities_container");
216   if (recentActivitiesTarget) {
217     var RECENT_ACTIVITY_WIDTH     = 280;
218     var NUM_RECENT_ACTIVITIES     = 8;
219     var RECENT_ACTIVITIES_HEADER  = 186 + (20 * 2);
220     var BAR_WIDTH                 = (RECENT_ACTIVITY_WIDTH * NUM_RECENT_ACTIVITIES) + RECENT_ACTIVITIES_HEADER;
221     
222     var FPS                       = 30;
223     var ANIMATION_STEP            = 2;
224     var LOAD_MORE_OFFSET          = 400;
225     
226     var currentIteration          = 0;
227     var currentAnimTimeout
228     
229
230     var getWindowWidth = function(){
231       return (window.innerWidth || document.documentElement.clientWidth);
232     }
233     
234     recentActivitiesTarget.setStyle({left: getWindowWidth() + "px"});
235     
236     var fetchNewBar = function(callback){
237       currentIteration++;
238       new Ajax.Request("/events/recent_for_homepage", {
239         onSuccess: function(response) {
240           // Expand the width of the container
241           recentActivitiesTarget.setStyle({width: BAR_WIDTH + parseInt(recentActivitiesTarget.getStyle("width")) + "px"});
242           
243           // Append the new bar
244           var newBar = response.responseText;
245           Element.insert(recentActivitiesTarget, newBar);
246           $$(".recent_activities_bar").each(function(element){
247             element.setStyle({width: BAR_WIDTH + "px"});
248           });
249           
250           if (callback) { callback.call() }
251         }
252       });
253     }
254     
255     var animateBar = function(){
256       var currentOffset = parseInt(recentActivitiesTarget.getStyle("left"));
257       var newOffset = currentOffset - ANIMATION_STEP
258       recentActivitiesTarget.setStyle({left: newOffset + "px"});
259       
260
261       if ((BAR_WIDTH * currentIteration) - (getWindowWidth() - newOffset) <= LOAD_MORE_OFFSET) {
262         fetchNewBar();
263       }
264       
265       currentAnimTimeout = setTimeout(animateBar, FPS);
266     }
267     
268
269     recentActivitiesTarget.observe("mouseover", function(e){
270       clearTimeout(currentAnimTimeout);
271     });
272     
273     recentActivitiesTarget.observe("mouseout", function(e){
274       if (!e.relatedTarget || !e.relatedTarget.descendantOf(this)) {
275         animateBar();
276       }
277     })
278     
279     fetchNewBar(animateBar);
280   }
281 });
282
283
284 // A class used for selecting ranges of objects
285 function SelectableRange(commitListUrl, targetBranchesUrl, statusElement)
286 {
287   this.commitListUrl = commitListUrl
288   this.targetBranchesUrl = targetBranchesUrl;
289   this.statusElement = statusElement;
290   this.endsAt = null;
291   this.sourceBranchName = null;
292   this.targetBranchName = null;
293   this.registerResponders = function() {
294     Ajax.Responders.register({
295       onCreate: function() {
296       if ($("spinner") && Ajax.activeRequestCount > 0)
297         Effect.Appear("spinner", { duration:0.3 })
298       },
299       onComplete: function() {
300         if ($("spinner") && Ajax.activeRequestCount == 0)
301           Effect.Fade("spinner", { duration:0.3 })
302       }
303     });
304   };
305   this.registerResponders();
306   
307   this.endSelected = function(el) {
308     this.endsAt = el;
309     this.update();
310   };
311   
312   this.onSourceBranchChange = function(event) {
313     if (sourceBranch = $F('merge_request_source_branch')) {
314       this.sourceBranchSelected(sourceBranch);
315     }
316   };
317   
318   this.onTargetRepositoryChange = function(event) {
319     new Ajax.Updater('target_branch_selection', this.targetBranchesUrl, {
320       method: 'post', 
321       parameters: Form.serialize($('new_merge_request'))
322     });
323     this._updateCommitList();
324   };
325   
326   this.onTargetBranchChange = function(event) {
327     if (targetBranch = $F('merge_request_target_branch')) {
328       this.targetBranchSelected(targetBranch);
329     }
330   };
331   
332   this.targetBranchSelected = function(branchName) {
333     if (branchName != this.targetBranchName) {
334       this.targetBranchName = branchName;
335       this._updateCommitList();
336     }
337   };
338   
339   this.sourceBranchSelected = function(branchName) {
340     if (branchName != this.sourceBranchName) {
341       this.sourceBranchName = branchName;
342       this._updateCommitList();
343     }
344   };
345   
346   this.update = function() {
347     if (this.endsAt) {
348       var commitRows = $$(".commit_row");
349       commitRows.each(function(el){ el.removeClassName('selected') });
350       var firstTr = this.endsAt.up().up();
351       firstTr.addClassName('selected');
352       firstTr.nextSiblings().each(function(tr) {
353         tr.addClassName('selected');        
354       });
355       // update the status field with the selected range
356       var to = firstTr.getElementsBySelector(".sha-abbrev a")[0].innerHTML;
357       var from = commitRows.last().getElementsBySelector(".sha-abbrev a")[0].innerHTML;
358       $$("." + this.statusElement).each(function(e) {
359         e.update(from + ".." + to);
360       });
361     }
362   };
363   
364   this._updateCommitList = function() {
365     new Ajax.Updater('commit_selection', this.commitListUrl, {
366       method: 'post', 
367       parameters: Form.serialize($('new_merge_request'))
368     });
369   }
370 }
371
372 function toggle_wiki_preview(target_url)
373 {
374   var wiki_preview = $('page_preview');
375   var wiki_edit = $('page_content');
376   var wiki_form = wiki_edit.form;
377   var toggler = $('wiki_preview_toggler');
378   if (wiki_preview.visible()) // Will hide preview
379   {
380     toggler.value = "Show preview"
381   }
382   else
383   {
384     toggler.value = "Hide preview"
385     wiki_preview.innerHTML = "";
386     new Ajax.Request(target_url, {asynchronous:true, evalScripts:true, method:'post', parameters:Form.serialize(wiki_form)});
387   }
388   [wiki_preview,wiki_edit].each(function(e){e.toggle()});
389 }