]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - inc/awesomplete.js
Keep up with master changes
[github/shaarli/Shaarli.git] / inc / awesomplete.js
index fae550e250f3e5c00a04bc0b28e50827ed4bce05..7303652a7fb26192222eaeae7d50f9206aae572d 100644 (file)
 
 (function () {
 
-    var _ = function (input, o) {
-        var me = this;
-
-        // Setup
-
-        this.input = $(input);
-        this.input.setAttribute("aria-autocomplete", "list");
-
-        o = o || {};
-
-        configure.call(this, {
-            minChars: 2,
-            maxItems: 10,
-            autoFirst: false,
-            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;
-            }
-        }, o);
-
-        this.index = -1;
-
-        // Create necessary elements
-
-        this.container = $.create("div", {
-            className: "awesomplete",
-            around: input
-        });
-
-        this.ul = $.create("ul", {
-            hidden: "",
-            inside: this.container
-        });
-
-        this.status = $.create("span", {
-            className: "visually-hidden",
-            role: "status",
-            "aria-live": "assertive",
-            "aria-relevant": "additions",
-            inside: this.container
-        });
-
-        // Bind events
-
-        $.bind(this.input, {
-            "input": this.evaluate.bind(this),
-            "blur": this.close.bind(this),
-            "keydown": function(evt) {
-                var c = evt.keyCode;
-
-                // If the dropdown `ul` is in view, then act on keydown for the following keys:
-                // Enter / Esc / Up / Down
-                if(me.opened) {
-                    if (c === 13 && me.selected) { // Enter
-                        evt.preventDefault();
-                        me.select();
-                    }
-                    else if (c === 27) { // Esc
-                        me.close();
-                    }
-                    else if (c === 38 || c === 40) { // Down/Up arrow
-                        evt.preventDefault();
-                        me[c === 38? "previous" : "next"]();
-                    }
-                }
-            }
-        });
-
-        $.bind(this.input.form, {"submit": this.close.bind(this)});
-
-        $.bind(this.ul, {"mousedown": function(evt) {
-            var li = evt.target;
-
-            if (li !== this) {
-
-                while (li && !/li/i.test(li.nodeName)) {
-                    li = li.parentNode;
-                }
-
-                if (li) {
-                    me.select(li);
-                }
-            }
-        }});
-
-        if (this.input.hasAttribute("list")) {
-            this.list = "#" + input.getAttribute("list");
-            input.removeAttribute("list");
-        }
-        else {
-            this.list = this.input.getAttribute("data-list") || o.list || [];
-        }
-
-        _.all.push(this);
-    };
-
-    _.prototype = {
-        set list(list) {
-            if (Array.isArray(list)) {
-                this._list = list;
-            }
-            else if (typeof list === "string" && list.indexOf(",") > -1) {
-                this._list = list.split(/\s*,\s*/);
-            }
-            else { // Element or CSS selector
-                list = $(list);
-
-                if (list && list.children) {
-                    this._list = slice.apply(list.children).map(function (el) {
-                        return el.innerHTML.trim();
-                    });
-                }
-            }
-
-            if (document.activeElement === this.input) {
-                this.evaluate();
-            }
-        },
-
-        get selected() {
-            return this.index > -1;
-        },
-
-        get opened() {
-            return this.ul && this.ul.getAttribute("hidden") == null;
-        },
-
-        close: function () {
-            this.ul.setAttribute("hidden", "");
-            this.index = -1;
-
-            $.fire(this.input, "awesomplete-close");
-        },
-
-        open: function () {
-            this.ul.removeAttribute("hidden");
-
-            if (this.autoFirst && this.index === -1) {
-                this.goto(0);
-            }
-
-            $.fire(this.input, "awesomplete-open");
-        },
-
-        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.selected? 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.selected) {
-                lis[this.index].setAttribute("aria-selected", "false");
-            }
-
-            this.index = i;
-
-            if (i > -1 && lis.length > 0) {
-                lis[i].setAttribute("aria-selected", "true");
-                this.status.textContent = lis[i].textContent;
-            }
-
-            $.fire(this.input, "awesomplete-highlight");
-        },
-
-        select: function (selected) {
-            selected = selected || this.ul.children[this.index];
-
-            if (selected) {
-                var prevented;
-
-                $.fire(this.input, "awesomplete-select", {
-                    text: selected.textContent,
-                    preventDefault: function () {
-                        prevented = true;
-                    }
-                });
-
-                if (!prevented) {
-                    this.replace(selected.textContent);
-                    this.close();
-                    $.fire(this.input, "awesomplete-selectcomplete");
-                }
-            }
-        },
-
-        evaluate: function() {
-            var me = this;
-            var value = this.input.value;
-
-            if (value.length >= this.minChars && this._list.length > 0) {
-                this.index = -1;
-                // Populate list with options that match
-                this.ul.innerHTML = "";
-
-                this._list
-                    .filter(function(item) {
-                        return me.filter(item, value);
-                    })
-                    .sort(this.sort)
-                    .every(function(text, i) {
-                        me.ul.appendChild(me.item(text, value));
-
-                        return i < me.maxItems - 1;
-                    });
-
-                if (this.ul.children.length === 0) {
-                    this.close();
-                } else {
-                    this.open();
-                }
-            }
-            else {
-                this.close();
-            }
-        }
-    };
+var _ = function (input, o) {
+       var me = this;
+
+       // Setup
+
+       this.input = $(input);
+       this.input.setAttribute("autocomplete", "off");
+       this.input.setAttribute("aria-autocomplete", "list");
+
+       o = o || {};
+
+       configure(this, {
+               minChars: 2,
+               maxItems: 10,
+               autoFirst: false,
+               data: _.DATA,
+               filter: _.FILTER_CONTAINS,
+               sort: _.SORT_BYLENGTH,
+               item: _.ITEM,
+               replace: _.REPLACE
+       }, o);
+
+       this.index = -1;
+
+       // Create necessary elements
+
+       this.container = $.create("div", {
+               className: "awesomplete",
+               around: input
+       });
+
+       this.ul = $.create("ul", {
+               hidden: "hidden",
+               inside: this.container
+       });
+
+       this.status = $.create("span", {
+               className: "visually-hidden",
+               role: "status",
+               "aria-live": "assertive",
+               "aria-relevant": "additions",
+               inside: this.container
+       });
+
+       // Bind events
+
+       $.bind(this.input, {
+               "input": this.evaluate.bind(this),
+               "blur": this.close.bind(this),
+               "keydown": function(evt) {
+                       var c = evt.keyCode;
+
+                       // If the dropdown `ul` is in view, then act on keydown for the following keys:
+                       // Enter / Esc / Up / Down
+                       if(me.opened) {
+                               if (c === 13 && me.selected) { // Enter
+                                       evt.preventDefault();
+                                       me.select();
+                               }
+                               else if (c === 27) { // Esc
+                                       me.close();
+                               }
+                               else if (c === 38 || c === 40) { // Down/Up arrow
+                                       evt.preventDefault();
+                                       me[c === 38? "previous" : "next"]();
+                               }
+                       }
+               }
+       });
+
+       $.bind(this.input.form, {"submit": this.close.bind(this)});
+
+       $.bind(this.ul, {"mousedown": function(evt) {
+               var li = evt.target;
+
+               if (li !== this) {
+
+                       while (li && !/li/i.test(li.nodeName)) {
+                               li = li.parentNode;
+                       }
+
+                       if (li && evt.button === 0) {  // Only select on left click
+                               evt.preventDefault();
+                               me.select(li, evt.target);
+                       }
+               }
+       }});
+
+       if (this.input.hasAttribute("list")) {
+               this.list = "#" + this.input.getAttribute("list");
+               this.input.removeAttribute("list");
+       }
+       else {
+               this.list = this.input.getAttribute("data-list") || o.list || [];
+       }
+
+       _.all.push(this);
+};
+
+_.prototype = {
+       set list(list) {
+               if (Array.isArray(list)) {
+                       this._list = list;
+               }
+               else if (typeof list === "string" && list.indexOf(",") > -1) {
+                               this._list = list.split(/\s*,\s*/);
+               }
+               else { // Element or CSS selector
+                       list = $(list);
+
+                       if (list && list.children) {
+                               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;
+                       }
+               }
+
+               if (document.activeElement === this.input) {
+                       this.evaluate();
+               }
+       },
+
+       get selected() {
+               return this.index > -1;
+       },
+
+       get opened() {
+               return !this.ul.hasAttribute("hidden");
+       },
+
+       close: function () {
+               this.ul.setAttribute("hidden", "");
+               this.index = -1;
+
+               $.fire(this.input, "awesomplete-close");
+       },
+
+       open: function () {
+               this.ul.removeAttribute("hidden");
+
+               if (this.autoFirst && this.index === -1) {
+                       this.goto(0);
+               }
+
+               $.fire(this.input, "awesomplete-open");
+       },
+
+       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.selected? 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.selected) {
+                       lis[this.index].setAttribute("aria-selected", "false");
+               }
+
+               this.index = i;
+
+               if (i > -1 && lis.length > 0) {
+                       lis[i].setAttribute("aria-selected", "true");
+                       this.status.textContent = lis[i].textContent;
+
+                       $.fire(this.input, "awesomplete-highlight", {
+                               text: this.suggestions[this.index]
+                       });
+               }
+       },
+
+       select: function (selected, origin) {
+               if (selected) {
+                       this.index = $.siblingIndex(selected);
+               } else {
+                       selected = this.ul.children[this.index];
+               }
+
+               if (selected) {
+                       var suggestion = this.suggestions[this.index];
+
+                       var allowed = $.fire(this.input, "awesomplete-select", {
+                               text: suggestion,
+                               origin: origin || selected
+                       });
+
+                       if (allowed) {
+                               this.replace(suggestion);
+                               this.close();
+                               $.fire(this.input, "awesomplete-selectcomplete", {
+                                       text: suggestion
+                               });
+                       }
+               }
+       },
+
+       evaluate: function() {
+               var me = this;
+               var value = this.input.value;
+
+               if (value.length >= this.minChars && this._list.length > 0) {
+                       this.index = -1;
+                       // Populate list with options that match
+                       this.ul.innerHTML = "";
+
+                       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)
+                               .slice(0, this.maxItems);
+
+                       this.suggestions.forEach(function(text) {
+                                       me.ul.appendChild(me.item(text, value));
+                               });
+
+                       if (this.ul.children.length === 0) {
+                               this.close();
+                       } else {
+                               this.open();
+                       }
+               }
+               else {
+                       this.close();
+               }
+       }
+};
 
 // Static methods/properties
 
-    _.all = [];
+_.all = [];
 
-    _.FILTER_CONTAINS = function (text, input) {
-        return RegExp($.regExpEscape(input.trim()), "i").test(text);
-    };
+_.FILTER_CONTAINS = function (text, input) {
+       return RegExp($.regExpEscape(input.trim()), "i").test(text);
+};
 
-    _.FILTER_STARTSWITH = function (text, input) {
-        return RegExp("^" + $.regExpEscape(input.trim()), "i").test(text);
-    };
+_.FILTER_STARTSWITH = function (text, input) {
+       return RegExp("^" + $.regExpEscape(input.trim()), "i").test(text);
+};
 
-    _.SORT_BYLENGTH = function (a, b) {
-        if (a.length !== b.length) {
-            return a.length - b.length;
-        }
+_.SORT_BYLENGTH = function (a, b) {
+       if (a.length !== b.length) {
+               return a.length - b.length;
+       }
 
-        return a < b? -1 : 1;
-    };
+       return a < b? -1 : 1;
+};
+
+_.ITEM = function (text, input) {
+       var html = input === '' ? 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) {
-        for (var i in properties) {
-            var initial = properties[i],
-                attrValue = this.input.getAttribute("data-" + i.toLowerCase());
-
-            if (typeof initial === "number") {
-                this[i] = +attrValue;
-            }
-            else if (initial === false) { // Boolean options must be false by default anyway
-                this[i] = attrValue !== null;
-            }
-            else if (initial instanceof Function) {
-                this[i] = null;
-            }
-            else {
-                this[i] = attrValue;
-            }
-
-            this[i] = this[i] || o[i] || initial;
-        }
-    }
+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 = instance.input.getAttribute("data-" + i.toLowerCase());
+
+               if (typeof initial === "number") {
+                       instance[i] = parseInt(attrValue);
+               }
+               else if (initial === false) { // Boolean options must be false by default anyway
+                       instance[i] = attrValue !== null;
+               }
+               else if (initial instanceof Function) {
+                       instance[i] = null;
+               }
+               else {
+                       instance[i] = attrValue;
+               }
+
+               if (!instance[i] && instance[i] !== 0) {
+                       instance[i] = (i in o)? o[i] : initial;
+               }
+       }
+}
 
 // Helpers
 
-    var slice = Array.prototype.slice;
-
-    function $(expr, con) {
-        return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
-    }
-
-    function $$(expr, con) {
-        return slice.call((con || document).querySelectorAll(expr));
-    }
-
-    $.create = function(tag, o) {
-        var element = document.createElement(tag);
-
-        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);
-                });
-            }
-        }
-    };
-
-    $.fire = function(target, type, properties) {
-        var evt = document.createEvent("HTMLEvents");
-
-        evt.initEvent(type, true, true );
-
-        for (var j in properties) {
-            evt[j] = properties[j];
-        }
-
-        target.dispatchEvent(evt);
-    };
-
-    $.regExpEscape = function (s) {
-        return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
-    }
+var slice = Array.prototype.slice;
+
+function $(expr, con) {
+       return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
+}
+
+function $$(expr, con) {
+       return slice.call((con || document).querySelectorAll(expr));
+}
+
+$.create = function(tag, o) {
+       var element = document.createElement(tag);
+
+       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);
+                       });
+               }
+       }
+};
+
+$.fire = function(target, type, properties) {
+       var evt = document.createEvent("HTMLEvents");
+
+       evt.initEvent(type, true, true );
+
+       for (var j in properties) {
+               evt[j] = properties[j];
+       }
+
+       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);
-        });
-    }
+function init() {
+       $$("input.awesomplete").forEach(function (input) {
+               new _(input);
+       });
+}
 
 // Are we in a browser? Check for Document constructor
-    if (typeof Document !== 'undefined') {
-        // DOM already loaded?
-        if (document.readyState !== "loading") {
-            init();
-        }
-        else {
-            // Wait for it
-            document.addEventListener("DOMContentLoaded", init);
-        }
-    }
-
-    _.$ = $;
-    _.$$ = $$;
+if (typeof Document !== "undefined") {
+       // DOM already loaded?
+       if (document.readyState !== "loading") {
+               init();
+       }
+       else {
+               // Wait for it
+               document.addEventListener("DOMContentLoaded", init);
+       }
+}
+
+_.$ = $;
+_.$$ = $$;
 
 // Make sure to export Awesomplete on self when in a browser
-    if (typeof self !== 'undefined') {
-        self.Awesomplete = _;
-    }
+if (typeof self !== "undefined") {
+       self.Awesomplete = _;
+}
 
 // Expose Awesomplete as a CJS module
-    if (typeof exports === 'object') {
-        module.exports = _;
-    }
+if (typeof module === "object" && module.exports) {
+       module.exports = _;
+}
 
-    return _;
+return _;
 
 }());