awesomplete.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /**
  2. * Simple, lightweight, usable local autocomplete library for modern browsers
  3. * Because there weren’t enough autocomplete scripts in the world? Because I’m completely insane and have NIH syndrome? Probably both. :P
  4. * @author Lea Verou http://leaverou.github.io/awesomplete
  5. * MIT license
  6. */
  7. (function () {
  8. var _ = function (input, o) {
  9. var me = this;
  10. // Setup
  11. this.input = $(input);
  12. this.input.setAttribute("aria-autocomplete", "list");
  13. o = o || {};
  14. configure.call(this, {
  15. minChars: 2,
  16. maxItems: 10,
  17. autoFirst: false,
  18. filter: _.FILTER_CONTAINS,
  19. sort: _.SORT_BYLENGTH,
  20. item: function (text, input) {
  21. return $.create("li", {
  22. innerHTML: text.replace(RegExp($.regExpEscape(input.trim()), "gi"), "<mark>$&</mark>"),
  23. "aria-selected": "false"
  24. });
  25. },
  26. replace: function (text) {
  27. this.input.value = text;
  28. }
  29. }, o);
  30. this.index = -1;
  31. // Create necessary elements
  32. this.container = $.create("div", {
  33. className: "awesomplete",
  34. around: input
  35. });
  36. this.ul = $.create("ul", {
  37. hidden: "",
  38. inside: this.container
  39. });
  40. this.status = $.create("span", {
  41. className: "visually-hidden",
  42. role: "status",
  43. "aria-live": "assertive",
  44. "aria-relevant": "additions",
  45. inside: this.container
  46. });
  47. // Bind events
  48. $.bind(this.input, {
  49. "input": this.evaluate.bind(this),
  50. "blur": this.close.bind(this),
  51. "keydown": function(evt) {
  52. var c = evt.keyCode;
  53. // If the dropdown `ul` is in view, then act on keydown for the following keys:
  54. // Enter / Esc / Up / Down
  55. if(me.opened) {
  56. if (c === 13 && me.selected) { // Enter
  57. evt.preventDefault();
  58. me.select();
  59. }
  60. else if (c === 27) { // Esc
  61. me.close();
  62. }
  63. else if (c === 38 || c === 40) { // Down/Up arrow
  64. evt.preventDefault();
  65. me[c === 38? "previous" : "next"]();
  66. }
  67. }
  68. }
  69. });
  70. $.bind(this.input.form, {"submit": this.close.bind(this)});
  71. $.bind(this.ul, {"mousedown": function(evt) {
  72. var li = evt.target;
  73. if (li !== this) {
  74. while (li && !/li/i.test(li.nodeName)) {
  75. li = li.parentNode;
  76. }
  77. if (li) {
  78. me.select(li);
  79. }
  80. }
  81. }});
  82. if (this.input.hasAttribute("list")) {
  83. this.list = "#" + input.getAttribute("list");
  84. input.removeAttribute("list");
  85. }
  86. else {
  87. this.list = this.input.getAttribute("data-list") || o.list || [];
  88. }
  89. _.all.push(this);
  90. };
  91. _.prototype = {
  92. set list(list) {
  93. if (Array.isArray(list)) {
  94. this._list = list;
  95. }
  96. else if (typeof list === "string" && list.indexOf(",") > -1) {
  97. this._list = list.split(/\s*,\s*/);
  98. }
  99. else { // Element or CSS selector
  100. list = $(list);
  101. if (list && list.children) {
  102. this._list = slice.apply(list.children).map(function (el) {
  103. return el.innerHTML.trim();
  104. });
  105. }
  106. }
  107. if (document.activeElement === this.input) {
  108. this.evaluate();
  109. }
  110. },
  111. get selected() {
  112. return this.index > -1;
  113. },
  114. get opened() {
  115. return this.ul && this.ul.getAttribute("hidden") == null;
  116. },
  117. close: function () {
  118. this.ul.setAttribute("hidden", "");
  119. this.index = -1;
  120. $.fire(this.input, "awesomplete-close");
  121. },
  122. open: function () {
  123. this.ul.removeAttribute("hidden");
  124. if (this.autoFirst && this.index === -1) {
  125. this.goto(0);
  126. }
  127. $.fire(this.input, "awesomplete-open");
  128. },
  129. next: function () {
  130. var count = this.ul.children.length;
  131. this.goto(this.index < count - 1? this.index + 1 : -1);
  132. },
  133. previous: function () {
  134. var count = this.ul.children.length;
  135. this.goto(this.selected? this.index - 1 : count - 1);
  136. },
  137. // Should not be used, highlights specific item without any checks!
  138. goto: function (i) {
  139. var lis = this.ul.children;
  140. if (this.selected) {
  141. lis[this.index].setAttribute("aria-selected", "false");
  142. }
  143. this.index = i;
  144. if (i > -1 && lis.length > 0) {
  145. lis[i].setAttribute("aria-selected", "true");
  146. this.status.textContent = lis[i].textContent;
  147. }
  148. $.fire(this.input, "awesomplete-highlight");
  149. },
  150. select: function (selected) {
  151. selected = selected || this.ul.children[this.index];
  152. if (selected) {
  153. var prevented;
  154. $.fire(this.input, "awesomplete-select", {
  155. text: selected.textContent,
  156. preventDefault: function () {
  157. prevented = true;
  158. }
  159. });
  160. if (!prevented) {
  161. this.replace(selected.textContent);
  162. this.close();
  163. $.fire(this.input, "awesomplete-selectcomplete");
  164. }
  165. }
  166. },
  167. evaluate: function() {
  168. var me = this;
  169. var value = this.input.value;
  170. if (value.length >= this.minChars && this._list.length > 0) {
  171. this.index = -1;
  172. // Populate list with options that match
  173. this.ul.innerHTML = "";
  174. this._list
  175. .filter(function(item) {
  176. return me.filter(item, value);
  177. })
  178. .sort(this.sort)
  179. .every(function(text, i) {
  180. me.ul.appendChild(me.item(text, value));
  181. return i < me.maxItems - 1;
  182. });
  183. if (this.ul.children.length === 0) {
  184. this.close();
  185. } else {
  186. this.open();
  187. }
  188. }
  189. else {
  190. this.close();
  191. }
  192. }
  193. };
  194. // Static methods/properties
  195. _.all = [];
  196. _.FILTER_CONTAINS = function (text, input) {
  197. return RegExp($.regExpEscape(input.trim()), "i").test(text);
  198. };
  199. _.FILTER_STARTSWITH = function (text, input) {
  200. return RegExp("^" + $.regExpEscape(input.trim()), "i").test(text);
  201. };
  202. _.SORT_BYLENGTH = function (a, b) {
  203. if (a.length !== b.length) {
  204. return a.length - b.length;
  205. }
  206. return a < b? -1 : 1;
  207. };
  208. // Private functions
  209. function configure(properties, o) {
  210. for (var i in properties) {
  211. var initial = properties[i],
  212. attrValue = this.input.getAttribute("data-" + i.toLowerCase());
  213. if (typeof initial === "number") {
  214. this[i] = +attrValue;
  215. }
  216. else if (initial === false) { // Boolean options must be false by default anyway
  217. this[i] = attrValue !== null;
  218. }
  219. else if (initial instanceof Function) {
  220. this[i] = null;
  221. }
  222. else {
  223. this[i] = attrValue;
  224. }
  225. this[i] = this[i] || o[i] || initial;
  226. }
  227. }
  228. // Helpers
  229. var slice = Array.prototype.slice;
  230. function $(expr, con) {
  231. return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
  232. }
  233. function $$(expr, con) {
  234. return slice.call((con || document).querySelectorAll(expr));
  235. }
  236. $.create = function(tag, o) {
  237. var element = document.createElement(tag);
  238. for (var i in o) {
  239. var val = o[i];
  240. if (i === "inside") {
  241. $(val).appendChild(element);
  242. }
  243. else if (i === "around") {
  244. var ref = $(val);
  245. ref.parentNode.insertBefore(element, ref);
  246. element.appendChild(ref);
  247. }
  248. else if (i in element) {
  249. element[i] = val;
  250. }
  251. else {
  252. element.setAttribute(i, val);
  253. }
  254. }
  255. return element;
  256. };
  257. $.bind = function(element, o) {
  258. if (element) {
  259. for (var event in o) {
  260. var callback = o[event];
  261. event.split(/\s+/).forEach(function (event) {
  262. element.addEventListener(event, callback);
  263. });
  264. }
  265. }
  266. };
  267. $.fire = function(target, type, properties) {
  268. var evt = document.createEvent("HTMLEvents");
  269. evt.initEvent(type, true, true );
  270. for (var j in properties) {
  271. evt[j] = properties[j];
  272. }
  273. target.dispatchEvent(evt);
  274. };
  275. $.regExpEscape = function (s) {
  276. return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
  277. }
  278. // Initialization
  279. function init() {
  280. $$("input.awesomplete").forEach(function (input) {
  281. new Awesomplete(input);
  282. });
  283. }
  284. // Are we in a browser? Check for Document constructor
  285. if (typeof Document !== 'undefined') {
  286. // DOM already loaded?
  287. if (document.readyState !== "loading") {
  288. init();
  289. }
  290. else {
  291. // Wait for it
  292. document.addEventListener("DOMContentLoaded", init);
  293. }
  294. }
  295. _.$ = $;
  296. _.$$ = $$;
  297. // Make sure to export Awesomplete on self when in a browser
  298. if (typeof self !== 'undefined') {
  299. self.Awesomplete = _;
  300. }
  301. // Expose Awesomplete as a CJS module
  302. if (typeof exports === 'object') {
  303. module.exports = _;
  304. }
  305. return _;
  306. }());