committers-autocomplete.js works only with WebKit based browsers
[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/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})/i;
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         parseType('Contributor', records);
131     }
132
133     function loadCommitters(callback) {
134         var xhr = new XMLHttpRequest();
135         xhr.open('GET', COMMITTERS_URL);
136
137         xhr.onload = function() {
138             parseCommittersPy(xhr.responseText);
139             callback();
140         };
141
142         xhr.onerror = function() {
143             console.log('Unable to load committers.py');
144             callback();
145         };
146
147         xhr.send();
148     }
149
150     function getCommitters(callback) {
151         if (m_committers) {
152             callback(m_committers);
153             return;
154         }
155
156         loadCommitters(function() {
157             callback(m_committers);
158         });
159     }
160
161     return {
162         "getCommitters": getCommitters
163     };
164 })();
165
166 (function() {
167     var SINGLE_EMAIL_INPUTS = ['email1', 'email2', 'requester', 'requestee', 'assigned_to'];
168     var EMAIL_INPUTS = SINGLE_EMAIL_INPUTS.concat(['cc', 'newcc', 'new_watchedusers']);
169
170     var m_menus = {};
171     var m_focusedInput;
172     var m_committers;
173     var m_prefix;
174     var m_selectedIndex;
175
176     function contactsMatching(prefix) {
177         var list = [];
178         if (!prefix)
179             return list;
180
181         for (var i = 0; i < m_committers.length; i++) {
182             if (isMatch(m_committers[i], prefix))
183                 list.push(m_committers[i]);
184         }
185         return list;
186     }
187
188     function startsWith(str, prefix) {
189         return str.toLowerCase().indexOf(prefix.toLowerCase()) == 0;
190     }
191
192     function startsWithAny(arry, prefix) {
193         for (var i = 0; i < arry.length; i++) {
194             if (startsWith(arry[i], prefix))
195                 return true;
196         }
197         return false;
198     }
199
200     function isMatch(contact, prefix) {
201         if (startsWithAny(contact.emails, prefix))
202             return true;
203
204         if (contact.irc && startsWithAny(contact.irc, prefix))
205             return true;
206
207         var names = contact.name.split(' ');
208         for (var i = 0; i < names.length; i++) {
209             if (startsWith(names[i], prefix))
210                 return true;
211         }
212         
213         return false;
214     }
215
216     function isMenuVisible() {
217         return getMenu().style.display != 'none';
218     }
219
220     function showMenu(shouldShow) {
221         getMenu().style.display = shouldShow ? '' : 'none';
222     }
223
224     function updateMenu() {
225         var newPrefix = m_focusedInput.value;
226         if (newPrefix) {
227             newPrefix = newPrefix.slice(getStart(), getEnd());
228             newPrefix = newPrefix.replace(/^\s+/, '');
229             newPrefix = newPrefix.replace(/\s+$/, '');
230         }
231
232         if (m_prefix == newPrefix)
233             return;
234
235         m_prefix = newPrefix;
236
237         var contacts = contactsMatching(m_prefix);
238         if (contacts.length == 0 || contacts.length == 1 && contacts[0].emails[0] == m_prefix) {
239             showMenu(false);
240             return;
241         }
242
243         var html = [];
244         for (var i = 0; i < contacts.length; i++) {
245             var contact = contacts[i];
246             html.push('<div style="padding:1px 2px;" ' + 'email=' +
247                 contact.emails[0] + '>' + contact.name + ' - ' + contact.emails[0]);
248             if (contact.irc)
249                 html.push(' (:' + contact.irc + ')');
250             if (contact.type)
251                 html.push(' (' + contact.type + ')');
252             html.push('</div>');
253         }
254         getMenu().innerHTML = html.join('');
255         selectItem(0);
256         showMenu(true);
257     }
258
259     function getIndex(item) {
260         for (var i = 0; i < getMenu().childNodes.length; i++) {
261             if (item == getMenu().childNodes[i])
262                 return i;
263         }
264         console.error("Couldn't find item.");
265     }
266
267     function getMenu() {
268         return m_menus[m_focusedInput.name];
269     }
270
271     function createMenu(name, input) {
272         if (!m_menus[name]) {
273             var menu = document.createElement('div');
274             menu.style.cssText =
275                 "position:absolute;border:1px solid black;background-color:white;-webkit-box-shadow:3px 3px 3px #888;";
276             menu.style.minWidth = m_focusedInput.offsetWidth + 'px';
277             m_focusedInput.parentNode.insertBefore(menu, m_focusedInput.nextSibling);
278
279             menu.addEventListener('mousedown', function(e) {
280                 selectItem(getIndex(e.target));
281                 e.preventDefault();
282             }, false);
283
284             menu.addEventListener('mouseup', function(e) {
285                 if (m_selectedIndex == getIndex(e.target))
286                     insertSelectedItem();
287             }, false);
288             
289             m_menus[name] = menu;
290         }
291     }
292
293     function getStart() {
294         var index = m_focusedInput.value.lastIndexOf(',', m_focusedInput.selectionStart - 1);
295         if (index == -1)
296             return 0;
297         return index + 1;
298     }
299
300     function getEnd() {
301         var index = m_focusedInput.value.indexOf(',', m_focusedInput.selectionStart);
302         if (index == -1)
303             return m_focusedInput.value.length;
304         return index;
305     }
306
307     function getItem(index) {
308         return getMenu().childNodes[index];
309     }
310
311     function selectItem(index) {
312         if (index < 0 || index >= getMenu().childNodes.length)
313             return;
314
315         if (m_selectedIndex != undefined) {
316             getItem(m_selectedIndex).style.backgroundColor = '';
317             getItem(m_selectedIndex).style.color = '';
318         }
319
320         getItem(index).style.backgroundColor = '#039';
321         getItem(index).style.color = 'white';
322
323         m_selectedIndex = index;
324     }
325
326     function insertSelectedItem() {
327         var selectedEmail = getItem(m_selectedIndex).getAttribute('email');
328         var oldValue = m_focusedInput.value;
329
330         var newValue = oldValue.slice(0, getStart()) + selectedEmail + oldValue.slice(getEnd());
331         if (SINGLE_EMAIL_INPUTS.indexOf(m_focusedInput.name) == -1 &&
332             newValue.charAt(newValue.length - 1) != ',')
333             newValue = newValue + ',';
334
335         m_focusedInput.value = newValue;
336         showMenu(false);    
337     }
338
339     function handleKeyDown(e) {
340         if (!isMenuVisible())
341             return;
342
343         switch (e.keyIdentifier) {
344             case 'Up':
345                 selectItem(m_selectedIndex - 1);
346                 e.preventDefault();
347                 break;
348             
349             case 'Down':
350                 selectItem(m_selectedIndex + 1);
351                 e.preventDefault();
352                 break;
353                 
354             case 'Enter':
355                 insertSelectedItem();
356                 e.preventDefault();
357                 break;
358         }
359     }
360
361     function handleKeyUp(e) {
362         if (e.keyIdentifier == 'Enter')
363             return;
364
365         if (m_focusedInput.selectionStart == m_focusedInput.selectionEnd)
366             updateMenu();
367         else
368             showMenu(false);
369     }
370
371     function enableAutoComplete(input) {
372         m_focusedInput = input;
373
374         if (!getMenu()) {
375             createMenu(m_focusedInput.name);
376             // Turn off autocomplete to avoid showing the browser's dropdown menu.
377             m_focusedInput.setAttribute('autocomplete', 'off');
378             m_focusedInput.addEventListener('keyup', handleKeyUp, false);
379             m_focusedInput.addEventListener('keydown', handleKeyDown, false);
380             m_focusedInput.addEventListener('blur', function() {
381                 showMenu(false);
382                 m_prefix = null;
383                 m_selectedIndex = 0;
384             }, false);
385             // Turn on autocomplete on submit to avoid breaking autofill on back/forward navigation.
386             m_focusedInput.form.addEventListener("submit", function() {
387                 m_focusedInput.setAttribute("autocomplete", "on");
388             }, false);
389         }
390         
391         updateMenu();
392     }
393
394     for (var i = 0; i < EMAIL_INPUTS.length; i++) {
395         var field = document.getElementsByName(EMAIL_INPUTS[i])[0];
396         if (field)
397             field.addEventListener("focus", function(e) { enableAutoComplete(e.target); }, false);
398     }
399
400     WebKitCommitters.getCommitters(function (committers) {
401         m_committers = committers;
402     });
403 })();