Unreviewed. Fix individual benchmark description urls to go to in-depth.html instead...
[WebKit-https.git] / Websites / bugs.webkit.org / committers-autocomplete.js
1 // Copyright (C) 2010 Ojan Vafai. All rights reserved.
2 // Copyright (C) 2010 Adam Barth. All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
6 //
7 // 1. Redistributions of source code must retain the above copyright notice,
8 // this list of conditions and the following disclaimer.
9 //
10 // 2. Redistributions in binary form must reproduce the above copyright notice,
11 // this list of conditions and the following disclaimer in the documentation
12 // and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
15 // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 // DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
18 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
24 // DAMAGE.
25
26 WebKitCommitters = (function() {
27     var COMMITTERS_URL = 'https://svn.webkit.org/repository/webkit/trunk/Tools/Scripts/webkitpy/common/config/contributors.json';
28     var m_committers;
29
30     function statusToType(status) {
31         if (status === 'reviewer')
32             return 'r';
33         if (status === 'committer')
34             return 'c';
35         return undefined;
36     }
37
38     function parseCommittersPy(text) {
39         var contributors = JSON.parse(text);
40
41         m_committers = [];
42
43         for (var name in contributors) {
44             var record = contributors[name];
45             m_committers.push({
46                 name: name,
47                 emails: record.emails,
48                 irc: record.nicks,
49                 type: statusToType(record.status),
50             });
51         }
52     }
53
54     function loadCommitters(callback) {
55         var xhr = new XMLHttpRequest();
56         xhr.open('GET', COMMITTERS_URL);
57
58         xhr.onload = function() {
59             parseCommittersPy(xhr.responseText);
60             callback();
61         };
62
63         xhr.onerror = function() {
64             console.log('Unable to load contributors.json');
65             callback();
66         };
67
68         xhr.send();
69     }
70
71     function getCommitters(callback) {
72         if (m_committers) {
73             callback(m_committers);
74             return;
75         }
76
77         loadCommitters(function() {
78             callback(m_committers);
79         });
80     }
81
82     return {
83         "getCommitters": getCommitters
84     };
85 })();
86
87 (function() {
88     var SINGLE_EMAIL_INPUTS = ['email1', 'email2', 'email3', 'requester', 'requestee', 'assigned_to'];
89     var EMAIL_INPUTS = SINGLE_EMAIL_INPUTS.concat(['cc', 'newcc', 'new_watchedusers']);
90
91     var m_menus = {};
92     var m_focusedInput;
93     var m_committers;
94     var m_prefix;
95     var m_selectedIndex;
96
97     function contactsMatching(prefix) {
98         var list = [];
99         if (!prefix)
100             return list;
101
102         for (var i = 0; i < m_committers.length; i++) {
103             if (isMatch(m_committers[i], prefix))
104                 list.push(m_committers[i]);
105         }
106         return list;
107     }
108
109     function startsWith(str, prefix) {
110         return str.toLowerCase().indexOf(prefix.toLowerCase()) == 0;
111     }
112
113     function startsWithAny(arry, prefix) {
114         for (var i = 0; i < arry.length; i++) {
115             if (startsWith(arry[i], prefix))
116                 return true;
117         }
118         return false;
119     }
120
121     function isMatch(contact, prefix) {
122         if (startsWithAny(contact.emails, prefix))
123             return true;
124
125         if (contact.irc && startsWithAny(contact.irc, prefix))
126             return true;
127
128         var names = contact.name.split(' ');
129         for (var i = 0; i < names.length; i++) {
130             if (startsWith(names[i], prefix))
131                 return true;
132         }
133         
134         return false;
135     }
136
137     function isMenuVisible() {
138         return getMenu().style.display != 'none';
139     }
140
141     function showMenu(shouldShow) {
142         getMenu().style.display = shouldShow ? '' : 'none';
143     }
144
145     function updateMenu() {
146         var newPrefix = m_focusedInput.value;
147         if (newPrefix) {
148             newPrefix = newPrefix.slice(getStart(), getEnd());
149             newPrefix = newPrefix.replace(/^\s+/, '');
150             newPrefix = newPrefix.replace(/\s+$/, '');
151         }
152
153         if (m_prefix == newPrefix)
154             return;
155
156         m_prefix = newPrefix;
157
158         var contacts = contactsMatching(m_prefix);
159         if (contacts.length == 0 || contacts.length == 1 && contacts[0].emails[0] == m_prefix) {
160             showMenu(false);
161             return;
162         }
163
164         var html = [];
165         for (var i = 0; i < contacts.length; i++) {
166             var contact = contacts[i];
167             html.push('<div style="padding:1px 2px;" ' + 'email=' +
168                 contact.emails[0] + '>' + contact.name + ' - ' + contact.emails[0]);
169             if (contact.irc)
170                 html.push(' (:' + contact.irc + ')');
171             if (contact.type)
172                 html.push(' (' + contact.type + ')');
173             html.push('</div>');
174         }
175         getMenu().innerHTML = html.join('');
176         selectItem(0);
177         showMenu(true);
178     }
179
180     function getIndex(item) {
181         for (var i = 0; i < getMenu().childNodes.length; i++) {
182             if (item == getMenu().childNodes[i])
183                 return i;
184         }
185         console.error("Couldn't find item.");
186     }
187
188     function getMenu() {
189         return m_menus[m_focusedInput.name];
190     }
191
192     function createMenu(name, input) {
193         if (!m_menus[name]) {
194             var menu = document.createElement('div');
195             menu.style.cssText =
196                 "position:absolute;border:1px solid black;background-color:white;-webkit-box-shadow:3px 3px 3px #888;";
197             menu.style.minWidth = m_focusedInput.offsetWidth + 'px';
198             m_focusedInput.parentNode.insertBefore(menu, m_focusedInput.nextSibling);
199
200             menu.addEventListener('mousedown', function(e) {
201                 selectItem(getIndex(e.target));
202                 e.preventDefault();
203             }, false);
204
205             menu.addEventListener('mouseup', function(e) {
206                 if (m_selectedIndex == getIndex(e.target))
207                     insertSelectedItem();
208             }, false);
209             
210             m_menus[name] = menu;
211         }
212     }
213
214     function getStart() {
215         var index = m_focusedInput.value.lastIndexOf(',', m_focusedInput.selectionStart - 1);
216         if (index == -1)
217             return 0;
218         return index + 1;
219     }
220
221     function getEnd() {
222         var index = m_focusedInput.value.indexOf(',', m_focusedInput.selectionStart);
223         if (index == -1)
224             return m_focusedInput.value.length;
225         return index;
226     }
227
228     function getItem(index) {
229         return getMenu().childNodes[index];
230     }
231
232     function selectItem(index) {
233         if (index < 0 || index >= getMenu().childNodes.length)
234             return;
235
236         if (m_selectedIndex != undefined) {
237             getItem(m_selectedIndex).style.backgroundColor = '';
238             getItem(m_selectedIndex).style.color = '';
239         }
240
241         getItem(index).style.backgroundColor = '#039';
242         getItem(index).style.color = 'white';
243
244         m_selectedIndex = index;
245     }
246
247     function insertSelectedItem() {
248         var selectedEmail = getItem(m_selectedIndex).getAttribute('email');
249         var oldValue = m_focusedInput.value;
250
251         var newValue = oldValue.slice(0, getStart()) + selectedEmail + oldValue.slice(getEnd());
252         if (SINGLE_EMAIL_INPUTS.indexOf(m_focusedInput.name) == -1 &&
253             newValue.charAt(newValue.length - 1) != ',')
254             newValue = newValue + ',';
255
256         m_focusedInput.value = newValue;
257         showMenu(false);    
258     }
259
260     function handleKeyDown(e) {
261         if (!isMenuVisible())
262             return;
263
264         switch (e.keyIdentifier) {
265             case 'Up':
266                 selectItem(m_selectedIndex - 1);
267                 e.preventDefault();
268                 break;
269             
270             case 'Down':
271                 selectItem(m_selectedIndex + 1);
272                 e.preventDefault();
273                 break;
274                 
275             case 'Enter':
276                 insertSelectedItem();
277                 e.preventDefault();
278                 break;
279         }
280     }
281
282     function handleKeyUp(e) {
283         if (e.keyIdentifier == 'Enter')
284             return;
285
286         if (m_focusedInput.selectionStart == m_focusedInput.selectionEnd)
287             updateMenu();
288         else
289             showMenu(false);
290     }
291
292     function enableAutoComplete(input) {
293         m_focusedInput = input;
294
295         if (!getMenu()) {
296             createMenu(m_focusedInput.name);
297             // Turn off autocomplete to avoid showing the browser's dropdown menu.
298             m_focusedInput.setAttribute('autocomplete', 'off');
299             m_focusedInput.addEventListener('keyup', handleKeyUp, false);
300             m_focusedInput.addEventListener('keydown', handleKeyDown, false);
301             m_focusedInput.addEventListener('blur', function() {
302                 showMenu(false);
303                 m_prefix = null;
304                 m_selectedIndex = 0;
305             }, false);
306             // Turn on autocomplete on submit to avoid breaking autofill on back/forward navigation.
307             m_focusedInput.form.addEventListener("submit", function() {
308                 m_focusedInput.setAttribute("autocomplete", "on");
309             }, false);
310         }
311         
312         updateMenu();
313     }
314
315     for (var i = 0; i < EMAIL_INPUTS.length; i++) {
316         var field = document.getElementsByName(EMAIL_INPUTS[i])[0];
317         if (field)
318             field.addEventListener("focus", function(e) { enableAutoComplete(e.target); }, false);
319     }
320
321     WebKitCommitters.getCommitters(function (committers) {
322         m_committers = committers;
323     });
324 })();