datalist: add bloat - jscancer - Javascript crap (relatively small)
 (HTM) git clone git://git.codemadness.org/jscancer
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit ce985f99424e909b959b7cf2736312074e00d4cf
 (DIR) parent f57d63c6ffac03ddf58f78a5fb3958c1caf9a7d2
 (HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
       Date:   Thu, 25 Apr 2024 20:04:43 +0200
       
       datalist: add bloat
       
       Diffstat:
         M datalist/README                     |      12 +++++++-----
         M datalist/datalist.css               |      23 +++++++++++++++++++++++
         M datalist/datalist.js                |     190 ++++++++++++++++++++++++++-----
         A datalist/example-data-cols-small.j… |      28 ++++++++++++++++++++++++++++
         A datalist/example-data-cols.json     |      79 +++++++++++++++++++++++++++++++
         A datalist/example-json.html          |      81 ++++++++++++++++++++++++++++++
       
       6 files changed, 377 insertions(+), 36 deletions(-)
       ---
 (DIR) diff --git a/datalist/README b/datalist/README
       @@ -1,25 +1,27 @@
        datalist
        ========
        
       -small dropdown filter / autocomplete script.
       +relatively small dropdown filter / autocomplete script.
        
        
        FEATURES
        --------
        
        - Small:
       -  - Filesize: +- 7.2KB.
       -  - Lines: +- 275, not much code, so hopefully easy to understand.
       +  - Filesize: +- 12KB.
       +  - Lines: +- 420, not much code, so hopefully easy to understand.
          - No dependencies on other libraries like jQuery.
        - (Graceful) fallback to HTML5 datalist if Javascript is disabled for inline
          datalist.
        - Filtering values: case-insensitively, tokenized (separated by space).
        - Supports querying a remote server for results using a JSON XMLHttpRequest.
       +- Show a table with multiple columns in the list, with nice alignment of items.
       +  - Support HTML, for example for thumbnail images.
       +  - Support a callback function per cell, row or to reformat the value.
        - Permissive ISC license, see LICENSE file.
       -- Officially supported browsers are:
       +- Supported browsers are:
          - Firefox and Firefox ESR.
          - Chrome and most recent webkit-based browsers.
       -  - IE10+.
        
        
        EXAMPLES
 (DIR) diff --git a/datalist/datalist.css b/datalist/datalist.css
       @@ -23,3 +23,26 @@
        datalist {
                display: none;
        }
       +
       +/* multi-column table in dropdown */
       +.datalist-dropdown table {
       +        border-collapse: collapse;
       +        width: 100%;
       +}
       +.datalist-dropdown thead {
       +        position: sticky;
       +        top: 0;
       +}
       +.datalist-dropdown table th {
       +        text-align: left;
       +        background-color: #eee;
       +        border-bottom: 2px solid #ccc;
       +}
       +.datalist-dropdown table th,
       +.datalist-dropdown table td {
       +        padding: 3px;
       +}
       +.datalist-dropdown table tr.sel td {
       +        background-color: #33bbff;
       +        color: #fff;
       +}
 (DIR) diff --git a/datalist/datalist.js b/datalist/datalist.js
       @@ -1,6 +1,16 @@
        function datalist_init(input) {
       -        var attrlist = input.getAttribute("list"), ellist = document.getElementById(attrlist);
       -
       +        var tablemode = false;
       +        var attrlist = input.getAttribute("data-table") || "";
       +        if (attrlist !== "")
       +                tablemode = true;
       +        else
       +                attrlist = input.getAttribute("list")
       +        var ellist = document.getElementById(attrlist);
       +        var thead = null;
       +        if (tablemode) {
       +                if (ellist.tHead)
       +                        thead = ellist.tHead;
       +        }
                input.removeAttribute("list");
                input.autocomplete = "off";
        
       @@ -10,14 +20,28 @@ function datalist_init(input) {
                        prevmatches = [],
                        prevvalue = null,
                        url = input.getAttribute("data-url") || "",
       -                urlfn = input.getAttribute("data-urlfn") || "";
       +                urlfn = input.getAttribute("data-urlfn") || "",
       +                indom = false;
                dropdown.className = "datalist-dropdown";
        
       +        var ctx = {input: input, dropdown: dropdown};
       +
                var getlabel = function(el) {
                        return el.textContent || el.innerText || "";
                };
        
       -        var getvalue = function(el) {
       +        // set new input value.
       +        var setvalue = function(el, value) {
       +                var oldvalue = el.value;
       +                if (input.oldvalue === value)
       +                        return;
       +                el.value = el.oldvalue = value; // set new value.
       +                el.dispatchEvent(new Event("value-changed"));
       +        };
       +        input.oldvalue = input.defaultValue; // store previous value.
       +
       +        // get last selection value (can be different from input value).
       +        var getselvalue = function(el) {
                        var value = el.getAttribute("data-value");
                        if (value !== null)
                                return value;
       @@ -27,39 +51,54 @@ function datalist_init(input) {
                        return el.textContent || el.innerText || "";
                };
        
       -        var setvalue = function(el, value) {
       -                if (el.value !== value) {
       -                        el.value = value;
       +        // set selection value (can be different from input value).
       +        var setselvalue = function(el, value) {
       +                el.dispatchEvent(new Event("selection"));
       +                var oldvalue = el.value;
       +                setvalue(el, value);
       +                if (oldvalue !== value)
                                el.dispatchEvent(new Event("selection-changed"));
       -                }
       +        };
       +
       +        var datalist_enabled = function() {
       +                return !input.readOnly && !input.disabled && !input.hidden;
                };
        
                // create item: expects string, or object with name and label attribute.
                var createitem = function(o) {
       -                var label, value, div = document.createElement("div");
       +                var label, row, value;
       +                if (!tablemode)
       +                        row = document.createElement("div");
                        if (typeof(o) === "string") {
                                label = value = o;
       +                        row.textContent = label;
                        } else if (typeof(o.getAttribute) == "function") {
                                // element
                                label = getlabel(o);
       -                        value = getvalue(o);
       +                        value = getselvalue(o);
       +                        if (tablemode)
       +                                row = o.cloneNode(true);
       +                        else
       +                                row.textContent = label;
                        } else {
                                // (JSON) object
                                label = o.label;
                                value = o.value;
       +                        row.textContent = label;
                        }
        
       -                div.innerHTML = label;
       -                div.setAttribute("data-value", value);
       -                div.addEventListener("mousedown", function() {
       -                        setvalue(input, getvalue(this));
       +                row.setAttribute("data-value", value);
       +                row.addEventListener("mousedown", function() {
       +                        if (!datalist_enabled())
       +                                return;
       +                        setselvalue(input, getselvalue(this));
                                datalist_show(false);
                        }, false);
       -                div.addEventListener("mousemove", function() {
       +                row.addEventListener("mousemove", function() {
                                if (mouse)
                                        datalist_setsel(this);
                        }, false);
       -                return { el: div, label: label, value: value };
       +                return { el: row, label: label, value: value };
                };
        
                if (url.length || urlfn.length) {
       @@ -72,6 +111,9 @@ function datalist_init(input) {
                        datalist_match = function(s, fn, ev) {
                                clearTimeout(timer);
        
       +                        if (!datalist_enabled())
       +                                return;
       +
                                var requrl = urlfn(s, input);
                                if (requrl === prevurl) {
                                        fn(prevmatches);
       @@ -93,8 +135,56 @@ function datalist_init(input) {
        
                                                prevmatches = [];
                                                var o = JSON.parse(x.responseText);
       -                                        for (var i = 0; i < o.length; i++)
       -                                                prevmatches.push(createitem(o[i]));
       +                                        tablemode = !!o.columns && !!o.items;
       +                                        if (tablemode) {
       +                                                // create table header from columns, set name as class.
       +                                                thead = document.createElement("thead");
       +                                                var tr = document.createElement("tr");
       +                                                var valueidx = 0; // default is first column.
       +
       +                                                for (var i = 0; i < o.columns.length; i++) {
       +                                                        var th = document.createElement("th");
       +                                                        th.className = o.columns[i].name;
       +                                                        th.textContent = o.columns[i].label;
       +                                                        if (o.columns[i].value)
       +                                                                valueidx = i;
       +                                                        tr.appendChild(th);
       +                                                }
       +                                                thead.appendChild(tr);
       +
       +                                                // add items as table rows: support simple array or object.
       +                                                for (var i = 0; i < o.items.length; i++) {
       +                                                        var tr = document.createElement("tr");
       +                                                        if (Array.isArray(o.items[i])) {
       +                                                                tr.setAttribute("data-value", o.items[i][valueidx]);
       +                                                                for (var j = 0; j < o.items[i].length; j++) {
       +                                                                        var td = document.createElement("td");
       +                                                                        td.textContent = o.items[i][j];
       +                                                                        tr.appendChild(td);
       +                                                                }
       +                                                        } else {
       +                                                                tr.setAttribute("data-value", o.items[i].value);
       +                                                                for (var j = 0; j < o.columns.length; j++) {
       +                                                                        var col = o.columns[j];
       +                                                                        var td = document.createElement("td");
       +                                                                        var value = o.items[i][col.name];
       +                                                                        // callback function for value.
       +                                                                        var fnn = "datalist_format_" + col.fn;
       +                                                                        if (typeof(window[fnn]) == "function")
       +                                                                                value = window[fnn](value, td, tr, i, j);
       +                                                                        if (o.columns[j].html)
       +                                                                                td.innerHTML = value; /* allow HTML */
       +                                                                        else
       +                                                                                td.textContent = value;
       +                                                                        tr.appendChild(td);
       +                                                                }
       +                                                        }
       +                                                        prevmatches.push(createitem(tr));
       +                                                }
       +                                        } else {
       +                                                for (var i = 0; i < o.length; i++)
       +                                                        prevmatches.push(createitem(o[i]));
       +                                        }
        
                                                prevurl = requrl;
                                                fn(prevmatches);
       @@ -108,10 +198,11 @@ function datalist_init(input) {
                                }, ev == "onchange" ? 150 : 1);
                        };
                } else {
       -                // use inline <datalist>.
       -                if (attrlist === null || ellist === undefined)
       +                // use inline <datalist> or table.
       +                if (attrlist === null || ellist === null)
                                return;
       -                for (var i = 0, ec = ellist.children, o; i < ec.length; i++) {
       +
       +                for (var i = 0, ec = (tablemode && ellist.tBodies.length) ? ellist.tBodies[0].children : ellist.children; i < ec.length; i++) {
                                var o = createitem(ec[i]);
                                o.search = o.label.toLowerCase().split(" ");
                                items.push(o);
       @@ -138,6 +229,9 @@ function datalist_init(input) {
                        };
        
                        datalist_match = function(s, fn) {
       +                        if (!datalist_enabled())
       +                                return;
       +
                                s = s.toLowerCase();
                                if (s === prevvalue) {
                                        fn(prevmatches);
       @@ -157,22 +251,55 @@ function datalist_init(input) {
                }
        
                var datalist_render = function(m) {
       -                var dd = dropdown.cloneNode(false);
       +                if (!indom) { // add to DOM on first use when needed.
       +                        document.body.appendChild(dropdown);
       +                        indom = true;
       +                }
       +
       +                var dd = dropdown.cloneNode(false), table, tbody;
       +                if (tablemode) {
       +                        table = document.createElement("table");
       +                        if (thead)
       +                                table.tHead = thead.cloneNode(true);
       +                        tbody = document.createElement("tbody");
       +                }
       +
                        var r = input.getClientRects() || [];
                        if (r.length) {
                                dd.style.left = String(r[0].left + window.pageXOffset) + "px";
                                dd.style.top = String(r[0].top + input.offsetHeight + window.pageYOffset) + "px";
                        }
       -                dd.style.minWidth = String(input.clientWidth) + "px";
       -                for (var i = 0; i < m.length; i++)
       -                        dd.appendChild(m[i].el);
       +                dd.style.minWidth = String(input.getAttribute("data-minwidth") || input.clientWidth) + "px";
       +                if (tablemode) {
       +                        for (var i = 0; i < m.length; i++)
       +                                tbody.appendChild(m[i].el);
       +                        table.appendChild(tbody);
       +                        dd.appendChild(table);
       +                } else {
       +                        for (var i = 0; i < m.length; i++)
       +                                dd.appendChild(m[i].el);
       +                }
                        dropdown.parentNode.replaceChild(dd, dropdown)
                        dropdown = dd;
                };
       +
                var datalist_visible = false;
                var datalist_show = function(status) {
       +                if (status && !datalist_enabled()) // do not show when disabled, but it can be hidden.
       +                        return;
                        datalist_visible = status;
       -                dropdown.className = "datalist-dropdown " + (status ? "visible" : "");
       +                dropdown.className = "datalist-dropdown " + (status ? "visible" : "") + " " + (input.getAttribute("data-class") || "");
       +                var r = dropdown.getClientRects();
       +                // if dropdown popup doesn't fit then adjust x, y scroll position so it fits on the screen.
       +                if (!r.length)
       +                        return;
       +                if (r[0].left + r[0].width >= window.innerWidth) {
       +                        dropdown.style.left = "auto";
       +                        dropdown.style.right = "0px";
       +                }
       +                var ri = input.getClientRects() || [];
       +                if (input.scrollIntoView && ri.length && r[0].top + dropdown.clientHeight + ri[0].height >= window.innerHeight)
       +                        input.scrollIntoView();
                };
                var datalist_setsel = function(el) {
                        if (cursel)
       @@ -187,9 +314,7 @@ function datalist_init(input) {
                        switch (e.which) {
                        case 13: // return
                                if (cursel)
       -                                setvalue(input, getvalue(cursel));
       -                        if (!datalist_visible)
       -                                return;
       +                                setselvalue(input, getselvalue(cursel));
                                datalist_show(false);
                                e.stopPropagation();
                                return !!e.preventDefault();
       @@ -198,7 +323,7 @@ function datalist_init(input) {
                        case 34: // page down.
                        case 38: // arrow up
                        case 40: // arrow down
       -                        var sel = cursel, dd = dropdown, dc = dropdown.children;
       +                        var sel = cursel, dd = dropdown, dc = tablemode ? dropdown.children[0].tBodies[0].children : dropdown.children;
        
                                // if last and down arrow switch to first item, if first and up arrow switch to last item.
                                if (dc.length) {
       @@ -237,6 +362,7 @@ function datalist_init(input) {
                }, false);
        
                var onchange = function() {
       +                setvalue(input, input.value);
                        datalist_match(input.value, function(m) {
                                // check if selection is still active in matches.
                                if (cursel) {
       @@ -253,6 +379,7 @@ function datalist_init(input) {
                                datalist_show(!!m.length);
                        }, "onchange");
                };
       +
                input.addEventListener("input", onchange);
                input.addEventListener("keyup", function(e) {
                        mouse = true;
       @@ -281,7 +408,8 @@ function datalist_init(input) {
                        datalist_setsel(null);
                        datalist_show(false);
                }, false);
       -        document.body.appendChild(dropdown);
       +
       +        return ctx;
        }
        
        var els = document.getElementsByClassName("datalist");
 (DIR) diff --git a/datalist/example-data-cols-small.json b/datalist/example-data-cols-small.json
       @@ -0,0 +1,28 @@
       +{
       +"columns" :
       +        [
       +                {
       +                        "name" : "name",
       +                        "label" : "Name",
       +                        "value" : true
       +                }, {
       +                        "name" : "type",
       +                        "label" : "Type"
       +                }, {
       +                        "name" : "proprietary",
       +                        "label" : "Proprietary?"
       +                }
       +        ],
       +"items" : [
       +        ["DragonflyBSD", "BSD", "No"],
       +        ["GNU/Hurd", "BSD-like", "No"],
       +        ["GNU/Linux", "Linux", "No"],
       +        ["FreeBSD", "BSD", "No"],
       +        ["MS-DOS 6.11", "DOS", "Yes"],
       +        ["OpenBSD", "BSD", "No"],
       +        ["OpenSolaris", "BSD-like", "No"],
       +        ["NetBSD", "BSD", "No"],
       +        ["Plan9", "Plan9", "No"],
       +        ["Windows", "Windows", "Yes"]
       +]
       +}
 (DIR) diff --git a/datalist/example-data-cols.json b/datalist/example-data-cols.json
       @@ -0,0 +1,79 @@
       +{
       +"columns" :
       +        [
       +                {
       +                        "name" : "name",
       +                        "label" : "Name"
       +                }, {
       +                        "name" : "type",
       +                        "label" : "Type"
       +                }, {
       +                        "name" : "proprietary",
       +                        "label" : "Proprietary?",
       +                        "fn" : "item_bool"
       +                }
       +        ],
       +"items" : [
       +        {
       +                "label" : "DragonflyBSD",
       +                "value" : "DragonflyBSD",
       +                "name" : "DragonflyBSD",
       +                "type" : "BSD",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "GNU/Hurd",
       +                "value" : "GNU/Hurd",
       +                "name" : "GNU/Hurd",
       +                "type" : "BSD-like",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "GNU/Linux",
       +                "value" : "GNU/Linux",
       +                "name" : "GNU/Linux",
       +                "type" : "Linux",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "FreeBSD",
       +                "value" : "FreeBSD",
       +                "name" : "FreeBSD",
       +                "type" : "BSD",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "MS-DOS 6.11",
       +                "value" : "MS-DOS 6.11",
       +                "name" : "MS-DOS 6.11",
       +                "type" : "DOS",
       +                "proprietary" : "Yes"
       +        }, {
       +                "label" : "OpenBSD",
       +                "value" : "OpenBSD",
       +                "name" : "OpenBSD",
       +                "type" : "BSD",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "OpenSolaris",
       +                "value" : "OpenSolaris",
       +                "name" : "OpenSolaris",
       +                "type" : "BSD-like",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "NetBSD",
       +                "value" : "NetBSD",
       +                "name" : "NetBSD",
       +                "type" : "BSD",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "Plan9",
       +                "value" : "Plan9",
       +                "name" : "Plan9",
       +                "type" : "Plan9",
       +                "proprietary" : "No"
       +        }, {
       +                "label" : "Windows",
       +                "value" : "Windows",
       +                "name" : "Windows",
       +                "type" : "Windows",
       +                "proprietary" : "Yes"
       +        }
       +]
       +}
 (DIR) diff --git a/datalist/example-json.html b/datalist/example-json.html
       @@ -0,0 +1,81 @@
       +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
       +<html>
       +<head>
       +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
       +<title>jsdatalist</title>
       +<link rel="stylesheet" type="text/css" href="datalist.css" />
       +<style type="text/css">
       +.yes {
       +        background-color: #92d543;
       +}
       +.no {
       +        background-color: #d54343;
       +}
       +</style>
       +</head>
       +<body>
       +
       +<form method="post" action="">
       +
       +<p>Inline &lt;datalist&gt;:</p>
       +
       +<label for="os">OS: </label>
       +<input type="text" placeholder="Select OS..." value="" list="list" name="os" id="os" class="datalist" /><br/>
       +
       +<datalist class="datalist" id="list">
       +        <option>DragonflyBSD</option>
       +        <option>GNU/Hurd</option>
       +        <option>GNU/Linux</option>
       +        <option>FreeBSD</option>
       +        <option>MS-DOS&nbsp;6.11</option>
       +        <option>OpenBSD</option>
       +        <option>OpenSolaris</option>
       +        <option>NetBSD</option>
       +        <option>Plan9</option>
       +        <option>Windows</option>
       +</datalist>
       +
       +<p>Using XMLHttpRequest + JSON:</p>
       +
       +<label for="remote">OS: </label>
       +<input type="text" placeholder="Select OS..." value="" data-url="example-data.json?q=" name="remote" id="remote" class="datalist" /><br/>
       +
       +<p>Using XMLHttpRequest + custom URL function + JSON:</p>
       +
       +<label for="remotecustom">OS: </label>
       +<input type="text" placeholder="Select OS..." value="" data-urlfn="custom_urlfn" name="remotecustom" id="remotecustom" class="datalist" /><br/>
       +
       +<label for="remotecustom2">OS: </label>
       +<input type="text" placeholder="Select OS..." value="" data-urlfn="custom_urlfn" name="remotecustom" id="remotecustom2" class="datalist" /><br/>
       +
       +<label for="remotecustom3">OS (multi-table): </label>
       +<input type="text" placeholder="Select OS..." value="" data-urlfn="custom_multi_urlfn" name="remotecustom3" id="remotecustom3" class="datalist" /><br/>
       +
       +<label for="remotecustom4">OS (multi-table, small dataset): </label>
       +<input type="text" placeholder="Select OS..." value="" data-urlfn="custom_multi_small_urlfn" name="remotecustom4" id="remotecustom4" class="datalist" autofocus /><br/>
       +
       +</form>
       +
       +<script type="text/javascript">
       +function custom_urlfn(s, input) {
       +        return "example-data.json?q=" + encodeURIComponent(s);
       +}
       +
       +function custom_multi_urlfn(s, input) {
       +        return "example-data-cols.json?q=" + encodeURIComponent(s);
       +}
       +
       +function custom_multi_small_urlfn(s, input) {
       +        return "example-data-cols-small.json?q=" + encodeURIComponent(s);
       +}
       +
       +function datalist_format_item_bool(value, td) {
       +        td.classList[value === "Yes" ? "add" : "remove"]("yes");
       +        td.classList[value !== "Yes" ? "add" : "remove"]("no");
       +        return value;
       +}
       +</script>
       +<script type="text/javascript" src="datalist.js"></script>
       +
       +</body>
       +</html>