datalist.js - jscancer - Javascript crap (relatively small)
(HTM) git clone git://git.codemadness.org/jscancer
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
datalist.js (12086B)
---
1 function datalist_init(input) {
2 var tablemode = false;
3 var attrlist = input.getAttribute("data-table") || "";
4 if (attrlist !== "")
5 tablemode = true;
6 else
7 attrlist = input.getAttribute("list")
8 var ellist = document.getElementById(attrlist);
9 var thead = null;
10 if (tablemode) {
11 if (ellist.tHead)
12 thead = ellist.tHead;
13 }
14 input.removeAttribute("list");
15 input.autocomplete = "off";
16
17 var cursel = null, items = [], mouse = true, // enable mouse event handling.
18 datalist_match,
19 dropdown = document.createElement("div"),
20 prevmatches = [],
21 prevvalue = null,
22 url = input.getAttribute("data-url") || "",
23 urlfn = input.getAttribute("data-urlfn") || "",
24 indom = false;
25 dropdown.className = "datalist-dropdown";
26
27 var ctx = {input: input, dropdown: dropdown};
28
29 var getlabel = function(el) {
30 return el.textContent || el.innerText || "";
31 };
32
33 // set new input value.
34 var setvalue = function(el, value) {
35 var oldvalue = el.value;
36 if (input.oldvalue === value)
37 return;
38 el.value = el.oldvalue = value; // set new value.
39 el.dispatchEvent(new Event("value-changed"));
40 };
41 input.oldvalue = input.defaultValue; // store previous value.
42
43 // get last selection value (can be different from input value).
44 var getselvalue = function(el) {
45 var value = el.getAttribute("data-value");
46 if (value !== null)
47 return value;
48 value = el.value;
49 if (value !== null)
50 return value;
51 return el.textContent || el.innerText || "";
52 };
53
54 // set selection value (can be different from input value).
55 var setselvalue = function(el, value) {
56 el.dispatchEvent(new Event("selection"));
57 var oldvalue = el.value;
58 setvalue(el, value);
59 if (oldvalue !== value)
60 el.dispatchEvent(new Event("selection-changed"));
61 };
62
63 var datalist_enabled = function() {
64 return !input.readOnly && !input.disabled && !input.hidden;
65 };
66
67 // create item: expects string, or object with name and label attribute.
68 var createitem = function(o) {
69 var label, row, value;
70 if (!tablemode)
71 row = document.createElement("div");
72 if (typeof(o) === "string") {
73 label = value = o;
74 row.textContent = label;
75 } else if (typeof(o.getAttribute) == "function") {
76 // element
77 label = getlabel(o);
78 value = getselvalue(o);
79 if (tablemode)
80 row = o.cloneNode(true);
81 else
82 row.textContent = label;
83 } else {
84 // (JSON) object
85 label = o.label;
86 value = o.value;
87 row.textContent = label;
88 }
89
90 row.setAttribute("data-value", value);
91 row.addEventListener("mousedown", function() {
92 if (!datalist_enabled())
93 return;
94 setselvalue(input, getselvalue(this));
95 datalist_show(false);
96 }, false);
97 row.addEventListener("mousemove", function() {
98 if (mouse)
99 datalist_setsel(this);
100 }, false);
101 return { el: row, label: label, value: value };
102 };
103
104 if (url.length || urlfn.length) {
105 urlfn = urlfn.length ? window[urlfn] : function(s, input) {
106 return url + encodeURIComponent(s);
107 };
108
109 // "throttled" JSON XMLHttpRequest.
110 var timer = null, prevurl = "";
111 datalist_match = function(s, fn, ev) {
112 clearTimeout(timer);
113
114 if (!datalist_enabled())
115 return;
116
117 var requrl = urlfn(s, input);
118 if (requrl === prevurl) {
119 fn(prevmatches);
120 return;
121 }
122
123 timer = setTimeout(function() {
124 // set class for loading indicator styling.
125 input.classList.add("loading");
126
127 var x = new(XMLHttpRequest);
128 x.onreadystatechange = function() {
129 // remove loading indicator when "DONE".
130 if (x.readyState == 4)
131 input.classList.remove("loading");
132
133 if (x.readyState != 4 || [ 0, 200 ].indexOf(x.status) == -1)
134 return;
135
136 prevmatches = [];
137 var o = JSON.parse(x.responseText);
138 tablemode = !!o.columns && !!o.items;
139 if (tablemode) {
140 // create table header from columns, set name as class.
141 thead = document.createElement("thead");
142 var tr = document.createElement("tr");
143 var valueidx = 0; // default is first column.
144
145 for (var i = 0; i < o.columns.length; i++) {
146 var th = document.createElement("th");
147 th.className = o.columns[i].name;
148 th.textContent = o.columns[i].label;
149 if (o.columns[i].value)
150 valueidx = i;
151 tr.appendChild(th);
152 }
153 thead.appendChild(tr);
154
155 // add items as table rows: support simple array or object.
156 for (var i = 0; i < o.items.length; i++) {
157 var tr = document.createElement("tr");
158 if (Array.isArray(o.items[i])) {
159 tr.setAttribute("data-value", o.items[i][valueidx]);
160 for (var j = 0; j < o.items[i].length; j++) {
161 var td = document.createElement("td");
162 td.textContent = o.items[i][j];
163 tr.appendChild(td);
164 }
165 } else {
166 tr.setAttribute("data-value", o.items[i].value);
167 for (var j = 0; j < o.columns.length; j++) {
168 var col = o.columns[j];
169 var td = document.createElement("td");
170 var value = o.items[i][col.name];
171 // callback function for value.
172 var fnn = "datalist_format_" + col.fn;
173 if (typeof(window[fnn]) == "function")
174 value = window[fnn](value, td, tr, i, j);
175 if (o.columns[j].html)
176 td.innerHTML = value; /* allow HTML */
177 else
178 td.textContent = value;
179 tr.appendChild(td);
180 }
181 }
182 prevmatches.push(createitem(tr));
183 }
184 } else {
185 for (var i = 0; i < o.length; i++)
186 prevmatches.push(createitem(o[i]));
187 }
188
189 prevurl = requrl;
190 fn(prevmatches);
191 };
192
193 x.open("GET", requrl + "&t=" + String(new Date().getTime()), true);
194 x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
195 x.timeout = 10000;
196 x.send();
197 // delay in ms: throttle request on change, but on focus/click open fast.
198 }, ev == "onchange" ? 150 : 1);
199 };
200 } else {
201 // use inline <datalist> or table.
202 if (attrlist === null || ellist === null)
203 return;
204
205 for (var i = 0, ec = (tablemode && ellist.tBodies.length) ? ellist.tBodies[0].children : ellist.children; i < ec.length; i++) {
206 var o = createitem(ec[i]);
207 o.search = o.label.toLowerCase().split(" ");
208 items.push(o);
209 }
210
211 var datalist_filter = function(data, s) {
212 var matches = [], tok = s.toLowerCase().split(" ");
213 for (var i = 0; i < data.length; i++) {
214 var fc = 0;
215 for (var k = 0; k < tok.length && fc < tok.length; k++) {
216 var f = false;
217 for (var j = 0; j < data[i].search.length && fc < tok.length && !f; j++)
218 for (var l = 0; l < data[i].search.length && !f; l++)
219 if (data[i].search[l].indexOf(tok[k]) != -1)
220 f = true;
221 if (f)
222 fc++;
223 }
224 // all tokens (separated by space) must match.
225 if (fc == tok.length)
226 matches.push(data[i]);
227 }
228 return matches;
229 };
230
231 datalist_match = function(s, fn) {
232 if (!datalist_enabled())
233 return;
234
235 s = s.toLowerCase();
236 if (s === prevvalue) {
237 fn(prevmatches);
238 return;
239 }
240
241 // if token string is different or string not in previous search: use raw data,
242 // else filter on existing data and no need to sort.
243 if (prevvalue === null || (prevvalue.split(" ").length != s.split(" ").length) ||
244 s.indexOf(prevvalue) == -1)
245 prevmatches = datalist_filter(items, s);
246 else
247 prevmatches = datalist_filter(prevmatches, s);
248 prevvalue = s;
249 fn(prevmatches);
250 };
251 }
252
253 var datalist_render = function(m) {
254 if (!indom) { // add to DOM on first use when needed.
255 document.body.appendChild(dropdown);
256 indom = true;
257 }
258
259 var dd = dropdown.cloneNode(false), table, tbody;
260 if (tablemode) {
261 table = document.createElement("table");
262 if (thead)
263 table.tHead = thead.cloneNode(true);
264 tbody = document.createElement("tbody");
265 }
266
267 var r = input.getClientRects() || [];
268 if (r.length) {
269 dd.style.left = String(r[0].left + window.pageXOffset) + "px";
270 dd.style.top = String(r[0].top + input.offsetHeight + window.pageYOffset) + "px";
271 }
272 dd.style.minWidth = String(input.getAttribute("data-minwidth") || input.clientWidth) + "px";
273 if (tablemode) {
274 for (var i = 0; i < m.length; i++)
275 tbody.appendChild(m[i].el);
276 table.appendChild(tbody);
277 dd.appendChild(table);
278 } else {
279 for (var i = 0; i < m.length; i++)
280 dd.appendChild(m[i].el);
281 }
282 dropdown.parentNode.replaceChild(dd, dropdown)
283 dropdown = dd;
284 };
285
286 var datalist_visible = false;
287 var datalist_show = function(status) {
288 if (status && !datalist_enabled()) // do not show when disabled, but it can be hidden.
289 return;
290 datalist_visible = status;
291 dropdown.className = "datalist-dropdown " + (status ? "visible" : "") + " " + (input.getAttribute("data-class") || "");
292 var r = dropdown.getClientRects();
293 // if dropdown popup doesn't fit then adjust x, y scroll position so it fits on the screen.
294 if (!r.length)
295 return;
296 if (r[0].left + r[0].width >= window.innerWidth) {
297 dropdown.style.left = "auto";
298 dropdown.style.right = "0px";
299 }
300 var ri = input.getClientRects() || [];
301 if (input.scrollIntoView && ri.length && r[0].top + dropdown.clientHeight + ri[0].height >= window.innerHeight)
302 input.scrollIntoView();
303 };
304 var datalist_setsel = function(el) {
305 if (cursel)
306 cursel.className = "";
307 cursel = el;
308 if (el)
309 el.className = "sel";
310 };
311
312 input.addEventListener("keydown", function(e) {
313 mouse = false;
314 switch (e.which) {
315 case 13: // return
316 if (cursel)
317 setselvalue(input, getselvalue(cursel));
318 datalist_show(false);
319 e.stopPropagation();
320 return !!e.preventDefault();
321 case 27: break; // escape
322 case 33: // page up.
323 case 34: // page down.
324 case 38: // arrow up
325 case 40: // arrow down
326 var sel = cursel, dd = dropdown, dc = tablemode ? dropdown.children[0].tBodies[0].children : dropdown.children;
327
328 // if last and down arrow switch to first item, if first and up arrow switch to last item.
329 if (dc.length) {
330 if (e.which == 38) { // up
331 if (!sel || !(sel = sel.previousSibling))
332 sel = dc[dc.length - 1];
333 } else if (e.which == 40) { // down
334 if (!sel || !(sel = sel.nextSibling))
335 sel = dc[0];
336 } else if (!sel) {
337 sel = dc[0];
338 }
339 }
340 if (cursel && (e.which == 33 || e.which == 34)) {
341 var n = sel.offsetHeight ? (dd.clientHeight / sel.offsetHeight) : 0;
342 if (e.which == 33) { // page up.
343 for (; n > 0 && sel && sel.previousSibling;
344 n--, sel = sel.previousSibling)
345 ;
346 } else { // page down.
347 for (; n > 0 && sel && sel.nextSibling;
348 n--, sel = sel.nextSibling)
349 ;
350 }
351 }
352 if (sel) {
353 datalist_setsel(sel);
354
355 // only update scroll if needed.
356 if (sel.offsetTop < dd.scrollTop)
357 dd.scrollTop = sel.offsetTop;
358 else if (sel.offsetTop + sel.offsetHeight > dd.scrollTop + dd.offsetHeight)
359 dd.scrollTop = sel.offsetTop;
360 }
361 }
362 }, false);
363
364 var onchange = function() {
365 setvalue(input, input.value);
366 datalist_match(input.value, function(m) {
367 // check if selection is still active in matches.
368 if (cursel) {
369 var hassel = false;
370 for (var i = 0; i < m.length && !(hassel = (m[i].el === cursel)); i++)
371 ;
372 if (!hassel)
373 datalist_setsel(null);
374 }
375 // only one match? select it.
376 if (m.length == 1)
377 datalist_setsel(m[0].el);
378 datalist_render(m);
379 datalist_show(!!m.length);
380 }, "onchange");
381 };
382
383 input.addEventListener("input", onchange);
384 input.addEventListener("keyup", function(e) {
385 mouse = true;
386 switch (e.which) {
387 case 13: // return
388 case 27: // escape
389 datalist_show(false);
390 case 33: // page up.
391 case 34: // page down.
392 case 38: // arrow up
393 case 40: // arrow down
394 return;
395 }
396 onchange();
397 }, false);
398 input.addEventListener("focus", function() {
399 datalist_setsel(null);
400 datalist_match(input.value, function(m) {
401 datalist_render(m);
402 datalist_show(!!m.length);
403 dropdown.scrollTop = 0; // reset scroll.
404 });
405 }, false);
406 input.addEventListener("blur", function() {
407 mouse = true;
408 datalist_setsel(null);
409 datalist_show(false);
410 }, false);
411
412 return ctx;
413 }
414
415 var els = document.getElementsByClassName("datalist");
416 if (els !== null)
417 for (var i = 0; i < els.length; i++)
418 datalist_init(els[i]);