summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLea Verou <lea@verou.me>2015-02-18 16:25:29 -0500
committerLea Verou <lea@verou.me>2015-02-18 16:25:29 -0500
commit718adf1d1ffb5b47589eb86bba4574a7f872424a (patch)
tree03a13ce93f2d1942f8f818d768ec6655cfa88d81
parent3ccf512d969a8dce346110fbb616aec7f669329a (diff)
downloadawesomplete-718adf1d1ffb5b47589eb86bba4574a7f872424a.zip
awesomplete-718adf1d1ffb5b47589eb86bba4574a7f872424a.tar.gz
awesomplete-718adf1d1ffb5b47589eb86bba4574a7f872424a.tar.bz2
First commit
-rw-r--r--awesomplete.css91
-rw-r--r--awesomplete.js267
-rw-r--r--index.html168
-rw-r--r--style.css193
4 files changed, 719 insertions, 0 deletions
diff --git a/awesomplete.css b/awesomplete.css
new file mode 100644
index 0000000..6466a0c
--- /dev/null
+++ b/awesomplete.css
@@ -0,0 +1,91 @@
+[hidden] { display: none; }
+
+div.awesomplete {
+ display: inline-block;
+ position: relative;
+}
+
+div.awesomplete > input {
+ display: block;
+}
+
+div.awesomplete > ul {
+ position: absolute;
+ left: 0;
+ z-index: 1;
+ min-width: 100%;
+ box-sizing: border-box;
+ list-style: none;
+ padding: 0;
+ border-radius: .3em;
+ margin: .2em 0 0;
+ background: hsla(0,0%,100%,.9);
+ background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
+ border: 1px solid rgba(0,0,0,.3);
+ box-shadow: .05em .2em .6em rgba(0,0,0,.2);
+ text-shadow: none;
+}
+
+div.awesomplete > ul[hidden],
+div.awesomplete > ul:empty {
+ display: none;
+}
+
+@supports (transform: scale(0)) {
+ div.awesomplete > ul {
+ transition: .3s cubic-bezier(.4,.2,.5,1.4);
+ transform-origin: 1em -.43em;
+ }
+
+ div.awesomplete > ul[hidden],
+ div.awesomplete > ul:empty {
+ opacity: 0;
+ transform: scale(0);
+ display: block;
+ transition-timing-function: ease;
+ }
+}
+
+ /* Pointer */
+ div.awesomplete > ul:before {
+ content: "";
+ position: absolute;
+ top: -.43em;
+ left: 1em;
+ width: 0; height: 0;
+ padding: .4em;
+ background: linear-gradient(to bottom right,white 50%, transparent 0);
+ border: inherit;
+ border-right: 0;
+ border-bottom: 0;
+ transform: rotate(45deg);
+ }
+
+ div.awesomplete > ul > li {
+ position: relative;
+ padding: .2em .5em;
+ cursor: pointer;
+ }
+
+ div.awesomplete > ul > li:hover {
+ background: hsl(200, 40%, 80%);
+ color: black;
+ }
+
+ div.awesomplete > ul > li[aria-selected="true"] {
+ background: hsl(205, 40%, 40%);
+ color: white;
+ }
+
+ div.awesomplete mark {
+ background: yellow;
+ }
+
+ div.awesomplete li:hover mark {
+ background: hsl(68, 101%, 41%);
+ }
+
+ div.awesomplete li[aria-selected="true"] mark {
+ background: hsl(86, 102%, 21%);
+ color: inherit;
+ } \ No newline at end of file
diff --git a/awesomplete.js b/awesomplete.js
new file mode 100644
index 0000000..ffe6a14
--- /dev/null
+++ b/awesomplete.js
@@ -0,0 +1,267 @@
+/**
+ * Simple, leightweight, usable local autocomplete library for modern browsers
+ * Because there weren’t enough autocomplete scripts in the world? Because I’m completely insane and have NIH syndrome? Probably both. :P
+ * @author Lea Verou
+ * MIT license
+ */
+
+(function () {
+
+function $(expr, con) {
+ if (!expr) return null;
+ return typeof expr === 'string'? (con || document).querySelector(expr) : expr;
+}
+
+function $$(expr, con) {
+ return Array.prototype.slice.call((con || document).querySelectorAll(expr));
+}
+
+$.create = function(tag, o) {
+ if (arguments.length == 1) {
+ o = tag;
+ tag = o.tag;
+ delete o.tag;
+ }
+
+ var element = o.element || document.createElement(tag);
+ delete o.element;
+
+ for (var i in o) {
+ var val = o[i];
+
+ if (i == "inside") {
+ $(val).appendChild(element);
+ }
+ else if (i == "around") {
+ var ref = $(val);
+ ref.parentNode.insertBefore(element, ref);
+ element.appendChild(ref);
+ }
+ else if (i in element) {
+ element[i] = val;
+ }
+ else {
+ element.setAttribute(i, val);
+ }
+ }
+
+ return element;
+}
+
+$.bind = function(element, o) {
+ if (element) {
+ for (var event in o) {
+ var callback = o[event];
+
+ event.split(/\s+/).forEach(function (event) {
+ element.addEventListener(event, callback);
+ });
+ }
+ }
+}
+
+var _ = self.Awesomplete = function (input, o) {
+ var me = this;
+
+ // Setup environment
+ o = o || {};
+
+ this.input = input;
+ input.setAttribute("aria-autocomplete", "list");
+
+ this.minChars = +input.getAttribute("data-minchars") || o.minChars || 2;
+ this.maxItems = +input.getAttribute("data-maxitems") || o.maxItems || 20;
+
+ var list = input.getAttribute("data-list") || o.list || [];
+
+ if (Array.isArray(list)) {
+ this.list = list;
+ }
+ else {
+ if (typeof list == "string" && list.indexOf(",") > -1) {
+ this.list = list.split(/\s*,\s*/);
+ }
+ else {
+ list = $(list);
+
+ if (list && list.children) {
+ list.setAttribute("hidden", "");
+
+ this.list = [].slice.apply(list.children).map(function (el) {
+ return el.innerHTML.trim();
+ });
+ }
+ }
+ }
+
+ this.filter = o.filter || _.FILTER_CONTAINS;
+ this.item = o.item || function (text, input) {
+ return $.create("li", {
+ innerHTML: text.replace(RegExp(regEscape(input.trim()), "gi"), "<mark>$&</mark>"),
+ "aria-selected": "false"
+ });
+ }
+
+ this.index = -1;
+
+ this.container = $.create("div", {
+ className: "awesomplete",
+ around: input
+ });
+
+ this.ul = $.create("ul", {
+ hidden: "",
+ inside: this.container
+ });
+
+ // Bind events
+
+ $.bind(this.input, {
+ "input focus": checkInput,
+ "blur": function () {
+ if (document.activeElement != this && document.activeElement != document.body) {
+ me.close();
+ }
+ },
+ "keydown": function(evt) {
+ switch (evt.keyCode) {
+ case 13: // Enter
+ if (me.index > -1) {
+ evt.preventDefault();
+ me.select();
+ }
+
+ break;
+ case 27: // Esc
+ me.close();
+ break;
+ case 40: // up arrow
+ me.next();
+ evt.preventDefault();
+ break;
+ case 38: // down arrow
+ me.previous();
+ evt.preventDefault();
+ break;
+ }
+ }
+ });
+
+ $.bind(this.input.form, {"submit": function(event) {
+ me.close();
+ }});
+
+ $.bind(this.ul, {"click": function(evt) {
+ var li = evt.target;
+
+ if (li != this) {
+
+ while (li && !/li/i.test(li.nodeName)) {
+ li = li.parentNode;
+ }
+
+ if (li) {
+ me.select(li);
+ }
+ }
+ }});
+
+ // Private functions
+
+ function checkInput() {
+ var value = me.input.value;
+
+ if (value.length >= me.minChars && me.list.length > 0) {
+ // Populate list with options that match
+ me.ul.innerHTML = "";
+
+ me.list.filter(function(item) {
+ return me.filter(item, value);
+ }).every(function(text, i) {
+ me.ul.appendChild(me.item(text, value));
+
+ return i < me.maxItems - 1;
+ });
+
+ me.open();
+ }
+ else {
+ me.close();
+ }
+ }
+};
+
+_.prototype = {
+ close: function () {
+ this.ul.setAttribute("hidden", "");
+ this.index = -1;
+ },
+
+ open: function () {
+ this.ul.removeAttribute("hidden");
+
+ var me = this;
+
+ document.addEventListener("click", function(evt) {
+ if (!me.container.contains(evt.target)) {
+ document.removeEventListener(arguments.callee);
+ me.close();
+ }
+ });
+ },
+
+ next: function () {
+ var count = this.ul.children.length;
+
+ this.goto(this.index < count - 1? this.index + 1 : -1);
+ },
+
+ previous: function () {
+ var count = this.ul.children.length;
+
+ this.goto(this.index > -1? this.index - 1 : count - 1);
+ },
+
+ // Should not be used, highlights specific item without any checks!
+ goto: function (i) {
+ var lis = this.ul.children;
+
+ if (this.index > -1) {
+ lis[this.index].setAttribute("aria-selected", "false");
+ }
+
+ this.index = i;
+
+ if (i > -1) {
+ lis[i].setAttribute("aria-selected", "true");
+ }
+ },
+
+ select: function (selected) {
+ selected = selected || this.ul.children[this.index];
+
+ if (selected) {
+ this.input.value = selected.textContent;
+ this.close();
+ }
+ }
+};
+
+_.FILTER_CONTAINS = function (text, input) {
+ return RegExp(regEscape(input.trim()), "i").test(text);
+};
+
+_.FILTER_STARTSWITH = function (text, input) {
+ return RegExp("^" + regEscape(input.trim()), "i").test(text);
+};
+
+function regEscape(s) { return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); }
+
+$$("input.awesomplete").forEach(function (input) {
+ new Awesomplete(input);
+});
+
+_.$ = $;
+_.$$ = $$;
+
+})(); \ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..fd25c96
--- /dev/null
+++ b/index.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+
+<meta charset="utf-8" />
+<title>Awesomplete: Lightweight autocomplete</title>
+<link rel="stylesheet" href="http://prismjs.com/themes/prism.css" />
+<link rel="stylesheet" href="awesomplete.css" />
+<link rel="stylesheet" href="style.css" />
+
+</head>
+<body class="language-markup">
+
+<header>
+<h1>Awesomplete</h1>
+
+<p class="size"><strong>1.5KB</strong> minified <span class="amp">&amp;</span> gzipped!</p>
+
+<p>Lightweight, simple, customizable autocomplete, built with modern standards. Because <code>&lt;datalist></code> still doesn’t cut it.</p>
+</header>
+
+<section>
+
+<h1>Demo (no JS, minimal options)</h1>
+<label>
+ Pick a programming language:<br />
+ <input class="awesomplete" data-list="Ada, Java, JavaScript, Brainfuck, LOLCODE, Node.js, Ruby on Rails" xautofocus />
+</label>
+
+<p>With Awesomplete, making something like this can be as simple as:</p>
+
+<pre class="language-markup"><code>&lt;input class="awesomplete"
+ data-list="Ada, Java, JavaScript, Brainfuck, LOLCODE, Node.js, Ruby on Rails" /></code></pre>
+
+<pre class="language-javascript"><code>// No extra JS needed for basic usage!</code></pre>
+
+</section>
+
+<section id="basic-usage">
+
+<h1>Basic usage</h1>
+<p>Before you try anything, you need to include <code>awesomplete.css</code> and <code>awesomplete.js</code> in your page, via the usual <code>&lt;link rel="stylesheet" href="awesomplete.css" /></code> and <code>&lt;script src="awesomplete.js">&lt;/script></code> tags.</p>
+
+<p>For the autocomplete, you just need an <code>&lt;input></code> or <code>&lt;textarea></code> box. Add <code>class="awsomplete"</code> for it to be <strong>automatically processed</strong> (you can still specify most options via HTML attributes), otherwise you can instantiate with a few lines of JS code, which allow for more customization.</p>
+<p>There are many ways <strong>to link an input to a list of suggestions</strong>. The simple example above could have also been made with the following code:</p>
+
+<pre class="language-markup"><code>&lt;input class="awesomplete" data-list="#mylist" />
+&lt;ul id="mylist">
+ &lt;li>Ada&lt;/li>
+ &lt;li>Java&lt;/li>
+ &lt;li>JavaScript&lt;/li>
+ &lt;li>Brainfuck&lt;/li>
+ &lt;li>LOLCODE&lt;/li>
+ &lt;li>Node.js&lt;/li>
+ &lt;li>Ruby on Rails&lt;/li>
+&lt;/ul></code></pre>
+
+<pre class="language-javascript"><code>// None!</code></pre>
+
+<p>Or the following, if we want to instantiate in JS:</p>
+
+<pre class="language-markup"><code>&lt;input id="myinput" />
+&lt;ul id="mylist">
+ &lt;li>Ada&lt;/li>
+ &lt;li>Java&lt;/li>
+ &lt;li>JavaScript&lt;/li>
+ &lt;li>Brainfuck&lt;/li>
+ &lt;li>LOLCODE&lt;/li>
+ &lt;li>Node.js&lt;/li>
+ &lt;li>Ruby on Rails&lt;/li>
+&lt;/ul></code></pre>
+<pre class="language-javascript"><code>var input = document.getElementById("myinput");
+new Awesomplete(input, {list: "#mylist"});</code></pre>
+
+<p>We can use an <strong>element reference</strong> for the list instead of a selector:</p>
+
+<pre class="language-markup"><code>&lt;input id="myinput" />
+&lt;ul id="mylist">
+ &lt;li>Ada&lt;/li>
+ &lt;li>Java&lt;/li>
+ &lt;li>JavaScript&lt;/li>
+ &lt;li>Brainfuck&lt;/li>
+ &lt;li>LOLCODE&lt;/li>
+ &lt;li>Node.js&lt;/li>
+ &lt;li>Ruby on Rails&lt;/li>
+&lt;/ul></code></pre>
+<pre class="language-javascript"><code>var input = document.getElementById("myinput");
+new Awesomplete(input, {list: document.querySelector("#mylist")});</code></pre>
+
+<p>We can also directly use an <strong>array of strings</strong>:</p>
+
+<pre class="language-markup"><code>&lt;input id="myinput" /></code></pre>
+ <pre class="language-javascript"><code>var input = document.getElementById("myinput");
+new Awesomplete(input, {
+ list: ["Ada", "Java", "JavaScript", "Brainfuck", "LOLCODE", "Node.js", "Ruby on Rails"]
+});</code></pre>
+
+</section>
+
+<section id="customization">
+
+ <h1>More customization</h1>
+
+ <p>Except the properties whose value needs to be a function, every property is settable via either a <code>data-</code> attribute on the <code>&lt;input></code> element or a JS property on the second argument of the <code>Awesomplete</code> constructor, like so:</p>
+
+ <pre class="language-javascript"><code>new Awesomplete(inputReference, {
+ minChars: 3,
+ maxItems: 15,
+ ...
+});</code></pre>
+ <p>You can of course combine both HTML attributes and JS properties. <strong>In case of conflict</strong> (e.g. you’ve specified both a <code>data-minchars</code> on the text field and a <code>minChars</code> JS property, <strong>the HTML attribute wins</strong>.</p>
+
+ <table>
+ <thead>
+ <tr>
+ <th>JS property</th>
+ <th>HTML attribute</th>
+ <th>Description</th>
+ <th>Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><code>list</code></td>
+ <td><code>data-list</code></td>
+ <td>Where to find the list of suggestions. Described in more detail in the “<a href="#basic-usage">Basic usage</a>” section above.</td>
+ <td>Array of strings, CSS selector (no commas), comma-separated list of items</td>
+ </tr>
+ <tr>
+ <td><code>minChars</code></td>
+ <td><code>data-minchars</code></td>
+ <td>Minimum characters the user has to type before the autocomplete popup shows up.</td>
+ <td>Number</td>
+ </tr>
+ <tr>
+ <td><code>maxItems</code></td>
+ <td><code>data-maxitems</code></td>
+ <td>Maximum number of suggestions to display.</td>
+ <td>Number</td>
+ </tr>
+ <tr>
+ <td><code>filter</code></td>
+ <td>-</td>
+ <td>Controls how entries get matched. By default, the input can match anywhere in the string and it’s a case insensitive match.</td>
+ <td>Function that takes two parameters, the first one being the current suggestion text that’s being tested and the second a string with the user’s input it’s matched against. Returns <code>true</code> if the match is successful and <code>false</code> if it is not. For example, to only match strings that <strong>start with the user’s input</strong>, <strong>case sensitive</strong>, we can do this:
+<pre class="language-javascript"><code>filter: function (text, input) {
+ return item.indexOf(input) === 0;
+}</code></pre> For case-<strong>in</strong>sensitive matching from the start of the word, there is a predefined filter that you can use, <code class="language-javascript">Awesomplete.FILTER_STARTSWITH</code>
+ </td>
+ </tr>
+ <tr>
+ <td><code>item</code></td>
+ <td>-</td>
+ <td>Controls how list items are generated.</td>
+ <td>Function that takes two parameters, the first one being the suggestion text and the second one the user’s input and returns a list item.</td>
+ </tr>
+ </tbody>
+ </table>
+
+</section>
+
+<footer>By <a href="http://lea.verou.me">Lea Verou</a></footer>
+
+<script src="awesomplete.js"></script>
+<script src="http://prismjs.com/prism.js" defer></script>
+
+</body>
+</html> \ No newline at end of file
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..30d5a52
--- /dev/null
+++ b/style.css
@@ -0,0 +1,193 @@
+body {
+ max-width: 50rem;
+ padding: 1rem;
+ margin: auto;
+ background: hsl(35, 80%, 94%);
+ color: hsl(35, 50%, 20%);
+ text-shadow: 0 1px 1px white;
+ font: 120%/1.6 Baskerville, Palatino Linotype, Palatino, serif;
+}
+
+a {
+ color: #58a;
+}
+
+h1 {
+ text-align: center;
+ font-weight: 600;
+}
+
+ section > h1 {
+ margin-top: 2em;
+ }
+
+h2 {
+ color: hsl(35, 50%, 40%);
+ font-weight: 600;
+ font-size: 120%;
+}
+
+strong {
+ font-weight: 600;
+}
+
+input, button {
+ font: inherit;
+ text-shadow: inherit;
+}
+
+button {
+ padding: .1em .5em;
+ border-radius: .3em;
+ background: hsl(80, 80%, 80%);
+ background: linear-gradient(hsl(40, 70%, 80%), hsl(40, 70%, 70%));
+ border: 1px solid rgba(0,0,0,.3);
+ box-shadow: 0 1px white inset, 0 .3em .3em -.3em rgba(0,0,0,.3);
+}
+
+input {
+ width: 12em;
+ padding: 0 .3em;
+ border: 0;
+ border: 1px solid hsl(35, 80%, 60%);
+ background: hsla(0,0%,100%,.2);
+ border-radius: .3em;
+ box-shadow: .05em .1em .3em rgba(0,0,0,.3) inset;
+}
+
+@keyframes pulsate {
+ to {
+ box-shadow: .05em .1em .3em rgba(0,0,0,.3) inset, 0 0 .3em .1em #58a;
+ }
+}
+
+input:focus {
+ outline: none;
+ border: 1px solid #58a;
+ animation: pulsate 2s infinite alternate linear;
+ background: hsla(0,0%,100%,.5);
+}
+
+header {
+ max-width: 37rem;
+ margin: 0 auto 2em;
+ text-align: center;
+ font-size: 150%;
+ font-style: italic;
+}
+
+ header h1 {
+ margin: .1em 0;
+ text-align: center;
+ color: hsl(35, 50%, 40%);
+ font-size: 400%;
+ line-height: 1;
+ font-style: italic;
+ font-weight: normal;
+ }
+
+ header p {
+ margin: 0;
+ }
+
+ .size {
+ position: absolute;
+ top: 1em;
+ right: 1em;
+ width: 4em;
+ padding: .7em .5em;
+ background: hsl(40, 80%, 40%);
+ color: hsl(35, 80%, 94%);
+ text-shadow: none;
+ text-align: center;
+ line-height: 1.1;
+ text-indent: -.1em;
+ box-shadow: 0 0 0 .2em hsl(40, 80%, 40%) inset, 0 0 0 .25em currentcolor inset;
+ }
+
+ .size::before {
+ content: "";
+ position: absolute;
+ top: -.5em;
+ left: -.5em;
+ padding: .5em;
+ background: currentColor;
+ border-radius: 50%;
+ box-shadow: 5em 0, 0 4.9em, 5em 4.9em;
+ overflow: hidden;
+ }
+
+ .size strong {
+ display: block;
+ font-weight: 900;
+ margin-bottom: .1em;
+ font-size: 110%;
+ font-style: normal;
+ }
+
+ .size .amp {
+ position: absolute;
+ left: 0; right: 0;
+ bottom: -.1em;
+ color: hsla(35, 80%, 94%,.3);
+ font-size: 350%;
+ }
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"] {
+ background: hsl(35, 70%, 88%);
+ font-size: 80%;
+ border-radius: .3em;
+}
+
+footer {
+ padding: .6em;
+ border-top: 1px solid rgba(0,0,0,.3);
+ margin-top: 1em;
+ text-align: center;
+ font-size: 75%;
+}
+
+pre {
+ position: relative;
+}
+
+pre::before {
+ position: absolute;
+ top: 0;
+ right: .5em;
+ padding: .3em .4em;
+ border-radius: 0 0 .3em .3em;
+ background: hsl(35, 50%, 60%);
+ color: hsl(35, 80%, 90%);
+ text-shadow: none;
+ font: bold 100%/1 Baskerville, Palatino Linotype, Palatino, serif;
+}
+
+pre.language-markup::before {
+ content: "HTML";
+}
+
+pre.language-javascript::before {
+ content: "JS";
+}
+
+table {
+ table-layout: fixed;
+ border-spacing: 0;
+ border-collapse: collapse;
+}
+
+th {
+ font-size: 75%;
+}
+
+td {
+ vertical-align: top;
+ padding: 0 .5em;
+ border: 1px solid rgba(0,0,0,.2);
+}
+
+:not(pre) > code[class*="language-"] {
+ background: none;
+} \ No newline at end of file