]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - inc/awesomplete.js
Upgrade awesomplete + fix multiple autocompletion fields
[github/shaarli/Shaarli.git] / inc / awesomplete.js
index fae550e250f3e5c00a04bc0b28e50827ed4bce05..32f49e5b4c703a178f22ed4a67f6be43731328fe 100644 (file)
 
         // Setup
 
+        this.isOpened = false;
+
         this.input = $(input);
+        this.input.setAttribute("autocomplete", "off");
         this.input.setAttribute("aria-autocomplete", "list");
 
         o = o || {};
 
-        configure.call(this, {
+        configure(this, {
             minChars: 2,
             maxItems: 10,
             autoFirst: false,
+            data: _.DATA,
             filter: _.FILTER_CONTAINS,
             sort: _.SORT_BYLENGTH,
-            item: function (text, input) {
-                return $.create("li", {
-                    innerHTML: text.replace(RegExp($.regExpEscape(input.trim()), "gi"), "<mark>$&</mark>"),
-                    "aria-selected": "false"
-                });
-            },
-            replace: function (text) {
-                this.input.value = text;
-            }
+            item: _.ITEM,
+            replace: _.REPLACE
         }, o);
 
         this.index = -1;
@@ -44,7 +41,7 @@
         });
 
         this.ul = $.create("ul", {
-            hidden: "",
+            hidden: "hidden",
             inside: this.container
         });
 
@@ -60,7 +57,7 @@
 
         $.bind(this.input, {
             "input": this.evaluate.bind(this),
-            "blur": this.close.bind(this),
+            "blur": this.close.bind(this, { reason: "blur" }),
             "keydown": function(evt) {
                 var c = evt.keyCode;
 
@@ -72,7 +69,7 @@
                         me.select();
                     }
                     else if (c === 27) { // Esc
-                        me.close();
+                        me.close({ reason: "esc" });
                     }
                     else if (c === 38 || c === 40) { // Down/Up arrow
                         evt.preventDefault();
@@ -82,7 +79,7 @@
             }
         });
 
-        $.bind(this.input.form, {"submit": this.close.bind(this)});
+        $.bind(this.input.form, {"submit": this.close.bind(this, { reason: "submit" })});
 
         $.bind(this.ul, {"mousedown": function(evt) {
             var li = evt.target;
                     li = li.parentNode;
                 }
 
-                if (li) {
-                    me.select(li);
+                if (li && evt.button === 0) {  // Only select on left click
+                    evt.preventDefault();
+                    me.select(li, evt.target);
                 }
             }
         }});
 
         if (this.input.hasAttribute("list")) {
-            this.list = "#" + input.getAttribute("list");
-            input.removeAttribute("list");
+            this.list = "#" + this.input.getAttribute("list");
+            this.input.removeAttribute("list");
         }
         else {
             this.list = this.input.getAttribute("data-list") || o.list || [];
                 list = $(list);
 
                 if (list && list.children) {
-                    this._list = slice.apply(list.children).map(function (el) {
-                        return el.innerHTML.trim();
+                    var items = [];
+                    slice.apply(list.children).forEach(function (el) {
+                        if (!el.disabled) {
+                            var text = el.textContent.trim();
+                            var value = el.value || text;
+                            var label = el.label || text;
+                            if (value !== "") {
+                                items.push({ label: label, value: value });
+                            }
+                        }
                     });
+                    this._list = items;
                 }
             }
 
         },
 
         get opened() {
-            return this.ul && this.ul.getAttribute("hidden") == null;
+            return this.isOpened;
         },
 
-        close: function () {
+        close: function (o) {
+            if (!this.opened) {
+                return;
+            }
+
             this.ul.setAttribute("hidden", "");
+            this.isOpened = false;
             this.index = -1;
 
-            $.fire(this.input, "awesomplete-close");
+            $.fire(this.input, "awesomplete-close", o || {});
         },
 
         open: function () {
             this.ul.removeAttribute("hidden");
+            this.isOpened = true;
 
             if (this.autoFirst && this.index === -1) {
                 this.goto(0);
 
         next: function () {
             var count = this.ul.children.length;
-
-            this.goto(this.index < count - 1? this.index + 1 : -1);
+            this.goto(this.index < count - 1 ? this.index + 1 : (count ? 0 : -1) );
         },
 
         previous: function () {
             var count = this.ul.children.length;
+            var pos = this.index - 1;
 
-            this.goto(this.selected? this.index - 1 : count - 1);
+            this.goto(this.selected && pos !== -1 ? pos : count - 1);
         },
 
         // Should not be used, highlights specific item without any checks!
             if (i > -1 && lis.length > 0) {
                 lis[i].setAttribute("aria-selected", "true");
                 this.status.textContent = lis[i].textContent;
-            }
 
-            $.fire(this.input, "awesomplete-highlight");
+                // scroll to highlighted element in case parent's height is fixed
+                this.ul.scrollTop = lis[i].offsetTop - this.ul.clientHeight + lis[i].clientHeight;
+
+                $.fire(this.input, "awesomplete-highlight", {
+                    text: this.suggestions[this.index]
+                });
+            }
         },
 
-        select: function (selected) {
-            selected = selected || this.ul.children[this.index];
+        select: function (selected, origin) {
+            if (selected) {
+                this.index = $.siblingIndex(selected);
+            } else {
+                selected = this.ul.children[this.index];
+            }
 
             if (selected) {
-                var prevented;
+                var suggestion = this.suggestions[this.index];
 
-                $.fire(this.input, "awesomplete-select", {
-                    text: selected.textContent,
-                    preventDefault: function () {
-                        prevented = true;
-                    }
+                var allowed = $.fire(this.input, "awesomplete-select", {
+                    text: suggestion,
+                    origin: origin || selected
                 });
 
-                if (!prevented) {
-                    this.replace(selected.textContent);
-                    this.close();
-                    $.fire(this.input, "awesomplete-selectcomplete");
+                if (allowed) {
+                    this.replace(suggestion);
+                    this.close({ reason: "select" });
+                    $.fire(this.input, "awesomplete-selectcomplete", {
+                        text: suggestion
+                    });
                 }
             }
         },
                 // Populate list with options that match
                 this.ul.innerHTML = "";
 
-                this._list
+                this.suggestions = this._list
+                    .map(function(item) {
+                        return new Suggestion(me.data(item, value));
+                    })
                     .filter(function(item) {
                         return me.filter(item, value);
                     })
                     .sort(this.sort)
-                    .every(function(text, i) {
-                        me.ul.appendChild(me.item(text, value));
+                    .slice(0, this.maxItems);
 
-                        return i < me.maxItems - 1;
-                    });
+                this.suggestions.forEach(function(text) {
+                    me.ul.appendChild(me.item(text, value));
+                });
 
                 if (this.ul.children.length === 0) {
-                    this.close();
+                    this.close({ reason: "nomatches" });
                 } else {
                     this.open();
                 }
             }
             else {
-                this.close();
+                this.close({ reason: "nomatches" });
             }
         }
     };
         return a < b? -1 : 1;
     };
 
+    _.ITEM = function (text, input) {
+        var html = input.trim() === '' ? text : text.replace(RegExp($.regExpEscape(input.trim()), "gi"), "<mark>$&</mark>");
+        return $.create("li", {
+            innerHTML: html,
+            "aria-selected": "false"
+        });
+    };
+
+    _.REPLACE = function (text) {
+        this.input.value = text.value;
+    };
+
+    _.DATA = function (item/*, input*/) { return item; };
+
 // Private functions
 
-    function configure(properties, o) {
+    function Suggestion(data) {
+        var o = Array.isArray(data)
+            ? { label: data[0], value: data[1] }
+            : typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data };
+
+        this.label = o.label || o.value;
+        this.value = o.value;
+    }
+    Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", {
+        get: function() { return this.label.length; }
+    });
+    Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () {
+        return "" + this.label;
+    };
+
+    function configure(instance, properties, o) {
         for (var i in properties) {
             var initial = properties[i],
-                attrValue = this.input.getAttribute("data-" + i.toLowerCase());
+                attrValue = instance.input.getAttribute("data-" + i.toLowerCase());
 
             if (typeof initial === "number") {
-                this[i] = +attrValue;
+                instance[i] = parseInt(attrValue);
             }
             else if (initial === false) { // Boolean options must be false by default anyway
-                this[i] = attrValue !== null;
+                instance[i] = attrValue !== null;
             }
             else if (initial instanceof Function) {
-                this[i] = null;
+                instance[i] = null;
             }
             else {
-                this[i] = attrValue;
+                instance[i] = attrValue;
             }
 
-            this[i] = this[i] || o[i] || initial;
+            if (!instance[i] && instance[i] !== 0) {
+                instance[i] = (i in o)? o[i] : initial;
+            }
         }
     }
 
             evt[j] = properties[j];
         }
 
-        target.dispatchEvent(evt);
+        return target.dispatchEvent(evt);
     };
 
     $.regExpEscape = function (s) {
         return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
-    }
+    };
+
+    $.siblingIndex = function (el) {
+        /* eslint-disable no-cond-assign */
+        for (var i = 0; el = el.previousElementSibling; i++);
+        return i;
+    };
 
 // Initialization
 
     function init() {
         $$("input.awesomplete").forEach(function (input) {
-            new Awesomplete(input);
+            new _(input);
         });
     }
 
 // Are we in a browser? Check for Document constructor
-    if (typeof Document !== 'undefined') {
+    if (typeof Document !== "undefined") {
         // DOM already loaded?
         if (document.readyState !== "loading") {
             init();
     _.$$ = $$;
 
 // Make sure to export Awesomplete on self when in a browser
-    if (typeof self !== 'undefined') {
+    if (typeof self !== "undefined") {
         self.Awesomplete = _;
     }
 
 // Expose Awesomplete as a CJS module
-    if (typeof exports === 'object') {
+    if (typeof module === "object" && module.exports) {
         module.exports = _;
     }
 
     return _;
 
-}());
+}());
\ No newline at end of file