The list of contributors in committers.py should be a separate JSON
[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 parseType(key, records, type) {
31         for (var name in records) {
32             var record = records[name];
33             result.name = name;
34             result.emails = record.emails;
35             result.irc = record.nicks;
36             result.type = type;
37             m_committers.push(result);
38         }
39     }
40
41     function parseCommittersPy(text) {
42         var parsedContributorsJSON = JSON.parse(text);
43
44         m_committers = [];
45
46         var records = text.split('\n');
47         parseType('Committer', parsedContributorsJSON['Committers'], 'c');
48         parseType('Reviewer', parsedContributorsJSON['Reviewers'], 'r');
49         parseType('Contributor', parsedContributorsJSON['Contributors']);
50     }
51
52     function loadCommitters(callback) {
53         var xhr = new XMLHttpRequest();
54         xhr.open('GET', COMMITTERS_URL);
55
56         xhr.onload = function() {
57             parseCommittersPy(xhr.responseText);
58             callback();
59         };
60
61         xhr.onerror = function() {
62             console.log('Unable to load contributors.json');
63             callback();
64         };
65
66         xhr.send();
67     }
68
69     function getCommitters(callback) {
70         if (m_committers) {
71             callback(m_committers);
72             return;
73         }
74
75         loadCommitters(function() {
76             callback(m_committers);
77         });
78     }
79
80     return {
81         "getCommitters": getCommitters
82     };
83 })();
84
85 (function() {
86     var SINGLE_EMAIL_INPUTS = ['email1', 'email2', 'requester', 'requestee', 'assigned_to'];
87     var EMAIL_INPUTS = SINGLE_EMAIL_INPUTS.concat(['cc', 'newcc', 'new_watchedusers']);
88
89     var m_menus = {};
90     var m_focusedInput;
91     var m_committers;
92     var m_prefix;
93     var m_selectedIndex;
94
95     function contactsMatching(prefix) {
96         var list = [];
97         if (!prefix)
98             return list;
99
100         for (var i = 0; i < m_committers.length; i++) {
101             if (isMatch(m_committers[i], prefix))
102                 list.push(m_committers[i]);
103         }
104         return list;
105     }
106
107     function startsWith(str, prefix) {
108         return str.toLowerCase().indexOf(prefix.toLowerCase()) == 0;
109     }
110
111     function startsWithAny(arry, prefix) {
112         for (var i = 0; i < arry.length; i++) {
113             if (startsWith(arry[i], prefix))
114                 return true;
115         }
116         return false;
117     }
118
119     function isMatch(contact, prefix) {
120         if (startsWithAny(contact.emails, prefix))
121             return true;
122
123         if (contact.irc && startsWithAny(contact.irc, prefix))
124             return true;
125
126         var names = contact.name.split(' ');
127         for (var i = 0; i < names.length; i++) {
128             if (startsWith(names[i], prefix))
129                 return true;
130         }
131         
132         return false;
133     }
134
135     function isMenuVisible() {
136         return getMenu().style.display != 'none';
137     }
138
139     function showMenu(shouldShow) {
140         getMenu().style.display = shouldShow ? '' : 'none';
141     }
142
143     function updateMenu() {
144         var newPrefix = m_focusedInput.value;
145         if (newPrefix) {
146             newPrefix = newPrefix.slice(getStart(), getEnd());
147             newPrefix = newPrefix.replace(/^\s+/, '');
148             newPrefix = newPrefix.replace(/\s+$/, '');
149         }
150
151         if (m_prefix == newPrefix)
152             return;
153
154         m_prefix = newPrefix;
155
156         var contacts = contactsMatching(m_prefix);
157         if (contacts.length == 0 || contacts.length == 1 && contacts[0].emails[0] == m_prefix) {
158             showMenu(false);
159             return;
160         }
161
162         var html = [];
163         for (var i = 0; i < contacts.length; i++) {
164             var contact = contacts[i];
165             html.push('<div style="padding:1px 2px;" ' + 'email=' +
166                 contact.emails[0] + '>' + contact.name + ' - ' + contact.emails[0]);
167             if (contact.irc)
168                 html.push(' (:' + contact.irc + ')');
169             if (contact.type)
170                 html.push(' (' + contact.type + ')');
171             html.push('</div>');
172         }
173         getMenu().innerHTML = html.join('');
174         selectItem(0);
175         showMenu(true);
176     }
177
178     function getIndex(item) {
179         for (var i = 0; i < getMenu().childNodes.length; i++) {
180             if (item == getMenu().childNodes[i])
181                 return i;
182         }
183         console.error("Couldn't find item.");
184     }
185
186     function getMenu() {
187         return m_menus[m_focusedInput.name];
188     }
189
190     function createMenu(name, input) {
191         if (!m_menus[name]) {
192             var menu = document.createElement('div');
193             menu.style.cssText =
194                 "position:absolute;border:1px solid black;background-color:white;-webkit-box-shadow:3px 3px 3px #888;";
195             menu.style.minWidth = m_focusedInput.offsetWidth + 'px';
196             m_focusedInput.parentNode.insertBefore(menu, m_focusedInput.nextSibling);
197
198             menu.addEventListener('mousedown', function(e) {
199                 selectItem(getIndex(e.target));
200                 e.preventDefault();
201             }, false);
202
203             menu.addEventListener('mouseup', function(e) {
204                 if (m_selectedIndex == getIndex(e.target))
205                     insertSelectedItem();
206             }, false);
207             
208             m_menus[name] = menu;
209         }
210     }
211
212     function getStart() {
213         var index = m_focusedInput.value.lastIndexOf(',', m_focusedInput.selectionStart - 1);
214         if (index == -1)
215             return 0;
216         return index + 1;
217     }
218
219     function getEnd() {
220         var index = m_focusedInput.value.indexOf(',', m_focusedInput.selectionStart);
221         if (index == -1)
222             return m_focusedInput.value.length;
223         return index;
224     }
225
226     function getItem(index) {
227         return getMenu().childNodes[index];
228     }
229
230     function selectItem(index) {
231         if (index < 0 || index >= getMenu().childNodes.length)
232             return;
233
234         if (m_selectedIndex != undefined) {
235             getItem(m_selectedIndex).style.backgroundColor = '';
236             getItem(m_selectedIndex).style.color = '';
237         }
238
239         getItem(index).style.backgroundColor = '#039';
240         getItem(index).style.color = 'white';
241
242         m_selectedIndex = index;
243     }
244
245     function insertSelectedItem() {
246         var selectedEmail = getItem(m_selectedIndex).getAttribute('email');
247         var oldValue = m_focusedInput.value;
248
249         var newValue = oldValue.slice(0, getStart()) + selectedEmail + oldValue.slice(getEnd());
250         if (SINGLE_EMAIL_INPUTS.indexOf(m_focusedInput.name) == -1 &&
251             newValue.charAt(newValue.length - 1) != ',')
252             newValue = newValue + ',';
253
254         m_focusedInput.value = newValue;
255         showMenu(false);    
256     }
257
258     function handleKeyDown(e) {
259         if (!isMenuVisible())
260             return;
261
262         switch (e.keyIdentifier) {
263             case 'Up':
264                 selectItem(m_selectedIndex - 1);
265                 e.preventDefault();
266                 break;
267             
268             case 'Down':
269                 selectItem(m_selectedIndex + 1);
270                 e.preventDefault();
271                 break;
272                 
273             case 'Enter':
274                 insertSelectedItem();
275                 e.preventDefault();
276                 break;
277         }
278     }
279
280     function handleKeyUp(e) {
281         if (e.keyIdentifier == 'Enter')
282             return;
283
284         if (m_focusedInput.selectionStart == m_focusedInput.selectionEnd)
285             updateMenu();
286         else
287             showMenu(false);
288     }
289
290     function enableAutoComplete(input) {
291         m_focusedInput = input;
292
293         if (!getMenu()) {
294             createMenu(m_focusedInput.name);
295             // Turn off autocomplete to avoid showing the browser's dropdown menu.
296             m_focusedInput.setAttribute('autocomplete', 'off');
297             m_focusedInput.addEventListener('keyup', handleKeyUp, false);
298             m_focusedInput.addEventListener('keydown', handleKeyDown, false);
299             m_focusedInput.addEventListener('blur', function() {
300                 showMenu(false);
301                 m_prefix = null;
302                 m_selectedIndex = 0;
303             }, false);
304             // Turn on autocomplete on submit to avoid breaking autofill on back/forward navigation.
305             m_focusedInput.form.addEventListener("submit", function() {
306                 m_focusedInput.setAttribute("autocomplete", "on");
307             }, false);
308         }
309         
310         updateMenu();
311     }
312
313     for (var i = 0; i < EMAIL_INPUTS.length; i++) {
314         var field = document.getElementsByName(EMAIL_INPUTS[i])[0];
315         if (field)
316             field.addEventListener("focus", function(e) { enableAutoComplete(e.target); }, false);
317     }
318
319     WebKitCommitters.getCommitters(function (committers) {
320         m_committers = committers;
321     });
322 })();