committers-autocomplete.js 10 KB

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