2011-02-23 Ojan Vafai <ojan@chromium.org>
[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 = 'http://svn.webkit.org/repository/webkit/trunk/Tools/Scripts/webkitpy/common/config/committers.py';
28     var m_committers;
29
30     function getValues(param) {
31         var nextQuote = /^[^"]*"/g;
32         var values = [];
33         nextQuote.lastIndex = 0;
34         while (nextQuote.exec(param) != null) {
35             var nextIndex = param.indexOf('"', nextQuote.lastIndex); // For emacs " to balance the quotes.
36             values.push(param.substring(nextQuote.lastIndex, nextIndex));
37             param = param.substring(nextIndex + 1);
38             nextQuote.lastIndex = 0;
39         }
40         return values;
41     }
42
43     function parseRecord(key, record) {
44         var keyIndex = record.indexOf(key);
45         if (keyIndex < 0)
46             return null;
47         record = record.substring(keyIndex + key.length);
48
49         var firstParen = /^\s*\(\s*/g;
50         firstParen.lastIndex = 0;
51         if (!firstParen.exec(record))
52             return null;
53         record = record.substring(firstParen.lastIndex);
54
55         var parsedResult = {};
56
57         // full name
58         var param = /^\s*((\[[^\]]+\])|(u?)("[^"]+"))\s*/g; // For emacs " to balance the quotes.
59         param.lastIndex = 0;
60         var nameParam = param.exec(record);
61         if (!nameParam)
62             return null;
63         record = record.substring(param.lastIndex);
64
65         // Save the name without the quotes.
66         var name = nameParam[4].slice(1, nameParam[4].length - 1);
67
68         // Convert unicode characters
69         if (nameParam[3] == 'u') {
70             var unicode = /\\u([a-f\d]{4})/gi;
71             var match = unicode.exec(name);
72             while (match) {
73                 name = name.replace(match[0], String.fromCharCode(parseInt(match[1], 16)));
74                 match = unicode.exec(name);
75             }
76         }
77
78         parsedResult.name = name;
79
80         var paramSeparator = /^\s*,\s*/g;
81         paramSeparator.lastIndex = 0;
82         if (!paramSeparator.exec(record))
83             return null;
84         record = record.substring(paramSeparator.lastIndex);
85
86         // email
87         param.lastIndex = 0;
88         emailParam = param.exec(record);
89         if (!emailParam)
90             return null;
91
92         emails = getValues(emailParam[0]);
93         parsedResult.emails = emails;
94         record = record.substring(param.lastIndex);
95
96         paramSeparator.lastIndex = 0;
97         if (!paramSeparator.exec(record))
98             return parsedResult;
99         record = record.substring(paramSeparator.lastIndex);
100
101         // irc
102         param.lastIndex = 0;
103         ircParam = param.exec(record);
104         if (!ircParam)
105             return parsedResult;
106         record = record.substring(param.lastIndex);
107
108         irc = getValues(ircParam[0]);
109         parsedResult.irc = irc;
110         return parsedResult;
111     }
112
113     function parseType(key, records, type) {
114         for (var i = 0; i < records.length; ++i) {
115             var record = records[i];
116             var result = parseRecord(key, record);
117             if (!result)
118                 continue;
119             result.type = type;
120             m_committers.push(result);
121         }
122     }
123
124     function parseCommittersPy(text) {
125         m_committers = [];
126
127         var records = text.split('\n');
128         parseType('Committer', records, 'c');
129         parseType('Reviewer', records, 'r');
130     }
131
132     function loadCommitters(callback) {
133         var xhr = new XMLHttpRequest();
134         xhr.open('GET', COMMITTERS_URL);
135
136         xhr.onload = function() {
137             parseCommittersPy(xhr.responseText);
138             callback();
139         };
140
141         xhr.onerror = function() {
142             console.log('Unable to load committers.py');
143             callback();
144         };
145
146         xhr.send();
147     }
148
149     function getCommitters(callback) {
150         if (m_committers) {
151             callback(m_committers);
152             return;
153         }
154
155         loadCommitters(function() {
156             callback(m_committers);
157         });
158     }
159
160     return {
161         "getCommitters": getCommitters
162     };
163 })();
164
165 (function() {
166     var SINGLE_EMAIL_INPUTS = ['email1', 'email2', 'requester', 'requestee', 'assigned_to'];
167     var EMAIL_INPUTS = SINGLE_EMAIL_INPUTS.concat(['cc', 'newcc', 'new_watchedusers']);
168
169     var m_menus = {};
170     var m_focusedInput;
171     var m_committers;
172     var m_prefix;
173     var m_selectedIndex;
174
175     function contactsMatching(prefix) {
176         var list = [];
177         if (!prefix)
178             return list;
179
180         for (var i = 0; i < m_committers.length; i++) {
181             if (isMatch(m_committers[i], prefix))
182                 list.push(m_committers[i]);
183         }
184         return list;
185     }
186
187     function startsWith(str, prefix) {
188         return str.toLowerCase().indexOf(prefix.toLowerCase()) == 0;
189     }
190
191     function startsWithAny(arry, prefix) {
192         for (var i = 0; i < arry.length; i++) {
193             if (startsWith(arry[i], prefix))
194                 return true;
195         }
196         return false;
197     }
198
199     function isMatch(contact, prefix) {
200         if (startsWithAny(contact.emails, prefix))
201             return true;
202
203         if (contact.irc && startsWithAny(contact.irc, prefix))
204             return true;
205
206         var names = contact.name.split(' ');
207         for (var i = 0; i < names.length; i++) {
208             if (startsWith(names[i], prefix))
209                 return true;
210         }
211         
212         return false;
213     }
214
215     function isMenuVisible() {
216         return getMenu().style.display != 'none';
217     }
218
219     function showMenu(shouldShow) {
220         getMenu().style.display = shouldShow ? '' : 'none';
221     }
222
223     function updateMenu() {
224         var newPrefix = m_focusedInput.value;
225         if (newPrefix) {
226             newPrefix = newPrefix.slice(getStart(), getEnd());
227             newPrefix = newPrefix.replace(/^\s+/, '');
228             newPrefix = newPrefix.replace(/\s+$/, '');
229         }
230
231         if (m_prefix == newPrefix)
232             return;
233
234         m_prefix = newPrefix;
235
236         var contacts = contactsMatching(m_prefix);
237         if (contacts.length == 0 || contacts.length == 1 && contacts[0].emails[0] == m_prefix) {
238             showMenu(false);
239             return;
240         }
241
242         var html = [];
243         for (var i = 0; i < contacts.length; i++) {
244             var contact = contacts[i];
245             html.push('<div style="padding:1px 2px;" ' + 'email=' +
246                 contact.emails[0] + '>' + contact.name + ' - ' + contact.emails[0]);
247             if (contact.irc)
248                 html.push(' (:' + contact.irc + ')');
249             html.push(' (' + contact.type + ')');
250             html.push('</div>');
251         }
252         getMenu().innerHTML = html.join('');
253         selectItem(0);
254         showMenu(true);
255     }
256
257     function getIndex(item) {
258         for (var i = 0; i < getMenu().childNodes.length; i++) {
259             if (item == getMenu().childNodes[i])
260                 return i;
261         }
262         console.error("Couldn't find item.");
263     }
264
265     function getMenu() {
266         return m_menus[m_focusedInput.name];
267     }
268
269     function createMenu(name, input) {
270         if (!m_menus[name]) {
271             var menu = document.createElement('div');
272             menu.style.cssText =
273                 "position:absolute;border:1px solid black;background-color:white;-webkit-box-shadow:3px 3px 3px #888;";
274             menu.style.minWidth = m_focusedInput.offsetWidth + 'px';
275             m_focusedInput.parentNode.insertBefore(menu, m_focusedInput.nextSibling);
276
277             menu.addEventListener('mousedown', function(e) {
278                 selectItem(getIndex(e.target));
279                 e.preventDefault();
280             }, false);
281
282             menu.addEventListener('mouseup', function(e) {
283                 if (m_selectedIndex == getIndex(e.target))
284                     insertSelectedItem();
285             }, false);
286             
287             m_menus[name] = menu;
288         }
289     }
290
291     function getStart() {
292         var index = m_focusedInput.value.lastIndexOf(',', m_focusedInput.selectionStart - 1);
293         if (index == -1)
294             return 0;
295         return index + 1;
296     }
297
298     function getEnd() {
299         var index = m_focusedInput.value.indexOf(',', m_focusedInput.selectionStart);
300         if (index == -1)
301             return m_focusedInput.value.length;
302         return index;
303     }
304
305     function getItem(index) {
306         return getMenu().childNodes[index];
307     }
308
309     function selectItem(index) {
310         if (index < 0 || index >= getMenu().childNodes.length)
311             return;
312
313         if (m_selectedIndex != undefined) {
314             getItem(m_selectedIndex).style.backgroundColor = '';
315             getItem(m_selectedIndex).style.color = '';
316         }
317
318         getItem(index).style.backgroundColor = '#039';
319         getItem(index).style.color = 'white';
320
321         m_selectedIndex = index;
322     }
323
324     function insertSelectedItem() {
325         var selectedEmail = getItem(m_selectedIndex).getAttribute('email');
326         var oldValue = m_focusedInput.value;
327
328         var newValue = oldValue.slice(0, getStart()) + selectedEmail + oldValue.slice(getEnd());
329         if (SINGLE_EMAIL_INPUTS.indexOf(m_focusedInput.name) == -1 &&
330             newValue.charAt(newValue.length - 1) != ',')
331             newValue = newValue + ',';
332
333         m_focusedInput.value = newValue;
334         showMenu(false);    
335     }
336
337     function handleKeyDown(e) {
338         if (!isMenuVisible())
339             return;
340
341         switch (e.keyIdentifier) {
342             case 'Up':
343                 selectItem(m_selectedIndex - 1);
344                 e.preventDefault();
345                 break;
346             
347             case 'Down':
348                 selectItem(m_selectedIndex + 1);
349                 e.preventDefault();
350                 break;
351                 
352             case 'Enter':
353                 insertSelectedItem();
354                 e.preventDefault();
355                 break;
356         }
357     }
358
359     function handleKeyUp(e) {
360         if (e.keyIdentifier == 'Enter')
361             return;
362
363         if (m_focusedInput.selectionStart == m_focusedInput.selectionEnd)
364             updateMenu();
365         else
366             showMenu(false);
367     }
368
369     function enableAutoComplete(input) {
370         m_focusedInput = input;
371
372         if (!getMenu()) {
373             createMenu(m_focusedInput.name);
374             // Turn off autocomplete to avoid showing the browser's dropdown menu.
375             m_focusedInput.setAttribute('autocomplete', 'off');
376             m_focusedInput.addEventListener('keyup', handleKeyUp, false);
377             m_focusedInput.addEventListener('keydown', handleKeyDown, false);
378             m_focusedInput.addEventListener('blur', function() {
379                 showMenu(false);
380                 m_prefix = null;
381                 m_selectedIndex = 0;
382             }, false);
383             // Turn on autocomplete on submit to avoid breaking autofill on back/forward navigation.
384             m_focusedInput.form.addEventListener("submit", function() {
385                 m_focusedInput.setAttribute("autocomplete", "on");
386             }, false);
387         }
388         
389         updateMenu();
390     }
391
392     document.addEventListener('focusin', function(e) {
393         for (var i = 0; i < EMAIL_INPUTS.length; i++) {
394             if (e.target.name == EMAIL_INPUTS[i]) {
395                 enableAutoComplete(e.target);
396                 break;
397             }
398         }
399     }, false);
400
401     WebKitCommitters.getCommitters(function (committers) {
402         m_committers = committers;
403     });
404 })();