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