1 // Copyright (C) 2010 Ojan Vafai. All rights reserved.
2 // Copyright (C) 2010 Adam Barth. All rights reserved.
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are met:
7 // 1. Redistributions of source code must retain the above copyright notice,
8 // this list of conditions and the following disclaimer.
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.
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
26 WebKitCommitters = (function() {
27 var COMMITTERS_URL = 'http://svn.webkit.org/repository/webkit/trunk/Tools/Scripts/webkitpy/common/config/committers.py';
30 function getValues(param) {
31 var nextQuote = /^[^"]*"/g;
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;
43 function parseRecord(key, record) {
44 var keyIndex = record.indexOf(key);
47 record = record.substring(keyIndex + key.length);
49 var firstParen = /^\s*\(\s*/g;
50 firstParen.lastIndex = 0;
51 if (!firstParen.exec(record))
53 record = record.substring(firstParen.lastIndex);
55 var parsedResult = {};
58 var param = /^\s*((\[[^\]]+\])|(u?)("[^"]+"))\s*/g; // For emacs " to balance the quotes.
60 var nameParam = param.exec(record);
63 record = record.substring(param.lastIndex);
65 // Save the name without the quotes.
66 var name = nameParam[4].slice(1, nameParam[4].length - 1);
68 // Convert unicode characters
69 if (nameParam[3] == 'u') {
70 var unicode = /\\u([a-f\d]{4})/i;
71 var match = unicode.exec(name);
73 name = name.replace(match[0], String.fromCharCode(parseInt(match[1], 16)));
74 match = unicode.exec(name);
78 parsedResult.name = name;
80 var paramSeparator = /^\s*,\s*/g;
81 paramSeparator.lastIndex = 0;
82 if (!paramSeparator.exec(record))
84 record = record.substring(paramSeparator.lastIndex);
88 emailParam = param.exec(record);
92 emails = getValues(emailParam[0]);
93 parsedResult.emails = emails;
94 record = record.substring(param.lastIndex);
96 paramSeparator.lastIndex = 0;
97 if (!paramSeparator.exec(record))
99 record = record.substring(paramSeparator.lastIndex);
103 ircParam = param.exec(record);
106 record = record.substring(param.lastIndex);
108 irc = getValues(ircParam[0]);
109 parsedResult.irc = irc;
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);
120 m_committers.push(result);
124 function parseCommittersPy(text) {
127 var records = text.split('\n');
128 parseType('Committer', records, 'c');
129 parseType('Reviewer', records, 'r');
130 parseType('Contributor', records);
133 function loadCommitters(callback) {
134 var xhr = new XMLHttpRequest();
135 xhr.open('GET', COMMITTERS_URL);
137 xhr.onload = function() {
138 parseCommittersPy(xhr.responseText);
142 xhr.onerror = function() {
143 console.log('Unable to load committers.py');
150 function getCommitters(callback) {
152 callback(m_committers);
156 loadCommitters(function() {
157 callback(m_committers);
162 "getCommitters": getCommitters
167 var SINGLE_EMAIL_INPUTS = ['email1', 'email2', 'requester', 'requestee', 'assigned_to'];
168 var EMAIL_INPUTS = SINGLE_EMAIL_INPUTS.concat(['cc', 'newcc', 'new_watchedusers']);
176 function contactsMatching(prefix) {
181 for (var i = 0; i < m_committers.length; i++) {
182 if (isMatch(m_committers[i], prefix))
183 list.push(m_committers[i]);
188 function startsWith(str, prefix) {
189 return str.toLowerCase().indexOf(prefix.toLowerCase()) == 0;
192 function startsWithAny(arry, prefix) {
193 for (var i = 0; i < arry.length; i++) {
194 if (startsWith(arry[i], prefix))
200 function isMatch(contact, prefix) {
201 if (startsWithAny(contact.emails, prefix))
204 if (contact.irc && startsWithAny(contact.irc, prefix))
207 var names = contact.name.split(' ');
208 for (var i = 0; i < names.length; i++) {
209 if (startsWith(names[i], prefix))
216 function isMenuVisible() {
217 return getMenu().style.display != 'none';
220 function showMenu(shouldShow) {
221 getMenu().style.display = shouldShow ? '' : 'none';
224 function updateMenu() {
225 var newPrefix = m_focusedInput.value;
227 newPrefix = newPrefix.slice(getStart(), getEnd());
228 newPrefix = newPrefix.replace(/^\s+/, '');
229 newPrefix = newPrefix.replace(/\s+$/, '');
232 if (m_prefix == newPrefix)
235 m_prefix = newPrefix;
237 var contacts = contactsMatching(m_prefix);
238 if (contacts.length == 0 || contacts.length == 1 && contacts[0].emails[0] == m_prefix) {
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]);
249 html.push(' (:' + contact.irc + ')');
251 html.push(' (' + contact.type + ')');
254 getMenu().innerHTML = html.join('');
259 function getIndex(item) {
260 for (var i = 0; i < getMenu().childNodes.length; i++) {
261 if (item == getMenu().childNodes[i])
264 console.error("Couldn't find item.");
268 return m_menus[m_focusedInput.name];
271 function createMenu(name, input) {
272 if (!m_menus[name]) {
273 var menu = document.createElement('div');
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);
279 menu.addEventListener('mousedown', function(e) {
280 selectItem(getIndex(e.target));
284 menu.addEventListener('mouseup', function(e) {
285 if (m_selectedIndex == getIndex(e.target))
286 insertSelectedItem();
289 m_menus[name] = menu;
293 function getStart() {
294 var index = m_focusedInput.value.lastIndexOf(',', m_focusedInput.selectionStart - 1);
301 var index = m_focusedInput.value.indexOf(',', m_focusedInput.selectionStart);
303 return m_focusedInput.value.length;
307 function getItem(index) {
308 return getMenu().childNodes[index];
311 function selectItem(index) {
312 if (index < 0 || index >= getMenu().childNodes.length)
315 if (m_selectedIndex != undefined) {
316 getItem(m_selectedIndex).style.backgroundColor = '';
317 getItem(m_selectedIndex).style.color = '';
320 getItem(index).style.backgroundColor = '#039';
321 getItem(index).style.color = 'white';
323 m_selectedIndex = index;
326 function insertSelectedItem() {
327 var selectedEmail = getItem(m_selectedIndex).getAttribute('email');
328 var oldValue = m_focusedInput.value;
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 + ',';
335 m_focusedInput.value = newValue;
339 function handleKeyDown(e) {
340 if (!isMenuVisible())
343 switch (e.keyIdentifier) {
345 selectItem(m_selectedIndex - 1);
350 selectItem(m_selectedIndex + 1);
355 insertSelectedItem();
361 function handleKeyUp(e) {
362 if (e.keyIdentifier == 'Enter')
365 if (m_focusedInput.selectionStart == m_focusedInput.selectionEnd)
371 function enableAutoComplete(input) {
372 m_focusedInput = input;
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() {
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");
394 document.addEventListener('focusin', function(e) {
395 for (var i = 0; i < EMAIL_INPUTS.length; i++) {
396 if (e.target.name == EMAIL_INPUTS[i]) {
397 enableAutoComplete(e.target);
403 WebKitCommitters.getCommitters(function (committers) {
404 m_committers = committers;