blob: d0ce49a45703957c87e2800dfdbc7852fb674676 [file] [log] [blame]
Nils Diewald2fe12e12015-03-06 16:47:06 +00001/**
Nils Diewald7148c6f2015-05-04 15:07:53 +00002 * Scrollable drop-down menus with view filter.
Nils Diewald2fe12e12015-03-06 16:47:06 +00003 *
4 * @author Nils Diewald
5 */
Nils Diewald2488d052015-04-09 21:46:02 +00006/*
Akron3c2730f2016-05-24 15:08:29 +02007 * TODO: Show the slider briefly on move (whenever screen is called).
Akron9c4d1ae2016-05-25 21:43:22 +02008 * TODO: Ignore alt+ and strg+ key strokes.
Akron0b92f692016-05-25 22:37:13 +02009 * TODO: Should scroll to a chosen value after prefixing, if the chosen value is live
Akron201b72a2016-06-03 01:46:19 +020010 * TODO: Add a "title" to a menu that is not scrollable.
11 * TODO: Make the menu responsive by showing less items on smaller screens
12 * or anytime items would be outside the screen.
Akron02360e42016-06-07 13:41:12 +020013 * TODO: Add a .match() method to items for scrolling and probably for prefixing.
14 * TODO: Add static header (for title, sortation fields, but also for menu points like "fragments" and "history".
Akrone91da782017-12-15 17:17:50 +010015 * TODO: Support space separated list of prefixes so "co no" will highlight "common noun"
Nils Diewald2488d052015-04-09 21:46:02 +000016 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000017define([
18 'menu/item',
19 'menu/prefix',
Akronc7448732016-04-27 14:06:58 +020020 'menu/lengthField',
Akron9905e2a2016-05-10 16:06:44 +020021 'menu/slider',
Nils Diewald0e6992a2015-04-14 20:13:52 +000022 'util'
23], function (defaultItemClass,
Akrone4961b12017-05-10 21:04:46 +020024 defaultPrefixClass,
25 defaultLengthFieldClass,
26 sliderClass) {
Nils Diewaldfda29d92015-01-22 17:28:01 +000027
Nils Diewald0e6992a2015-04-14 20:13:52 +000028 // Default maximum number of menu items
29 var menuLimit = 8;
30
31 function _codeFromEvent (e) {
32 if (e.charCode && (e.keyCode == 0))
33 return e.charCode
34 return e.keyCode;
Nils Diewald59c02fc2015-03-07 01:29:09 +000035 };
36
Nils Diewald86dad5b2015-01-28 15:09:07 +000037
38 /**
39 * List of items for drop down menu (complete).
40 * Only a sublist of the menu is filtered (live).
41 * Only a sublist of the filtered menu is visible (shown).
42 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000043 return {
Nils Diewald86dad5b2015-01-28 15:09:07 +000044 /**
45 * Create new Menu based on the action prefix
46 * and a list of menu items.
47 *
Akron7524be12016-06-01 17:31:33 +020048 *
49 * Accepts an associative array containg the elements
50 * itemClass, prefixClass, lengthFieldClass
51 *
Nils Diewald86dad5b2015-01-28 15:09:07 +000052 * @this {Menu}
53 * @constructor
54 * @param {string} Context prefix
55 * @param {Array.<Array.<string>>} List of menu items
56 */
Akron7524be12016-06-01 17:31:33 +020057 create : function (list, params) {
58 return Object.create(this)._init(list, params);
Nils Diewald86dad5b2015-01-28 15:09:07 +000059 },
60
Akron5240b8c2016-05-20 09:17:41 +020061 // Initialize list
Akron7524be12016-06-01 17:31:33 +020062 _init : function (list, params) {
Akrona92fd8d2016-05-24 21:13:41 +020063
Akron7524be12016-06-01 17:31:33 +020064 if (params === undefined)
Akrone4961b12017-05-10 21:04:46 +020065 params = {};
Akron7524be12016-06-01 17:31:33 +020066
67 this._itemClass = params["itemClass"] || defaultItemClass;
Akron5240b8c2016-05-20 09:17:41 +020068
69 // Add prefix object
Akron7524be12016-06-01 17:31:33 +020070 if (params["prefixClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020071 this._prefix = params["prefixClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020072 }
73 else {
Akrone4961b12017-05-10 21:04:46 +020074 this._prefix = defaultPrefixClass.create();
Akron5240b8c2016-05-20 09:17:41 +020075 };
76 this._prefix._menu = this;
77
78 // Add lengthField object
Akron7524be12016-06-01 17:31:33 +020079 if (params["lengthFieldClass"] !== undefined) {
Akrone4961b12017-05-10 21:04:46 +020080 this._lengthField = params["lengthFieldClass"].create();
Akron5240b8c2016-05-20 09:17:41 +020081 }
82 else {
Akrone4961b12017-05-10 21:04:46 +020083 this._lengthField = defaultLengthFieldClass.create();
Akron5240b8c2016-05-20 09:17:41 +020084 };
85 this._lengthField._menu = this;
86
87 // Initialize slider
88 this._slider = sliderClass.create(this);
89
90 // Create the element
Akron9c4d1ae2016-05-25 21:43:22 +020091 var el = document.createElement("ul");
92 with (el) {
Akrone4961b12017-05-10 21:04:46 +020093 style.outline = 0;
94 setAttribute('tabindex', 0);
95 classList.add('menu', 'roll');
96 appendChild(this._prefix.element());
97 appendChild(this._lengthField.element());
98 appendChild(this._slider.element());
Akron9c4d1ae2016-05-25 21:43:22 +020099 };
Akron5240b8c2016-05-20 09:17:41 +0200100
101 // This has to be cleaned up later on
Akron9c4d1ae2016-05-25 21:43:22 +0200102 el["menu"] = this;
Akron5240b8c2016-05-20 09:17:41 +0200103
104 // Arrow keys
Akron9c4d1ae2016-05-25 21:43:22 +0200105 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200106 'keydown',
107 this._keydown.bind(this),
108 false
Akron5240b8c2016-05-20 09:17:41 +0200109 );
110
111 // Strings
Akron9c4d1ae2016-05-25 21:43:22 +0200112 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200113 'keypress',
114 this._keypress.bind(this),
115 false
Akron5240b8c2016-05-20 09:17:41 +0200116 );
117
118 // Mousewheel
Akron9c4d1ae2016-05-25 21:43:22 +0200119 el.addEventListener(
Akrone4961b12017-05-10 21:04:46 +0200120 'wheel',
121 this._mousewheel.bind(this),
122 false
Akron5240b8c2016-05-20 09:17:41 +0200123 );
Akrona1159ff2018-07-22 13:28:31 +0200124
125 // Touch
126 el.addEventListener(
127 'touchstart',
128 this._touch.bind(this),
129 false
130 );
131 el.addEventListener(
132 'touchend',
133 this._touch.bind(this),
134 false
135 );
136 el.addEventListener(
137 'touchmove',
138 this._touch.bind(this),
139 false
140 );
141
142
Akron9c4d1ae2016-05-25 21:43:22 +0200143 this._element = el;
Akroneaba63e2018-01-26 19:49:30 +0100144
145 this._limit = menuLimit;
Akrone4961b12017-05-10 21:04:46 +0200146
Akron5240b8c2016-05-20 09:17:41 +0200147 this._items = new Array();
Akron5240b8c2016-05-20 09:17:41 +0200148
Akroneaba63e2018-01-26 19:49:30 +0100149 // TODO:
150 // Make this separate from _init
151 this.readItems(list);
152
hebastaf95226b2019-09-19 11:37:00 +0200153 this.dontHide = false;
154
Akroneaba63e2018-01-26 19:49:30 +0100155 return this;
156 },
157
158 // Read items to add to list
159 readItems : function (list) {
160
161 this._list = undefined;
162
163 // Remove circular reference to "this" in items
164 for (var i = 0; i < this._items.length; i++) {
165 delete this._items[i]["_menu"];
166 delete this._items[i];
167 };
168
169 this._items = new Array();
170 this.removeItems();
171
172
173 // Initialize items
174 this._lengthField.reset();
175
Akron9c4d1ae2016-05-25 21:43:22 +0200176 var i = 0;
Akron5240b8c2016-05-20 09:17:41 +0200177 // Initialize item list based on parameters
Akron678c26f2020-10-09 08:52:50 +0200178 list.forEach(function(i){
179 var obj = this._itemClass.create(i);
Akron5240b8c2016-05-20 09:17:41 +0200180
Akrone4961b12017-05-10 21:04:46 +0200181 // This may become circular
182 obj["_menu"] = this;
Akron678c26f2020-10-09 08:52:50 +0200183 this._lengthField.add(i);
Akrone4961b12017-05-10 21:04:46 +0200184 this._items.push(obj);
Akron678c26f2020-10-09 08:52:50 +0200185 }, this);
Akron5240b8c2016-05-20 09:17:41 +0200186
Akron9c4d1ae2016-05-25 21:43:22 +0200187 this._slider.length(this.liveLength())
Akrone4961b12017-05-10 21:04:46 +0200188 .limit(this._limit)
189 .reInit();
190
Akroneaba63e2018-01-26 19:49:30 +0100191 this._firstActive = false;
192 // Show the first item active always?
Akron9c4d1ae2016-05-25 21:43:22 +0200193 this.offset = 0;
194 this.position = 0;
Akron5240b8c2016-05-20 09:17:41 +0200195 },
Akroneaba63e2018-01-26 19:49:30 +0100196
Akron5240b8c2016-05-20 09:17:41 +0200197 // Initialize the item list
198 _initList : function () {
199
200 // Create a new list
201 if (this._list === undefined) {
Akrone4961b12017-05-10 21:04:46 +0200202 this._list = [];
Akron5240b8c2016-05-20 09:17:41 +0200203 }
204 else if (this._list.length !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200205 this._boundary(false);
206 this._list.length = 0;
Akron5240b8c2016-05-20 09:17:41 +0200207 };
208
209 // Offset is initially zero
Akron9c4d1ae2016-05-25 21:43:22 +0200210 this.offset = 0;
Akron5240b8c2016-05-20 09:17:41 +0200211
212 // There is no prefix set
213 if (this.prefix().length <= 0) {
214
Akrone4961b12017-05-10 21:04:46 +0200215 // add all items to the list and lowlight
216 var i = 0;
217 for (; i < this._items.length; i++) {
218 this._list.push(i);
219 this._items[i].lowlight();
220 };
Akron5240b8c2016-05-20 09:17:41 +0200221
Akroneaba63e2018-01-26 19:49:30 +0100222 this._slider.length(i).reInit();
Akron97752a72016-05-25 14:43:07 +0200223
Akrone4961b12017-05-10 21:04:46 +0200224 return true;
Akron5240b8c2016-05-20 09:17:41 +0200225 };
226
227 /*
228 * There is a prefix set, so filter the list!
229 */
230 var pos;
Akronacffc652017-12-18 21:21:25 +0100231 var prefixList = this.prefix().toLowerCase().split(" ");
232
233 var items = [];
234 var maxPoints = 1; // minimum 1
Akron5240b8c2016-05-20 09:17:41 +0200235
236 // Iterate over all items and choose preferred matching items
237 // i.e. the matching happens at the word start
238 for (pos = 0; pos < this._items.length; pos++) {
Akronacffc652017-12-18 21:21:25 +0100239
240 var points = 0;
241
242 for (pref = 0; pref < prefixList.length; pref++) {
243 var prefix = " " + prefixList[pref];
244
245 // Check if it matches at the beginning
Akron3b253d32018-07-15 10:16:06 +0200246 // if ((this.item(pos).lcField().indexOf(prefix)) >= 0) {
247 if ((this.item(pos).lcField().includes(prefix))) {
Akronacffc652017-12-18 21:21:25 +0100248 points += 5;
249 }
250
251 // Check if it matches anywhere
Akron3b253d32018-07-15 10:16:06 +0200252 // else if ((this.item(pos).lcField().indexOf(prefix.substring(1))) >= 0) {
253 else if ((this.item(pos).lcField().includes(prefix.substring(1)))) {
Akronacffc652017-12-18 21:21:25 +0100254 points += 1;
255 };
256 };
257
258 if (points > maxPoints) {
259 this._list = [pos];
260 maxPoints = points;
261 }
262 else if (points == maxPoints) {
Akrone4961b12017-05-10 21:04:46 +0200263 this._list.push(pos);
Akronacffc652017-12-18 21:21:25 +0100264 }
Akron5240b8c2016-05-20 09:17:41 +0200265 };
266
267 // The list is empty - so lower your expectations
268 // Iterate over all items and choose matching items
269 // i.e. the matching happens anywhere in the word
Akronacffc652017-12-18 21:21:25 +0100270 /*
Akron6ffad5d2016-05-24 17:16:58 +0200271 prefix = prefix.substring(1);
Akron5240b8c2016-05-20 09:17:41 +0200272 if (this._list.length == 0) {
Akrone4961b12017-05-10 21:04:46 +0200273 for (pos = 0; pos < this._items.length; pos++) {
274 if ((this.item(pos).lcField().indexOf(prefix)) >= 0)
275 this._list.push(pos);
276 };
Akron5240b8c2016-05-20 09:17:41 +0200277 };
Akronacffc652017-12-18 21:21:25 +0100278 */
Akron5240b8c2016-05-20 09:17:41 +0200279
Akron9c4d1ae2016-05-25 21:43:22 +0200280 this._slider.length(this._list.length).reInit();
Akron6ed13992016-05-23 18:06:05 +0200281
Akron5240b8c2016-05-20 09:17:41 +0200282 // Filter was successful - yeah!
283 return this._list.length > 0 ? true : false;
284 },
285
286
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000287 /**
288 * Destroy this menu
289 * (in case you don't trust the
290 * mark and sweep GC)!
291 */
292 destroy : function () {
Akron47c086c2016-05-18 21:22:06 +0200293
Akron5240b8c2016-05-20 09:17:41 +0200294 // Remove circular reference to "this" in menu
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000295 if (this._element != undefined)
Akrone4961b12017-05-10 21:04:46 +0200296 delete this._element["menu"];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000297
Akron5240b8c2016-05-20 09:17:41 +0200298 // Remove circular reference to "this" in items
Akron678c26f2020-10-09 08:52:50 +0200299 this._items.forEach(function(i) {
300 delete i["_menu"];
301 });
Akron5240b8c2016-05-20 09:17:41 +0200302
303 // Remove circular reference to "this" in prefix
Nils Diewald5c5a7472015-04-02 22:13:38 +0000304 delete this._prefix['_menu'];
Akron5240b8c2016-05-20 09:17:41 +0200305 delete this._lengthField['_menu'];
306 delete this._slider['_menu'];
Nils Diewald6e43ffd2015-03-25 18:55:39 +0000307 },
308
Nils Diewald7148c6f2015-05-04 15:07:53 +0000309
310 /**
311 * Focus on this menu.
312 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000313 focus : function () {
314 this._element.focus();
315 },
316
Nils Diewald7148c6f2015-05-04 15:07:53 +0000317
Nils Diewald59c02fc2015-03-07 01:29:09 +0000318 // mouse wheel treatment
319 _mousewheel : function (e) {
320 var delta = 0;
Nils Diewald5975d702015-03-09 17:45:42 +0000321 delta = e.deltaY / 120;
322 if (delta > 0)
Akrone4961b12017-05-10 21:04:46 +0200323 this.next();
Nils Diewald5975d702015-03-09 17:45:42 +0000324 else if (delta < 0)
Akrone4961b12017-05-10 21:04:46 +0200325 this.prev();
Nils Diewald59c02fc2015-03-07 01:29:09 +0000326 e.halt();
327 },
328
Akrona1159ff2018-07-22 13:28:31 +0200329 // touchmove treatment
330 _touch : function (e) {
331 var s = this.slider();
332 if (e.type === "touchstart") {
333 // s.active(true);
334 var t = e.touches[0];
335 this._lastTouch = t.clientY;
336 }
337 else if (e.type === "touchend") {
338 // s.active(false);
339 this._lastTouch = undefined;
Akrona1159ff2018-07-22 13:28:31 +0200340 }
341 else if (e.type === "touchmove") {
342 var t = e.touches[0];
Akrone817b882018-08-31 14:09:17 +0200343
344 // TODO:
345 // Instead of using 26px, choose the item height
346 // or use the menu height // shownItems
347
Akrona1159ff2018-07-22 13:28:31 +0200348 // s.movetoRel(t.clientY - this._initTouch);
349 if ((this._lastTouch + 26) < t.clientY) {
Akrone817b882018-08-31 14:09:17 +0200350 this.viewDown();
Akrona1159ff2018-07-22 13:28:31 +0200351 this._lastTouch = t.clientY;
352 }
353 else if ((this._lastTouch - 26) > t.clientY) {
Akrone817b882018-08-31 14:09:17 +0200354 this.viewUp();
Akrona1159ff2018-07-22 13:28:31 +0200355 this._lastTouch = t.clientY;
356 }
357 e.halt();
358 };
359 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000360
Nils Diewald59c02fc2015-03-07 01:29:09 +0000361 // Arrow key and prefix treatment
Nils Diewald47f366b2015-04-15 20:06:35 +0000362 _keydown : function (e) {
Nils Diewald59c02fc2015-03-07 01:29:09 +0000363 var code = _codeFromEvent(e);
364
Nils Diewald59c02fc2015-03-07 01:29:09 +0000365 switch (code) {
366 case 27: // 'Esc'
Akrone4961b12017-05-10 21:04:46 +0200367 e.halt();
368 this.hide();
369 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000370
Nils Diewald59c02fc2015-03-07 01:29:09 +0000371 case 38: // 'Up'
Akrone4961b12017-05-10 21:04:46 +0200372 e.halt();
373 this.prev();
374 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000375 case 33: // 'Page up'
Akrone4961b12017-05-10 21:04:46 +0200376 e.halt();
377 this.pageUp();
378 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000379 case 40: // 'Down'
Akrone4961b12017-05-10 21:04:46 +0200380 e.halt();
381 this.next();
382 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000383 case 34: // 'Page down'
Akrone4961b12017-05-10 21:04:46 +0200384 e.halt();
385 this.pageDown();
386 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000387 case 39: // 'Right'
Akrone4961b12017-05-10 21:04:46 +0200388 if (this._prefix.active())
389 break;
Nils Diewalde8518f82015-03-18 22:41:49 +0000390
Akrone4961b12017-05-10 21:04:46 +0200391 var item = this.liveItem(this.position);
392
393 if (item["further"] !== undefined) {
394 item["further"].bind(item).apply();
395 };
396
397 e.halt();
398 break;
Nils Diewald5975d702015-03-09 17:45:42 +0000399 case 13: // 'Enter'
400
Akrone4961b12017-05-10 21:04:46 +0200401 // Click on prefix
402 if (this._prefix.active())
403 this._prefix.onclick(e);
Nils Diewald5975d702015-03-09 17:45:42 +0000404
Akrone4961b12017-05-10 21:04:46 +0200405 // Click on item
406 else
407 this.liveItem(this.position).onclick(e);
408 e.halt();
409 break;
Nils Diewald59c02fc2015-03-07 01:29:09 +0000410 case 8: // 'Backspace'
Akrone4961b12017-05-10 21:04:46 +0200411 this._prefix.chop();
412 this.show();
413 e.halt();
414 break;
Nils Diewald47f366b2015-04-15 20:06:35 +0000415 };
416 },
Nils Diewald59c02fc2015-03-07 01:29:09 +0000417
Nils Diewald47f366b2015-04-15 20:06:35 +0000418 // Add characters to prefix
419 _keypress : function (e) {
Akron9c2f9382016-05-25 16:36:04 +0200420 if (e.charCode !== 0) {
Akrone4961b12017-05-10 21:04:46 +0200421 e.halt();
422 var c = String.fromCharCode(_codeFromEvent(e));
423
424 // Add prefix
425 this._prefix.add(c);
426 this.show();
Akron9c2f9382016-05-25 16:36:04 +0200427 };
Nils Diewald59c02fc2015-03-07 01:29:09 +0000428 },
429
Akron47c086c2016-05-18 21:22:06 +0200430 /**
Akron5240b8c2016-05-20 09:17:41 +0200431 * Show a screen with a given offset
432 * in the viewport.
Akron47c086c2016-05-18 21:22:06 +0200433 */
434 screen : function (nr) {
Akrone817b882018-08-31 14:09:17 +0200435
436 // Normalize negative values
Akron3c2730f2016-05-24 15:08:29 +0200437 if (nr < 0) {
Akrone4961b12017-05-10 21:04:46 +0200438 nr = 0
Akron3c2730f2016-05-24 15:08:29 +0200439 }
Akrone817b882018-08-31 14:09:17 +0200440
441 // The shown list already shows everything
442 else if (this.liveLength() < this.limit()) {
443 return false;
444 }
445
446 // Move relatively to the next screen
Akron6ac58442016-05-24 16:52:29 +0200447 else if (nr > (this.liveLength() - this.limit())) {
Akrone4961b12017-05-10 21:04:46 +0200448 nr = (this.liveLength() - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200449 };
450
Akrone817b882018-08-31 14:09:17 +0200451 // no change
Akron9c4d1ae2016-05-25 21:43:22 +0200452 if (this.offset === nr)
Akrone817b882018-08-31 14:09:17 +0200453 return false;
Akron5a1f5bb2016-05-23 22:00:39 +0200454
Akron47c086c2016-05-18 21:22:06 +0200455 this._showItems(nr);
Akrone817b882018-08-31 14:09:17 +0200456
457 return true;
Akron47c086c2016-05-18 21:22:06 +0200458 },
459
Akrone817b882018-08-31 14:09:17 +0200460
Nils Diewald2fe12e12015-03-06 16:47:06 +0000461 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000462 * Get the associated dom element.
Nils Diewald2fe12e12015-03-06 16:47:06 +0000463 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000464 element : function () {
465 return this._element;
466 },
467
Nils Diewald2fe12e12015-03-06 16:47:06 +0000468 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000469 * Get the creator class for items
Nils Diewald2fe12e12015-03-06 16:47:06 +0000470 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000471 itemClass : function () {
472 return this._itemClass;
473 },
474
475 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +0000476 * Get and set the numerical value
477 * for the maximum number of items visible.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000478 */
479 limit : function (limit) {
Nils Diewald5975d702015-03-09 17:45:42 +0000480 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200481 if (this._limit !== limit) {
482 this._limit = limit;
483 this._slider.limit(limit).reInit();
484 };
485 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000486 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000487 return this._limit;
488 },
489
Nils Diewald7148c6f2015-05-04 15:07:53 +0000490
Nils Diewald86dad5b2015-01-28 15:09:07 +0000491 /**
492 * Upgrade this object to another object,
493 * while private data stays intact.
494 *
Nils Diewald2fe12e12015-03-06 16:47:06 +0000495 * @param {Object} An object with properties.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000496 */
497 upgradeTo : function (props) {
498 for (var prop in props) {
Akrone4961b12017-05-10 21:04:46 +0200499 this[prop] = props[prop];
Nils Diewald86dad5b2015-01-28 15:09:07 +0000500 };
501 return this;
502 },
503
Nils Diewald7148c6f2015-05-04 15:07:53 +0000504
Nils Diewald86dad5b2015-01-28 15:09:07 +0000505 /**
Akron97752a72016-05-25 14:43:07 +0200506 * Filter the list and make it visible.
507 * This is always called once the prefix changes.
Nils Diewald86dad5b2015-01-28 15:09:07 +0000508 *
509 * @param {string} Prefix for filtering the list
510 */
Akron6ed13992016-05-23 18:06:05 +0200511 show : function (active) {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000512
Akron5240b8c2016-05-20 09:17:41 +0200513 // show menu based on initial offset
Akron6ac58442016-05-24 16:52:29 +0200514 this._unmark(); // Unmark everything that was marked before
Akrona92fd8d2016-05-24 21:13:41 +0200515 this.removeItems();
Akron6ed13992016-05-23 18:06:05 +0200516
517 // Initialize the list
518 if (!this._initList()) {
Akron6ac58442016-05-24 16:52:29 +0200519
Akrone4961b12017-05-10 21:04:46 +0200520 // The prefix is not active
521 this._prefix.active(true);
Akron6ed13992016-05-23 18:06:05 +0200522
Akrone4961b12017-05-10 21:04:46 +0200523 // finally show the element
524 this._element.classList.add('visible');
525
526 return true;
Akron6ed13992016-05-23 18:06:05 +0200527 };
528
529 var offset = 0;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000530
Akron9c2f9382016-05-25 16:36:04 +0200531 // Set a chosen value to active and move the viewport
Akron6ed13992016-05-23 18:06:05 +0200532 if (arguments.length === 1) {
533
Akrone4961b12017-05-10 21:04:46 +0200534 // Normalize active value
535 if (active < 0) {
536 active = 0;
537 }
538 else if (active >= this.liveLength()) {
539 active = this.liveLength() - 1;
540 };
Akron6ed13992016-05-23 18:06:05 +0200541
Akrone4961b12017-05-10 21:04:46 +0200542 // Item is outside the first viewport
543 if (active >= this._limit) {
544 offset = active;
545 if (offset > (this.liveLength() - this._limit)) {
546 offset = this.liveLength() - this._limit;
547 };
548 };
549
550 this.position = active;
Akron6ed13992016-05-23 18:06:05 +0200551 }
552
Akron9c2f9382016-05-25 16:36:04 +0200553 // Choose the first item
Akron6ed13992016-05-23 18:06:05 +0200554 else if (this._firstActive) {
Akrone4961b12017-05-10 21:04:46 +0200555 this.position = 0;
Akron47c086c2016-05-18 21:22:06 +0200556 }
Akroncb351d62016-05-19 23:10:33 +0200557
Akron9c2f9382016-05-25 16:36:04 +0200558 // Choose no item
Akron47c086c2016-05-18 21:22:06 +0200559 else {
Akrone4961b12017-05-10 21:04:46 +0200560 this.position = -1;
Akron6ed13992016-05-23 18:06:05 +0200561 };
562
Akron9c4d1ae2016-05-25 21:43:22 +0200563 this.offset = offset;
Akron6ed13992016-05-23 18:06:05 +0200564 this._showItems(offset); // Show new item list
565
566 // Make chosen value active
567 if (this.position !== -1) {
Akrone4961b12017-05-10 21:04:46 +0200568 this.liveItem(this.position).active(true);
Akron6ed13992016-05-23 18:06:05 +0200569 };
Akron37513a62015-11-17 01:07:11 +0100570
Akron5240b8c2016-05-20 09:17:41 +0200571 // The prefix is not active
Nils Diewalde8518f82015-03-18 22:41:49 +0000572 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000573
Akron5240b8c2016-05-20 09:17:41 +0200574 // finally show the element
Akron6bb71582016-06-10 20:41:08 +0200575 this._element.classList.add('visible');
Nils Diewald2fe12e12015-03-06 16:47:06 +0000576
Nils Diewald86dad5b2015-01-28 15:09:07 +0000577 // Add classes for rolling menus
578 this._boundary(true);
Akron6bb71582016-06-10 20:41:08 +0200579
Nils Diewald59c02fc2015-03-07 01:29:09 +0000580 return true;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000581 },
582
Nils Diewald7148c6f2015-05-04 15:07:53 +0000583
584 /**
585 * Hide the menu and call the onHide callback.
586 */
Nils Diewald2fe12e12015-03-06 16:47:06 +0000587 hide : function () {
hebastaf95226b2019-09-19 11:37:00 +0200588 if(!this.dontHide){
589 this.removeItems();
590 this._prefix.clear();
591 this.onHide();
592 this._element.classList.remove('visible');
593 }
594 /* this._element.blur(); */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000595 },
596
Nils Diewald7148c6f2015-05-04 15:07:53 +0000597 /**
598 * Function released when the menu hides.
599 * This method is expected to be overridden.
600 */
Nils Diewald5c5a7472015-04-02 22:13:38 +0000601 onHide : function () {},
602
Nils Diewald86dad5b2015-01-28 15:09:07 +0000603 /**
604 * Get the prefix for filtering,
Nils Diewald2fe12e12015-03-06 16:47:06 +0000605 * e.g. &quot;ve&quot; for &quot;verb&quot;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000606 */
Nils Diewald5975d702015-03-09 17:45:42 +0000607 prefix : function (pref) {
608 if (arguments.length === 1) {
Akrone4961b12017-05-10 21:04:46 +0200609 this._prefix.value(pref);
610 return this;
Nils Diewald5975d702015-03-09 17:45:42 +0000611 };
612 return this._prefix.value();
Nils Diewald86dad5b2015-01-28 15:09:07 +0000613 },
614
Akronc7448732016-04-27 14:06:58 +0200615 /**
616 * Get the lengthField object.
617 */
618 lengthField : function () {
619 return this._lengthField;
620 },
Nils Diewald7148c6f2015-05-04 15:07:53 +0000621
Akron5240b8c2016-05-20 09:17:41 +0200622 /**
623 * Get the associated slider object.
624 */
625 slider : function () {
626 return this._slider;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000627 },
628
Akron5240b8c2016-05-20 09:17:41 +0200629
Nils Diewald86dad5b2015-01-28 15:09:07 +0000630 /**
631 * Delete all visible items from the menu element
632 */
Akrona92fd8d2016-05-24 21:13:41 +0200633 removeItems : function () {
Nils Diewald86dad5b2015-01-28 15:09:07 +0000634 var child;
Nils Diewald2fe12e12015-03-06 16:47:06 +0000635
Nils Diewald2fe12e12015-03-06 16:47:06 +0000636 // Remove all children
Nils Diewald5975d702015-03-09 17:45:42 +0000637 var children = this._element.childNodes;
Akronc7448732016-04-27 14:06:58 +0200638 // Leave the prefix and lengthField
Akron9905e2a2016-05-10 16:06:44 +0200639 for (var i = children.length - 1; i >= 3; i--) {
Akrone4961b12017-05-10 21:04:46 +0200640 this._element.removeChild(
641 children[i]
642 );
Nils Diewald5975d702015-03-09 17:45:42 +0000643 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000644 },
645
Nils Diewald2fe12e12015-03-06 16:47:06 +0000646 /**
647 * Get a specific item from the complete list
648 *
649 * @param {number} index of the list item
650 */
651 item : function (index) {
652 return this._items[index]
653 },
654
655
Nils Diewald86dad5b2015-01-28 15:09:07 +0000656 /**
657 * Get a specific item from the filtered list
658 *
659 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000660 * in the filtered list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000661 */
662 liveItem : function (index) {
663 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200664 if (!this._initList())
665 return;
Nils Diewald86dad5b2015-01-28 15:09:07 +0000666
667 return this._items[this._list[index]];
668 },
669
Nils Diewald86dad5b2015-01-28 15:09:07 +0000670
671 /**
Akron5240b8c2016-05-20 09:17:41 +0200672 * Get a specific item from the viewport list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000673 *
674 * @param {number} index of the list item
Nils Diewald2fe12e12015-03-06 16:47:06 +0000675 * in the visible list
Nils Diewald86dad5b2015-01-28 15:09:07 +0000676 */
677 shownItem : function (index) {
678 if (index >= this.limit())
Akrone4961b12017-05-10 21:04:46 +0200679 return;
Akron9c4d1ae2016-05-25 21:43:22 +0200680 return this.liveItem(this.offset + index);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000681 },
682
683
Nils Diewald2fe12e12015-03-06 16:47:06 +0000684 /**
Akron9c4d1ae2016-05-25 21:43:22 +0200685 * Get the length of the full item list
Nils Diewald2fe12e12015-03-06 16:47:06 +0000686 */
687 length : function () {
688 return this._items.length;
689 },
690
691
692 /**
Akron5240b8c2016-05-20 09:17:41 +0200693 * Length of the filtered item list.
694 */
695 liveLength : function () {
696 if (this._list === undefined)
Akrone4961b12017-05-10 21:04:46 +0200697 this._initList();
Akron5240b8c2016-05-20 09:17:41 +0200698 return this._list.length;
699 },
700
Akrone817b882018-08-31 14:09:17 +0200701
Akron5240b8c2016-05-20 09:17:41 +0200702 /**
Nils Diewald2fe12e12015-03-06 16:47:06 +0000703 * Make the next item in the filtered menu active
Nils Diewald86dad5b2015-01-28 15:09:07 +0000704 */
705 next : function () {
Nils Diewald5975d702015-03-09 17:45:42 +0000706
Akronb38afb22016-05-25 19:30:01 +0200707 // No list
708 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200709 return;
Akronb38afb22016-05-25 19:30:01 +0200710
Akron9c4d1ae2016-05-25 21:43:22 +0200711 // Deactivate old item
712 if (this.position !== -1 && !this._prefix.active()) {
Akrone4961b12017-05-10 21:04:46 +0200713 this.liveItem(this.position).active(false);
Akron9c4d1ae2016-05-25 21:43:22 +0200714 };
Nils Diewalde8518f82015-03-18 22:41:49 +0000715
Akron9c4d1ae2016-05-25 21:43:22 +0200716 // Get new active item
717 this.position++;
718 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000719
Nils Diewald5975d702015-03-09 17:45:42 +0000720 // The next element is undefined - roll to top or to prefix
Nils Diewald86dad5b2015-01-28 15:09:07 +0000721 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000722
Akrone4961b12017-05-10 21:04:46 +0200723 // Activate prefix
724 var prefix = this._prefix;
Nils Diewald5975d702015-03-09 17:45:42 +0000725
Akrone4961b12017-05-10 21:04:46 +0200726 // Prefix is set and not active - choose!
727 if (prefix.isSet() && !prefix.active()) {
728 this.position--;
729 prefix.active(true);
730 return;
731 }
Akron9c4d1ae2016-05-25 21:43:22 +0200732
Akrone4961b12017-05-10 21:04:46 +0200733 // Choose first item
734 else {
735 newItem = this.liveItem(0);
736 // choose first item
737 this.position = 0;
738 this._showItems(0);
739 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000740 }
741
Akron5a1f5bb2016-05-23 22:00:39 +0200742 // The next element is after the viewport - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200743 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200744 this.screen(this.position - this.limit() + 1);
Akron5a1f5bb2016-05-23 22:00:39 +0200745 }
746
747 // The next element is before the viewport - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200748 else if (this.position <= this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200749 this.screen(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000750 };
Nils Diewald5975d702015-03-09 17:45:42 +0000751
752 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000753 newItem.active(true);
754 },
755
Nils Diewalde8518f82015-03-18 22:41:49 +0000756 /*
Nils Diewald86dad5b2015-01-28 15:09:07 +0000757 * Make the previous item in the menu active
758 */
Nils Diewald86dad5b2015-01-28 15:09:07 +0000759 prev : function () {
Nils Diewald2d210752015-03-09 19:01:15 +0000760
Akronb38afb22016-05-25 19:30:01 +0200761 // No list
762 if (this.liveLength() === 0)
Akrone4961b12017-05-10 21:04:46 +0200763 return;
Akronb38afb22016-05-25 19:30:01 +0200764
Akron9c4d1ae2016-05-25 21:43:22 +0200765 // Deactivate old item
Nils Diewald2d210752015-03-09 19:01:15 +0000766 if (!this._prefix.active()) {
Akron9c4d1ae2016-05-25 21:43:22 +0200767
Akrone4961b12017-05-10 21:04:46 +0200768 // No active element set
769 if (this.position === -1) {
770 this.position = this.liveLength();
771 }
Akron9c4d1ae2016-05-25 21:43:22 +0200772
Akrone4961b12017-05-10 21:04:46 +0200773 // No active element set
774 else {
775 this.liveItem(this.position--).active(false);
776 };
Nils Diewald2d210752015-03-09 19:01:15 +0000777 };
778
Akron9c4d1ae2016-05-25 21:43:22 +0200779 // Get new active item
780 var newItem = this.liveItem(this.position);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000781
782 // The previous element is undefined - roll to bottom
783 if (newItem === undefined) {
Nils Diewald5975d702015-03-09 17:45:42 +0000784
Akrone4961b12017-05-10 21:04:46 +0200785 // Activate prefix
786 var prefix = this._prefix;
787 var offset = this.liveLength() - this.limit();
788
789 // Normalize offset
790 offset = offset < 0 ? 0 : offset;
Nils Diewald2d210752015-03-09 19:01:15 +0000791
Akrone4961b12017-05-10 21:04:46 +0200792 // Choose the last item
793 this.position = this.liveLength() - 1;
794
795 // Prefix is set and not active - choose!
796 if (prefix.isSet() && !prefix.active()) {
797 this.position++;
798 prefix.active(true);
799 this.offset = offset;
800 return;
801 }
Nils Diewald2d210752015-03-09 19:01:15 +0000802
Akrone4961b12017-05-10 21:04:46 +0200803 // Choose last item
804 else {
805 newItem = this.liveItem(this.position);
806 this._showItems(offset);
807 };
Nils Diewald86dad5b2015-01-28 15:09:07 +0000808 }
809
Akron5a1f5bb2016-05-23 22:00:39 +0200810 // The previous element is before the view - roll up
Akron9c4d1ae2016-05-25 21:43:22 +0200811 else if (this.position < this.offset) {
Akrone4961b12017-05-10 21:04:46 +0200812 this.screen(this.position);
Akron5a1f5bb2016-05-23 22:00:39 +0200813 }
814
815 // The previous element is after the view - roll down
Akron9c4d1ae2016-05-25 21:43:22 +0200816 else if (this.position >= (this.limit() + this.offset)) {
Akrone4961b12017-05-10 21:04:46 +0200817 this.screen(this.position - this.limit() + 2);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000818 };
Nils Diewald2fe12e12015-03-06 16:47:06 +0000819
Nils Diewald5975d702015-03-09 17:45:42 +0000820 this._prefix.active(false);
Nils Diewald86dad5b2015-01-28 15:09:07 +0000821 newItem.active(true);
822 },
Nils Diewald86dad5b2015-01-28 15:09:07 +0000823
Akron3c2730f2016-05-24 15:08:29 +0200824 /**
825 * Move the page up by limit!
826 */
827 pageUp : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200828 this.screen(this.offset - this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200829 },
830
831
832 /**
833 * Move the page down by limit!
834 */
835 pageDown : function () {
Akron9c4d1ae2016-05-25 21:43:22 +0200836 this.screen(this.offset + this.limit());
Akron3c2730f2016-05-24 15:08:29 +0200837 },
838
Nils Diewald86dad5b2015-01-28 15:09:07 +0000839
Akrone817b882018-08-31 14:09:17 +0200840 /**
841 * Move the view one item up
842 */
843 viewUp : function () {
844 this.screen(this.offset - 1);
845 },
846
847
848 /**
849 * Move the view one item down
850 */
851 viewDown : function () {
852 this.screen(this.offset + 1);
853 },
854
Akron5240b8c2016-05-20 09:17:41 +0200855 // Unmark all items
856 _unmark : function () {
Akron678c26f2020-10-09 08:52:50 +0200857 this._list.forEach(function(it){
858 var item = this._items[it];
Akrone4961b12017-05-10 21:04:46 +0200859 item.lowlight();
Akron678c26f2020-10-09 08:52:50 +0200860 item.active(false);
861 }, this);
Akron5240b8c2016-05-20 09:17:41 +0200862 },
863
Akron5240b8c2016-05-20 09:17:41 +0200864 // Set boundary for viewport
865 _boundary : function (bool) {
Akron55a343b2018-04-06 19:57:36 +0200866 if (this._list.length === 0)
867 return;
Akron5240b8c2016-05-20 09:17:41 +0200868 this.item(this._list[0]).noMore(bool);
869 this.item(this._list[this._list.length - 1]).noMore(bool);
870 },
871
872
873 // Append Items that should be shown
874 _showItems : function (off) {
875
Akrona92fd8d2016-05-24 21:13:41 +0200876 // optimization: scroll down one step
Akron9c4d1ae2016-05-25 21:43:22 +0200877 if (this.offset === (off - 1)) {
Akrone4961b12017-05-10 21:04:46 +0200878 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200879
Akrone4961b12017-05-10 21:04:46 +0200880 // Remove the HTML node from the first item
881 // leave lengthField/prefix/slider
882 this._element.removeChild(this._element.children[3]);
883 var pos = this.offset + this.limit() - 1;
884 this._append(this._list[pos]);
Akrona92fd8d2016-05-24 21:13:41 +0200885 }
Akron5240b8c2016-05-20 09:17:41 +0200886
Akrona92fd8d2016-05-24 21:13:41 +0200887 // optimization: scroll up one step
Akron9c4d1ae2016-05-25 21:43:22 +0200888 else if (this.offset === (off + 1)) {
Akrone4961b12017-05-10 21:04:46 +0200889 this.offset = off;
Akron9c4d1ae2016-05-25 21:43:22 +0200890
Akrone4961b12017-05-10 21:04:46 +0200891 // Remove the HTML node from the last item
892 this._element.removeChild(this._element.lastChild);
Akron9c4d1ae2016-05-25 21:43:22 +0200893
Akrone4961b12017-05-10 21:04:46 +0200894 this._prepend(this._list[this.offset]);
Akrona92fd8d2016-05-24 21:13:41 +0200895 }
896 else {
Akrone4961b12017-05-10 21:04:46 +0200897 this.offset = off;
Akron5240b8c2016-05-20 09:17:41 +0200898
Akrone4961b12017-05-10 21:04:46 +0200899 // Remove all items
900 this.removeItems();
Akron5240b8c2016-05-20 09:17:41 +0200901
Akrone4961b12017-05-10 21:04:46 +0200902 // Use list
903 var shown = 0;
904 var i;
Akron5240b8c2016-05-20 09:17:41 +0200905
Akron678c26f2020-10-09 08:52:50 +0200906 for (let i = 0; i < this._list.length; i++) {
Akrona92fd8d2016-05-24 21:13:41 +0200907
Akrone4961b12017-05-10 21:04:46 +0200908 // Don't show - it's before offset
909 shown++;
910 if (shown <= off)
911 continue;
Akrona92fd8d2016-05-24 21:13:41 +0200912
Akrone4961b12017-05-10 21:04:46 +0200913 var itemNr = this._list[i];
914 var item = this.item(itemNr);
915 this._append(itemNr);
916
917 if (shown >= (this.limit() + off))
918 break;
919 };
Akron5240b8c2016-05-20 09:17:41 +0200920 };
921
922 // set the slider to the new offset
Akron9c4d1ae2016-05-25 21:43:22 +0200923 this._slider.offset(this.offset);
Akron5240b8c2016-05-20 09:17:41 +0200924 },
925
926
927 // Append item to the shown list based on index
928 _append : function (i) {
929 var item = this.item(i);
930
931 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200932 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200933 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200934 };
935
Akron5240b8c2016-05-20 09:17:41 +0200936
937 // Append element
938 this.element().appendChild(item.element());
939 },
940
941
942 // Prepend item to the shown list based on index
943 _prepend : function (i) {
944 var item = this.item(i);
945
946 // Highlight based on prefix
Akrona92fd8d2016-05-24 21:13:41 +0200947 if (this.prefix().length > 0) {
Akrone4961b12017-05-10 21:04:46 +0200948 item.highlight(this.prefix().toLowerCase());
Akrona92fd8d2016-05-24 21:13:41 +0200949 };
Akron5240b8c2016-05-20 09:17:41 +0200950
951 var e = this.element();
Akron9c4d1ae2016-05-25 21:43:22 +0200952
Akron5240b8c2016-05-20 09:17:41 +0200953 // Append element after lengthField/prefix/slider
954 e.insertBefore(
Akrone4961b12017-05-10 21:04:46 +0200955 item.element(),
956 e.children[3]
Akron5240b8c2016-05-20 09:17:41 +0200957 );
Nils Diewald2fe12e12015-03-06 16:47:06 +0000958 }
Nils Diewald86dad5b2015-01-28 15:09:07 +0000959 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000960});